From 9cc8053ec21819e4b06ae241e8f5221d2b5b0caf Mon Sep 17 00:00:00 2001 From: doxa-maps Date: Fri, 11 Sep 2026 23:38:31 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20direct-embed=20vs=20staging=20framing?= =?UTF-8?q?=20contract=20=E2=80=94=20serving-layer=20owns=20frame-ancestor?= =?UTF-8?q?s=20(direct=3D*=20,=20staging=3Dself);=20per-host=20recipes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture/embeddability.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/architecture/embeddability.md b/docs/architecture/embeddability.md index 2ba40f03c..98b7e29ba 100644 --- a/docs/architecture/embeddability.md +++ b/docs/architecture/embeddability.md @@ -55,3 +55,28 @@ Both are supported on **built** pages. The one "never iframe" rule applies only staging** script (`internal/staging.js`), which mounts elements directly because a dev-server iframe crosses realms (`instanceof` breaks). Built pages have no such problem — the generated staging (`staging-forge.js`) itself uses an iframe stage. + +## Direct-embed vs staging — the framing contract (serving layer) + +The built tree has TWO kinds of page, and they need DIFFERENT framing policy: + +| Surface | Paths | `frame-ancestors` | +|---|---|---| +| **Direct embed** (the product) | `//index.html`, `///index.html` — the bare map pages the manifest's `iframeUrl` points at | **`*`** — framable on any partner site (that's the whole point) | +| **Staging** (internal preview) | tree root `index.html`, per-bundle `/index.html`, the SPA shell | **`'self'`** — preview only | + +`frame-ancestors` can ONLY be set by an HTTP response header (HTML `` cannot set it), so +**the server/CDN owns this split — the bundler cannot enforce it.** Whoever serves the tree must +apply it. Rule of thumb: **a path with ≥2 directory segments under the tree root is a direct-embed +page → serve it framable.** + +Portable ways to implement the split, by host: +- **Traefik** (nuxtbase/go-base): a higher-priority router on `PathRegexp(^/plugin-assets/[^/]+/[^/]+/)` + sets `Content-Security-Policy: frame-ancestors *` + clears `X-Frame-Options`. (Live example: + `/data/coolify/proxy/dynamic/doxa-maps-embed.yaml`.) +- **Cloudflare Pages / Netlify**: a `_headers` file — `//*/* Content-Security-Policy: frame-ancestors *`. +- **Nginx**: `location ~ ^/[^/]+/[^/]+/ { add_header Content-Security-Policy "frame-ancestors *"; }` +- **GitHub Pages**: serves no custom headers → pages are framable by default (no frame-ancestors), which is already correct for direct embeds. + +If you serve the WHOLE tree with one blanket `frame-ancestors 'self'`, direct embeds break on +partner sites (the staging policy leaks onto the product pages). That is the one mistake to avoid.