Improve SEO and social card wiring

Add SearchAction and optional Product JSON-LD, per-page OG images/alts, and document the og build and cache headers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
michilis
2026-08-21 02:17:35 +02:00
co-authored by Cursor
parent 6f17b572b1
commit 1c5df18e81
10 changed files with 120 additions and 60 deletions
+42 -3
View File
@@ -282,6 +282,32 @@ OpenGraph and Twitter tags, a social card, a self-referencing canonical, a full
set and a JSON-LD graph. `sitemap.xml` lists every indexable one with its `xhtml:link`
alternates, and `robots.txt` sits beside it.
### Social cards
`pnpm build` starts with `pnpm og` (`web/scripts/og/build-og.mjs`), which renders one
1200x630 PNG per mint and federation into `web/public/og/` from the same API data the
pages use — satori lays the card out and turns the text into glyph paths (the six
static TTFs under `web/scripts/og/fonts/` are the same faces the site uses),
`@resvg/resvg-js` rasterises it. No browser involved; a full run of ~70 cards takes a
few seconds, and a rerun with unchanged data renders nothing: each card's inputs are
hashed into its filename (`mint.example.com.a1b2c3d4e5.png`) and
`web/src/generated/og-manifest.json` records what is already on disk. The hashed name
is also the cache-busting story — link-preview scrapers cache an `og:image` by URL, so
a mint whose rating moved or that went offline gets a new URL on the next scheduled
rebuild, while the stable-named `default.png` (brand plus network stats, used by every
non-mint page) is served with a one-hour cache instead (see the nginx block below).
The images are one English render shared by every locale; titles, descriptions and alt
text translate per page. Relative times stay out of the PNGs on purpose — a "3d ago"
would go stale inside a static file — so the cards carry absolute month/year stamps,
and the one exception, the "Offline {n}d" chip, is derived from a day-granular clock so
it regenerates at most once a day.
`pnpm og:fixtures` renders the six edge cases in `web/scripts/og/fixtures.mjs`
(30-character name, no icon, zero reviews, offline, melt only, announced-only
federation) into `web/og-fixtures/` at a pinned timestamp; those snapshots are
committed, so eyeball them after any template change.
### What is indexed, and what is not
One rule, in `web/src/lib/seo.ts`, decides it, and it covers both ecosystems: a page is
@@ -739,14 +765,27 @@ server {
add_header Cache-Control "public, immutable";
}
# The social card, the icons and the manifest. Not fingerprinted, since their names
# are referenced from outside the site, so a week with revalidation rather than a
# year of immutability.
# The favicons and the manifest. Not fingerprinted, since their names are referenced
# from outside the site, so a week with revalidation rather than a year of immutability.
location ~* ^/(og\.png|favicon\.(ico|svg)|apple-touch-icon\.png|icon-\d+\.png|site\.webmanifest)$ {
expires 7d;
add_header Cache-Control "public";
}
# The generated social cards (`pnpm og`). The default card keeps a stable name and
# changes with the site's stats, so it gets an hour; every per-mint card carries a
# content hash in its filename and a stale hash is never referenced again, so those
# are immutable. (The hash is also what actually refreshes link previews: the big
# scrapers cache an og:image by URL and ignore these headers.)
location = /og/default.png {
add_header Cache-Control "public, max-age=3600";
}
location /og/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Same-origin API and icons, matching the default empty PUBLIC_API_URL. Drop these two
# blocks only if you build with PUBLIC_API_URL pointing at a separate API host.
location /api/ {