Uh oh!
There was an error while loading. Please reload this page.
fix(docs): reference the per-page Open Graph cards that were already being built - #12325
Merged
Conversation
…being built `app/og/docs/[...slug]/route.tsx` prerenders a 1200x630 card for every one of the 403 doc pages, and until now nothing linked any of them: `getPageImage()` was called only by the route that *produces* the image, and no `openGraph` or `twitter` key existed anywhere in the app. Every page shared as a blank card. Wire the cards into the metadata of all five URL shapes: - docs pages and `/docs` (the empty-slug case) reference their own generated card through the same `getPageImage()` the generator maps over, so the reference and the generator cannot disagree about the slug shape; - the homepage and the blog reference `public/hero-cover-dark.png`, which is already shipped and already this page's video poster -- the card generator is docs-only and has no slug to render either of them from. Image URLs stay site-relative so `metadataBase` remains the single place the origin is spelled, and every `og:url` is the same absolute URL as the page's canonical link. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 25, 2026
Closed
os-zhuang
marked this pull request as ready for review
August 25, 2026 18:19
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#12235
What was already true, re-measured before writing anything
The card's premise holds, and both halves were re-derived rather than inherited:
200 image/pngon production<loc>in the livesitemap.xmlmapped to its card URL and fetched — 403 requests, zero non-200, zero non-image/pnggetPageImagehad exactly 3 source hits — its own declaration and the two lines of the route that produces the imagegrep -rnoverapps/docs, positive controlgenerateMetadata= 2 hits, so the zero is absence rather than a broken querygrep -rn 'openGraph|twitter'overapps/docs= 0 hitsPopulation also re-derived, because this epic has corrected five PM measurements already: the live sitemap carries 408 URLs = 403 docs (including
/docs, the empty-slug case) +/+/blog+ 3 blog posts. That is five URL shapes across three files, confirming the correction made on #12234 rather than the four this epic originally listed.What this changes
generateMetadata/metadatain the three page modules, and nothing else. No file underapp/og/**is touched — the images were never the problem./docsreference their own generated card via the samegetPageImage()that the generator'sgenerateStaticParamsmaps over. The generator and the reference are now one expression, so a slug shape that stops matching breaks the build instead of shipping anog:imagethat 404s.public/hero-cover-dark.png. The card generator is docs-only: it renders from asource.getPage()result and neither the homepage nor the blog has an MDX file behind it, so there is no slug to hand it. The hero cover is already shipped and is already this page's video poster, so the shared card costs no extra bytes and no extra route.metadataBase(PR fix(docs): set metadataBase and emit one absolute canonical per page type #12305) absolutises them, so the origin keeps being spelled in exactly one place. Everyog:urlis the same absolute string as that page's existing canonical link —og:urlis the identity a social platform de-duplicates shares by, and a spelling that differs from the canonical splits one page into two.Verification — one page of each route type, fetched
Measured against
next devrunning this branch at commita1293e84c, and each emitted URL then fetched from both that server and production:All five emit
og:title,og:description,og:url,og:imageandtwitter:card=summary_large_image.file(1)on the downloaded bytes confirms the declared dimensions are not a claim: docs cards arePNG image data, 1200 x 630, the hero isPNG image data, 2400 x 1200— matching theog:image:width/og:image:heighteach page emits.The homepage card is distinct from the docs template by URL and by bytes.
The index page's card works — and the reason is not the one anyone wrote down
The dispatch asked whether
generateStaticParamsandgetPageImage()agree on the slug shape forcontent/docs/index.mdx. They do:/og/docs/image.pngis in the 403 that answered200 image/png.A reverse verification was run to check the measurement was sensitive to the failure it was looking for, not just to a happy path.
getPageImagewas mutated to drop the trailingimage.pngmarker — the exact off-by-one the question describes — on a committed tree, with the mutation proved on disk by anchoredgrep -cFcounts on both the deleted and the injected text (not bydiff --stat, which any other edit in the round would have turned green), and restored viagit checkout HEAD -- <abs path>with the restore proved bygit hash-objectmatching the HEAD blob andgit diff HEADempty.Predicted:
/docs404s,/docs/ai/agentsreturns200with the wrong page's card, because the route'sslug.slice(0, -1)would resolve['ai'].Observed: both 404. The prediction was half wrong, and the reason is a coupling worth recording:
proxy.ts's matcher excludes any path containing a dot. The trailing marker segment is therefore doing two jobs, only one of which is documented: it is the sacrificial segmentslug.slice(0, -1)discards, and its dot is what keeps every OG URL out of the locale rewriter. Its name is irrelevant —x.pngresolves the same page — but a marker without a dot takes the whole card surface to 404. That is why this PR references the card throughgetPageImage()rather than rebuilding the URL locally.Gates
Derived from the real diff with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no hand-written path list), at commita1293e84c; all five ran green, each quoted by its own verdict line rather than by a shell$?:check:docs-locale-catch-all—✓ 1 top-level dynamic segment(s), 1 guarded; dotted paths bypass proxy.ts: truecheck:page-declaration-shape—OK — 34 page entries across 2188 sources ... all reach the kernel through a discoverable declarationcheck:published-files—✓ 69 publishable package(s) of 78 workspace member(s) ...check:test-source-alias—check-test-source-alias OK — 72 packages with tests scannedcheck:type-source-resolution—check-type-source-resolution OK — 93 tsc program(s) across 77 packages scannedPlus
pnpm --filter @objectstack/docs typecheck(exit 0) and repo-widepnpm lint=eslint . --no-inline-config(exit 0, 47s — the full population, not a narrowed run).tsc --noEmit --listFilesconfirms all three edited files are in the program, so "typecheck is clean" is a statement about these edits and not about a program that never read them.check:nul-bytesOK.flock, soscripts/pm/os-verify-lock.sh --statusreports DECLARED UNLOCKED MODE — nothing can hold the shared verify lock here and the entry point takes it from nobody. The commands above ran without mutual exclusion because none is available on this host, not because the lock was bypassed.Not in this PR
skip-changesetapplied.app/og/**route, which is outside this card's file surface and is a decision rather than a default. The blog is wired to the shared site card instead, so no page on the site ships a blank card today.public/hero-cover-dark.pngis now referenced from page metadata in two files, not just from the page body. Renaming, re-encoding to another format, or deleting it now breaks the homepage and blogog:imageas well as the LCP image. Both call sites carry a comment saying so.Generated by Claude Code