Server-side render OG + Twitter Card meta for the routes where rich
link previews matter:
/ — homepage
/personvern — privacy + how-it-works
/a/:id — activity permalink
/tags/:tag — tag page
/<username>/list — public list (opt-in)
Everything else falls through to the unmodified SPA shell.
Approach: in production mode, register Hono handlers BEFORE the
catch-all that read the prebuilt index.html template, swap <title>
and <meta name="description"> with route-specific values, and inject
an OG/Twitter meta block before </head>. Same HTML to scrapers and
real users — no UA sniffing. The SPA still bootstraps the same way
(the <script type="module"> and <div id="app"> are untouched).
Information-leak guard: private activities and not-opted-in public
lists fall back to the SAME generic "not found" OG as truly missing
URLs. Otherwise a scraper could distinguish "exists but hidden"
from "doesn't exist," which the regular API correctly hides.
Implementation notes:
- Template read once on first request and cached. New deploys
require a server restart to pick up a rebuilt index.html, which
is the normal deploy flow anyway.
- All meta attribute values pass through escapeAttr() so user
content (activity titles, tag names, display names) can't break
out of the attribute or inject HTML.
- Description capped at 200 chars (what most scrapers actually
render).
- Base URL prefers PUBLIC_BASE_URL env var, falling back to
request URL — works behind a reverse proxy if the env var is set.
- PNG fallback for OG image is deliberately not shipped; the SVG
works on every modern scraper that matters. iOS home-screen
previews are the only place this falls back to the page
screenshot, which is fine.
Smoke-tested via curl against a production-mode server: all five
routes render the right title, description, og:url, and image, and
the "not found" + "hidden" cases collapse to the same OG shape.