Audited README.md and all 35 documentation/*.md against main. Script names and markdown links were already clean — 59 documented scripts all exist in package.json, 0 broken links. What was wrong was file-map.md describing files that no longer exist, and three paths that had moved. REMOVED, all archived inafb998f17or earlier and still documented as current: internal/staging.js — the repo-root staging page; the plugin studio is the only entry now index.html — the same page's HTML library/composables/useDoxaSearch.js — deleted inea0c47b27, and dropped from a prose list too REPOINTED: the three colour strategies documented under plugin-designs/doxa-research-map/src/colors/ live in library/colors/. THIRD-PARTY.md's two vendored files live under data/vendor/. embeddability.md's reference to the staging script now says it was archived and where the entry is. WHAT I DID NOT "FIX", because it was not broken: ~20 relative shorthand paths in the architecture docs (`composables/useMapData.js` inside a document about library/) read correctly in context, and three paths name things that are not repo files at all — a runtime endpoint (public/api/maps/token.json), a generated artifact (out/_records.json) and a placeholder (./bundle/bundle.js). Reporting those as defects would have buried the five real ones in 23 false positives. TWO MEASUREMENT TRAPS I fell into on the way, both corrected before reporting: my first env-var scan grepped only internal/, plugin-designs/ and library/ and declared 13 documented env vars unread — 11 of them are read in internal/build-lock.sh, .env.example, deployment/cloud/Dockerfile and the Go backend. And an earlier sweep of my own data files reported six "rega" hits that were all the word "aggregates".
92 lines
5.4 KiB
Markdown
92 lines
5.4 KiB
Markdown
# Architecture — the embeddability design (generated pages + manifest)
|
|
|
|
> The simple recipes are in [../how-to.md](../how-to.md). This is the design that makes
|
|
> **every map embeddable by default**: the build generates a self-contained page per map.
|
|
|
|
## What `bun run tree` generates
|
|
|
|
After the per-bundle vite builds, `internal/generate-tree.mjs` writes the served tree:
|
|
|
|
```
|
|
public/doxa-maps/
|
|
├── index.html ← staging root (semantic tree of every map)
|
|
├── manifest.json ← machine-readable catalog + ready-made embed snippets
|
|
└── <bundle>/
|
|
├── <bundle>.js ← the bundle IIFE (the island)
|
|
├── index.html ← per-bundle staging/showcase page
|
|
├── <profile>/index.html ← ★ standalone page per profile — link it or iframe it
|
|
└── <instance>/index.html ← ★ same, per parameterized instance (e.g. prayer/)
|
|
```
|
|
|
|
The ★ pages are the **embeddable-by-default surface**: every `.vue` profile (and every
|
|
instance in a `*.instances.json`) automatically gets a directly linkable, iframe-able URL —
|
|
no per-map work.
|
|
|
|
## Anatomy of a generated profile page
|
|
|
|
Each `<profile>/index.html` is a **bare full-page map** — no chrome, so the URL *is* the map
|
|
("Share shares THE MAP"). It loads: the Mapbox GL peer (once, same realm), the bundle IIFE via
|
|
a relative `../<bundle>.js`, and mounts the element with that profile's config. The token
|
|
resolves by the ladder (prop → `?tk=` → baked → `window.MAP_TOKEN_URL` → `tokenUrl` →
|
|
same-origin `/api/maps/token`), fetched at page load — tokens are treated as data, never eval'd.
|
|
|
|
All URLs stay **relative** unless `embedBaseUrl` is set, so the whole tree can be dropped under
|
|
any web-root or subpath and keeps working (`check:drop` gates this).
|
|
|
|
## The manifest — embed snippets as data
|
|
|
|
`manifest.json` (`doxa-maps-manifest@1`) lists every bundle → profiles, each with `element`,
|
|
`scriptUrl`, `iframeUrl`, and a ready-made `embed` object:
|
|
|
|
- `embed.iframe` — a paste-ready `<iframe src=".../<profile>/index.html" …>` snippet
|
|
- `embed.script` — the `<script src>` + `<element profile-config='…'>` pair
|
|
|
|
Hosts, staging pages, and the share button all read this one catalog — snippets are generated
|
|
once, never hand-written.
|
|
|
|
## Two embed modes, one rule about iframes
|
|
|
|
| Mode | What | When |
|
|
|---|---|---|
|
|
| **script + element** | host page loads the IIFE, drops the tag | full integration: host styles/sizes it, passes config |
|
|
| **iframe / direct link** | point at the generated `<profile>/index.html` | zero-effort embeds, CMSs that only allow iframes, shared links |
|
|
|
|
Both are supported on **built** pages. The one "never iframe" rule applies only to the **dev
|
|
staging** script (the archived repo-root staging page, removed in afb998f17 — the plugin studio is the only entry now), 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) | `<bundle>/<profile>/index.html`, `<bundle>/<profile>/<instance>/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 `<bundle>/index.html` | **`'self'`** — preview only |
|
|
|
|
The Research Gallery (`doxa-maps-spa/doxa-maps-spa/index.html`) is a direct-embed page like any
|
|
other leaf. Its full embed contract (script and iframe) is in
|
|
`plugin-designs/doxa-maps-spa/EMBED.md`.
|
|
|
|
**CORS is a separate requirement, and it covers the whole tree.** A script embed runs in the
|
|
partner's document, so everything a composed surface `fetch()`es — `manifest.json` at the tree
|
|
root, and `assets/` — needs `Access-Control-Allow-Origin: *`. Applying it only to the ≥2-segment
|
|
embed pages leaves `manifest.json` uncovered. `bun run check:gallery-embed` proves the whole
|
|
chain from a second origin.
|
|
|
|
`frame-ancestors` can ONLY be set by an HTTP response header (HTML `<meta>` 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** (a self-hosted reverse proxy): a higher-priority router on `PathRegexp(^/plugin-assets/[^/]+/[^/]+/)`
|
|
sets `Content-Security-Policy: frame-ancestors *` + clears `X-Frame-Options`. (Live example: a Traefik
|
|
dynamic-configuration file on the test host.)
|
|
- **Cloudflare Pages / Netlify**: a `_headers` file — `/<bundle>/*/* 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.
|