Repository navigation
feat(seo): give each card link its own preview image (#1133) - #1134
Conversation
A card link shared on Facebook, Discord or X showed the site banner. Each
card page's og:image and twitter:image now name a 1200x630 JPEG of the card:
its art, name and version, how many cards it pairs with, and its three best
partners, each tagged with the group it scores highest in. A card without
synergies shows its ink, rarity and type and no partner row.
- scripts/generate-og-images.mjs draws every card in the deploy build, after
the web build and before the crawl, from the build's own data, fonts and art
(scripts/og/card-template.html), into dist/og/card/{id}.{hash}.jpg.
- The crawl swaps index.html's image tags and their alt text for the card's
(scripts/lib/cardOgImage.mjs); every other page keeps the banner.
- Check 12 of check-rendered-html fails a deploy whose card page kept the
banner or names an image the build lacks.
- /og/ is served immutable and stays out of the worker's navigation fallback.
- index.html gains og:site_name and image alt tags.
Closes #1133
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjrxDUg8t6RHUrGiKw1neQ
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
🎨 Design-surface change detected This PR touches files mirrored by the Inkweave design system project. What changed:
To refresh: open the design system Claude project and prompt:
This is a reminder, not a blocker. Configured in |
size-limit report 📦
|
There was a problem hiding this comment.
cubic analysis
All reported issues were addressed across 16 files
Linked issue analysis
Linked issue: Doberjohn/inkweave#1133: Give each card page its own link preview image, with its art and three best partners
| Status | Acceptance criteria | Notes |
|---|---|---|
| Give each card page its own preview image | The crawl swaps tags for cards in the manifest, but generation and check 12 skip cards without imageHash. | |
| ✅ | Show the card, its details, and best partners | The template renders card art, name, version, partner count, and tagged partners, with a no-partner layout. |
| ✅ | Turn Location art upright | Location cards get landscape styling that rotates their portrait art for both hero and partner images. |
| ✅ | Hash image URLs and serve them immutable | JPEG filenames include a SHA-256 prefix, and Vercel applies immutable caching to the /og/ route. |
| Fail the deploy when a card preview image is missing | Check 12 catches missing files for cards with art, but cards without imageHash are excluded from the check. | |
| ✅ | Add Inkweave as the site name on every page | The shared index template adds og:site_name, which the prerendered pages inherit. |
| ✅ | Skip image generation for staging builds | The generator exits before rendering when SKIP_PRERENDER is set to 1. |
| ✅ | Keep app JavaScript and card data unchanged | The changes add build scripts and metadata handling without modifying app JavaScript or card data. |
Tip: instead of fixing issues one by one fix them all with cubic
View guided diff | Re-trigger cubic
… and partial images when drawing fails (#1133) - The template loads every font face before the first fit: a face loads only once text uses it, so each page's first card was measured in the fallback serif (567 px against Marcellus's 560 for one name). - build:vercel runs the crawl even when the image step fails, so check 12 fails the deploy instead of every page losing its prerender. - A drawing page that fails to open leaves its cards to the others, and the record of what was drawn is written however the run ends. - Chromium runs without partial raster: with it, about 70 of 1,228 images changed bytes between identical builds (the logo's masks antialiased differently), so unchanged cards got new URLs. Three runs now match. - Drops the unused Plus Jakarta Sans 600 face. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjrxDUg8t6RHUrGiKw1neQ
…at a time (#1133) - The tag swap passes replacer functions, so `$&`, `$'` or `$$` in a card's name can't splice page HTML into its alt text. - The generator reads the synergy files synchronously: about 1,200 read at once could run out of file descriptors on a runner with a low limit. - The partner pick finds each tag's first pair in one pass (same picks: all 1,228 images are byte for byte the same). - Alt text follows the issue: "{card} and its best Lorcana partners on Inkweave", and "{card}, a Lorcana card on Inkweave" without partners. - CLAUDE.md says a card without art keeps the banner; doc comments drop their issue tags. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjrxDUg8t6RHUrGiKw1neQ
build:vercel, the deploy's whole build order, lives there, and only the dry run runs it on a PR. The lockfile stays out: every Dependabot bump changes it, and CI's build and E2E cover those. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjrxDUg8t6RHUrGiKw1neQ
There was a problem hiding this comment.
Gates Passed
6 Quality Gates Passed
See analysis details in CodeScene
Quality Gate Profile: Pay Down Tech Debt
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.
Closes #1133
What changes
A card link pasted into Facebook, Discord, Reddit or X now previews with an image of that card, not the site banner. Each image is 1200x630 and shows:
A card with no synergies shows "{Ink} · {Rarity} {type}" and no partner row. Locations are turned upright, both as the main card and as a partner.
scripts/generate-og-images.mjsdraws every card inbuild:vercel, afterpnpm build:weband before the crawl, in the Chromium the crawl installs.scripts/og/card-template.htmlfrom the build's own card data, synergy files, fonts and art. The page is served from a made-up origin, with every path answered fromdist.dist/og/card/{id}.{sha256 prefix}.jpg.apps/web/node_modules/.cache/og-cards.json, outsidedist) of each card's file and alt text, removing an earlier build's record first.scripts/lib/cardOgImage.mjsholds the pure parts: the partner pick, what the template fills, the file name, the alt text and the tag swap. Its tests are inscripts/lib/__tests__/cardOgImage.test.mjs.scripts/prerender.mjsswaps each card page'sog:image,twitter:imageand both alt tags for the card's own. Every other page keeps the banner. The swap throws when the page doesn't hold exactly one of each tag.scripts/check-rendered-html.mjsfails the deploy when a card page with art kept the banner, when itstwitter:imagediffers from itsog:image, or when the image it names isn't in the build.vercel.jsonserves/og/immutable, since every name carries its hash.vite.config.tskeeps/og/out of the worker's navigation fallback.apps/web/index.htmladdsog:site_name"Inkweave" and alt text for the banner on every page.deploy.ymlruns the PR dry run when the generator, its template, check 12 or the rootpackage.json(which holdsbuild:vercel) changes. The lockfile stays out, since every Dependabot bump changes it.CLAUDE.mdandIMAGES.md.Where it differs from the issue
pairsis an object keyed by partner id, not an array.dist, notpublic/data, so it draws exactly the data and art the deploy ships.--disable-partial-raster. Without it, about 70 of 1,228 images changed bytes between identical builds because the logo's masks antialiased differently, so unchanged cards got new URLs. Three full runs now match byte for byte.prerender.test.mjsorknip.jsoncchange was needed. The swap is tested in the helper's tests, and knip sees the generator throughpackage.json.Verification
pnpm test:scripts: 1176 passed (12 new helper tests, 6 for check 12, 2 for the build order)lint:root, web lint and typecheck, knip, CodeScene gate (Code Health 10 on the new and changed scripts)https://inkweave.ink/og/card/{id}.{hash}.jpgin both image tags with the card's alt text, and/browsekeeps/og-image.pngAfter deploy:
curlthe og:image of/card/3354/max-goof-karaoke-star, check the image's headers, and try the link in Facebook's Sharing Debugger and a Discord message.🤖 Generated with Claude Code
https://claude.ai/code/session_01DjrxDUg8t6RHUrGiKw1neQ