Share (link previews)¶
Purpose¶
The share domain serves server-rendered Open Graph pages so a shared Tomoda link unfurls as a rich preview card in iMessage, WhatsApp, Slack, Discord, and the like. The app is a JavaScript SPA that crawlers cannot execute, so the tags must be rendered without JS: an edge worker sends only crawler traffic to these endpoints and redirects humans to the app.
Mental model¶
tomoda.life/events/123 ──edge worker──▶ bot? ──▶ /api/v1/og/events/123 (this domain)
human ──▶ the app (SPA / native via universal links)
The public host (config.Public.BaseURL) is what users share; api.* is never
a shared link. The worker rewrites tomoda.life/<path> to /api/v1/og/<path>
for crawlers. Each renderer builds a normalized OGCard{Type,Title,Description,
Image,URL} and executes one html/template (og.html), which contextually
escapes every field.
HTTP endpoints¶
Public and unauthenticated. Registered in
backend/internal/services/share/routes.go, mounted at /api/v1/og in
backend/internal/wiring/router.go.
Paths mirror the app's public routes so a crawler and a human resolve the same
URL (the /u/{id} screen accepts a UUID handle).
| Route | Card |
|---|---|
GET /og/events/{id} |
event title, Hosted by … · date · place, cover image |
GET /og/u/{id} |
display name, @handle + bio, brand default image |
GET /og/m/{id} |
{user} on Tomoda, caption, asset image |
GET /og/places/{id} |
place name, category · city, first photo |
GET /og/img/{type}/{id} |
302 to the entity's real image, or the brand default |
GET /og/* (NotFound) |
generic brand card, so any path still unfurls |
A /places/{id} share link lands humans on the discover map for that location
(a redirect route to /place-map?type=location&id=…). The legacy
GET /api/v1/share/events/{id} (event domain) predates this and is superseded by
/og/events/{id}.
Durable images¶
og:image points at the stable /img/{type}/{id} endpoint, never at the asset
directly, because prod asset URLs are CloudFront-signed and expire (30 min) while
crawlers cache the HTML for days. On each fetch the image endpoint re-runs the
same visibility gate, then 302-redirects to a freshly signed asset URL (or the
brand default). The HTML holds a URL that never expires; the bytes come from a
fresh signed URL every time, and no bytes are proxied through the backend.
Invariants & gotchas¶
- Visibility gates before the template. A missing, private, or expired
target returns
ErrNotShareable, and the handler renders the generic brand card instead. A private event's title never reaches the HTML, so it cannot leak into an unfurl. Gates: events useEventVisibleTo(anonymous viewer); users are shown only whenPassportVisibilityispublic; moments require public visibility, not soft-deleted, and journaled or not-yet-expired. - Always HTTP 200. Even the not-found path returns 200 with the brand card, so a crawler never records a failure for a real link.
- The image endpoint re-applies the gate.
/img/{type}/{id}builds the same card and only redirects to the real asset when it passes; a private, expired, or missing target redirects to the brand default (indistinguishable from each other), so a gated asset is never served and its existence is not confirmed. - Images fall back to
Public.BaseURL + /og-default.pngwhen the entity has none. - Reads via inbound ports. The slice depends on
EventReader,UserReader,ProfileReader,LocationReader,MomentReader(indeps.go), each bound to the owning domain's service ininternal/wiring/providers.go. It owns no store. - Crawlers cache aggressively; use each platform's re-scrape debugger when testing tag changes.
Configuration¶
Public.BaseURLis the share host, the canonical app host so the edge worker can run (worker routes only fire on proxied Cloudflare hosts):https://tomoda.life(prod) andhttps://app-dev.tomoda.life(dev), set inconfig.{prod,dev}.yaml. It drivesog:url, the stableog:image, and the frontend share links (EXPO_PUBLIC_WEB_URL).app.tomoda.liferedirects to the apex.- The og-default card is
frontend/public/og-default.png.
Cross-links¶
- Wiring for the DI + route composition.
- The edge worker (crawler routing, universal-links
.well-knownfiles) lives in the devops repo's Cloudflare infrastructure.