README: three outputs, not five methods; a Languages section; screenshot gates see every shot again
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions

- Deployment is now organised around what the build makes: (1) one web
  folder (536 files, 76 MiB), embedded by iframe and JavaScript (proven) and
  WordPress (just for fun); (2) the mobile app (Capacitor, built); (3) the
  desktop app (Tauri, proof of concept). A hosting table says where the folder
  can go: dx-life's public/, Cloudflare Pages drag-and-drop (fits its 1,000-file
  / 25 MiB limits), GitHub + jsDelivr (JS embeds only: measured, jsDelivr serves
  HTML as text/plain), or any web server, with the one _headers line JS embeds
  need. No Docker. The diagram and the map-cruncher picture say three outputs.
- Languages: 11 locales via ?lang=, Arabic right to left, with an Arabic shot of
  UPG-100 (Big Picture still leaks English in Arabic; reported). The partner
  profile-config locale is deliberately not documented until it is fixed.
- check-shot-freshness and check-shot-distinctness parsed only '...' paths, so
  after 44ef31b2 turned them into `/${TREE_NAME}/...` templates they saw 2 of 26
  shots: distinctness failed, freshness passed vacuously and hid 22 stale shots.
  Both now accept either form and resolve TREE_NAME, and exit 75 (NOT MEASURED)
  unless every `await shot(` call is parsed (the one dynamic dx-life call is
  marked). Negative-tested. The 22 shots are retaken and checked by eye.
- how-to: the shell no longer overwrites the Studio (it now lives in the output
  folder); README serve line points at doxa-maps/index.html.
- Docs follow the rename to dx-maps (09014f8a): every output-folder path and URL in
  README, how-to, reference, CONTRIBUTING, the CLI section and the develop guide now says
  public/dx-maps/. The repo name, the config file name, the WordPress plugin folder and
  the doxa-* tag names are unchanged (tags are a separate landing).
- Hosting: a live example, the dx-life branch research-gallery, which drops the folder
  into dx-life public/ and fills /research with the Research Gallery.
This commit is contained in:
doxa-maps-88655c70 2026-09-25 17:58:19 +00:00
parent a4a7ddd6fd
commit 2509a6e347
34 changed files with 266 additions and 170 deletions

239
README.md
View file

@ -8,15 +8,15 @@ Made by Vibe Coders for Vibe Coders to prepare the way of the Lord and make His
**Write a map once in `plugin-designs/<your-map>/`, run one build, and drop it on any site** — **Write a map once in `plugin-designs/<your-map>/`, run one build, and drop it on any site** —
iframe, script embed, mobile app — or open every map at once as one app. Five plugin iframe, script embed, mobile app — or open every map at once as one app. Five plugin
capabilities, five deployment methods, one shared library, one build. capabilities, three outputs, one shared library, one build.
| What dx-maps set out to do | Proof | | What dx-maps set out to do | Proof |
|---|---| |---|---|
| **One build → one folder** you can drop on any host | `bun run check:drop` | | **One build → one folder** you can drop on any host | `bun run check:drop` |
| **Every map embeddable** from a copy-paste snippet | [deployment methods](#the-5-deployment-methods) · `bun run check:embed-targets` | | **Every map embeddable** from a copy-paste snippet | [the 3 outputs](#what-the-build-makes--3-outputs) · `bun run check:embed-targets` |
| **Every map interoperable** through props and events | `bun run verify:interop` | | **Every map interoperable** through props and events | `bun run verify:interop` |
| **One app nests every map** — and each stays embeddable on its own | [capability 5](#capability-5--app-shell-the-research-gallery) · `bun run check:composite-reg` | | **One app nests every map** — and each stays embeddable on its own | [capability 5](#capability-5--app-shell-the-research-gallery) · `bun run check:composite-reg` |
| **5 capabilities × 5 deployment methods** | `bun run verify:5-caps` | | **5 capabilities, every one shippable in every output** | `bun run verify:5-caps` |
| **Shared library, private plugins** — reuse without breaking each other | `bun run check:library-index` | | **Shared library, private plugins** — reuse without breaking each other | `bun run check:library-index` |
| **One agent per plugin, one shepherd, one working `main`** | [agent swarm](#build-it-with-an-agent-swarm) | | **One agent per plugin, one shepherd, one working `main`** | [agent swarm](#build-it-with-an-agent-swarm) |
@ -88,7 +88,7 @@ and graduates to a named tab when it's ready.
<sub>Also on the shelf: UPG neighbor, all people groups, country posters, the vision card, and the PPLR data maps.</sub> <sub>Also on the shelf: UPG neighbor, all people groups, country posters, the vision card, and the PPLR data maps.</sub>
<details> <details>
<summary><b>How one build becomes five ways to ship — the "map cruncher"</b></summary> <summary><b>How one build becomes three outputs — the "map cruncher"</b></summary>
```text ```text
map · map · map · map · map · map · map many maps, each once its own codebase map · map · map · map · map · map · map many maps, each once its own codebase
@ -99,8 +99,12 @@ and graduates to a named tab when it's ready.
| |
ONE build step ONE build step
| |
public/doxa-maps/ one drop-anywhere folder + manifest.json public/dx-maps/ OUTPUT 1: one drop-anywhere web folder
/ | \ \ iframe mobile JS embed WordPress desktop / | \ how a page shows a map:
iframe JS embed WordPress (WordPress just for fun)
the same folder, wrapped:
mobile app (OUTPUT 2) · desktop app (OUTPUT 3)
``` ```
</details> </details>
@ -259,43 +263,134 @@ one custom HTML tag, like `<all-people-groups>`, that works on any web page.
--- ---
## The 5 deployment methods ## What the build makes — 3 outputs
### 1. iframe — the prayer map on any site ✅ production **One build step makes one folder. That folder is the product** — every other choice is *where you
put it* and *how a page shows it*.
**Paste one line of HTML and the map appears** — on any website, no coding. | # | Output | What it is | Status |
|---|---|---|---|
| **1** | **The web folder** `public/dx-maps/` | every map, each embeddable on its own, all working together — 536 files, 76 MiB (about 80 MB) | ✅ production |
| **2** | **Mobile app** | the same folder, wrapped as an Android app by Capacitor | ✅ built |
| **3** | **Desktop app** | the same folder, wrapped as a desktop app by Tauri | 🧪 proven, not yet in production |
![Method 1 — the prayer map embedded in a host page's iframe](documentation/readme-images/deploy1-iframe.png) ### Output 1 — the web folder
Two questions: **where does the folder go** (hosting), and **how does a page show a map** (embedding).
#### Where the folder goes
| Put the folder… | How | iframe | JS embed | Good to know |
|---|---|:-:|:-:|---|
| **in dx-life's `public/` folder** | copy one folder; dx-life becomes the host | ✅ | ✅ | this is how the Research Gallery runs on the dx-life research page (below) |
| **on Cloudflare Pages** | drag and drop the folder in the dashboard | ✅ | ✅ | fits the drag-and-drop limits (1,000 files, 25 MiB per file); add the `_headers` file below for JS embeds on other sites |
| **on GitHub, served by jsDelivr** | push the repo to a public GitHub repo | — | ✅ | a free CDN for scripts; it serves HTML as plain text, so iframes won't render — use it for JS embeds only |
| **on any web server** | copy the folder | ✅ | ✅ | send `Access-Control-Allow-Origin: *` for JS embeds on other sites |
**Live example:** the dx-life branch [`research-gallery`](https://git.nuxtbase.app/doxa-life/doxa-maps-cdn/src/branch/research-gallery) drops this folder into dx-life's `public/` and fills `/research` with the Research Gallery. Run it with `bun install && bun run dev`, then open `/research/`.
<details> <details>
<summary><b>More about method 1 — iframe</b></summary> <summary><b>More about hosting the folder</b></summary>
An iframe is a window in your page that shows another page — here, the map's own page. It's the - **The folder works from any address.** Every link inside it is relative, so you can put it at
easiest way to share a map: it works on any site that accepts HTML, and the map keeps its own look. `https://your-site/dx-maps/` or at a CDN root and nothing breaks (`bun run check:drop` proves it).
Every plugin's exact snippet is `embed.iframe` in `public/doxa-maps/manifest.json`: - **iframes need nothing special.** A map's page loads inside the frame from wherever the folder is.
- **JS embeds on another site need one header.** The embed loads scripts and `manifest.json` across
sites with a security hash, so the host must allow it. On Cloudflare Pages (or Netlify), put a file
named `_headers` in the folder you upload:
```html ```text
<iframe src="https://YOUR_HOST/doxa-maps/doxa-simple-map/doxa-simple-map/prayer/index.html?utm_source=YOUR_SOURCE" /*
width="100%" height="600" style="border:none;border-radius:12px;" allowfullscreen></iframe> Access-Control-Allow-Origin: *
``` ```
Host the built `public/doxa-maps/` folder anywhere (it works from any address), then point the - **jsDelivr** serves any public GitHub repo as a CDN, e.g.
iframe at it. `https://cdn.jsdelivr.net/gh/<owner>/<repo>@<tag>/public/dx-maps/<plugin>/<plugin>.js`. It needs a
**GitHub** repo (this project's public copy is on its own git host), files under 20 MB (the largest
here is 7.5 MB), and it returns HTML as plain text — hence scripts only.
- **Netlify Drop** (drag and drop at app.netlify.com/drop) works best under 50 MB; at about 80 MB, use the
Netlify CLI instead.
</details> </details>
### 2. Mobile app — UPG-100 on a phone ✅ built #### How a page shows a map
**The same maps, wrapped as a phone app.** ##### iframe — the prayer map on any site ✅ production
<p align="center"><img src="documentation/readme-images/deploy2-mobile-upg100.png" alt="Method 2 — the UPG-100 list at phone size" width="320"></p> **Paste one line of HTML and the map appears** — on any website, no coding.
![iframe — the prayer map embedded in a host page](documentation/readme-images/deploy1-iframe.png)
<details> <details>
<summary><b>More about method 2 — mobile app</b></summary> <summary><b>More about the iframe</b></summary>
[Capacitor](https://capacitorjs.com) wraps the built maps into an Android app — the same files, An iframe is a window in your page that shows another page — here, the map's own page. It's the
no new code. Shown here: the UPG-100 prayer lists and posters at phone size, each poster with its easiest way to share a map: it works on any site that accepts HTML, and the map keeps its own look.
own print options. Every plugin's exact snippet is `embed.iframe` in `public/dx-maps/manifest.json`:
```html
<iframe src="https://YOUR_HOST/dx-maps/doxa-simple-map/doxa-simple-map/prayer/index.html?utm_source=YOUR_SOURCE"
width="100%" height="600" style="border:none;border-radius:12px;" allowfullscreen></iframe>
```
</details>
##### JavaScript embed — the Research Gallery on dx-life ✅ production
**Two lines of code put a whole app inside your own page** — here, the Research Gallery on the
dx-life research page.
![JavaScript embed — the Research Gallery on the dx-life research page](documentation/readme-images/deploy3-js-embed-dxlife.png)
<details>
<summary><b>More about the JavaScript embed</b></summary>
Instead of a window onto another page, the map becomes **part of your page**, so your page can talk
to it. On dx-life, one embed of the Research Gallery replaces the separate research map, list and
regions dashboards. The exact snippet is `embed.script` in `manifest.json`; copy its `integrity`
hash from there, because it changes with every build:
```html
<script>window.MAP_APP_API_URL="https://pray.doxa.life";</script>
<script src="https://YOUR_HOST/dx-maps/doxa-maps-spa/doxa-maps-spa.js"
integrity="sha384-…" crossorigin="anonymous"></script>
<doxa-maps-spa profile-config='{"profile":"doxa-maps-spa","tk":"YOUR_MAPBOX_TOKEN","utmSource":"YOUR_SOURCE","assetsBase":"https://YOUR_HOST/dx-maps/assets"}'></doxa-maps-spa>
```
</details>
##### WordPress / Disciple Tools — 🧪 proven, just for fun
**We got a JAMstack map running inside WordPress and Disciple Tools.**
![WordPress / Disciple Tools — proven, not yet in production](documentation/readme-images/deploy4-wordpress.png)
<details>
<summary><b>More about WordPress / Disciple Tools</b></summary>
It works, but it isn't yet a production feature of the map bundler. Two ways to try it:
- **Easiest:** paste the iframe or JavaScript snippet into a WordPress **Custom HTML** block.
- **As a plugin:** install `deployment/wordpress/doxa-maps-embed/` in `wp-content/plugins/`. It adds
a `[doxa_map]` shortcode and a block for the block editor.
`bun run check:wp-embed` confirms the plugin writes the right HTML, without needing WordPress
installed.
</details>
### Output 2 — the mobile app ✅ built
**The same maps, wrapped as a phone app.** No new code — the maps are JAMstack: they fetch their data
from a remote API, which is exactly what a mobile app needs.
<p align="center"><img src="documentation/readme-images/deploy2-mobile-upg100.png" alt="Mobile app — the UPG-100 list at phone size" width="320"></p>
<details>
<summary><b>More about the mobile app</b></summary>
[Capacitor](https://capacitorjs.com) wraps the built folder into an Android app. Shown here: the
UPG-100 prayer lists and posters at phone size, each poster with its own print options.
```bash ```bash
bun run check:mobile # checks the app is wired up correctly — needs no Android tools bun run check:mobile # checks the app is wired up correctly — needs no Android tools
@ -306,59 +401,16 @@ bun run phone # builds the app and installs it on a phone or emulator
</details> </details>
### 3. JavaScript embed — the Research Gallery on dx-life ✅ production ### Output 3 — the desktop app 🧪 proven, not yet in production
**Two lines of code put a whole app inside your own page** — here, the Research Gallery on dx-life.
![Method 3 — the Research Gallery JS-embedded on the dx-life research page](documentation/readme-images/deploy3-js-embed-dxlife.png)
<details>
<summary><b>More about method 3 — JavaScript embed</b></summary>
Instead of a window onto another page, the map becomes **part of your page**, so your page can talk
to it. The dx-life research page replaced its separate research map, list and regions dashboards
with this one embed of the Research Gallery. The exact snippet is `embed.script` in `manifest.json`;
copy its `integrity` hash from there, because it changes with every build:
```html
<script>window.MAP_APP_API_URL="https://pray.doxa.life";</script>
<script src="https://YOUR_HOST/doxa-maps/doxa-maps-spa/doxa-maps-spa.js"
integrity="sha384-…" crossorigin="anonymous"></script>
<doxa-maps-spa profile-config='{"profile":"doxa-maps-spa","tk":"YOUR_MAPBOX_TOKEN","utmSource":"YOUR_SOURCE","assetsBase":"https://YOUR_HOST/doxa-maps/assets"}'></doxa-maps-spa>
```
</details>
### 4. WordPress / Disciple Tools — 🧪 proven, not yet in production
**We got a JAMstack map running inside WordPress and Disciple Tools.**
![Method 4 — WordPress / Disciple Tools, proven but not yet in production](documentation/readme-images/deploy4-wordpress.png)
<details>
<summary><b>More about method 4 — WordPress / Disciple Tools</b></summary>
It works, but it isn't yet a production feature of the map bundler. Two ways to try it:
- **Easiest:** paste a method 1 or method 3 snippet into a WordPress **Custom HTML** block.
- **As a plugin:** install `deployment/wordpress/doxa-maps-embed/` in `wp-content/plugins/`. It adds
a `[doxa_map]` shortcode and a block for the block editor.
`bun run check:wp-embed` confirms the plugin writes the right HTML, without needing WordPress
installed.
</details>
### 5. Desktop app (Tauri) — 🧪 proven, not yet in production
**We got the JAMstack map app running as a desktop app.** **We got the JAMstack map app running as a desktop app.**
![Method 5 — desktop app (Tauri), proven but not yet in production](documentation/readme-images/deploy5-desktop.png) ![Desktop app (Tauri) — proven, not yet in production](documentation/readme-images/deploy5-desktop.png)
<details> <details>
<summary><b>More about method 5 — desktop app</b></summary> <summary><b>More about the desktop app</b></summary>
[Tauri](https://tauri.app) wraps the same built maps into a desktop program: it produced a Linux [Tauri](https://tauri.app) wraps the same built folder into a desktop program: it produced a Linux
app and a `.deb` installer. It isn't in the dx bundler for production yet — and the Research Gallery app and a `.deb` installer. It isn't in the dx bundler for production yet — and the Research Gallery
could become a strong desktop app. could become a strong desktop app.
@ -373,6 +425,39 @@ bun run tauri:build # builds the app — needs Rust, the Tauri CLI and webvie
--- ---
## Languages
**Every map speaks 11 languages** — add `?lang=<code>` to a map's page address. Arabic reads right to left.
![UPG-100 List in Arabic, right to left](documentation/readme-images/lang-arabic-upg100.png)
<sub>UPG-100 List with `?lang=ar`: the interface is in Arabic; people-group and place names come from the data.</sub>
| Code | Language | | Code | Language |
|---|---|---|---|---|
| `en` | English | | `hi` | हिन्दी — Hindi |
| `ar` | العربية — Arabic (right to left) | | `it` | Italiano — Italian |
| `de` | Deutsch — German | | `pt` | Português — Portuguese |
| `es` | Español — Spanish | | `ro` | Română — Romanian |
| `fr` | Français — French | | `ru` | Русский — Russian |
| `zh` | 中文 — Chinese | | | |
<details>
<summary><b>More about languages</b></summary>
- **Pick a language with the address:** `…/prayer/index.html?lang=fr` shows the map in French. It
works on every map's own page and on the Research Gallery. `?locale=` works too.
- **Without it,** a map follows the page's own language (`<html lang>`, which WordPress sets), and
otherwise shows English.
- **Right to left:** in Arabic the whole layout mirrors — the Research Gallery's tab rail moves to
the right.
- **Where the words live:** `library/i18n/locales/<code>/`, one folder per language. Adding a
language: [documentation/architecture/i18n.md](./documentation/architecture/i18n.md).
</details>
---
## Build it with an agent swarm ## Build it with an agent swarm
**Many AI agents can build many plugins at once without breaking each other's work.** Every plugin **Many AI agents can build many plugins at once without breaking each other's work.** Every plugin
@ -504,8 +589,8 @@ The studio is a plugin like any other: `plugin-designs/plugin-studio/plugin-stud
|---|---| |---|---|
| `bun run dev` | The Plugin Studio, updating as you save, at http://localhost:5173 | | `bun run dev` | The Plugin Studio, updating as you save, at http://localhost:5173 |
| `BUNDLE=<name> bun x vite build` | Build one plugin only | | `BUNDLE=<name> bun x vite build` | Build one plugin only |
| `BUILD_ALLOW=1 bun run build` | Build everything into `public/doxa-maps/` (~4 min; refuses without the flag, to protect a shared project) | | `BUILD_ALLOW=1 bun run build` | Build everything into `public/dx-maps/` (~4 min; refuses without the flag, to protect a shared project) |
| `bun run serve` | Serve the built maps at http://localhost:4173 | | `bun run serve` | Serve the built output folder — the Plugin Studio is http://localhost:4173/dx-maps/index.html |
| `bun run gates:pre` · `bun run gates:post` | The checks that keep `main` working | | `bun run gates:pre` · `bun run gates:post` | The checks that keep `main` working |
| `bun run shots` | Retake these screenshots (`ONLY=<name>` for one) | | `bun run shots` | Retake these screenshots (`ONLY=<name>` for one) |
@ -533,7 +618,7 @@ doxa-maps/
│ ├── colors/ styles/ colour strategies and the house style │ ├── colors/ styles/ colour strategies and the house style
│ ├── i18n/locales/ the 11 languages │ ├── i18n/locales/ the 11 languages
│ └── data/ api/ lookup tables and data sources │ └── data/ api/ lookup tables and data sources
├── public/doxa-maps/ the BUILD OUTPUT — the drop-anywhere folder + manifest.json ├── public/dx-maps/ the BUILD OUTPUT — the drop-anywhere folder + manifest.json
├── deployment/ mobile (Capacitor) · WordPress · desktop (Tauri) ├── deployment/ mobile (Capacitor) · WordPress · desktop (Tauri)
├── internal/ the build and its checks ├── internal/ the build and its checks
└── documentation/ tutorial · how-to · reference · architecture └── documentation/ tutorial · how-to · reference · architecture

View file

@ -33,7 +33,7 @@ Full walkthrough + the migration story:
| Command | What | Writes to `public/`? | | Command | What | Writes to `public/`? |
|---|---|---| |---|---|---|
| `bun run dev` | HMR plugin gallery at `localhost:5173` — iterate on a map in isolation | **No** | | `bun run dev` | HMR plugin gallery at `localhost:5173` — iterate on a map in isolation | **No** |
| `bun run build` | every bundle → `public/doxa-maps/<bundle>/<bundle>.js` + generated pages | **Yes** | | `bun run build` | every bundle → `public/dx-maps/<bundle>/<bundle>.js` + generated pages | **Yes** |
To see a change on the served site: `bun run build` (or `BUNDLE=<name> bun run build:bundle`), To see a change on the served site: `bun run build` (or `BUNDLE=<name> bun run build:bundle`),
then refresh. `dev` never updates the served tree. then refresh. `dev` never updates the served tree.

View file

@ -60,7 +60,7 @@ The scripts these commands are grounded in (from this branch's `package.json`):
"phone": "bun run apk:debug && bun run apk:install" "phone": "bun run apk:debug && bun run apk:install"
``` ```
`BUILD_ALLOW=1 bun run build` writes the whole deployable tree to `public/doxa-maps/`, including `BUILD_ALLOW=1 bun run build` writes the whole deployable tree to `public/dx-maps/`, including
`manifest.json`: the machine-readable index that carries a ready-to-paste **iframe** snippet and a `manifest.json`: the machine-readable index that carries a ready-to-paste **iframe** snippet and a
**`<script src>` + SRI** snippet for every plugin. **`<script src>` + SRI** snippet for every plugin.
@ -68,7 +68,7 @@ The scripts these commands are grounded in (from this branch's `package.json`):
| # | Deployment method | Install first | Command(s) | Status | | # | Deployment method | Install first | Command(s) | Status |
|---|---|---|---|---| |---|---|---|---|---|
| 1 | **iframe** | Bun | `bun install` → `BUILD_ALLOW=1 bun run build` → drop `public/doxa-maps/` on any host | ✅ production | | 1 | **iframe** | Bun | `bun install` → `BUILD_ALLOW=1 bun run build` → drop `public/dx-maps/` on any host | ✅ production |
| 2 | **Mobile app (Android)** | Bun for the check; + Android SDK · `adb` · JDK 17 for the APK | `bun run check:mobile` (no toolchain) · `bun run phone` | ✅ built | | 2 | **Mobile app (Android)** | Bun for the check; + Android SDK · `adb` · JDK 17 for the APK | `bun run check:mobile` (no toolchain) · `bun run phone` | ✅ built |
| 3 | **JavaScript embed** | Bun | `BUILD_ALLOW=1 bun run build`, then paste the `embed.script` snippet from `manifest.json` | ✅ production | | 3 | **JavaScript embed** | Bun | `BUILD_ALLOW=1 bun run build`, then paste the `embed.script` snippet from `manifest.json` | ✅ production |
| 4 | **WordPress / Disciple Tools** | Bun (+ `php` for the check) | paste a snippet into a Custom HTML block, or install `deployment/wordpress/doxa-maps-embed/` · `bun run check:wp-embed` | 🧪 proof of concept | | 4 | **WordPress / Disciple Tools** | Bun (+ `php` for the check) | paste a snippet into a Custom HTML block, or install `deployment/wordpress/doxa-maps-embed/` · `bun run check:wp-embed` | 🧪 proof of concept |
@ -82,7 +82,7 @@ The scripts these commands are grounded in (from this branch's `package.json`):
```bash ```bash
bun install bun install
BUILD_ALLOW=1 bun run build # builds every plugin-designs/* bundle -> public/doxa-maps/ BUILD_ALLOW=1 bun run build # builds every plugin-designs/* bundle -> public/dx-maps/
bun run serve # optional: serves public/ at http://localhost:4173 bun run serve # optional: serves public/ at http://localhost:4173
``` ```
@ -94,7 +94,7 @@ Each profile in `manifest.json` carries an `embed.iframe` snippet:
style="border:none;border-radius:12px;" allowfullscreen></iframe> style="border:none;border-radius:12px;" allowfullscreen></iframe>
``` ```
**The folder is position-independent.** Snippet URLs are relative to the tree root, so `doxa-maps/` **The folder is position-independent.** Snippet URLs are relative to the tree root, so `dx-maps/`
works dropped on any static host. For absolute URLs, set `embedBaseUrl` in the build config. works dropped on any static host. For absolute URLs, set `embedBaseUrl` in the build config.
### 2. Mobile app — Android / Capacitor ### 2. Mobile app — Android / Capacitor

View file

@ -54,12 +54,12 @@ The build is for publishing, not for looking.
## Step 4 — Build the deployable tree ## Step 4 — Build the deployable tree
```sh ```sh
BUILD_ALLOW=1 bun run build # discovers plugin-designs/*, bundles each, writes public/doxa-maps/<plugin>/<plugin>.js BUILD_ALLOW=1 bun run build # discovers plugin-designs/*, bundles each, writes public/dx-maps/<plugin>/<plugin>.js
``` ```
**Exit 0 + every guard PASS = a good build.** Your plugin now exists under `public/doxa-maps/<your-plugin>/` **Exit 0 + every guard PASS = a good build.** Your plugin now exists under `public/dx-maps/<your-plugin>/`
with a standalone leaf page and a `manifest.json` entry. `BUILD_ALLOW=1` is deliberate: a full build first with a standalone leaf page and a `manifest.json` entry. `BUILD_ALLOW=1` is deliberate: a full build first
wipes `public/doxa-maps/`, so it refuses (exit 75) in a shared checkout unless you mean it. To compile only wipes `public/dx-maps/`, so it refuses (exit 75) in a shared checkout unless you mean it. To compile only
your plugin: `BUNDLE=<your-plugin> bun x vite build`. Recipes: [how-to.md](how-to.md). your plugin: `BUNDLE=<your-plugin> bun x vite build`. Recipes: [how-to.md](how-to.md).
## Step 5 — Prove it mounts in a real browser ## Step 5 — Prove it mounts in a real browser
@ -79,7 +79,7 @@ build config, as above ([why](#notes-and-history)).
## Step 6 — Publish and see it live ## Step 6 — Publish and see it live
**Copy the built tree to any static host, then embed from it.** The tree is `public/doxa-maps/`; every **Copy the built tree to any static host, then embed from it.** The tree is `public/dx-maps/`; every
plugin's ready-made snippets are in its `manifest.json`. plugin's ready-made snippets are in its `manifest.json`.
```sh ```sh
@ -87,7 +87,7 @@ set -e # first line: a failing check must stop
bun run check:settled # never publish a tree a build is rewriting bun run check:settled # never publish a tree a build is rewriting
bun run check:sri # every leaf's integrity hash matches its bundle bun run check:sri # every leaf's integrity hash matches its bundle
bun run check:embed-targets # every embed snippet points at a real file bun run check:embed-targets # every embed snippet points at a real file
rsync -a --delete public/doxa-maps/ <your-host-checkout>/doxa-maps/ rsync -a --delete public/dx-maps/ <your-host-checkout>/dx-maps/
# commit and push the host checkout; for a large tree: git -c http.postBuffer=524288000 push … # commit and push the host checkout; for a large tree: git -c http.postBuffer=524288000 push …
``` ```

View file

@ -30,7 +30,7 @@ element with its own `profile-config`.
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js"></script> <script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v5.0.0/mapbox-gl-geocoder.min.js"></script>
<!-- The bundle (relative path = host serves it; absolute https = external CDN) --> <!-- The bundle (relative path = host serves it; absolute https = external CDN) -->
<script src="/doxa-maps/my-map/my-map.js"></script> <script src="/dx-maps/my-map/my-map.js"></script>
<!-- The map. `profile` = your .vue filename. `tk` = the Mapbox token. --> <!-- The map. `profile` = your .vue filename. `tk` = the Mapbox token. -->
<my-map profile-config='{"profile":"my-map","tk":"pk.eyJ..."}'></my-map> <my-map profile-config='{"profile":"my-map","tk":"pk.eyJ..."}'></my-map>
@ -48,18 +48,18 @@ You don't have to use the script tag. **Every profile also builds its own standa
a bare, full-screen map at: a bare, full-screen map at:
``` ```
https://<your-cdn>/doxa-maps/<bundle>/<profile>/index.html https://<your-cdn>/dx-maps/<bundle>/<profile>/index.html
``` ```
Link it, share it, or iframe it (perfect for CMSs and tools that only accept iframes, like Link it, share it, or iframe it (perfect for CMSs and tools that only accept iframes, like
Start.me or Notion): Start.me or Notion):
```html ```html
<iframe src="https://<your-cdn>/doxa-maps/doxa-research-map/research-map/index.html" <iframe src="https://<your-cdn>/dx-maps/doxa-research-map/research-map/index.html"
width="100%" height="600" style="border:none;border-radius:12px;" allowfullscreen></iframe> width="100%" height="600" style="border:none;border-radius:12px;" allowfullscreen></iframe>
``` ```
You never write these snippets by hand — `public/doxa-maps/manifest.json` lists a ready-made You never write these snippets by hand — `public/dx-maps/manifest.json` lists a ready-made
`embed.iframe` and `embed.script` snippet for every profile, and the staging pages show them `embed.iframe` and `embed.script` snippet for every profile, and the staging pages show them
copy-paste ready. Design details: [architecture/embeddability.md](./architecture/embeddability.md). copy-paste ready. Design details: [architecture/embeddability.md](./architecture/embeddability.md).
@ -94,7 +94,7 @@ with `_` or `.` — those are skipped by the build.
Import each with the `@map/…` alias; do not copy their code into your folder. Import each with the `@map/…` alias; do not copy their code into your folder.
6. **Build and verify.** 6. **Build and verify.**
```bash ```bash
BUNDLE=my-new-map bun run build:bundle # builds just yours → public/doxa-maps/my-new-map/my-new-map.js BUNDLE=my-new-map bun run build:bundle # builds just yours → public/dx-maps/my-new-map/my-new-map.js
bun run verify # data guards (fast, no browser) bun run verify # data guards (fast, no browser)
``` ```
A green build is not a working map — the build proves the bundle *compiles*; the guards A green build is not a working map — the build proves the bundle *compiles*; the guards
@ -126,7 +126,7 @@ One parameterized profile can emit **many standalone embed pages** — no extra
} }
``` ```
`bun run build` then emits a page **per instance** at `doxa-maps/<bundle>/<id>/index.html` — `bun run build` then emits a page **per instance** at `dx-maps/<bundle>/<id>/index.html` —
each directly linkable/iframe-able with its preset props baked into the page (the base profile each directly linkable/iframe-able with its preset props baked into the page (the base profile
still accepts live props). Instances *split* one profile into many render targets; `tabs` still accepts live props). Instances *split* one profile into many render targets; `tabs`
*nest* many views into one element — different tools. *nest* many views into one element — different tools.
@ -219,7 +219,7 @@ Pick one home and **every partner site embeds the same map from it** — change
updates everywhere. updates everywhere.
**How it's wired** (one file: `vite.doxa-maps-build-config.json`; full field table in **How it's wired** (one file: `vite.doxa-maps-build-config.json`; full field table in
**[reference.md](./reference.md)**): `bun run build` always writes to `public/doxa-maps/<bundle>/`. **[reference.md](./reference.md)**): `bun run build` always writes to `public/dx-maps/<bundle>/`.
Set `paths` to also copy the tree into another host's folder (e.g. doxa-life's `public/`). Set Set `paths` to also copy the tree into another host's folder (e.g. doxa-life's `public/`). Set
`embedBaseUrl` to the public URL of your chosen home — it generates absolute, paste-anywhere embed `embedBaseUrl` to the public URL of your chosen home — it generates absolute, paste-anywhere embed
snippets (leave it empty for relative URLs that work wherever the folder is dropped). snippets (leave it empty for relative URLs that work wherever the folder is dropped).
@ -240,13 +240,12 @@ Several maps + one layout = a single-page app, served from the same `public/` tr
- `nav.js` — add a sidebar entry (`{ kind, id, label, icon, target }`; `target` controls - `nav.js` — add a sidebar entry (`{ kind, id, label, icon, target }`; `target` controls
desktop/mobile packaging). desktop/mobile packaging).
- the `PROFILES` object in its `index.html` — tell the shell what to mount: - the `PROFILES` object in its `index.html` — tell the shell what to mount:
`{ tag: '<your-element>', src: './doxa-maps/<bundle>/<bundle>.js', views: ['map'] }`. `{ tag: '<your-element>', src: './dx-maps/<bundle>/<bundle>.js', views: ['map'] }`.
3. **Install the shell**: `bun run shell` — copies the wrapper to `public/index.html`, so the 3. **Install the shell**: `bun run shell` — copies the wrapper to `public/index.html`, so the
site root IS the SPA and the bundles sit beneath it. (Also required for the mobile app.) site root IS the SPA and the bundles sit beneath it. (Also required for the mobile app.)
> Note: this OVERWRITES the generated root page, which since 2026-09-22 is the **plugin studio** > Note: the build's own front page — the **Plugin Studio** — is `public/dx-maps/index.html`,
> host. That is the intended trade for this opt-in wrapper, and it is not sticky — the next > inside the output folder, so installing the shell at `public/index.html` does not replace it.
> `bun run build` regenerates the studio root. The studio's own leaf stays at > The next `bun run build` leaves the shell's copy alone and regenerates the output folder.
> `public/doxa-maps/plugin-studio/plugin-studio/`, and the archived gallery at `/__gallery`.
4. **Build & serve**: `bun run build`, then serve `public/` — the shell lazy-loads each bundle 4. **Build & serve**: `bun run build`, then serve `public/` — the shell lazy-loads each bundle
the first time you open its nav entry. the first time you open its nav entry.
@ -257,7 +256,7 @@ Contracts and gotchas (PROFILES shape, tag-vs-bundle names, `--no-token`):
## Build the Android / iOS app (optional) ## Build the Android / iOS app (optional)
**Mobile is separate from `bun run build`.** The default build produces **only** the `doxa-maps` **Mobile is separate from `bun run build`.** The default build produces **only** the `dx-maps`
web folder — the embeddable maps (and the SPA, if configured). It never touches Capacitor. The web folder — the embeddable maps (and the SPA, if configured). It never touches Capacitor. The
Android/iOS app is an **opt-in** step that *reuses* that web build: Capacitor's `webDir` points at Android/iOS app is an **opt-in** step that *reuses* that web build: Capacitor's `webDir` points at
`public/` (`appId: life.doxa.maps`, `appName: "Doxa Maps"`), so `cap sync` wraps the already-built `public/` (`appId: life.doxa.maps`, `appName: "Doxa Maps"`), so `cap sync` wraps the already-built
@ -296,7 +295,7 @@ The engine is a **runtime parameter** — same bundle, no rebuild. Resolution or
<my-map profile-config='{"profile":"my-map","provider":"maplibre"}'></my-map> <my-map profile-config='{"profile":"my-map","provider":"maplibre"}'></my-map>
<!-- or flip any generated page from the URL --> <!-- or flip any generated page from the URL -->
https://<cdn>/doxa-maps/<bundle>/<profile>/index.html?provider=maplibre https://<cdn>/dx-maps/<bundle>/<profile>/index.html?provider=maplibre
``` ```
`maplibre` runs token-free on the OpenFreeMap style (the right GL script/CSS is injected `maplibre` runs token-free on the OpenFreeMap style (the right GL script/CSS is injected

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 820 KiB

After

Width:  |  Height:  |  Size: 816 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 KiB

After

Width:  |  Height:  |  Size: 827 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 MiB

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 MiB

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 425 KiB

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 891 KiB

After

Width:  |  Height:  |  Size: 954 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 368 KiB

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 751 KiB

After

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 676 KiB

After

Width:  |  Height:  |  Size: 704 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View file

@ -1,35 +1,35 @@
{ {
"cap1-single-element": { "cap1-single-element": {
"bundle": "all-people-groups", "bundle": "all-people-groups",
"sha256": "c990929277d9aa5fd0d4ef78362f7b43e328a7c073ac6bf97aadfde5cb942de9" "sha256": "715fa7a854ea0b819f5dbe4ae256d95264984584e6c6933ed321bbb4230b49f7"
}, },
"cap2-adoption": { "cap2-adoption": {
"bundle": "doxa-simple-map", "bundle": "doxa-simple-map",
"sha256": "0e11603ed79a26b16456f23d582549830365a2c5095ba9ad7ad363c7d1ba9a90" "sha256": "4f7ecd217d30bcb87a86c3f07f9840b2a500c9637037c4ff64a5e10aba009bf5"
}, },
"cap2-engagement": { "cap2-engagement": {
"bundle": "doxa-simple-map", "bundle": "doxa-simple-map",
"sha256": "0e11603ed79a26b16456f23d582549830365a2c5095ba9ad7ad363c7d1ba9a90" "sha256": "4f7ecd217d30bcb87a86c3f07f9840b2a500c9637037c4ff64a5e10aba009bf5"
}, },
"cap2-prayer": { "cap2-prayer": {
"bundle": "doxa-simple-map", "bundle": "doxa-simple-map",
"sha256": "0e11603ed79a26b16456f23d582549830365a2c5095ba9ad7ad363c7d1ba9a90" "sha256": "4f7ecd217d30bcb87a86c3f07f9840b2a500c9637037c4ff64a5e10aba009bf5"
}, },
"cap3-monolithic-research": { "cap3-monolithic-research": {
"bundle": "doxa-research-map", "bundle": "doxa-research-map",
"sha256": "b849fb71c4a96810d840d9f2dfc208c3e13648429e0ae1a1c5784693e6e5774f" "sha256": "ea9638be007ff6127514e663f1ee79644be5fee4ba7340ef8d3ad6bf18cafc69"
}, },
"cap4-upg100-list": { "cap4-upg100-list": {
"bundle": "my-upg-100-list", "bundle": "my-upg-100-list",
"sha256": "fab621d68bdad964da2be67135d0385d40417c6180f008f36cbc0f50076f5e4a" "sha256": "50e6d3caaa49467f9c867e4cc04b5121d965092c96e09a1f3d0a78a7483db480"
}, },
"cap4-upg100-mfe": { "cap4-upg100-mfe": {
"bundle": "my-upg-100-list", "bundle": "my-upg-100-list",
"sha256": "fab621d68bdad964da2be67135d0385d40417c6180f008f36cbc0f50076f5e4a" "sha256": "50e6d3caaa49467f9c867e4cc04b5121d965092c96e09a1f3d0a78a7483db480"
}, },
"cap4-upg100-poster-wall": { "cap4-upg100-poster-wall": {
"bundle": "my-upg-100-list", "bundle": "my-upg-100-list",
"sha256": "fab621d68bdad964da2be67135d0385d40417c6180f008f36cbc0f50076f5e4a" "sha256": "50e6d3caaa49467f9c867e4cc04b5121d965092c96e09a1f3d0a78a7483db480"
}, },
"cap5-doxa-maps-spa": { "cap5-doxa-maps-spa": {
"bundle": "doxa-maps-spa", "bundle": "doxa-maps-spa",
@ -37,51 +37,55 @@
}, },
"cap5-plugin-studio": { "cap5-plugin-studio": {
"bundle": "plugin-studio", "bundle": "plugin-studio",
"sha256": "4d3910b06d55c7fccdba7102be4f9420a0e1368efd1695a6abaf12442b4fcda0" "sha256": "6b8f3ce6cb17075ec26859f12ec0afe0c78bee5dc02b1f9f81b8d699931dc44a"
}, },
"deploy2-mobile-upg100": { "deploy2-mobile-upg100": {
"bundle": "my-upg-100-list", "bundle": "my-upg-100-list",
"sha256": "fab621d68bdad964da2be67135d0385d40417c6180f008f36cbc0f50076f5e4a" "sha256": "50e6d3caaa49467f9c867e4cc04b5121d965092c96e09a1f3d0a78a7483db480"
}, },
"exp-prayer-walk-planner": { "exp-prayer-walk-planner": {
"bundle": "prayer-walk-planner", "bundle": "prayer-walk-planner",
"sha256": "cee5f2806bd5cc8a7fa4c49db200df643d07cf303fd75ee739e606dd9ee93aee" "sha256": "a06c505f4ae24d03f29657065806f65c4dfc36348a1000178d6cb843aaf455e8"
}, },
"exp-wall-of-people-groups": { "exp-wall-of-people-groups": {
"bundle": "wall-of-people-groups", "bundle": "wall-of-people-groups",
"sha256": "ba718e082f77f50d5775ebf43007385b7293f8099e2ca4d79e8ad1753ac4133f" "sha256": "de94f7f53fcfbb757ed8216679e5f563db90cde005ec401104d617fc59c5f105"
},
"lang-arabic-upg100": {
"bundle": "my-upg-100-list",
"sha256": "50e6d3caaa49467f9c867e4cc04b5121d965092c96e09a1f3d0a78a7483db480"
}, },
"prod-big-picture": { "prod-big-picture": {
"bundle": "dashboard-plus-grid", "bundle": "dashboard-plus-grid",
"sha256": "e90739f622678db4c421c76855a5c39882aabd4ed8dbdd65cbe1385a45c67edc" "sha256": "4ec8da3d155150d391ecbc0ac2f96f60bd2efbb12b3b5e8bdf0a869846dbfa16"
}, },
"prod-cities-internet": { "prod-cities-internet": {
"bundle": "cities-internet", "bundle": "cities-internet",
"sha256": "ad145b5a63e7d4b7468a54db53f2f71f397216956fec698d357ae5b841fa8e3b" "sha256": "a62739180efc54c2d9e9216eafe0b290921da6ef83827749ead027945f56dbd7"
}, },
"prod-cities-internet-phone": { "prod-cities-internet-phone": {
"bundle": "cities-internet", "bundle": "cities-internet",
"sha256": "ad145b5a63e7d4b7468a54db53f2f71f397216956fec698d357ae5b841fa8e3b" "sha256": "a62739180efc54c2d9e9216eafe0b290921da6ef83827749ead027945f56dbd7"
}, },
"prod-gospel-resources": { "prod-gospel-resources": {
"bundle": "gospel-resources", "bundle": "gospel-resources",
"sha256": "943fa394060c009d88a3a8e17651980e9c40077be315fb69c98fed92aa17dce0" "sha256": "8b31fa704a5217be541a804d350551b3f7b7d064eb1096eaecb226ad10e578f4"
}, },
"prod-gospel-resources-phone": { "prod-gospel-resources-phone": {
"bundle": "gospel-resources", "bundle": "gospel-resources",
"sha256": "943fa394060c009d88a3a8e17651980e9c40077be315fb69c98fed92aa17dce0" "sha256": "8b31fa704a5217be541a804d350551b3f7b7d064eb1096eaecb226ad10e578f4"
}, },
"prod-prayer-phone": { "prod-prayer-phone": {
"bundle": "doxa-simple-map", "bundle": "doxa-simple-map",
"sha256": "0e11603ed79a26b16456f23d582549830365a2c5095ba9ad7ad363c7d1ba9a90" "sha256": "4f7ecd217d30bcb87a86c3f07f9840b2a500c9637037c4ff64a5e10aba009bf5"
}, },
"prod-regions": { "prod-regions": {
"bundle": "regions-dashboard", "bundle": "regions-dashboard",
"sha256": "8420509dde0ea1390e0d4ae8d49f80a303d17fa8a38c37755a82b07ab09567f5" "sha256": "3609a2c914d6af247cb6618585d234e9a2c857df83bfe03c2a699c2615d1828d"
}, },
"prod-regions-phone": { "prod-regions-phone": {
"bundle": "regions-dashboard", "bundle": "regions-dashboard",
"sha256": "8420509dde0ea1390e0d4ae8d49f80a303d17fa8a38c37755a82b07ab09567f5" "sha256": "3609a2c914d6af247cb6618585d234e9a2c857df83bfe03c2a699c2615d1828d"
}, },
"prod-research-gallery-phone": { "prod-research-gallery-phone": {
"bundle": "doxa-maps-spa", "bundle": "doxa-maps-spa",
@ -89,10 +93,10 @@
}, },
"prod-simple-research": { "prod-simple-research": {
"bundle": "research-map-plus-grid", "bundle": "research-map-plus-grid",
"sha256": "9e081907bd697b5ac63531cedbdb34d3f47a09d1d5aee3634722128af1136d0f" "sha256": "8091ad4c3369928fb322ca69fb9319567fc1351a381e1bffbca518a13e78cb53"
}, },
"prod-simple-research-phone": { "prod-simple-research-phone": {
"bundle": "research-map-plus-grid", "bundle": "research-map-plus-grid",
"sha256": "9e081907bd697b5ac63531cedbdb34d3f47a09d1d5aee3634722128af1136d0f" "sha256": "8091ad4c3369928fb322ca69fb9319567fc1351a381e1bffbca518a13e78cb53"
} }
} }

View file

@ -161,35 +161,23 @@
<text x="1032.0" y="1710" font-size="16" fill="#475569">the checks must pass, main stays working</text> <text x="1032.0" y="1710" font-size="16" fill="#475569">the checks must pass, main stays working</text>
<line x1="750" y1="956" x2="750" y2="1786" stroke="#334155" stroke-width="2.5" stroke-dasharray="2 5" opacity=".0"/> <line x1="750" y1="956" x2="750" y2="1786" stroke="#334155" stroke-width="2.5" stroke-dasharray="2 5" opacity=".0"/>
<rect x="60" y="1780" width="1380" height="230" rx="20" fill="#f8fafc" stroke="#cbd5e1" stroke-width="2"/> <rect x="60" y="1780" width="1380" height="230" rx="20" fill="#f8fafc" stroke="#cbd5e1" stroke-width="2"/>
<text x="750" y="1824" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">main → ONE build → public/doxa-maps/ → five ways to ship</text> <text x="750" y="1824" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">main → ONE build → one web folder → three outputs</text>
<rect x="80.0" y="1852" width="236" height="130" rx="14" fill="#fff" stroke="#15803d" stroke-width="2.5"/> <rect x="80.0" y="1852" width="420" height="130" rx="14" fill="#fff" stroke="#15803d" stroke-width="2.5"/>
<text x="198.0" y="1874" text-anchor="middle" font-size="13" font-weight="800" fill="#64748b">METHOD 1</text> <text x="290.0" y="1874" text-anchor="middle" font-size="13" font-weight="800" fill="#64748b">OUTPUT 1</text>
<text x="198.0" y="1906" text-anchor="middle" font-size="21" font-weight="800" fill="#0f172a">iframe</text> <text x="290.0" y="1906" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">The web folder</text>
<text x="198.0" y="1932" text-anchor="middle" font-size="15" fill="#475569">any website</text> <text x="290.0" y="1932" text-anchor="middle" font-size="15" fill="#475569">iframe · JS embed · WordPress (for fun)</text>
<rect x="90.0" y="1946" width="216" height="26" rx="13" fill="#15803d" opacity=".13"/> <rect x="190.0" y="1946" width="200" height="26" rx="13" fill="#15803d" opacity=".13"/>
<text x="198.0" y="1964" text-anchor="middle" font-size="11.5" font-weight="800" fill="#15803d">PRODUCTION</text> <text x="290.0" y="1964" text-anchor="middle" font-size="12" font-weight="800" fill="#15803d">PRODUCTION</text>
<rect x="356.0" y="1852" width="236" height="130" rx="14" fill="#fff" stroke="#15803d" stroke-width="2.5"/> <rect x="540.0" y="1852" width="420" height="130" rx="14" fill="#fff" stroke="#15803d" stroke-width="2.5"/>
<text x="474.0" y="1874" text-anchor="middle" font-size="13" font-weight="800" fill="#64748b">METHOD 2</text> <text x="750.0" y="1874" text-anchor="middle" font-size="13" font-weight="800" fill="#64748b">OUTPUT 2</text>
<text x="474.0" y="1906" text-anchor="middle" font-size="21" font-weight="800" fill="#0f172a">Mobile app</text> <text x="750.0" y="1906" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">Mobile app</text>
<text x="474.0" y="1932" text-anchor="middle" font-size="15" fill="#475569">Capacitor</text> <text x="750.0" y="1932" text-anchor="middle" font-size="15" fill="#475569">the same folder, wrapped by Capacitor</text>
<rect x="366.0" y="1946" width="216" height="26" rx="13" fill="#15803d" opacity=".13"/> <rect x="650.0" y="1946" width="200" height="26" rx="13" fill="#15803d" opacity=".13"/>
<text x="474.0" y="1964" text-anchor="middle" font-size="11.5" font-weight="800" fill="#15803d">BUILT</text> <text x="750.0" y="1964" text-anchor="middle" font-size="12" font-weight="800" fill="#15803d">BUILT</text>
<rect x="632.0" y="1852" width="236" height="130" rx="14" fill="#fff" stroke="#15803d" stroke-width="2.5"/> <rect x="1000.0" y="1852" width="420" height="130" rx="14" fill="#fff" stroke="#d97706" stroke-width="2.5"/>
<text x="750.0" y="1874" text-anchor="middle" font-size="13" font-weight="800" fill="#64748b">METHOD 3</text> <text x="1210.0" y="1874" text-anchor="middle" font-size="13" font-weight="800" fill="#64748b">OUTPUT 3</text>
<text x="750.0" y="1906" text-anchor="middle" font-size="21" font-weight="800" fill="#0f172a">JavaScript embed</text> <text x="1210.0" y="1906" text-anchor="middle" font-size="22" font-weight="800" fill="#0f172a">Desktop app</text>
<text x="750.0" y="1932" text-anchor="middle" font-size="15" fill="#475569">script + SRI</text> <text x="1210.0" y="1932" text-anchor="middle" font-size="15" fill="#475569">the same folder, wrapped by Tauri</text>
<rect x="642.0" y="1946" width="216" height="26" rx="13" fill="#15803d" opacity=".13"/> <rect x="1110.0" y="1946" width="200" height="26" rx="13" fill="#d97706" opacity=".13"/>
<text x="750.0" y="1964" text-anchor="middle" font-size="11.5" font-weight="800" fill="#15803d">PRODUCTION</text> <text x="1210.0" y="1964" text-anchor="middle" font-size="12" font-weight="800" fill="#d97706">PROVEN · NOT YET LIVE</text>
<rect x="908.0" y="1852" width="236" height="130" rx="14" fill="#fff" stroke="#d97706" stroke-width="2.5"/>
<text x="1026.0" y="1874" text-anchor="middle" font-size="13" font-weight="800" fill="#64748b">METHOD 4</text>
<text x="1026.0" y="1906" text-anchor="middle" font-size="21" font-weight="800" fill="#0f172a">WordPress</text>
<text x="1026.0" y="1932" text-anchor="middle" font-size="15" fill="#475569">Disciple Tools</text>
<rect x="918.0" y="1946" width="216" height="26" rx="13" fill="#d97706" opacity=".13"/>
<text x="1026.0" y="1964" text-anchor="middle" font-size="11.5" font-weight="800" fill="#d97706">PROVEN · NOT YET LIVE</text>
<rect x="1184.0" y="1852" width="236" height="130" rx="14" fill="#fff" stroke="#d97706" stroke-width="2.5"/>
<text x="1302.0" y="1874" text-anchor="middle" font-size="13" font-weight="800" fill="#64748b">METHOD 5</text>
<text x="1302.0" y="1906" text-anchor="middle" font-size="21" font-weight="800" fill="#0f172a">Desktop</text>
<text x="1302.0" y="1932" text-anchor="middle" font-size="15" fill="#475569">Tauri</text>
<rect x="1194.0" y="1946" width="216" height="26" rx="13" fill="#d97706" opacity=".13"/>
<text x="1302.0" y="1964" text-anchor="middle" font-size="11.5" font-weight="800" fill="#d97706">PROVEN · NOT YET LIVE</text>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -14,7 +14,7 @@ Each bundle is one self-contained `.js` file that registers a custom HTML elemen
| **Fix a legend or control for ALL maps** | edit it once in `library/` (`@map`); rebuild | | **Fix a legend or control for ALL maps** | edit it once in `library/` (`@map`); rebuild |
| **Add a colour strategy** | a new file in `@map/colors/` (shared) or the bundle's `src/colors/` (map-private) | | **Add a colour strategy** | a new file in `@map/colors/` (shared) or the bundle's `src/colors/` (map-private) |
| **See my change while working** | `bun run dev` → http://localhost:5173/ (the Plugin Studio) | | **See my change while working** | `bun run dev` → http://localhost:5173/ (the Plugin Studio) |
| **Build everything** | `BUILD_ALLOW=1 bun run build` → `public/doxa-maps/<bundle>/<bundle>.js` | | **Build everything** | `BUILD_ALLOW=1 bun run build` → `public/dx-maps/<bundle>/<bundle>.js` |
| **Build just one bundle** | `BUNDLE=<name> bun x vite build` | | **Build just one bundle** | `BUNDLE=<name> bun x vite build` |
| **See a change on the served site** | rebuild, then refresh (the dev server never writes `public/`) | | **See a change on the served site** | rebuild, then refresh (the dev server never writes `public/`) |
| **Repoint the API without a rebuild** | set `window.MAP_APP_API_URL` before the bundle loads | | **Repoint the API without a rebuild** | set `window.MAP_APP_API_URL` before the bundle loads |
@ -86,7 +86,7 @@ doxa-maps/
├── deployment/ ← capacitor/ (mobile) · wordpress/ · src-tauri/ (desktop) ├── deployment/ ← capacitor/ (mobile) · wordpress/ · src-tauri/ (desktop)
├── public/ ← THE SERVED WEB-ROOT, committed: ├── public/ ← THE SERVED WEB-ROOT, committed:
│ ├── index.html the Plugin Studio host page │ ├── index.html the Plugin Studio host page
│ ├── doxa-maps/<bundle>/ built bundles + manifest.json + leaf pages │ ├── dx-maps/<bundle>/ built bundles + manifest.json + leaf pages
│ └── assets/ posters & data the maps fetch at runtime │ └── assets/ posters & data the maps fetch at runtime
├── vite.config.js ← multi-entry: discovers plugin-designs/<name>/ folders ├── vite.config.js ← multi-entry: discovers plugin-designs/<name>/ folders
└── vite.doxa-maps-build-config.json ← output tree name + copy destinations + token settings └── vite.doxa-maps-build-config.json ← output tree name + copy destinations + token settings
@ -112,7 +112,7 @@ colours or language with **no rebuild**.
// also per page via ?provider= or window.MAP_PROVIDER // also per page via ?provider= or window.MAP_PROVIDER
"assetsBase": "/assets", // where tables/, boundaries/, previews/ are served (see below) "assetsBase": "/assets", // where tables/, boundaries/, previews/ are served (see below)
"shareUrl": "https://…", // optional; the WHOLE url the Share button links/embeds "shareUrl": "https://…", // optional; the WHOLE url the Share button links/embeds
"embedBaseUrl": "https://…/doxa-maps", // optional; tree root the Share button joins embed pages to "embedBaseUrl": "https://…/dx-maps", // optional; tree root the Share button joins embed pages to
"tabs": [ /* parameterized screens read this */ ] "tabs": [ /* parameterized screens read this */ ]
}'></doxa-map> }'></doxa-map>
``` ```
@ -139,7 +139,7 @@ i18n, under a **distinct** tag, since `customElements.define()` is one-shot per
| Script | What it does | | Script | What it does |
|---|---| |---|---|
| `bun run dev` | Vite dev server with hot reload at http://localhost:5173. **`/` is the Plugin Studio**; `/__gallery` is the archived bundle index. Serves every bundle at its own html path and `/assets/*` from `public/assets/`. **Writes nothing to `public/`.** | | `bun run dev` | Vite dev server with hot reload at http://localhost:5173. **`/` is the Plugin Studio**; `/__gallery` is the archived bundle index. Serves every bundle at its own html path and `/assets/*` from `public/assets/`. **Writes nothing to `public/`.** |
| `BUILD_ALLOW=1 bun run build` | `clean` → one `vite build` per `plugin-designs/*/` folder → `tree` → static gates. Refuses (exit 75) without `BUILD_ALLOW=1`, because it wipes `public/doxa-maps/` first. Takes a lock; `BUILD_LOCK_WAIT=600` queues instead of failing | | `BUILD_ALLOW=1 bun run build` | `clean` → one `vite build` per `plugin-designs/*/` folder → `tree` → static gates. Refuses (exit 75) without `BUILD_ALLOW=1`, because it wipes `public/dx-maps/` first. Takes a lock; `BUILD_LOCK_WAIT=600` queues instead of failing |
| `BUNDLE=<name> bun x vite build` | builds ONE bundle without cleaning the rest of the tree | | `BUNDLE=<name> bun x vite build` | builds ONE bundle without cleaning the rest of the tree |
| `bun run build:watch` | rebuilds into `public/` on every change | | `bun run build:watch` | rebuilds into `public/` on every change |
| `bun run clean` | wipes and recreates the `public/<name>/` output tree | | `bun run clean` | wipes and recreates the `public/<name>/` output tree |
@ -153,9 +153,9 @@ i18n, under a **distinct** tag, since `customElements.define()` is one-shot per
by `BUNDLE=<name>`) as a self-contained **IIFE**; the full build loops every folder. Sizes differ with what each by `BUNDLE=<name>`) as a self-contained **IIFE**; the full build loops every folder. Sizes differ with what each
bundle imports (simple ≈ 420 KB, research ≈ 1.3 MB). bundle imports (simple ≈ 420 KB, research ≈ 1.3 MB).
**Where output goes.** One bundle → one file: `public/doxa-maps/<bundle>/<bundle>.js`, with CSS injected by **Where output goes.** One bundle → one file: `public/dx-maps/<bundle>/<bundle>.js`, with CSS injected by
JS (no separate `.css`). Beyond it, `tree` writes a standalone, iframe-able `index.html` per bundle, per JS (no separate `.css`). Beyond it, `tree` writes a standalone, iframe-able `index.html` per bundle, per
profile and per preset instance, plus `public/doxa-maps/manifest.json` with ready-made profile and per preset instance, plus `public/dx-maps/manifest.json` with ready-made
`embed.iframe` / `embed.script` snippets for every profile. Full design: `embed.iframe` / `embed.script` snippets for every profile. Full design:
[architecture/embeddability.md](./architecture/embeddability.md). Preset instances: [architecture/embeddability.md](./architecture/embeddability.md). Preset instances:
[how-to.md](./how-to.md#ship-n-preset-variants-of-one-screen-instances). [how-to.md](./how-to.md#ship-n-preset-variants-of-one-screen-instances).
@ -200,7 +200,7 @@ export MAPBOX_TOKEN=$(python3 -c "import json;print(json.load(open('vite.doxa-ma
```sh ```sh
( node internal/serve-strict.mjs --port 4950 & ) # serve the built tree, loopback only ( node internal/serve-strict.mjs --port 4950 & ) # serve the built tree, loopback only
B=http://127.0.0.1:4950/doxa-maps; ARGS="" B=http://127.0.0.1:4950/dx-maps; ARGS=""
for p in demo-map/demo-map cities-internet/connectivity-map doxa-research-map/research-map \ for p in demo-map/demo-map cities-internet/connectivity-map doxa-research-map/research-map \
country-posters/countries-map doxa-simple-map/doxa-simple-map \ country-posters/countries-map doxa-simple-map/doxa-simple-map \
gospel-resources/resources-map my-upg-100-list/upg-100-list my-upg-100-list/upg-100-mfe; do gospel-resources/resources-map my-upg-100-list/upg-100-list my-upg-100-list/upg-100-mfe; do
@ -239,7 +239,7 @@ Edit this one file, then rebuild.
| Field | What it does | | Field | What it does |
|---|---| |---|---|
| `name` | output folder name (default `doxa-maps`) | | `name` | output folder name (`dx-maps`; a config with no name falls back to `doxa-maps`) |
| `paths` | **extra** folders to copy the whole built tree into — point at a host repo's `public/` and that host serves the maps. Empty ⇒ `public/<name>/` only | | `paths` | **extra** folders to copy the whole built tree into — point at a host repo's `public/` and that host serves the maps. Empty ⇒ `public/<name>/` only |
| `embedBaseUrl` | the public URL the tree is served from, for **absolute, paste-anywhere** snippets. Empty ⇒ relative URLs (position-independent; required for Capacitor) | | `embedBaseUrl` | the public URL the tree is served from, for **absolute, paste-anywhere** snippets. Empty ⇒ relative URLs (position-independent; required for Capacitor) |
| `token` | a `pk.` public Mapbox token baked into every page, so a map works on any host. It is public by design — protected by URL restrictions in the Mapbox dashboard, not by secrecy; `check:public-hygiene` allows `pk.` and fails on `sk.` | | `token` | a `pk.` public Mapbox token baked into every page, so a map works on any host. It is public by design — protected by URL restrictions in the Mapbox dashboard, not by secrecy; `check:public-hygiene` allows `pk.` and fails on `sk.` |

View file

@ -50,21 +50,25 @@ if (!existsSync(SRC)) {
const src = readFileSync(SRC, 'utf8') const src = readFileSync(SRC, 'utf8')
// Each `await shot('name', '/path', { ...opts })` — capture the opts blob so we can read `vp`. // Each `await shot('name', '/path', { ...opts })` — capture the opts blob so we can read `vp`.
const RE = /shot\(\s*'([^']+)'\s*,\s*'([^']*)'\s*(?:,\s*(\{[\s\S]*?\}))?\s*\)/g const RE = /shot\(\s*'([^']+)'\s*,\s*(?:'([^']*)'|`([^`]*)`)\s*(?:,\s*(\{[\s\S]*?\}))?\s*\)/g
const shots = [] const shots = []
for (const m of src.matchAll(RE)) { for (const m of src.matchAll(RE)) {
const [, name, path, opts = ''] = m const [, name, quoted, templated, opts = ''] = m
const path = quoted ?? templated // '…' or `/${TREE_NAME}/…` — compared as written, so either form is consistent
const vp = opts.match(/vp:\s*\{\s*width:\s*(\d+)\s*,\s*height:\s*(\d+)/) const vp = opts.match(/vp:\s*\{\s*width:\s*(\d+)\s*,\s*height:\s*(\d+)/)
shots.push({ name, path, vp: vp ? `${vp[1]}x${vp[2]}` : 'default' }) shots.push({ name, path, vp: vp ? `${vp[1]}x${vp[2]}` : 'default' })
} }
// A matcher that finds almost nothing reads exactly like a clean pass — see check-quoted-scripts. // A matcher that finds almost nothing reads exactly like a clean pass — see check-quoted-scripts.
const MIN_EXPECTED = 8 // FLOOR: every `await shot(` call must be parsed. If not, the matcher has drifted from the file (it did
// once: a codemod turned the paths into template literals and this gate saw 2 of 26), so the result is
// NOT MEASURED (exit 75), never a pass and never a product failure.
const CALLS = src.split('\n').filter((l, i, a) => /await shot\(/.test(l) && !/shot-floor: dynamic/.test(a[i - 1] || '')).length
console.log(`SHOT-DISTINCTNESS: ${shots.length} shot(s) declared.`) console.log(`SHOT-DISTINCTNESS: ${shots.length} shot(s) declared.`)
if (shots.length < MIN_EXPECTED) { if (shots.length !== CALLS) {
console.error(`\nSHOT-DISTINCTNESS: FAIL — only ${shots.length} shots parsed (expected >= ${MIN_EXPECTED}).`) console.error(`\nSHOT-DISTINCTNESS: NOT MEASURED — parsed ${shots.length} of ${CALLS} shot( calls in readme-shots.mjs.`)
console.error(' The MATCHER is broken, not the shot list. Fix the regex before trusting this.') console.error(' The MATCHER is out of date with the file, not the shot list. Fix the regex, then re-run.')
process.exit(1) process.exit(75)
} }
const bySignature = new Map() const bySignature = new Map()

View file

@ -80,7 +80,19 @@ function stamp(file) {
// Parse `await shot('name', '/path/...')` — the generator is the source of truth for its own map. // Parse `await shot('name', '/path/...')` — the generator is the source of truth for its own map.
const src = readFileSync(SHOTS_SRC, 'utf8') const src = readFileSync(SHOTS_SRC, 'utf8')
const shots = [...src.matchAll(/shot\(\s*'([^']+)'\s*,\s*'([^']*)'/g)].map(m => ({ name: m[1], path: m[2] })) // Paths are written either as '...' or as `/${TREE_NAME}/...` (the tree name comes from the build
// config); accept both and resolve the placeholder, or a template-literal shot would be silently skipped.
const shots = [...src.matchAll(/shot\(\s*'([^']+)'\s*,\s*(['`])([^'`]*)\2/g)]
.map(m => ({ name: m[1], path: m[3].replace(/\$\{TREE_NAME\}/g, TREE_NAME) }))
// FLOOR: every `await shot(` call must be parsed, or the gate would silently check fewer shots than
// exist (it did: after the TREE_NAME codemod it compared 2 of 26 and passed). Mismatch = NOT MEASURED.
{
const calls = src.split('\n').filter((l, i, a) => /await shot\(/.test(l) && !/shot-floor: dynamic/.test(a[i - 1] || '')).length
if (shots.length !== calls) {
console.error(`SHOT-FRESHNESS: NOT MEASURED — parsed ${shots.length} of ${calls} shot( calls in readme-shots.mjs; the matcher is out of date with the file.`)
process.exit(75)
}
}
const stale = [], missing = [], skipped = [] const stale = [], missing = [], skipped = []
let checked = 0 let checked = 0

View file

@ -390,6 +390,9 @@ await shot('prod-gospel-resources-phone', `/${TREE_NAME}/gospel-resources/resour
await shot('exp-wall-of-people-groups', `/${TREE_NAME}/wall-of-people-groups/wall-of-people-groups/index.html`, { wait: 12000, minElements: 40 }) await shot('exp-wall-of-people-groups', `/${TREE_NAME}/wall-of-people-groups/wall-of-people-groups/index.html`, { wait: 12000, minElements: 40 })
await shot('exp-prayer-walk-planner', `/${TREE_NAME}/prayer-walk-planner/prayer-walk-planner/index.html`, { wait: 12000, minElements: 40 }) await shot('exp-prayer-walk-planner', `/${TREE_NAME}/prayer-walk-planner/prayer-walk-planner/index.html`, { wait: 12000, minElements: 40 })
// ---- LANGUAGES: UPG-100 in Arabic (?lang=ar), right-to-left — the most completely translated map ----
await shot('lang-arabic-upg100', `/${TREE_NAME}/my-upg-100-list/upg-100-list/index.html?lang=ar`, { wait: 12000, minElements: 200 })
// ---- DEPLOY2: mobile — the UPG-100 list as the phone app ---- // ---- DEPLOY2: mobile — the UPG-100 list as the phone app ----
// The mobile example is UPG-100 (ruled 2026-09-24; Simple Research was retired as the example because // The mobile example is UPG-100 (ruled 2026-09-24; Simple Research was retired as the example because
// it was never built for phones). Shot once its owner marked the phone layout ready at 88a566f: // it was never built for phones). Shot once its owner marked the phone layout ready at 88a566f:
@ -403,6 +406,7 @@ await shot('deploy2-mobile-upg100', `/${TREE_NAME}/my-upg-100-list/upg-100-mfe/i
// server is up: DXLIFE_URL=http://127.0.0.1:<port> bun run shots. Without it the shot is skipped // server is up: DXLIFE_URL=http://127.0.0.1:<port> bun run shots. Without it the shot is skipped
// and the committed image stays as it is. // and the committed image stays as it is.
if (process.env.DXLIFE_URL) { if (process.env.DXLIFE_URL) {
// shot-floor: dynamic — the URL comes from DXLIFE_URL at run time, so the gates cannot parse it
await shot('deploy3-js-embed-dxlife', process.env.DXLIFE_URL.replace(/\/$/, '') + '/research', await shot('deploy3-js-embed-dxlife', process.env.DXLIFE_URL.replace(/\/$/, '') + '/research',
{ vp: { width: 1440, height: 1100 }, wait: 16000, tokenFree: true, minElements: 300 }) { vp: { width: 1440, height: 1100 }, wait: 16000, tokenFree: true, minElements: 300 })
} else if (!ONLY || ONLY.includes('deploy3-js-embed-dxlife')) { } else if (!ONLY || ONLY.includes('deploy3-js-embed-dxlife')) {