doxa-maps/plugin-designs/doxa-maps-spa/EMBED.md
doxa-maps-88655c70 3a2cf0e8f2 Research Gallery: embed on someone else's page, in both modes
The gallery is going onto the DX-life research page, and nothing had tried
it on a page it didn't generate. From a second origin, a bare
<script> + <doxa-maps-spa> drew over the host's header, and 8 of 13 tabs
showed an error card.

Three causes, each fixed in the shell:

- No :host box. The layout root is position:absolute, so it anchored to the
  viewport instead of the element. :host is now a positioned block with a
  default height the host can override.
- A foreign page sets none of the globals a leaf page bakes. Every data call
  hit the host's own origin and got HTML back. The manifest now publishes a
  `runtime` block (apiBaseUrl + the public token every leaf already carries).
  The shell fills window.MAP_APP_API_URL / window.MAP_TOKEN only when the page
  hasn't set them, and forwards tk in profile-config.
- Nested elements resolved assets to <tree>/../assets, a sibling pool the
  publisher doesn't ship (404 live). The shell now forwards an absolute
  <tree>/assets. This also fixes the iframe mode, where Cities with Internet
  and Gospel Resources were failing the same way.

Also adds a `tab` attribute (plus ?tab=) to deep-link into one map, and a
`tab-change` event for the host.

check:gallery-embed drives every tab in both modes from a second origin. It
passes here and fails on the previous tree (7 findings).

EMBED.md documents the contract. The Traefik template now sends CORS for the
whole tree: live manifest.json answers without Access-Control-Allow-Origin.
2026-09-24 19:03:37 +00:00

3.3 KiB

Embedding the Research Gallery

<doxa-maps-spa> (labeled Research Gallery) is the capability-5 app shell: one element whose tab rail nests every production map. It is built to drop onto somebody else's page. Below, TREE means the URL of the built doxa-maps/ folder wherever it is served, for example https://YOUR-HOST/doxa-maps.

<script src="TREE/doxa-maps-spa/doxa-maps-spa.js"></script>
<doxa-maps-spa></doxa-maps-spa>

That is the whole embed. The element finds its tree from its own <script> URL, reads TREE/manifest.json, and takes the data API and public map token from the manifest's runtime block. It sizes itself as a block that is 85vh tall (minimum 480px) and lays out inside its own box. To change the height, either:

<doxa-maps-spa style="height: 900px"></doxa-maps-spa>
<!-- or, from your stylesheet -->
<style> doxa-maps-spa { --doxa-maps-spa-height: 70vh; } </style>

iframe embed

<iframe src="TREE/doxa-maps-spa/doxa-maps-spa/index.html"
        style="width:100%; height:85vh; border:0" allow="fullscreen"></iframe>

Add ?tab=<id> to the src to open a specific map.

Attributes (all optional)

Attribute Default What it does
tab device default (Big Picture on desktop, Simple Research on phones) Tab id to open first. Changing it later switches tabs. ?tab= on the page URL works too.
language parent page / saved choice UI language pushed to every tab (en, es, fr, …).
api-base-url manifest runtime.apiBaseUrl Data API. Only applied when the page has not already set window.MAP_APP_API_URL.
tk manifest runtime.token Mapbox public token. Only fills window.MAP_TOKEN when the page has not set one.
token-url none Token endpoint forwarded to each map.
tree-base derived from the script URL Pin the tree root explicitly (needed only when the script is re-hosted apart from its tree).
heading DX-Maps Shell title (shown only when the shell header is enabled).

Tab ids: dashboard-plus-grid (Big Picture), research-map-plus-grid (Simple Research), regions-dashboard, upg-100, prayer-map (Prayer Progress), cities-internet, gospel-resources, and every Experimental bundle by its bundle name.

Events

The element dispatches tab-change whenever the visible map changes, so the host page can mirror it into its own URL or analytics:

document.querySelector('doxa-maps-spa')
  .addEventListener('tab-change', (e) => console.log(e.detail[0].id, e.detail[0].label))

What the server must send

Both headers are required on a host other than the one serving the tree:

  • Access-Control-Allow-Origin: * on the whole tree, including manifest.json at its root and assets/. The gallery and its maps fetch() these after mounting, and fetch() enforces CORS; a <script> tag alone does not. If manifest.json lacks the header, every tab shows "Could not load …".
  • Content-Security-Policy: frame-ancestors * on doxa-maps-spa/doxa-maps-spa/index.html, for the iframe embed only.

serving/traefik-embed-headers.yaml has a ready rule for both.

Prove it

bun run check:gallery-embed     # serves the built tree + a foreign host page, drives every tab in both modes