Skip to content

feat(og): per-tool share cards, generated and committed#70

Merged
TTigger merged 1 commit into
masterfrom
feat/og-images
Jul 9, 2026
Merged

feat(og): per-tool share cards, generated and committed#70
TTigger merged 1 commit into
masterfrom
feat/og-images

Conversation

@TTigger

@TTigger TTigger commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Every page currently shares the same og:imagepwa-512x512.png, a 512px square logo. A link to any of the 32 tools looks identical, and the card renders squashed. This generates a real 1200×630 card per tool per locale: 66 cards.

Design

Follows the site's own tokens (cream, ink, terracotta, Newsreader) rather than inventing a look. What varies is structure:

  • a category-coloured spine down the left edge, like a file tab in a toolbox drawer
  • the tool's own emoji as the hero, in a tinted rounded slot that echoes the logo's geometry
  • the logo demoted to a footer signature

Chinese titles are set in Noto Serif TC to match Newsreader's serif voice; English cards use Newsreader itself.

Why a manual script, not a build step

npm run og, deliberately not wired into npm run build. Cards change only when a tool is added or renamed. Fetching fonts at build time would fail at deploy, not on a dev machine — and we've just seen that a Vercel deployment can go READY with a broken artifact inside it.

The trade is a manual step that someone will forget. src/lib/__tests__/og.test.ts fails if any tool lacks a card, so that's caught in CI rather than by a broken share card in the wild.

Lib-first

  • src/lib/contrast.ts — WCAG luminance and contrast ratio, plus darkenUntilReadable(). The brand terracotta only reaches 2.96:1 on cream, too weak for the small eyebrow text, so each spine colour gets a darkened twin at ≥ 4.5:1. Measured, not eyeballed.
  • src/data/categories.ts — the six spine colours and their eyebrow twins as literals, keeping src/data free of runtime imports. A test locks each literal back to darkenUntilReadable()'s output so they can't drift.
  • src/lib/og.ts — the /og/<locale>/<id>.png convention, shared by the generator and (next PR) BaseLayout.

The PWA precache trap

injectManifest's glob (**/*.{js,css,html,svg,png,...}) swept all 66 PNGs into the service worker — 2.3MB that only the Facebook, X and LINE crawlers ever fetch, downloaded by every visitor. globIgnores: ['og/**'] drops precache entries from 194 to 128; the images still ship to dist/.

Scope

No <meta> tag changes here. The cards are generated but not yet referenced, so nothing that works today can break. BaseLayout gets wired up in the next PR, together with twitter:cardsummary_large_image.

Fonts (Noto Serif/Sans TC, ~16MB together) and twemoji SVGs cache under scripts/.cache/, gitignored. twemoji is pinned to 15.1.0 so upstream art changes don't silently redraw the cards.

Tests

typecheck:api, 508 unit (18 new), 48 e2e. Cards inspected by eye for tofu boxes, long-title overflow, and the footer mark — the first draft silently dropped the logo's T, because icon.svg draws it with <text> in Georgia and resvg has no such font.

🤖 Generated with Claude Code

Every page currently shares the same og:image — pwa-512x512.png, a 512
square logo — so a link to any of the 32 tools looks identical and the
card renders squashed. This generates a real 1200x630 card per tool per
locale, 66 in all.

The card design follows the site's own tokens (cream, ink, terracotta,
Newsreader) rather than inventing a look. What varies is structure: a
category-coloured spine down the left edge like a file tab in a toolbox
drawer, the tool's own emoji as the hero in a tinted rounded slot that
echoes the logo's geometry, and the logo demoted to a footer signature.
Chinese titles set in Noto Serif TC to match Newsreader's serif voice;
English cards use Newsreader itself.

Generation is a manual script (`npm run og`), deliberately not wired
into the build. Cards change only when a tool is added or renamed, and
fetching fonts at build time would fail at deploy rather than on a dev
machine. `src/lib/__tests__/og.test.ts` fails if any tool is missing a
card, so "added a tool, forgot the image" is caught in CI instead of by
a broken share card in the wild.

Lib-first, as usual:

- src/lib/contrast.ts — WCAG luminance and contrast ratio, plus
  darkenUntilReadable(). The brand terracotta only reaches 2.96:1 on
  cream, too weak for the small eyebrow text, so each spine colour gets
  a darkened twin at >= 4.5:1. Measured, not eyeballed.
- src/data/categories.ts — the six spine colours and their eyebrow
  twins as literals, keeping src/data free of runtime imports. A test
  locks each literal back to darkenUntilReadable()'s output so they
  cannot drift.
- src/lib/og.ts — the /og/<locale>/<id>.png path convention, shared by
  the generator and (next) BaseLayout.

The PWA precache needed excluding. injectManifest's glob swept all 66
PNGs into the service worker — 2.3MB that only the Facebook, X and LINE
crawlers ever fetch, downloaded by every visitor. globIgnores drops
precache entries from 194 to 128; the images still ship to dist/.

Fonts (Noto Serif/Sans TC are ~16MB together) and twemoji SVGs are
cached under scripts/.cache/ and gitignored. twemoji is pinned so
upstream art changes don't silently redraw the cards.

No <meta> tags change here — the cards are generated but not yet
referenced, so nothing that works today can break. BaseLayout gets
wired up next.

Tests: typecheck:api, 508 unit (18 new), 48 e2e. Cards inspected by eye
for tofu boxes, long-title overflow and the footer mark.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tiglet Ready Ready Preview, Comment Jul 9, 2026 9:27am

@TTigger TTigger merged commit c3e34af into master Jul 9, 2026
3 checks passed
@TTigger TTigger deleted the feat/og-images branch July 9, 2026 09:35
TTigger added a commit that referenced this pull request Jul 9, 2026
feat(og): point every page at its own share card

The 66 cards from #70 existed but nothing referenced them. BaseLayout
resolves og:image and twitter:image through ogImagePath(toolId, locale),
so /tools/gear-calculator advertises /og/zh/gear-calculator.png and its
English twin advertises /og/en/gear-calculator.png. Pages with no toolId
fall back to the home card.

- twitter:card summary -> summary_large_image. The old value rendered a
  small square thumbnail, which is what the 512px logo was shaped for;
  a 1200x630 card needs the large variant or X crops it.
- og:image:width / height / alt added. Facebook and LINE size the card
  from the tags before fetching the image, and skip the preview entirely
  on a cold cache if they have to guess.
- meta name=author added.
- The Chinese home description was 14 characters. Our own URL Audit
  scores anything under 50 as a warning, and it was right: the old text
  ("TTigger's tool collection and portfolio") said nothing about what
  the site does. Rewritten to name the tool families and the fact that
  nothing is uploaded. The English default was already 51 and stands.

Tool pages already pass their own descriptions, so only the home default
needed lengthening.

e2e covers what unit tests can't reach here, since BaseLayout is an
Astro component: the resolved og:image URL per locale, the card
dimensions and twitter:card value, that the advertised image actually
returns 200 image/png rather than a 404, and that the home description
lands inside the 50-160 window the audit tool checks for.

Tests: typecheck:api, 508 unit, 54 e2e (6 new).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant