Skip to content

Updates: 7 new posts for 2026-08 - #156

Open
vannyle wants to merge 43 commits into
mainfrom
blog-draft/2026-08-1786375656342
Open

Updates: 7 new posts for 2026-08#156
vannyle wants to merge 43 commits into
mainfrom
blog-draft/2026-08-1786375656342

Conversation

@vannyle

@vannyle vannyle commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Auto-generated monthly Updates draft(s). Review the Vercel preview(s) before merging.

  • A leaner AI harness and a full workspace of components in the new Toolcraft release
  • Four creative tools we built while shipping the Toolcraft site
  • Introducing Aval: an open source format for interactive video on the web
  • A $15 asset pack and a few million AI tokens: building a web-native grass simulation
  • Inside our brand and design work for the Databricks Developer Portal
  • Behind the scenes of Railway's launch video: from request to release in under two weeks
  • Our text animation skill for AI agents works with any framework or language

Before merging

  • Does any draft re-cover ground an existing post already made?
  • Is each draft carried by real substance, or is it a short post padded out to article length?
  • Does the voice read as the author writing, rather than an article written about them?
  • Should any of these get their own cover image instead of the shared placeholder?
  • Do the embedded videos actually play? Their URLs point at X and can rot without warning.

vannyle added 30 commits July 21, 2026 11:10
Specs the pipeline that turns Alex's qualifying X posts into a monthly
company-voice blog post, gated on his informal review before merge.
Addresses Alex's concern that some source posts are too thin to make a
worthwhile blog post: raises the classify bar from "on-topic" to "stands
alone for a reader with zero context," makes drafting explicitly editorial
instead of reformatting, scales the length filter back to a cheap
one-liner check, and lets a run produce multiple posts (grouped) instead
of forcing everything into one.
Bare directory args to `node --test` don't recurse into matching test
files on this Node version - it tries to require the directory path
itself and fails with MODULE_NOT_FOUND. Explicit globs fix it.
…slug/env hardening)

- Quote and escape the summary field in publish-post.js's frontmatter
  the same way title already is, so a summary containing a colon no
  longer produces invalid YAML.
- Make blog-draft branch/PR names unique per run (append GITHUB_RUN_ID,
  falling back to Date.now()) so a same-month re-run no longer collides
  with an existing branch/PR.
- Sanitize the LLM-provided slug (lowercase, collapse non-alphanumeric
  runs to a hyphen, trim leading/trailing hyphens) before using it in
  the post folder name.
- Add an assertRequiredEnv check at the top of main() in run.js so a
  missing X_API_BEARER_TOKEN/OPENAI_API_KEY/SLACK_WEBHOOK_URL fails
  fast with a clear error instead of an obscure downstream failure.
Proves the YAML-escaping fix by round-tripping a colon-containing
summary through the real frontmatter parser Gatsby uses, not just
checking the output string shape.
Replaces the openai SDK with @anthropic-ai/sdk across the classify and
draft steps. The Anthropic API has no json_object response mode, so the
JSON contract is now enforced by structured outputs (output_config.format)
with real schemas instead of a prompt-level request.

Adds anthropic-json.js to share the request shape between both steps and
to turn refusals and token-limit truncation into named errors, so they
surface as Slack alerts rather than JSON parse failures.
Rewrites the classify and draft tasks around @anthropic-ai/sdk and
structured outputs, and adds a new Task 7 for the shared anthropic-json
helper (renumbering the tasks after it). Also corrects the prerequisites:
the key is now an Anthropic one, and X removed its free tier in Feb 2026.
The listing predated the review-fixes commit, so it was missing
assertRequiredEnv, the empty-group filter, and the GITHUB_RUN_ID branch
suffix. Spliced in from the real file rather than retranscribed, so the
two are byte-identical apart from the file-annotation comment.
A dry run stops after drafting, so demanding SLACK_WEBHOOK_URL up front
blocked local testing for a webhook that is never called. Also skips the
failure notification when no webhook is configured, so a dry-run error
isn't buried under a second failure from posting to an undefined URL.
A dry run against real June 2026 posts surfaced two problems. Drafts
referred to "Alex Barashkov, our CEO" in the third person while being
published under his byline; the draft prompt now states who the narrator
is. And a group re-covering an existing post slipped through, so the
classify schema now requires a per-group already_covered verdict that
the code filters on, instead of trusting the model to omit it silently.
Near-duplicate detection is a judgment call the classify step should not
make alone, so the PR gate is where it belongs. The checklist names the
failure modes an actual dry run produced rather than generic advice, and
lives where the reviewer already is instead of in the Actions log.
classifyAndGroupPosts now returns { groups, skipped } so a group dropped
as a duplicate reaches the reviewer instead of vanishing into the Actions
log — a wrong drop was previously as invisible as a duplicate slipping
through. Extracts the PR body into a tested module rather than building
it inline, since the skipped section is a branch real data rarely hits.

Adds sync-plan-code.js, which rewrites every code listing in the plan
from the file it documents. The listings had drifted repeatedly, in
files this branch never touched.
Adds the media expansion to the timeline call, assigns filenames before
drafting so the model references names that exist, downloads them into the
post folder, and strips any reference that failed to download rather than
shipping a broken image.

A survey of 35 days of posts found no author-supplied alt text on any of
42 media items, so the model writes it — it renders as the visible caption.
publishPost becomes async because the body can only be finalised once the
downloads are known. Video stays excluded: it is 23 of those 42 items and
still needs a hosting decision.
Requests variants/width/height from the X API, picks the highest-bitrate
mp4, and emits the site's <Video> component with the exact attributes it
requires. The poster frame is downloaded into the post folder, where
gatsby-node picks it up by filename like any other image.

The mp4 is hotlinked from video.twimg.com rather than rehosted, because
the pipeline has no write access to the S3 bucket existing posts use.
Those URLs can rot, so a published video may stop playing later — hence
the new PR checklist item asking the reviewer to play them. A <Video>
whose poster failed to download is dropped entirely: a missing poster
throws in video.jsx and would fail the whole site build.
vannyle added 13 commits August 10, 2026 13:27
1200x630, 276KB — down from a 3840x2160 7.2MB placeholder that was copied
into every generated post folder.
A real end-to-end run failed at gh pr create with 'GraphQL: not all refs
are readable' — git push returns before GitHub has indexed the new ref.
The identical command succeeded moments later. A CI runner is faster than
a laptop, so this is more likely there, not less; it would have failed the
first scheduled run.

Retries only that error, with backoff, so a bad token still fails fast
into the Slack alert instead of stalling. Also passes --base main
explicitly rather than depending on the repo's default branch. git-pr was
untested on the theory that mocking child_process only tests the mock; the
retry policy is real logic, so runImpl/sleepImpl are now injectable.
gatsby-node.js:135 collects posters with name: { regex: "/video-cover/" }.
Posters were named video-1-cover.jpg, which does not match, so the file
downloaded correctly but was invisible to the query — leaving videoCovers
empty and making video.jsx:18 throw, which fails the entire site build
rather than degrading to a missing image.

Renamed to video-cover-N.jpg, matching both the query and the convention
existing posts already use. A test now asserts poster names satisfy that
same regex, so the coupling can't silently break again.

Found by a real gatsby build; stripUnusableVideos did not catch it because
the download succeeded — the file was simply unreachable by name.
A real run produced a second Toolcraft post whose summary reused the
existing post's own 'starter kit and UI library' framing. The classify
verdict was defensible — the group carried genuine news (toolcraft.sh, an
AI harness overhaul) — so tightening the duplicate check would have
suppressed a legitimate release announcement. The duplication was
introduced one step later, by a drafter that had never been told an
existing post already explains the product.

Classify now names related existing posts per group, separately from the
already_covered verdict, and read-existing-posts resolves each to its blog
path via the site's own getBlogPostPath. The drafter is told to open with
what is new and link to the background rather than reintroduce it.

Verified live: the reused framing is gone, and 5 of 6 drafts now link to
relevant existing posts — including ones I had not anticipated, like the
Aval post citing the earlier ffmpeg video piece.
The blog is sentence case throughout ('Taking automated web page
screenshots with Puppeteer and Sharp'), but drafts drifted into Title Case
On Every Word on roughly a third of titles. The rule needs the
counter-example alongside it — naming the wrong form is what makes it
stick, and spelling out that AI/Blender/Novu stay capitalised for what they
are keeps proper nouns intact.

Verified live: all 8 titles came back in sentence case, including two that
were title-cased in the previous run.
Estimating the per-run bill was guesswork because thinking tokens bill as
output and dominate the total — input is only about a tenth of it. The run
now prints actual token counts and a dollar figure, so budget questions are
answered by measurement instead of arithmetic.

Prices are hardcoded and will drift; the token counts stay accurate
regardless.
handlePlay called videoRef.current.play() immediately after
setIsPlaying(true), but the <video> element is only rendered when isPlaying
is true — so the ref was still null and the call threw. Awaiting a setState
call does not wait for the re-render. The rejection also skipped
setShowCover(false), so the cover stayed up and clicking play did nothing.

Moved play() into an effect keyed on isPlaying, which runs after the
element mounts, and restored the cover if the browser refuses playback so
the reader gets the play button back instead of a dead frame.

This is a site component, not the pipeline: it affects the existing
comparing-gatsby-and-nextjs post too, which is the only other post using
the poster path. Most posts use autoPlay and take a different branch, which
is why this went unnoticed.
bestMp4 took the top of X's encoding ladder, so a post ended up serving a
3840x2160 file into a column that is 696px wide — and one clip offered
1600x1200 at 10,368 kbps next to 960x720 at 2,176. None of that resolution
reaches the reader; it just costs them bandwidth and delays first frame.

Now picks the smallest variant that still covers the column at 2x, falling
back to the largest when every option is below that, and to bit rate when a
url carries no dimensions. On the current month's videos that drops the 4K
encode to 1920x1080 and leaves the already-reasonable ones untouched — 19%
less bitrate overall.
X returns 403 for any request to video.twimg.com carrying a Referer from
another domain. Browsers always send one and referrerPolicy is ignored on
<video>, so the hotlinked src could never have worked from a real page —
curl only succeeded because it sends no Referer, which masked this.

Routes the mp4 through a /x-video/:path* rewrite in vercel.json, matching
the /aval and /api external proxies already there. Vercel fetches
server-side, so no browser Referer reaches X; verified that the origin
returns 206 for a browser User-Agent with no Referer.

Unverified until a preview deploy: whether Vercel forwards the client
Referer upstream. If it does, this fails the same way and the fallback is a
poster image linking to the post on X.
The Slack message read '6 new monthly blog drafts ready for review:
undefined'. openDraftPr became async when the PR-create retry was added,
but run.js kept destructuring it synchronously, so prUrl was a property of
a Promise. The PR still got created, which is why this survived a real run.

The message now leads with the PR link, lists each draft title, and notes
how many posts were skipped as duplicates. The preview URL is deliberately
not included: Vercel posts it as a comment on the PR after the build, so it
does not exist at the moment this message is sent.
Video is served from X through the /x-video rewrite. X returns 403 for any
request carrying a Referer from another domain, and Vercel forwards the
browser's Referer upstream — verified on a preview deploy, where the same
URL returns 206 with no Referer and 403 with one. With no Referer sent
there is none to forward, and the proxy returns video/mp4.

Scoped to the blog post template rather than set site-wide: the homepage
and other routes are unaffected. The cost is that outbound links from
articles no longer carry attribution to pixelpoint.io.
Drafts carried no outbound links at all. X rewrites every link as an opaque
t.co shortlink, and handed one of those the model cannot tell what it points
at, so it drops it. Requesting entities and substituting expanded_url gives
real destinations — 46 of 51 recent posts now carry one.

Announcements are often made by quoting someone else's post, which left the
pipeline blind to both the link and the screenshot being pointed at: the
Novu homepage post shipped with no media and no link for exactly this
reason. referenced_tweets.id pulls the quoted post in; its media is used
when the announcing post has none of its own, and its text is passed as
context only — the draft prompt says to write about our work, not about the
other person's post.
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pixelpoint-website Ready Ready Preview Aug 10, 2026 3:31pm

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant