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:
Bare iframe route —
routes/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 functionhandleGetOperativeCard(username)→OperativeCardData(routes/api/v1/u/[username]/card.ts) and a static render of theCardBodyvisual fromislands/OperativeCard.tsx. Must strip the_app.tsxchrome (route-group_layout.tsxor a path conditional) and size to fill its container (width/height: 100%).oEmbed endpoint —
routes/api/oembed.ts. Accepts?url=https://swamp.club/u/{username}&format=json, validates the URL is one of ours, and returns oEmbedtype: "rich"JSON whosehtmlis the iframe pointing at the embed route. RawResponse.json(...), mirroring the cache-header pattern inroutes/api/v1/u/[username]/card.ts.oEmbed discovery link — on
routes/u/[username].tsx(via the_app.tsxhead 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.ts→handleGetOperativeCardreturns the exact card-shapedOperativeCardDatapayload (username, name, tierName, tier, score, bio, memberSince,topBadges[], nameStyle).islands/OperativeCard.tsxCardBodyis the card visual in the screenshot.lib/tier.ts/lib/app/resolve-tier.ts/lib/badge.tssupply 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, andlib/og/templates.tshas anoperativeCardtemplate plus the OG routeroutes/og/u/[username].ts), so a README-friendlycard.pngis 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 oEmbedrichJSON.- The profile page advertises the oEmbed discovery
<link>. - Unknown username → clean 404 (no chrome / no error page leaking).
Closed
No activity in this phase yet.
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.
swamp_lord commented 10/22/2026, 6:41:21 PM
Decayed - deleteing
Sign in to post a ripple.