12% of our posts had an image. The fix: a commit gate, a sha256, and one directory

A Bluesky account owner set a goal of media in half of posts. By adding a commit gate that blocks adjacent text-only posts and enforcing a single media directory with hashed sidecars, the team raised the media ratio from 12% to 50%. The new pipeline also made image cards phone‑friendly and introduc…

On September 6, 2026 a Bluesky account that posts automatically sent 42 scheduled messages, only five of which carried an image. That 12% figure fell far short of the owner’s simple target: one in every two posts should include an image or video. The shortfall was not due to a lack of creative ideas; the agent could generate an image card whenever it wanted. It simply never did, because nothing in the system forced it to, and nothing flagged the absence.

Introducing a Commit Gate

To close the gap, the team added a commit gate that rejects any queue where two text‑only posts sit side by side. The gate scans the planned schedule, sorts the rows by their intended posting time, and walks adjacent pairs. If it finds a pair of consecutive rows that both lack media, it flags the violation and prints the offending rows so the queue can be reordered. This adjacency rule is stronger than a simple ratio; it guarantees that no two consecutive slots are text‑only, ensuring a media‑rich feed regardless of how the posts are shuffled.

Restricting Media to a Single Directory

Before the change, a post’s media reference was a full file path. That design let a wrong path in a JSON line point to any file on disk, potentially exposing private images or product covers. The new rule confines all media to content/posts-media/. A stock row now refers only to a basename, for example: {"media":{"kind":"video","file":"2026-09-03-debt-gate.mp4","altText":"…"}}. The resolver rejects any path that contains a separator, is absolute, or has an incorrect extension. After resolving, it double‑checks that the absolute path still begins with the allowed directory. Two independent checks make the property “never read outside this directory” hard to violate.

Ensuring Rendered Files Match the Commit

Simply restricting the directory is not enough; a hand‑edited PNG could still slip in. Every file the renderer writes gets a sidecar JSON file that records the original spec, a SHA‑256 hash of the output, and metadata such as size and aspect ratio. The function loadPostMedia is used by both the commit gate and the poster. It throws if the file or its sidecar is missing, if the kinds disagree, or if the hash does not match. This guarantees that only files produced by the renderer are ever posted, and any post that fails the gate will also fail the poster, preventing silent errors on the unattended runner.

Phone‑Friendly Image Cards

The existing image cards were rendered by headless Chromium from an HTML template. On a phone, a 1200‑pixel wide image is displayed at roughly 360 CSS pixels, making 26‑pixel body text unreadable. The team increased the body font to 34 px and the title to 44 px, which scales to about 10 px on a phone. They also capped each card to seven lines of 48 characters, reducing the maximum from nine lines. The renderer now throws if a card exceeds the line count or if the title wraps to a second line, forcing the creator to split the content instead of posting a truncated card.

A Deterministic Video Pipeline

Video rendering required a new approach. The prototype relied on CSS animations that ran on wall‑clock time, making the output non‑deterministic. The production pipeline defines a scene as a JSON object—either a sequence of steps, a terminal command, or a line chart. A pure function turns the scene into a timeline of cues, which is used twice: once to generate a WebVTT captions file and once by a seek(t) function that sets the DOM to the exact state at time t. Playwright then drives the page frame‑by‑frame, taking screenshots at 30 fps up to 20 seconds, producing up to 600 PNG frames. FFmpeg turns the PNG sequence into an H.264 MP4 at 1280×720. Because the scene is deterministic, the renderer retries any failed screenshot up to three times with a 200 ms pause, ensuring reliability.

After rendering, the video file is uploaded to video.bsky.app/uploadVideo, which returns a job ID. The runner polls the job status until completion, then embeds the resulting blob with app.bsky.embed.video, attaching the WebVTT captions and alt text. The current stock contains 16 media rows—12 images and 4 videos—but no videos have yet been posted; the first is queued.

Impact and Trade‑offs

Adding the commit gate slows stock refills because rendering must finish before a commit can be made. Rendering a batch of 19 files takes a few minutes on a laptop, but this keeps the unattended runner lightweight, avoiding a browser and FFmpeg in the post‑queue path. The adjacency rule also forces re‑sequencing when a timely news post without media is inserted next to another text‑only slot; a small realignment tool pulls the next media row forward. The SHA‑256 check ensures that a simple typo in a card forces a re‑render, adding friction but preventing silent failures. Overall, the new system keeps the Bluesky account for Rulestack—which sells AI coding rules and skill packs—consistent, media‑rich, and reliable.

Why it matters

By enforcing strict media rules and deterministic rendering, the team guarantees a balanced, engaging feed while preventing silent failures that could degrade user experience.

Key points

  • Commit gate blocks adjacent text‑only posts
  • All media must live in a single, hashed directory
  • Image cards are resized for phone readability
  • Video pipeline is deterministic and retries failures
  • SHA‑256 sidecars ensure only rendered files are posted

Frequently asked questions

What is a commit gate?

A check that runs before code is committed to the repository, rejecting any queue that violates defined rules, such as two consecutive text‑only posts.

Why restrict media to one directory?

To prevent accidental uploads of private or unintended files and to make the system’s file access predictable and auditable.

How does the deterministic video renderer work?

It converts a JSON scene into a timeline of cues, renders each frame with Playwright, captures screenshots, and compiles them into an MP4 with FFmpeg.

Reporting drawn from

More from World

Felo News, House 42, Bridge Colony, Kot Lakhpat, Lahore, Pakistan
+92 308 4354717 · felopronews@gmail.com