vinterliste/server
Ole-Morten Duesund 7964d499e2 OpenGraph meta on the SPA's shareable routes
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.
2026-05-25 16:05:43 +02:00
..
activities.ts Friends + friends-only visibility + blocking 2026-05-25 14:47:20 +02:00
admin.ts Admin role, root/home URL split, activity permalinks 2026-05-25 13:23:13 +02:00
auth.ts test: coverage for all major server features 2026-05-25 15:37:53 +02:00
db.ts Friends + friends-only visibility + blocking 2026-05-25 14:47:20 +02:00
feedback.ts fix(feedback): stop exposing done_by user id in API responses 2026-05-25 13:54:07 +02:00
friends.ts Friends + friends-only visibility + blocking 2026-05-25 14:47:20 +02:00
index.ts OpenGraph meta on the SPA's shareable routes 2026-05-25 16:05:43 +02:00
invites.ts Self-registry toggle, invite links with attribution, first-user-admin 2026-05-25 13:45:32 +02:00
og.ts OpenGraph meta on the SPA's shareable routes 2026-05-25 16:05:43 +02:00
roles.ts Admin role, root/home URL split, activity permalinks 2026-05-25 13:23:13 +02:00
session.ts Scaffold Vinterliste — end-to-end encrypted winter activity list 2026-05-25 12:27:14 +02:00
settings.ts Self-registry toggle, invite links with attribution, first-user-admin 2026-05-25 13:45:32 +02:00
tags.ts Scaffold Vinterliste — end-to-end encrypted winter activity list 2026-05-25 12:27:14 +02:00
users.ts Bookmarks on public/semi activities, surfaced on /home 2026-05-25 14:11:58 +02:00