Skip to content

feat(api): auto-generated Open Graph images for links + invoices (#165) - #166

Draft
rahXephonz wants to merge 1 commit into
mainfrom
rfc-165-og-image
Draft

feat(api): auto-generated Open Graph images for links + invoices (#165)#166
rahXephonz wants to merge 1 commit into
mainfrom
rfc-165-og-image

Conversation

@rahXephonz

Copy link
Copy Markdown
Member

Implements RFC #165.

What

Render a per-document 1200×630 PNG preview card for payment links and
invoices, and inject the matching og:image / twitter:image meta tags into
the checkout and invoice page shells. A pay-testnet.mayarin.xyz/checkout/:id
or /invoices/:id/view link shared in WhatsApp / Telegram / X now unfurls with
the merchant name and the human-form amount instead of the generic landing
image.

How

  • apps/api/src/services/og-image.tssatori (element tree → SVG) +
    @resvg/resvg-js (SVG → PNG). One template, brand palette lifted from
    apps/checkout-ui/src/styles.css. Money is rendered with formatMoneyLocale
    (the id-ID human form) — never toDecimalString, never a number.
  • Fonts — static Geist instances (geist-regular/semibold/bold.ttf) bundled
    in apps/api/assets/. The variable TTF chokes satori's bundled opentype
    parser on the fvar axes, so static instances are required. OFL license
    included.
  • RoutesGET /checkout/:id/og.png and GET /invoices/:id/og.png.
    Missing / disabled / voided document or any render failure → cached generic
    card, 200, never 500 (a shared link never loses its preview card).
    Cache-Control: public, max-age=86400 (link content is immutable after
    create).
  • Shell metacheckout-shell.ts injects per-document OG meta via a new
    <!--__OG__--> placeholder (alongside <!--__BOOTSTRAP__-->). Link + invoice
    pages build it from the document; the pay page keeps the generic landing image
    (its intent id is unguessable, not a shared URL).
  • og:image URL — same-origin via requestOrigin (RFC RFC: Dedicated checkout host — pay.mayarin.xyz (Phase 4) #163), so a pay-host
    link points at pay-testnet.../checkout/:id/og.png through the proxy.
  • Pay proxy — allowlist forwards /invoices/:id/og.png (the
    /checkout/:id/og.png is already covered by the /checkout prefix).

Cards

  • Fixed / catalog link → merchant name + formatMoneyLocale(total) + asset code.
  • Open link (no amount) → title, or Pay {merchant} when it has none.
  • Invoice → merchant name + Invoice {number} + outstanding total.
  • Settled invoice → Settled state label + total. Voided → Voided label.

Verification

  • bun run typecheck — green.
  • bun test — 1329 pass, 0 fail. New tests: og-image.test.ts (card + meta
    builders, pure) and checkout-shell.test.ts (/checkout/:id/og.png renders a
    real PNG, missing link → 200 generic, page shells carry the right og:image).
  • apps/checkout-ui build preserves both placeholders in dist/index.html.

Out of scope (per RFC #165 non-goals)

AI image generation, per-merchant branded backgrounds, merchant logo, video
previews, mainnet hosting, OG for landing/dashboard/demo.

Closes #165.

Render a per-document 1200x630 PNG preview card for payment links and
invoices so a shared pay-host URL unfurls with the merchant name and the
human-form amount instead of the generic landing image.

- apps/api/src/services/og-image.ts: satori (element tree -> SVG) +
  @resvg/resvg-js (SVG -> PNG). One template, brand palette from
  checkout-ui. Money via formatMoneyLocale (id-ID human form), never a
  number. Static Geist font instances bundled in apps/api/assets (the
  variable TTF chokes satori's opentype parser).
- /checkout/:id/og.png and /invoices/:id/og.png routes; missing/disabled/
  voided or render failure -> cached generic card, 200, never 500.
- checkout-shell injects per-document og:image/twitter:image meta via a
  new <!--__OG__--> placeholder; link + invoice pages build it from the
  document, pay page keeps the generic landing image (intent id is
  unguessable, not shared).
- pay-proxy allowlist forwards /invoices/:id/og.png (/checkout/:id/og.png
  already covered by the /checkout prefix).
- og:image url is same-origin via requestOrigin, so pay-host links point
  at the pay host through the proxy.

232 api tests + pay-proxy allowlist tests green; typecheck + format clean.
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.

RFC: Auto-generated Open Graph image for payment links + invoices (Phase 4)

1 participant