Skip to main content
← Back to list
01Issue
FeatureIn ProgressSwamp Club
Assigneesswamp_lord

Relationships

#1226 Embeddable operative profile cards (iframe + oEmbed)

Opened by swamp_lord · 7/17/2026

Problem

Operatives have a rich profile card (the Discord-style hover card rendered by islands/OperativeCard.tsx — avatar, username, tier + PTS, bio, badges, "since" date). Today that card only lives inside swamp.club. There's no way for an operative to show off their swamp identity on an external site — a personal blog, a Notion page, a docs site, a company wiki.

We want operatives to be able to drop their profile card into any other website as an embed.

Proposed solution

Ship a public, live HTML embed of the operative profile card plus oEmbed auto-expansion, so pasting a swamp.club/u/{username} URL into an oEmbed-aware host (Notion, WordPress, Ghost, many CMSes) auto-expands into the card, and anyone can hand-place an <iframe> on a site they control.

Scope decisions already made:

  • Formats: iframe embed + oEmbed. (Not the static-image/README path — see Alternatives.)
  • Access: public / unauthenticated for any profile, like a GitHub contribution graph. No owner opt-in or tokens for v1.

Three pieces to build:

  1. Bare iframe routeroutes/embed/u/[username].tsx. Server-rendered Fresh route that renders only the card (no site nav/footer, not the popover island). Reuses the existing data function handleGetOperativeCard(username)OperativeCardData (routes/api/v1/u/[username]/card.ts) and a static render of the CardBody visual from islands/OperativeCard.tsx. Must strip the _app.tsx chrome (route-group _layout.tsx or a path conditional) and size to fill its container (width/height: 100%).

  2. oEmbed endpointroutes/api/oembed.ts. Accepts ?url=https://swamp.club/u/{username}&format=json, validates the URL is one of ours, and returns oEmbed type: "rich" JSON whose html is the iframe pointing at the embed route. Raw Response.json(...), mirroring the cache-header pattern in routes/api/v1/u/[username]/card.ts.

  3. oEmbed discovery link — on routes/u/[username].tsx (via the _app.tsx head that already emits canonical/OG), add <link rel="alternate" type="application/json+oembed" href="…/api/oembed?url=…"> so hosts can auto-discover the embed.

Key gotcha to solve

Frame headers. The app very likely sends X-Frame-Options: DENY (or CSP frame-ancestors 'self') globally for clickjacking protection. If so, browsers will refuse to embed the route anywhere. The routes/embed/** route must be carved out to omit X-Frame-Options and set Content-Security-Policy: frame-ancestors * (or an allowlist). Locate where those headers are set before implementing.

Reuse — most of the data layer already exists

  • routes/api/v1/u/[username]/card.tshandleGetOperativeCard returns the exact card-shaped OperativeCardData payload (username, name, tierName, tier, score, bio, memberSince, topBadges[], nameStyle).
  • islands/OperativeCard.tsx CardBody is the card visual in the screenshot.
  • lib/tier.ts / lib/app/resolve-tier.ts / lib/badge.ts supply tier + badge styling.

Alternatives considered

  • Static image (card.png / card.svg) for README embedding. Deliberately out of scope for v1. GitHub/markdown strip <iframe>, so READMEs need an image, not an iframe. Worth a fast follow — the PNG pipeline already exists (lib/og/render.ts + @resvg/resvg-wasm, and lib/og/templates.ts has an operativeCard template plus the OG route routes/og/u/[username].ts), so a README-friendly card.png is cheap to add later.
  • Owner-only / token-gated embeds. Rejected for v1 in favor of the simpler, more viral public model; can layer opt-in later if abuse warrants.

Acceptance criteria

  • GET /embed/u/{username} returns a standalone HTML card with no site chrome, embeddable via <iframe> on a third-party origin (verified in a real browser, not just a request).
  • GET /api/oembed?url=https://swamp.club/u/{username} returns valid oEmbed rich JSON.
  • The profile page advertises the oEmbed discovery <link>.
  • Unknown username → clean 404 (no chrome / no error page leaking).
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 2 MOREREVIEW+ 2 MOREPR_LINKED

In Progress

7/17/2026, 7:23:34 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
swamp_lord assigned swamp_lord7/17/2026, 7:05:01 PM
Editable. Press Enter to edit.

swamp_lord commented 7/17/2026, 7:21:15 PM

Adversarial finding ADV-2 (site-wide clickjacking gap) filed as security issue #1227. Coordination point: when #1227 adds a global frame-ancestors 'none'/X-Frame-Options: DENY, the /embed/** subtree from this issue must stay exempt (permissive frame-ancestors * via routes/embed/_middleware.ts). The two must not fight.

Sign in to post a ripple.