Skip to content

Fix the social card for "Open For Business", and check cards in CI - #127

Merged
kixelated merged 3 commits into
mainfrom
claude/og-tags-issue-ip1z52
Aug 21, 2026
Merged

Fix the social card for "Open For Business", and check cards in CI#127
kixelated merged 3 commits into
mainfrom
claude/og-tags-issue-ip1z52

Conversation

@kixelated

@kixelated kixelated commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Two bugs, one specific to this post and one affecting every page — plus a CI check so neither class recurs.

The cover was an SVG

open-for-business.mdx set cover: "/blog/open-for-business/viewers-1000.svg" — one of the four scaling icons reused from the post body. No scraper renders SVG for a social card; Facebook, X, LinkedIn, Slack, Discord and iMessage all skip the image and fall back to a bare text card. It's the only post of the 25 with a non-raster cover, which is why this one link looked broken and the others didn't.

The icon wouldn't have worked even if SVG were supported — it's a 256×256 black-on-transparent glyph drawn to sit on the page's dark background, so it would render as an invisible smudge on a card.

Switched the cover to splash.png, which shipped in the same commit as the post (#123) and was never referenced anywhere. It's the moq.pro hero at 2324×1206 — a 1.93:1 image, i.e. the social card aspect ratio — so it was drawn for exactly this and just never got wired up.

og:image:width / og:image:height were hardcoded

global.astro emitted a fixed 163 × 150 on every page regardless of the actual cover, so they were wrong essentially everywhere:

Image Declared Actual
on-a-boat/boat.png 163×150 1920×1080
open-for-business/splash.png 163×150 2324×1206
/layout/icon.png (the default) 163×150 325×300

Wrong dimensions are worse than absent ones, because scrapers believe them. 163×150 is below X's 300×157 floor for summary_large_image, so a card that should render large gets downgraded to a thumbnail — which likely made other posts' cards look off too, not just this one.

Removed both tags so scrapers measure the image themselves. Computing them at build time from the PNG/JPEG headers is possible, but these covers live in public/ and so aren't processed by Astro; that's more machinery than the correctness win justifies. Happy to add it if you'd rather declare them.

Checking cards in CI

Nothing caught the SVG, because nothing looks at the cards. CI ran bun run check (biome + tsc) and bun test, whose three files cover Worker routing, vanity imports and the broadcast URL scheme. None of it touches Open Graph — and CI never built the site at all, so a cover pointing at a missing file wouldn't have failed either.

scripts/check-og.ts builds and asserts over dist/**/*.html: every page has og:title, og:description, og:url, og:image, and every image reference is an absolute https URL pointing at a real raster image that exists in the build.

It separates broken from merely suboptimal:

  • Fails — an SVG, a missing file, a relative or non-https URL, or bytes that don't decode as the format the extension claims. No image renders at all.
  • Warns — a cover under 300×157. It still renders, just as a thumbnail instead of a large card.

The warning tier exists because two posts already sit under the threshold, and which cover to use is an editorial call rather than a bug:

dist/blog/distribution-at-twitch/index.html
  og:image is 240x240 … https://moq.dev/blog/kixelCat.png
dist/blog/moqbs/index.html
  og:image is 256x256 … https://moq.dev/blog/moqbs/moqbs.png

moqbs has larger images in its own directory (watch.png at 1470×1242) if you want a bigger card there; distribution-at-twitch has no alternative in the repo. Both left alone — say the word and I'll swap them.

dimensions() validates the header against the extension for all five accepted formats, and scripts/check-og.test.ts covers that directly: each format read back at its known size, plus the two rejection paths (non-image bytes, and an extension that disagrees with the bytes).

Verification

Full sequence from a clean tree: bun run check ✅ · bun test (17 tests, 4 files) ✅ · bun astro build --mode live (35 pages) ✅ · bun scripts/check-og.ts

The check is verified against real breakage, not just the happy path. Each of these was reproduced end-to-end — cover changed, site rebuilt, check run — and each exits 1:

Cover Reported as
the original viewers-1000.svg og:image is .svg, which scrapers won't render
a path with no file behind it og:image has no file at dist/…/nope.png
text bytes renamed to .png og:image is not a valid PNG file

Built output for the fixed post — og:image and twitter:image both at https://moq.dev/blog/open-for-business/splash.png, twitter:card still summary_large_image, and no og:image:width / og:image:height on any page. Spot-checked /blog/on-a-boat/ and / for regressions.

Note that cards won't update on already-shared links until each platform's cache expires or is manually refreshed (X's Card Validator, Facebook's Sharing Debugger).

claude added 2 commits August 21, 2026 14:29
The post's cover was viewers-1000.svg, one of the four scaling icons from
the body. No scraper renders SVG for a social card -- Facebook, X, LinkedIn,
Slack, Discord and iMessage all skip the image and fall back to a bare text
card. It's the only post of the 25 with a non-raster cover, which is why
this one link looked broken and the rest didn't. The icon wouldn't have
worked even if SVG were supported: it's a 256x256 black-on-transparent glyph
meant to sit on the page's dark background.

splash.png shipped in the same commit as the post and was never referenced.
It's the moq.pro hero at 2324x1206 -- a 1.93:1 image, which is the card
aspect ratio -- so it was drawn for this and just never got wired up.

Also drop og:image:width/height. They were hardcoded to 163x150 and emitted
on every page regardless of the actual cover, so they were wrong everywhere:
boat.png is 1920x1080, and even the /layout/icon.png default they seem to
describe is really 325x300. Wrong dimensions are worse than none, because
scrapers believe them -- 163x150 is under X's 300x157 floor for
summary_large_image, so a correct large card gets downgraded to a thumbnail.
Omitting them lets scrapers measure the image themselves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXDsRD3HaMB1MU3kP767ak
Nothing caught the SVG cover in the previous commit, because nothing looks
at the cards. CI runs `bun run check` (biome + tsc) and `bun test`, and the
three test files cover Worker routing, vanity imports and the broadcast URL
scheme. None of it touches Open Graph, and CI never built the site at all,
so a cover pointing at a missing file wouldn't have failed either.

The tags are assembled in the layout from post frontmatter, so the built
HTML is the only place the result is visible. This builds in CI and asserts
over dist/**/*.html: every page has og:title, og:description, og:url and
og:image, and every image reference is an absolute https URL pointing at a
raster file that exists in the build.

Broken and merely suboptimal are kept apart. An SVG, a missing file or a
relative URL means no image renders at all, so those fail. An undersized
cover still renders, just as a thumbnail instead of a large card, so those
warn and let the build pass -- which cover to use is the author's call, and
two posts already sit under the threshold (kixelCat.png at 240x240 and
moqbs.png at 256x256).

Verified against the real bug: restoring the .svg cover and rebuilding fails
the check with "og:image is .svg, which scrapers won't render", and pointing
a cover at a nonexistent file fails with the resolved path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXDsRD3HaMB1MU3kP767ak
@kixelated kixelated changed the title Fix the social card for "Open For Business" Fix the social card for "Open For Business", and check cards in CI Aug 21, 2026
@kixelated
kixelated marked this pull request as ready for review August 21, 2026 16:18
@kixelated
kixelated merged commit fd1196c into main Aug 21, 2026
1 check passed
@kixelated
kixelated deleted the claude/og-tags-issue-ip1z52 branch August 21, 2026 16:18
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9dddb2b5-4883-44e5-a930-879b0a12b978

📥 Commits

Reviewing files that changed from the base of the PR and between e48befe and 2a527f9.

📒 Files selected for processing (5)
  • .github/workflows/pr.yml
  • scripts/check-og.test.ts
  • scripts/check-og.ts
  • src/layouts/global.astro
  • src/pages/blog/open-for-business.mdx

Walkthrough

The pull request adds a Bun check for Open Graph metadata and social-card images in generated pages. The check validates image URLs, file formats, image headers, local files, and minimum dimensions. Tests cover supported raster formats and invalid image cases. The pull request workflow now builds the site and runs the check. The layout no longer emits fixed Open Graph image dimensions, and one blog post uses a PNG cover image.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/og-tags-issue-ip1z52

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a527f9d7e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/check-og.ts
// Every image the sites reference is served from the same build, so the
// pathname doubles as its location in dist. An off-origin image would need
// fetching instead, and none exists yet.
const path = join(DIST, url.pathname);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject off-origin URLs before mapping them into dist

When a cover uses an absolute URL on another host, this discards the origin and validates only the corresponding local pathname. For example, https://does-not-exist.invalid/layout/icon.png passes whenever dist/layout/icon.png exists, even though no scraper can retrieve the referenced image. Check that the URL has the expected site origin before mapping it into dist, or fetch and validate off-origin images instead.

Useful? React with 👍 / 👎.

Comment thread scripts/check-og.ts
buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) &&
buf.toString("ascii", 12, 16) === "IHDR"
) {
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Decode the image before accepting it as valid

When an image is truncated after its dimension header, this returns a size and the CI check accepts a card that scrapers cannot decode. A 24-byte file containing only a PNG signature and IHDR dimensions, with no complete IHDR data, IDAT, CRCs, or IEND, is reported as valid; the GIF, JPEG, and WebP branches similarly return before validating the complete file. The check therefore needs a real decoder or sufficient structural validation before declaring the asset usable.

Useful? React with 👍 / 👎.

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.

2 participants