Skip to content

feat(passport): public share URL + OG image + copy-link button#58

Merged
harshithmullapudi merged 1 commit into
mainfrom
feat/passport-share-url
Jul 13, 2026
Merged

feat(passport): public share URL + OG image + copy-link button#58
harshithmullapudi merged 1 commit into
mainfrom
feat/passport-share-url

Conversation

@harshithmullapudi

Copy link
Copy Markdown
Member

Follow-up to #57 (merged). Turns every Town passport into a shareable social artifact — any passport is now visitable by its public id, with proper OG + Twitter card metadata so pasted links preview as real cards on X, Slack, Discord, LinkedIn.

Related

What's new

Public passport URL

  • /passport/[passportId] — public server component. Example: town.getcore.me/passport/TP-2026-000042. Case-insensitive; renders the same book-style SVG the private page uses, plus a Download PDF button.
  • 404 for unknown ids and for guests (guests all share the placeholder TP-GUEST id and don't have a public page — by design).

OG image

  • /passport/[passportId]/og.png — 1200×630 PNG generated by rasterizing the first spread SVG with sharp (already in deps). The page-color background letterboxes the ~1.6:1 spread inside the ~1.9:1 OG aspect. 5-minute CDN cache — keeps scrapers happy without letting the card go too stale.

Metadata

  • generateMetadata() on the public page emits title / description / OG / Twitter tags with absolute URLs resolved from x-forwarded-host, mirroring the pattern already in /[town]/page.tsx.
  • Title: <name> · Town Passport. Description: TP-<id> · N stamps · N towns owned · ….
  • Twitter card: summary_large_image. Canonical URL set.

Public PDF download

  • /passport/[passportId]/pdf — colocated public download route. Same renderer, keyed by URL segment instead of session cookie.

Data

  • loadPassportDataByPassportId(passportId) — shared loader used by the page, OG image, and PDF routes. Normalises to uppercase.

Copy share link (private page)

  • On /passport (the signed-in user's own page), a new Copy share link button appears next to Download PDF. Only shown for authed users with an assigned passportId. Uses navigator.clipboard.writeText() with a window.prompt fallback for clipboard-denied browsers. Shows Copied ✓ for 1.5 s.

Test plan

  • Signed-in, visit /passportCopy share link button appears next to Download PDF; click copies https://<host>/passport/TP-<year>-<seq> to clipboard.
  • Paste that URL in a new incognito browser → renders the passport publicly, no login required.
  • View source on the public page → og:title, og:description, og:image, twitter:card all present with absolute URLs.
  • Hit /passport/<id>/og.png directly → 1200×630 PNG with the identity spread letterboxed on the page color.
  • Test the OG in Twitter Card Validator / opengraph.xyz — card previews cleanly.
  • Hit /passport/<id>/pdf while signed out → downloads a valid PDF of that user's passport.
  • Hit /passport/DOES-NOT-EXIST → 404.
  • Uppercase / lowercase URL both resolve (/passport/tp-2026-000042 also works).

Not in this PR (follow-ups)

  • Handle-based URLs (/passport/@harshith) — needs User.handle column + backfill + settings UI. Cleaner social share when it lands; passport-id URLs keep working alongside.
  • Visibility toggle — currently all passports are public-by-default. Add User.passportPublic if privacy becomes a concern.
  • Passport OG image tuned for share (e.g. showing top 3 stamps as a strip). Current version letterboxes the whole first spread; fine but could be more optimized for a scroll-past thumb.

🤖 Generated with Claude Code

Turns every passport into a shareable social artifact. Any passport is
now visitable by its public id (e.g. /passport/TP-2026-000042) with
proper OG + Twitter card metadata so pasting the link into
Twitter/Slack/Discord previews as a real card.

- `/passport/[passportId]` — public server component. Case-insensitive.
  Renders the same book-style SVG the private page uses, plus a
  Download PDF button. 404 for unknown ids and for guests (guests share
  a placeholder `TP-GUEST` id and don't have a public page).
- `/passport/[passportId]/og.png` — 1200×630 PNG generated by
  rasterizing the first spread SVG with `sharp` (already in deps). The
  page-color background letterboxes the ~1.6:1 spread inside the ~1.9:1
  OG aspect. 5-minute cache to keep scrapers happy without letting the
  card go stale for long.
- `/passport/[passportId]/pdf` — public download route colocated with
  the page. Same renderer, keyed by URL segment instead of session.
- `generateMetadata()` on the public page emits title / description /
  OG / Twitter tags with absolute URLs resolved from x-forwarded-host,
  mirroring the pattern in /[town]/page.tsx.
- `loadPassportDataByPassportId(passportId)` — shared loader used by
  the page, OG image, and PDF routes. Normalises to uppercase.
- On the private /passport page, a **Copy share link** button now
  appears next to Download PDF for authed users with an assigned id.

Related: #50

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@harshithmullapudi
harshithmullapudi merged commit 0d6dc09 into main Jul 13, 2026
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