Repository navigation
Description
Context
Since #1133, each card page with art previews with its own image. Every other page still previews with the site banner, https://inkweave.ink/og-image.png (the INKWEAVE wordmark over "Master Lorcana Synergies", no cards), which apps/web/index.html sets once for the whole site.
Vercel Web Analytics' referrers for 6 Sep to 5 Nov 2026 put /reveals first for Facebook and Reddit visits, almost all of them in the two reveal seasons, and the January reveals are the next wave. Home is second. The playstyle pages get mostly search visits today, and they are the pages the marketing plan's weekly synergy posts will link.
The owner compared samples of six candidates on 2026-11-05 (a private page) and picked reveals, the 25 playstyle pages and a new home banner, which also becomes the image of every page without one of its own.
Decisions, with the alternative each turns down:
- Drawn in the deploy build, by the Give each card page its own link preview image, with its art and three best partners #1133 generator, before the card images and in the same Chromium. 27 more images add seconds. A request-time function is turned down for Give each card page its own link preview image, with its art and three best partners #1133's reasons: card art exists only as AVIF, and the data changes only when a deploy does.
- The new home banner replaces
/og-image.pngon every captured page that has no image of its own: the playstyles gallery, the ink pages,/browse,/vote,/about, the legal pages, release notes, and a card without art.apps/web/index.htmlkeeps/og-image.pngfor builds without the crawl (staging,SKIP_PRERENDER=1) and for404.html. The routes the crawl never captures (compare, in-depth vote, account) already get the home page's capture fromvercel.json, so they get the new banner too. - Reveals is drawn only in season: the same test the crawl uses to decide whether to capture
/revealsat all (isRevealSeasonActiveinscripts/prerender.mjs). Off season/revealsserves the home capture, so it shows the home banner. Every reveal already deploys, so the image is never older than the newest reveal. - The image's numbers come from the same places the pages read: a playstyle's cards from
data/synergies/_playstyles.json, sorted by full name as the gallery sorts them; its hero and accent fromPLAYSTYLE_UIinapps/web/src/shared/constants/playstyleUi.ts; the reveal set and its total fromREVEAL_SET_CODEandPER_INKinapps/web/src/shared/constants/revealSet.ts. A root script can't import the app's TypeScript, so the helper reads those two files as text, and its tests fail when either can't be read or misses a playstyle the engine lists (the pattern ofINK_GLOW's test). - Each file is named
{key}.{hash}.jpgunder/og/page/, the hash being the first 16 hex characters of its sha256, sovercel.json's existingimmutablerule for/og/stays truthful (Content-address card image URLs to fix Vercel cache poisoning #323) and a changed image reaches Facebook as a new URL.
The layouts are the approved samples:
- Reveals: "{Set name} reveals" kicker, the set's name, "{n} of {total} cards revealed" with a gold progress bar, a line on what the page offers, and the five newest revealed cards with art fanned out on the right. With fewer than five, the fan shows what there is.
- Playstyle: the gallery tile's fan (the hero in front, then the next four cards with art by name; Locations, which are printed sideways, left out), "Lorcana playstyle · {n} cards", the playstyle's name and tagline, and "Key cards" naming the fan's first three, glowing in the playstyle's accent.
- Home: the Inkweave lockup and
inkweave.ink, "Disney Lorcana synergy finder", "Find the cards that work together", and the six featured cards (data/featuredCards.json, which Featured cards in admin writes) in an arc, each glowing in its ink.
Out of scope: an image of its own for the playstyles gallery, the ink pages or /vote (each costs little later, on the same template); compare and in-depth vote links, which aren't prerendered, so a pair would need a request-time function; refreshing Facebook's cached previews of links already shared.
Success: a link to /, a playstyle page or, in season, /reveals pasted into Facebook or Discord shows that page's own image, and every other page shows the new home banner instead of the old wordmark.
Prerequisites
- Write access to
Doberjohn/inkweave, on a branch offmaster - Node 24 and pnpm 10.34.7, after
pnpm install --frozen-lockfile, and Chromium for Playwright - A local build with card art:
node scripts/download-card-images.mjs,pnpm build:engine,node scripts/precompute-synergies.mjs,pnpm build:web - Know Give each card page its own link preview image, with its art and three best partners #1133's pieces:
scripts/generate-og-images.mjs,scripts/lib/cardOgImage.mjs, the crawl's tag swap inscripts/prerender.mjs, and check 12 ofscripts/check-rendered-html.mjs
Implementation Steps
Step 1: The pure helpers
scripts/lib/pageOgImage.mjs exports:
parsePlaystyleUi(tsText):{[id]: {accent, heroId}}fromplaystyleUi.ts.parseRevealSet(tsText):{code, total}fromrevealSet.ts, the total being the sum ofPER_INK.revealSeasonSet(preview, now): the code of a set inpreviewCards.jsonwhose release date is still ahead, or null.isRevealSeasonActiveinscripts/prerender.mjscalls it, so the crawl and the generator never disagree.homeOgData,playstyleOgData,revealsOgData: what the template fills, each with its alt text: "Inkweave: find the Lorcana cards that work together", "{Playstyle} Lorcana playstyle on Inkweave, with {hero} and more of its cards", "{Set} reveals on Inkweave: {n} of {total} cards revealed".pageOgImagesByRoute(record): each page's{url, alt}by route, and the home image as the default.
withCardOgImage in scripts/lib/cardOgImage.mjs becomes withOgImage, since pages use it too.
pnpm test:scripts -- pageOgImageVerify: scripts/lib/__tests__/pageOgImage.test.mjs passes: the real playstyleUi.ts parses with an entry for every engine playstyle, the real revealSet.ts parses to a positive total, the season test, a playstyle's fan (name order, hero first, no Location, no card without art), reveals with fewer than five cards, home's six cards, the alt texts, and the default.
Step 2: The template
scripts/og/page-template.html: the three layouts at 1200x630 as sampled, with a window.fillPage(kind, data) that waits for its fonts and images like card-template.html's fillCard. A name or title steps down in size until it fits.
Step 3: The generator
scripts/generate-og-images.mjs draws the page images on one page with the page template before it draws the cards, writes dist/og/page/{home|reveals|playstyle-<id>}.{hash}.jpg, and records them in apps/web/node_modules/.cache/og-pages.json, {[route]: {file, alt}}, removing an earlier build's record first. One that fails to draw is logged and left out, as a card is. The log line gives the count, size and time for pages and cards.
Verify: after pnpm build:web && node scripts/generate-og-images.mjs, dist/og/page/ holds 26 JPEGs off season (home and 25 playstyles), each 1200x630.
Step 4: The crawl writes them
scripts/prerender.mjs reads both records and passes each captured page through withOgImage with its own image: a card page its card's, /, each playstyle page and /reveals their own, and every other page the home image.
Verify: after a sample crawl, dist/index.html and dist/playstyles/bounce/index.html name their own /og/page/ image in both image tags, and dist/browse/index.html names the home image.
Step 5: Deploy check 13
scripts/check-rendered-html.mjs adds check 13: / and each playstyle page name their own /og/page/ image, /reveals too when the crawl wrote it, and every other route in apps/web/src/prerenderedRoutes.json names the home image; twitter:image equals og:image; the build holds the file. Tests cover a passing build, a page that kept /og-image.png, a playstyle page with the home image, and a missing file.
Verify: the tests pass, and deleting dist/og/page/playstyle-bounce.*.jpg from a sample output makes node scripts/check-rendered-html.mjs apps/web/dist fail naming that page.
Step 6: Docs and the dry run
CLAUDE.md's Card link previews note and apps/web/src/features/cards/IMAGES.md cover the page images; deploy.yml's dry-run paths add scripts/lib/pageOgImage.mjs.
Files Affected
Created
| File | Purpose |
|---|---|
scripts/lib/pageOgImage.mjs |
What each page image shows, its alt text, the parsers, the season test |
scripts/lib/__tests__/pageOgImage.test.mjs |
Their cases |
scripts/og/page-template.html |
The three 1200x630 layouts |
Modified
| File | Change |
|---|---|
scripts/generate-og-images.mjs |
Draws the page images and writes their record |
scripts/lib/cardOgImage.mjs, its test |
withCardOgImage becomes withOgImage |
scripts/prerender.mjs, scripts/prerender.test.mjs |
Every page gets its image; the season test moves to the helper |
scripts/check-rendered-html.mjs, scripts/check-rendered-html.test.mjs |
Check 13 |
.github/workflows/deploy.yml |
The new helper in the dry run's paths |
CLAUDE.md, apps/web/src/features/cards/IMAGES.md |
Page images |
Testing / Verification
Gates
-
pnpm test:scripts,pnpm lint:root, knip, and the CodeScene gate pass - The PR's Deploy dry run passes, its log shows the page images' count, and check 13 passes
- No app JavaScript or card data changes
Images
- A sheet shown to the owner before merge: the home banner,
/revealsdrawn over a faked season (Set 14's cards standing in, once with five reveals and once with two), and playstyles with a long name (Location Control), a long tagline, a dual-ink hero (Self-Discard), and one whose first cards include a Location → nothing overlaps or runs out of its box - Each JPEG is 1200x630 and under 300 KB
- Two identical runs give the same file names
After deploy
-
curl -s https://inkweave.ink/ | grep og:image→/og/page/home.<hash>.jpg, and/browsenames the same -
curl -s https://inkweave.ink/playstyles/bounce | grep og:image→/og/page/playstyle-bounce.<hash>.jpg -
curl -sIon one → 200,image/jpeg,max-age=31536000, immutable - Facebook's Sharing Debugger shows the home and a playstyle link with their images (Scrape Again for home, which Facebook has cached)
Acceptance Criteria
-
/and each of the 25 playstyle pages name their own 1200x630 JPEG inog:imageandtwitter:image, with alt text - In reveal season,
/revealsnames its own image, showing the set, its revealed count against the set's total, and its newest revealed cards - Every other prerendered page names the home image; none names
/og-image.png - Image URLs carry a hash of their bytes and are served
immutable - The deploy fails when a page lacks its image, and a planted fault proves it
- The tests fail when
playstyleUi.tsorrevealSet.tsstops parsing, or a playstyle has no entry - Staging's build (
SKIP_PRERENDER=1) draws no images - The app's JavaScript and card data are unchanged
Rollback
<merge> is the merge commit of the PR that closes this issue; each command runs on a branch off master, followed by a commit and a PR.
- Full rollback:
git revert -m 1 <merge>, then merge that PR. The pieces depend on each other (the crawl names what the generator records, and check 13 reads what the crawl wrote), so a partial revert isn't offered. Card images stay; every other page shows/og-image.pngagain. Links Facebook already scraped keep the new image until it scrapes them again.
References
- Give each card page its own link preview image, with its art and three best partners #1133 and PR feat(seo): give each card link its own preview image (#1133) #1134: card link previews, the generator, check 12
- Content-address card image URLs to fix Vercel cache poisoning #323: why only content-addressed URLs are
immutable - Hand off first visits from the prerendered page to the app without a flash #915, Make card & playstyle pages crawlable: build-time SSG, real card route, link tiles, per-route meta #486: how the crawl captures and writes each page
- Add a staging.inkweave.ink deployment of master built with Preview flags #741: staging builds without the crawl
- Samples: the owner's private page, https://claude.ai/artifact/6wwaPU7mhwkjB42EF18m89
- The Open Graph protocol: https://ogp.me/
- Meta, Sharing Debugger: https://developers.facebook.com/tools/debug/
Activity
fix(seo): count preview cards in page images, fail a missing page in …
Merge pull request #1137 from Doberjohn/claude/project-thread-s0ce6e