README-SECTION-3 reproduces the scripts block verbatim and carries its own plea to re-transcribe after every package.json edit. It drifted anyway, twice: cap:sync still showed the bunx form that does not exist here, and build:unlocked was missing check:public-hygiene and check:label-joins. The second is the dangerous shape. An omission in a quoted list has no wrong character to notice, so a reader auditing what the build verifies concludes the secret scan is not in the chain. The warning was already there and did not work, because re-transcribing by hand is what gets skipped when the edit felt unrelated. Blocking in CI: it diffs two files already in the checkout, so a failure is always real. Negative-tested both ways, including a matcher that matches too little failing rather than passing vacuously.
11 KiB
3. CLI commands + required installs (per deployment method)
Everything runs through bun and the scripts in package.json. The build loop
iterates every folder under plugin-designs/ (each folder = one bundle), so once a
map exists there, bun run build picks it up automatically — no per-map wiring.
Install once (all methods): Bun (
curl -fsSL https://bun.sh/install | bash) — Node ≥ 18 also works (npm run <script>; the scripts are identical). Then:bun install
The scripts these commands are grounded in (from this branch's package.json):
// TRANSCRIBED FROM package.json 2026-09-20. The previous copy had DRIFTED: it listed
// `check:drop` twice, omitted `check:template-literal` and `check:sri`, and still showed
// the old inline `build` pipeline — which has since moved behind a LOCK WRAPPER.
// ★ `build` is now `bash internal/build-lock.sh`; the real pipeline is `build:unlocked`.
// The lock SERIALIZES concurrent builds (a `clean` rm -rf's the shared output tree at the
// START of every build, so two builds racing give opposite answers seconds apart). Use
// BUILD_LOCK_WAIT=600 to QUEUE rather than collide. Note the lock serializes but does NOT
// dedupe: N queued builds = N full rebuilds.
// If you edit package.json, re-transcribe this block — a doc that QUOTES a config is
// claiming to be ground truth, and this one had silently stopped being it.
"dev": "bun x vite",
"build": "bash internal/build-lock.sh",
"build:unlocked": "bun run check:template-literal && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run gallery",
"preview": "bun x vite preview",
"serve": "python3 -m http.server 4173 --bind 0.0.0.0 --directory public",
"cap:sync": "./node_modules/.bin/cap sync android",
"cap:verify": "test -f capacitor/android/app/src/main/assets/public/index.html && echo \"OK webDir copied\" || (echo \"FAIL: empty assets/public -> blank white screen. Check webDir.\" && exit 1)",
"apk:debug": "bun run build && bun run cap:sync && bun run cap:verify && cd capacitor/android && ./gradlew assembleDebug",
"apk:install": "adb install -r capacitor/android/app/build/outputs/apk/debug/app-debug.apk",
"phone": "bun run apk:debug && bun run apk:install"
bun run build writes the whole deployable tree to public/doxa-maps/, including
manifest.json — the machine-readable index that carries the ready-to-paste
iframe and <script src> + SRI embed snippets for every map.
Summary table
| # | Deploy method | Install first (prereqs) | Command(s) |
|---|---|---|---|
| 1 | iframe embed | Bun | bun install → bun run build → bun run serve (or drop public/doxa-maps/ on any host) |
| 2 | Mobile app (Android) | Bun ALONE for check:mobile; + Android SDK · adb on PATH · JDK 17 (Gradle) for the APK |
bun run check:mobile (sync + webDir assertion, NO toolchain needed) · bun run phone (= apk:debug + apk:install to a device/emulator) |
| 3 | Direct-JS / dashboard | Bun | bun run build, then paste the <script src> + integrity (SRI) snippet from manifest.json. Proven same-root AND foreign-host (verified 2026-09-20: gl + attribution, 0 errors, 0 404s). Fill the YOUR_* placeholders the snippet ships with |
| 4 | WordPress | Bun (+ php CLI to run the contract gate) |
bun run build, then either paste the iframe/script snippet into a Custom HTML block, or install wordpress/doxa-maps-embed/ (shortcode + Gutenberg block). bun run check:wp-embed verifies the emitted markup. (Contract-proven; not yet shot on a live WP install.) |
| 5 | Desktop (Tauri) | Rust toolchain + Tauri CLI (not yet scaffolded) | Deferred POC — no command exists on this branch yet. |
1. iframe embed (required)
Install: Bun.
bun install
bun run build # builds every plugin-designs/* bundle -> public/doxa-maps/
bun run serve # optional: serves public/ at http://localhost:4173
The build writes public/doxa-maps/manifest.json. Each profile carries an
embed.iframe snippet, e.g.:
<iframe src="./all-people-groups/all-people-groups/index.html"
width="100%" height="600"
style="border:none;border-radius:12px;" allowfullscreen></iframe>
URLs are root-relative, so the whole doxa-maps/ folder is position-independent —
drop it on Cloudflare/Vercel/any static host and every iframe resolves. (For an
external absolute base, set embedBaseUrl in the build config before building.)
2. Mobile app — Android / Capacitor (required)
Install: Bun · Android SDK (sdkmanager, adb on your PATH) · JDK 17
(the Gradle wrapper needs it) · Capacitor (@capacitor/* are already devDependencies,
so bun install covers them). A connected device or running emulator for install.
bun run check:mobile # CONTRACT check — needs NO JDK/SDK/device
bun run phone # full path — needs the toolchain above
check:mobile is runnable anywhere. cap sync is a file copy plus plugin wiring, so it
needs no Android toolchain at all; cap:verify then asserts
capacitor/android/app/src/main/assets/public/index.html exists. That guards the soft
failure capacitor.config.json warns about — a wrong webDir yields an EMPTY
assets/public and a BLANK WHITE SCREEN at launch, not an error. Verified 2026-09-20 with
java, gradle, adb and sdkmanager all absent: sync EXIT=0, 21 bundles staged,
cap:verify OK. Negative-tested by removing index.html — EXIT=1.
Status: built + contract-proven; not shot. The APK itself (gradlew assembleDebug) and
its launch on a device still need JDK 17 + the Android SDK + adb.
phone chains the whole path: apk:debug (which runs bun run build, then
cap:sync to copy the web output into the Android project, then cap:verify, then
./gradlew assembleDebug) and finally apk:install (adb install -r ...) onto the
device. Run the halves separately if you want to inspect between steps:
bun run apk:debug # build the APK only
bun run apk:install # push the built APK to a connected device
Only Android is scaffolded on this branch (capacitor/android, capacitor.config.json);
iOS is not set up.
3. Direct-JS / dashboard (required)
Install: Bun.
bun run build
Then paste the profile's embed.script snippet from manifest.json — a <script src>
with a Subresource-Integrity (SRI) hash plus the custom-element tag:
<script src="./all-people-groups/all-people-groups.js"
integrity="sha384-j5Ypx+EI9DGlTClzgLs1R8wAnmTnkQ7HgAhHrxyGoVoHDMTlFVY7slU6JNicn77T"
crossorigin="anonymous"></script>
<all-people-groups
profile-config='{"profile":"all-people-groups","tk":"YOUR_MAPBOX_TOKEN"}'>
</all-people-groups>
The element accepts props / emits events, so it can wire into a surrounding page as a unified research dashboard (deploy-3 proof). The SRI hash regenerates on every build.
Embedding on a FOREIGN host (a partner site) — the two things that differ
Both embed methods were run to their first failure on a foreign host page (2026-09-20). deploy-1 needed nothing; deploy-3 needed two values, and the snippet now ships both.
Why they differ: an iframe carries its own document base, so the leaf page's relative asset and API paths resolve correctly. A direct-JS embed executes in the host's document, so those same relative paths resolve against the host — and 404. Same bundle, same data, different containing document.
| iframe (deploy-1) | direct-JS (deploy-3) | |
|---|---|---|
assetsBase |
not needed | required — in profile-config |
| API base | not needed | required — window.MAP_APP_API_URL (a host global, not a profile-config key) |
| Mapbox token | inherited by the leaf | replace YOUR_MAPBOX_TOKEN |
Both are emitted into embed.script already: the API base uses the configured value, and
assetsBase falls back to a YOUR_TREE_URL/assets placeholder when assetsBaseUrl is unset.
One rewrite either way. Snippet URLs are tree-relative (./bundle/...) because
embedBaseUrl is empty by default — that is what keeps a dropped folder position-independent
and the check:drop gate green. A partner whose page is not at the tree root must either
prefix the path (./doxa-maps/...) or set embedBaseUrl in the build config to emit
absolute URLs. That is a deployment choice, not a defect.
4. WordPress (built + contract-proven; not yet shot on a live WP install)
Corrected 2026-09-20. This section previously read "deferred POC — no dedicated WordPress build step exists on this branch". That is no longer true: a first-class plugin exists at
wordpress/doxa-maps-embed/(a[doxa_map]shortcode + a Gutenberg block), and its emitted markup is now verified by a gate.
The simple path — no plugin needed, same artifacts as methods 1 and 3:
bun run build
Then paste either the iframe snippet or the <script src> + SRI snippet from
manifest.json into a WordPress Custom HTML block.
The plugin path — wordpress/doxa-maps-embed/ registers the bundle through WordPress's
own dependency graph, so the SRI hash and crossorigin survive and the script loads once
per page rather than once per embed.
bun run check:wp-embed # renders the REAL shortcode with no WordPress installed
That gate stubs the ~20 WordPress functions the plugin actually calls (wordpress/tests/),
renders the shortcode against the REAL built manifest.json, and asserts the contract
HOW-TO-TEST.md defines: the element tag is emitted, profile-config decodes to valid
JSON, the script src equals the catalog path, the SRI integrity is injected by the
script_loader_tag filter, crossorigin="anonymous" is present — plus a negative test
that a malformed integrity is rejected.
Status honesty — say "contract-proven", not "proven". What is proven: the plugin emits
the correct markup, repeatably and in CI. What is not: that WordPress renders the page,
that the block editor registers it, or that a browser executes the result. Those still need
a real WP install (wp-env start — see wordpress/doxa-maps-embed/HOW-TO-TEST.md).
5. Desktop — Tauri (deferred POC)
Intended prereqs: the Rust toolchain (rustup) + the Tauri CLI, wrapping the same
bun run build web output as a native desktop shell.
Status: deferred POC. No Tauri scaffolding or build script exists on this branch yet, so there is no copy-paste command to run today. Documented here for completeness of the 5-method model.