---
path: /scene/[id]/embed
surface: customer
domain: scene
auth: anon
source: apps/oshun/web/src/app/scene/[id]/embed/page.tsx
status: walked
last_walked:
  '2026-05-29 automated runtime walk (Playwright headless) — render, /v1 data
  (2xx), console/page-errors, expected content, screenshot verified; live
  screen-reader, touch, offline, and telemetry-delivery checks pending a manual
  AT pass. Evidence: WALKTHROUGH/results/runtime-sweep-2026-05-29.md; content
  re-verified 2026-06-03 against current source'
---

# Scene · Embed (iframe-safe)

## Purpose

Iframe-safe minimal embed of a public living offering. Designed to be mounted
inside third-party pages as a single full-bleed preview tile. The playable tile
carries less chrome than the full viewer (no report link, no download grant, no
accessibility panel). It is **not** fully chrome-free, though: the
password-required and tenant-auth-required branches each render an interactive
"Open offering" link back to `/scene/<id>` (see Layout regions).

`/scene` is in `apps/oshun/web/src/proxy.ts` `PUBLIC_PATHS` so the embed loads
anonymously.

## Entry points

- `<iframe src="https://oshun.app/scene/<id>/embed">` mounted in a partner page
  / blog / CMS
- Direct URL — opens the embed full-window (not typical use)
- Sharing tools or platforms that consume the OSHUN embed URL

## Layout regions

`page.tsx` resolves `fetchPublicLivingScene(params.id)` with **no unlock
token**. It branches in three ways (page.tsx ~15-45): a password/tenant gate
branch, the generic non-playable fallthrough, and the playable preview overlay.

### kind === 'password-required' || kind === 'tenant-auth-required'

A centred card (page.tsx ~15-37) explaining the embed cannot show this offering:

- **Heading**: `Password required` or `Tenant access required` (chosen by
  `kind`)
- **Body**: "This offering is not embeddable. Open it on OSHUN to view."
- **"Open offering" link**
  (`<a href="/scene/<id>" target="_top" rel="noopener">`, teal button) — the one
  interactive element on the embed surface; breaks out of the iframe
  (`target="_top"`) to the full viewer where the gate can be satisfied

### kind !== 'playable' (generic fallthrough, e.g. tombstone)

- **Main**: full-screen grid centred message — "This offering has been retired."
  This bare tile is reached only after the password/tenant branch above; it has
  no interactive elements.

### kind === 'playable'

- **Main (relative, overflow-hidden)**:
  - **Background layer**: absolute-positioned
    `linear-gradient(120deg, rgba(3,7,18,0.18), rgba(3,7,18,0.66)), url(<openGraph.imageUrl>)`
    cover-centred
  - **Bottom gradient panel**: absolute bottom strip from black/80 to
    transparent, padding 4
    - Title (`openGraph.title`, 2xl serif)
    - Description (`openGraph.description`, smaller slate text)
    - Visible-mark short code (mono, smaller, near-bottom)

## States

- [ ] **Password required** — `kind === 'password-required'` → "Password
      required" card with the "Open offering" break-out link
- [ ] **Tenant-auth required** — `kind === 'tenant-auth-required'` → "Tenant
      access required" card with the same "Open offering" link
- [ ] **Retired / other non-playable** — generic fallthrough → minimal "This
      offering has been retired." tile (no interactive elements)
- [ ] **Playable (default)** — image background + title + description + short
      code
- [ ] **No tenant branding** — the playable embed does not surface tenant logo
      or attribution (unlike the main viewer)
- [ ] **Inside an iframe** — verify the page sets headers (e.g.,
      `X-Frame-Options`, CSP `frame-ancestors`) consistent with embed use; not
      configured in `page.tsx` itself
- [ ] **Standalone visit** — embed loads but lacks navigation; intended to be
      embedded, not navigated to directly
- [ ] **Reduced motion** — no animations on this surface; no special handling
      needed
- [ ] **Offline** — SW-cache may serve a previously fetched embed

## Interactions

The playable and the bare-retired tiles are read-only (no links, buttons, or
forms). The two gate branches are the exception:

- [ ] **"Open offering" link** (password-required / tenant-auth-required
      branches only) — `<a href="/scene/<id>" target="_top" rel="noopener">`;
      breaks out of the iframe to the full viewer. This is the embed's only
      interactive element.
- [ ] Verify the **playable** and **generic-retired** branches contain no anchor
      / button / form descendants (only the gate branches render a link)

## Data & contracts

- **Reads**: `fetchPublicLivingScene(params.id)` from
  `@/lib/living-scenes-public` (no unlock token)
- **Writes**: none
- **Realtime**: none
- **Caching**: request-time server fetch; verify whether the embed is separately
  cached (it should be more aggressively cacheable than the full viewer)
- **Auth/role check**: anon (`/scene` is in `src/proxy.ts` `PUBLIC_PATHS`)
- **Metadata**: `title: 'Embedded Living Offering'` (bare; the `| OSHUN` suffix
  comes from the root layout's `title.template = '%s | OSHUN'`),
  `robots: { index: false, follow: false }`

## Cross-references

- Sibling scene routes:
  - [`scene-id.md`](./scene-id.md) — full viewer
  - [`scene-id-report.md`](./scene-id-report.md) — abuse report
- Component sources:
  - `apps/oshun/web/src/lib/living-scenes-public.ts`

## Open questions / known gaps

- [x] **Password-protected scenes through the embed** — `fetchPublicLivingScene`
      is called without an unlock token, so password-required (and
      tenant-auth-required) scenes render the "not embeddable" card with an
      "Open offering" break-out link rather than the preview. Confirm this
      break-out-to-full-viewer behaviour is the intended design for private
      scenes (the embed itself never accepts a password). Verified in
      `scene-password-and-grants.spec.ts`: password-protected embed renders
      `target="_top"` "Open offering" and no full viewer chrome / Report /
      Download-grant controls.
- [ ] No CSP `frame-ancestors` header or `X-Frame-Options` configuration visible
      at the page level; check the Next config / middleware / response headers
      for iframe-allow rules
- [ ] No analytics / view telemetry from this embed surface — verify whether
      view tracking happens server-side
- [ ] No accessibility panel; embed does not advertise caption / transcript
      availability — confirm whether the embedding partner is responsible for
      that
