fix(web-og): real per-commit OG card rendered a 0-byte PNG (blank unfurl)#56
Conversation
…url) The dynamic OG image (`/r/:owner/:repo/c/:sha.png` and the federated `/h/...` variant) served HTTP 200 `image/png` with an EMPTY body for every resolved result — so every social unfurl that used the real per-commit card (issue #8's whole feature) showed a blank image. The placeholder branch was unaffected, which is why the liveness probe (it only checks `placeholder.png`) and any status/content-type check were blind to it. Root cause: satori (inside workers-og) throws "Expected <div> to have explicit display:flex ... if it has more than one child node" when it renders `<div>commit {sha}</div>` — that JSX is TWO children (the literal "commit " and the {sha} expression) on a div with no `display: flex`. The throw happens mid-stream, AFTER the 200 + headers are flushed, so the response is a valid-looking 200 with a 0-byte body. Reproduced in the workerd runtime via `wrangler dev` (ResultCard → 0 bytes; fixed → 28KB valid PNG); the placeholder has only single-child text nodes, so it always rendered. Fix: make it a single text node (`{`commit ${sha}`}`). Guard: the test harness already captures the satori node tree, so this adds `satoriDisplayViolations()` — a structural assertion encoding satori's multi-child-needs-display rule over the captured tree, run for the released, not-yet-released, and placeholder cards. It fails on the old `commit {sha}` and passes after the fix, catching any future violation without a real WASM render. Note: PR #55 wires bot deferred unfurls onto this dynamic path — it should land on top of (or after) this fix, or bots get the blank card. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
In-house adversarial review (Copilot is quota-blocked on this repo, so no automated review lands — per Verdict: ✅ approve — merge-ready. Skeptical pass found no blocking issues. What I checked, and why each concern cleared:
This was a day-one latent iceberg: a shipped feature (#8 federated OG) blank in every real per-commit unfurl, invisible to status/content-type checks. The fix is minimal and TDD-guarded, and the live |
What
The dynamic OG image —
/r/:owner/:repo/c/:sha.pngand the federated/h/:host/r/:projectPath/c/:sha.png— served HTTP 200image/pngwith an empty (0-byte) body for every resolved result. So every social unfurl that used the real per-commit card (issue #8's whole feature) showed a blank image. The placeholder branch was fine.Confirmed live before the fix (read-only):
/r/honojs/hono/c/f82aba…(→ v4.12.11)/h/gitlab.gnome.org/r/GNOME%2Fglib/c/0a90037…(→ 2.88.2)/r/facebook/react/c/<bad sha>/placeholder.pngIt was invisible to the liveness probe (it only checks
placeholder.png) and to any status/content-type check, because the failure is a 200 with the right headers and an empty body.Root cause
satori (inside
workers-og) throws:<div>commit {sha}</div>is two child nodes — the literal"commit "and the{sha}expression — on a div with nodisplay: flex. satori throws mid-stream, after the 200 + headers are already flushed, so the body comes back empty. Reproduced in the workerd runtime viawrangler dev(ResultCard → 0 bytes; fixed → 28 KB valid PNG). The placeholder card has only single-child text nodes, so it always rendered.Fix
One line: make it a single text node —
{commit ${sha}}.Regression guard (TDD)
The test harness already captures the satori node tree, so this adds
satoriDisplayViolations()— a structural assertion encoding satori's "multi-child element needs an explicit display" rule over the captured tree, run for the released, not-yet-released, and placeholder cards. It fails on the oldcommit {sha}and passes after the fix, catching any future violation without a real WASM render.Verification
wrangler devreal render: ResultCard 0 → 28618 bytes, valid PNG, no satori error.scripts/validate.sh): build, typecheck, all package tests (incl. the 3 new web-og cases), a11y contrast, biome lint, shellcheck, actionlint, gitleaks, publint; osv clean (only the documented warn-only js-yaml 3.14.2 Medium).Coordination
PR #55 wires bot deferred unfurls onto this exact dynamic path. This should land on top of (or before) #55, otherwise #55 switches bot unfurls from the working placeholder to the blank dynamic card.
🤖 Generated with Claude Code