docs: skimmer pass — README to 1 diagram + 2 tables; how-to task index; docs/ router; CONTRIBUTING 384→61 (patterns+history → architecture)

This commit is contained in:
doxa-maps 2026-09-11 18:42:35 +00:00
parent f0ceefa6db
commit 19e1213e71
7 changed files with 208 additions and 435 deletions

View file

@ -1,384 +1,61 @@
# Contributing to doxa-maps — building, embedding, and shipping a map bundle
# Contributing to doxa-maps
Maps sit on the edge of your ecosystem: built once, they migrate with you.
WordPress → Nuxt → whatever comes next, the maps come along unchanged. Embed them
anywhere with a single script tag.
Everything a contributor needs, short. Recipes live in [docs/how-to.md](./docs/how-to.md),
lookups in [docs/reference.md](./docs/reference.md), depth in
[docs/architecture/](./docs/architecture/).
> **TL;DR — the one rule that surprises everyone:**
> To see your map changes in the served site, run **`bun run build`** — **NOT
> `bun run dev`**. The build is what writes your bundle into `public/`, the
> served web-root. There is no HMR across that boundary.
> `bun run dev` is the fast HMR loop for working on a map **in isolation**.
## The mental model
This is a **standalone Vite project** with no host framework of any kind. It
compiles each map into a self-contained `.js` bundle — an IIFE that registers a
custom element — which any page can load as a plain `<script src>` static asset.
That single fact is what gives the maps their reach: the same artifact runs three
ways without modification.
1. In **this project's** own dev server (isolated HMR staging).
2. In **the app shell** at `public/index.html`, which mounts several bundles as
one single-page app — the same page a native build wraps.
3. On **any other site** (loaded from wherever `public/` is served).
## The mental model — three zones
```
library/ + app-profiles/ + layouts/
│
│ bun run build
▼
public/ ← the served web-root, committed
├── index.html the app shell (mounts several bundles)
├── doxa-maps/<bundle>/ one IIFE per bundle — what a partner embeds
└── assets/ posters and data the maps fetch at runtime
│
├──► served by any static host → the app, and embeddable bundles
└──► copied by `cap sync` → the Android / iOS web assets
library/ (@map) app-profiles/<map>/ internal/
shared, parameterized ──► your map: its screens ──► the build machine
legend · map · controls (.vue) + private colors, (scripts, staging)
REUSE, never customize api wiring, components NEVER touch
per-map BUILD EVERYTHING HERE
```
Because a bundle is a **build artifact** in `public/` — not a source import — a
served page only sees a change **after you rebuild**.
Editing `.vue`/`.js` files here does nothing to the parent until the build runs.
Rule of thumb: **local by default** — code moves into `library/` only when a *second* map
genuinely needs it. One fix in `library/` reaches every map on the next build.
---
## Two ways one bundle shows many maps
## The doxa maps template (what's shared, what's yours)
- **Pattern A — parameterized**: one `.vue`, many instances differing only by config
(`*.instances.json` → the build emits one standalone page per instance).
- **Pattern B — multi-profile**: several `.vue` screens in one bundle, selected by
`profile-config.profile`.
This project is built on **the doxa maps template** — an **Apache-2.0**, open-source
base that anyone can use. The template is the **shared half** of the repo:
Pick A when the maps differ by *configuration*, B when they differ by *layout/behavior*.
Full walkthrough + the migration story:
[docs/architecture/patterns-and-provenance.md](./docs/architecture/patterns-and-provenance.md).
- `library/` (aliased `@map`) — the shared component library: legends, toolbars,
composables, color strategies, i18n, the data-source engine.
- `vite.config.js` and the build tooling — the multi-bundle build pipeline.
- the **bundle pattern** itself — one folder per map under `app-profiles/`, each
`.vue` a profile, each folder compiling to one self-contained `<script src>` bundle.
## The two build steps (never confuse them)
What you add on top — your folders under `app-profiles/<your-map>/` — is **yours**:
your color choices, your data connections, your i18n, your maps. The template is
common to everyone; your application profiles are private and uniquely yours.
> **This host ships a tree-shaken build of the template.** Each built bundle only
> includes the parts of the shared template a given map actually imports — so a
> simple map's `.js` is small and a feature-rich map's is larger. You consume the
> whole Apache template as source, but every output bundle is a lean slice of it.
When this guide says "the template," it means exactly this Apache-2.0 shared
library + build tooling + bundle pattern — nothing host-specific.
### The reuse rule (reusable → `library/`, unique → profile config)
The same rule governs **dashboards** (config-driven CMV kit) and **maps**:
- **Reusable across profiles → `library/` (`@map`).** Logic that more than one profile would
copy lives in the library, declared ONCE. Examples: the dashboard panels and search
header (`library/components/dashboards/`), the geocoder **kind-switch** (`library/composables/
useGeocoderSearch.js` — `KIND_PROPERTY` + the filter-expression builder), the selection
bus, the CSV exporter. If you are about to paste a `FACET_ACC` / kind→property map /
filter loop into a 2nd file: **STOP** — that is the smell the library exists to kill.
- **Unique to one profile → config, not code.** A dashboard's facets / columns / charts /
KPIs / data source / the 4 search-bar definitions live in its `dashboard.config.js`; a
map's colors / data connection / i18n live in its `app-profiles/<map>/` folder.
The headline proof: a dashboard's 4 search bars reuse the SAME `useGeocoderSearch`
composable the map profiles use, swapping only the **sink** (a `busAdapter` that lands the
result on the dashboard's `selectionBus`) — the kind-switch is never re-implemented. See
`library/components/dashboards/README.md` for how a new dashboard reuses the kit.
---
## Two patterns for multiple maps from one bundle
A single bundle can present **several different maps**. There are **two ways** to
do it, and they solve different problems. Neither is "more correct" — pick the one
that matches *why* your maps differ.
> **Vocabulary.** A **bundle** is one folder under `app-profiles/<name>/` → one
> `<name>.js` script. A **profile** is a `.vue` file at that folder's root,
> discovered by `import.meta.glob('./*.vue')` in the bundle's `index.js`. The host
> picks which profile to mount with the `profile` field of `profile-config`; the
> rest of `profile-config` (token, data source, colors, instanceId…) are
> **parameters** passed to that profile.
### Pattern A — Parameterized element (one profile, many instances)
**Use when:** the maps are *the same map* differing only by **configuration** —
data source, color set, starting tab, labels.
One bundle, **one** `.vue` profile. You mount the element as many times as you
like, each with a different `profile-config` — same `profile`, different
parameters. The bundle's script loads **once**; each element instance is
independent (its own Pinia + i18n).
**Example — `doxa-simple-map`:** a single `doxa-simple-map.vue` profile drives
three maps (Engagement / Prayer / Adoption). All three slots use
`profile: 'doxa-simple-map'`; only the surrounding config differs.
```html
<script src="/doxa-maps/doxa-simple-map/doxa-simple-map.js"></script>
<doxa-map profile-config='{"profile":"doxa-simple-map","tk":"pk…","dataSource":"engagement"}'></doxa-map>
<doxa-map profile-config='{"profile":"doxa-simple-map","tk":"pk…","dataSource":"prayer"}'></doxa-map>
```
### Pattern B — Multi-profile bundle (several profiles, selected by name)
**Use when:** the maps are **genuinely different map types** (different `.vue`
logic/layout), but you want them to **share one build** because they import the
same slice of the template.
One bundle, **multiple** `.vue` profiles at the folder root. The host chooses which
profile to mount via the `profile` field — `ProfileLoader` resolves it against the
bundle's `import.meta.glob('./*.vue')` registry (key `./<profile>.vue`). Still
**one script tag**; you select the map per element with `profile`.
**Example — `my-upg-100-list`:** two profiles in one bundle —
`upg-100-list.vue` and `research-map-clone.vue` — both served by the single
`my-upg-100-list-map` element.
```html
<script src="/doxa-maps/my-upg-100-list/my-upg-100-list.js"></script>
<my-upg-100-list-map profile-config='{"profile":"upg-100-list","tk":"pk…"}'></my-upg-100-list-map>
<my-upg-100-list-map profile-config='{"profile":"research-map-clone","tk":"pk…"}'></my-upg-100-list-map>
```
> **Note — profiles are selected by `profile`, not by separate script tags.** A
> bundle ships exactly one `<name>.js`. Whether it has one profile (Pattern A) or
> many (Pattern B), you load that one script and pick the profile per element via
> `profile-config`. (A bundle *may* additionally register more than one element
> **tag** — e.g. `doxa-simple-map` registers both `<doxa-map>` and
> `<doxa-simple-map>` for back-compat — but that's a tag-aliasing concern, separate
> from how many maps the bundle offers.)
### Which to pick
| | Pattern A — parameterized | Pattern B — multi-profile |
| Command | What | Writes to `public/`? |
|---|---|---|
| `.vue` profiles in the bundle | one | several |
| Maps differ by | config **parameters** | distinct `.vue` **logic** |
| Select a map with | different `profile-config` (same `profile`) | the `profile` field |
| Script tags | one | one |
| Example | `doxa-simple-map` (3 instances) | `my-upg-100-list` (2 profiles) |
| `bun run dev` | HMR playground at `localhost:5173` — iterate on a map in isolation | **No** |
| `bun run build` | every bundle → `public/doxa-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`),
then refresh. `dev` never updates the served tree.
## Where this comes from — the migration story
**Two-terminal workflow** for integration work: terminal 1 `bun run build:watch` (rebuilds
into `public/` on save) · terminal 2 serves the host site / `public/`.
These maps were built **modular-first**, as standalone embeddable widgets, while
the host site still ran on **WordPress**. Each map was a `<script src>` + a custom
element you could drop onto any page — never tied to WordPress internals, themes,
or plugins. It was just a self-contained bundle that booted itself.
## Where the bundles go
When the host site **migrated from WordPress to Nuxt**, the maps came across
**unchanged**. Nothing in this project had to be ported, rewritten, or adapted to
the new framework. The Nuxt host simply loads the same IIFE bundle as a static
asset the same way the WordPress page did — `<script src>` then the custom
element. The build target moved from one site's static directory to another's;
the bundles themselves did not change a line.
`public/` is the committed web-root — serve it from anywhere and every map is embeddable from
it (script tag or the generated per-map page). Same-origin vs external-CDN modes, `paths`,
`embedBaseUrl`: [docs/how-to.md → "Where do the maps get served from?"](./docs/how-to.md).
That is the payoff of the modular-first design: **the maps are framework-agnostic
build outputs, not framework components.** A future migration to yet another host
would be the same story — re-point the build output, embed the script tag, done.
## Ground rules
---
## The two CDN modes
A built bundle is delivered to the page by exactly one of two mechanisms. They are
not configured anywhere in this project — they are simply *where the page loads the
`<script src>` from*. The bundle code is identical in both.
### Same-origin mode (your own site serves it)
`bun run build` writes the bundle into `public/doxa-maps/<bundle>/`, the served
web-root. Whatever hosts that folder serves it from its own domain,
e.g. `/doxa-maps/<bundle>/<bundle>.js`. The host *is* the CDN — same origin, no third party.
```html
<!-- a page on the same origin that serves public/ -->
<script src="/doxa-maps/<bundle>/<bundle>.js"></script>
<doxa-map profile-config='{ ... }'></doxa-map>
```
This is the default for any page served from the same web-root. The
build writes straight to where the host reads, so there is no copy step, symlink,
or upload — see "Where the bundles go" below.
### External CDN mode (a third party serves it)
The exact same built `<bundle>.js` can be published to a **public CDN** and loaded
from an absolute URL by a page on **any other site**. Nothing about the bundle
changes — it is self-contained, registers its own custom element, and pulls the
Mapbox peer from Mapbox's own CDN.
```html
<!-- a page on a completely separate site -->
<script src="https://cdn.example.org/doxa-maps/<bundle>/<bundle>.js"></script>
<doxa-map profile-config='{ ... }'></doxa-map>
```
The choice between modes is purely **the `src` URL on the page** — relative
(`/doxa-maps/...`, same-origin) vs absolute (`https://cdn.../...`, external).
The build itself only ever produces one thing: a portable IIFE.
Either way the map needs a **Mapbox token**, and a page outside this web-root has
no automatic way to get one. Pass it in the element's `profile-config` as `tk`, or
set `window.MAP_TOKEN_URL` to an endpoint that returns it — the same contract the
app shell and the generated pages use. Without one the bundle loads and the
basemap stays blank.
### Partner-site embedding
A **partner site** is any external site that wants to show one of these maps. It
uses **external CDN mode**: it loads the Mapbox peer scripts, then the bundle from
a CDN URL, then drops in the custom element with its own `profile-config`. No
framework, no build step, and no access to this repo is required on their end.
```html
<!-- everything a partner page needs -->
<link href="https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/v3.24.0/mapbox-gl.js"></script>
<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://cdn.example.org/doxa-maps/<bundle>.js"></script>
<doxa-map profile-config='{"profile":"<bundle>","tk":"pk.eyJ..."}'></doxa-map>
```
Because the bundle is the same artifact in every mode, a map you tested in
isolation here, and that runs in the app shell, will run on a partner site too —
the only difference is the URL the partner points their script tag at.
---
## The two build steps
There are two commands, and they do **different jobs**. Knowing which is which is
the whole game.
### `bun run dev` — HMR staging (work on a map in isolation)
```bash
bun run dev # Vite dev server for THIS project, with HMR
```
`dev` serves every bundle at its own staging page **inside this project**, with
hot module replacement — save a `.vue`, see it instantly. This is the fast loop
for building and styling a map **on its own**, away from the host.
**`dev` does NOT touch the served web-root.** It writes nothing to
`public/doxa-maps/`. If you only ever run `dev`, the served site keeps showing
the **old** bundle.
### `bun run build` — bundle to `public/doxa-maps/`
```bash
bun run build # build every bundle → public/doxa-maps/<bundle>/<bundle>.js
```
`build` compiles each bundle and writes it into the web-root's static
directory. **This is the only command that updates what the parent (or a CDN)
serves.** After a build, refresh the parent page to see the change.
| | `bun run dev` | `bun run build` |
|---|---|---|
| Purpose | iterate on a map in isolation | publish the bundle for hosts/CDNs |
| HMR | yes — instant on save | n/a (one-shot compile) |
| Writes to `public/doxa-maps/` | **no** | **yes** |
| Updates the served site | **no** | **yes** (after a page refresh) |
| Updates a partner/CDN copy | no | yes (it produces the file to publish) |
> Bun is the primary toolchain (see `bun.lock`); `npm` works identically — the
> scripts are the same. Use whichever the rest of your environment uses.
---
## The two-terminal dev workflow
For day-to-day work you want **two terminal tabs** open at once:
**Tab 1 — this project (the map source).** Pick one:
```bash
# in the doxa-maps repo
bun run dev # HMR staging: iterate on the map in isolation
# — or, when you want the parent to update on every save —
bun run build:watch # rebuild into public/doxa-maps/ on every change
```
**Tab 2 — the app shell.** Run the host's own dev server (from the host
root), and keep its page open in the browser:
```bash
# in the served site root
bun run dev # the host dev server; open the page that embeds the map
```
The split is deliberate:
- **Tab 1 with `bun run dev`** is for *building the map* — fast HMR, no host
involved. When it looks right, run `bun run build` once to push it across.
- **Tab 1 with `bun run build:watch`** is for *integration work* — every save
rebuilds into the web-root, and the **Tab 2** host page picks up the
new bundle on refresh. This is the closest you get to a live loop across the
boundary.
You do not need both at once. Use `dev` while shaping a map, switch to
`build`/`build:watch` when you want to see it inside the host.
---
## Where the bundles go (and why it just works)
`vite.config.js` sets the output directory **three levels up**, into the parent
app's public assets:
```js
build: {
outDir: resolve(__dirname, 'public/doxa-maps'), // ← the served web-root
emptyOutDir: false, // ← never wipes sibling bundles
}
```
- The build writes straight into **`public/doxa-maps/`**, which is inside the
served web-root — so no copy step or symlink is needed; it writes exactly where
a host reads. (To serve a bundle from an **external CDN**, publish that same
file to the CDN.)
- `emptyOutDir: false` means a build of **one** bundle leaves the **other**
bundles untouched. You can rebuild a single map without breaking the rest.
Each folder under `app-profiles/<bundle>/` produces exactly one file:
`public/doxa-maps/<bundle>/<bundle>.js`. `bun run build` loops over every
`app-profiles/*/` folder and builds them all.
### Build just one bundle (faster iteration)
`bun run build` rebuilds every bundle. To rebuild only the one you're editing:
```bash
BUNDLE=<bundle-name> bun run build:bundle
# e.g. BUNDLE=my-map bun run build:bundle → public/doxa-maps/my-map/my-map.js
```
(`<bundle-name>` is the folder name under `app-profiles/`.)
---
## Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Changed a `.vue`, parent shows old map | You ran `dev`, or didn't rebuild | Run `bun run build`, then refresh the parent page |
| Build error: `Set BUNDLE=<name> for build` | Ran `vite build` directly with no bundle selected | Use `bun run build` (loops all) or `BUNDLE=<name> bun run build:bundle` |
| New bundle doesn't appear in parent | Missing `index.html` or `index.js` in the `app-profiles/<bundle>/` folder | Vite auto-discovers a bundle only when both files exist |
| Old bundle still loads after a hard build | Browser cached the static `.js` | Hard-refresh / disable cache in devtools |
| Partner/CDN copy is stale | The CDN still serves the old file | Re-publish the freshly built `<bundle>.js` to the CDN (and bust its cache) |
| Map is blank on a partner page | Mapbox peer scripts not loaded, or no token | Load `mapbox-gl.js` + geocoder **before** the bundle; pass a valid `tk` in `profile-config` |
---
- Never edit `library/` to customize one map (bundle-private code goes in your `src/`).
- Never bake tokens or data into a bundle — everything arrives at runtime via `profile-config`.
- Before a PR: `bun run build && bun run verify` must pass (adds drop-portability + parse gates).
- Colors are values, not logic: [contributing/COLORS.md](./contributing/COLORS.md).
## One-line recap
**Edit here → `bun run dev` to shape it → `bun run build` to ship it.** The build
*is* the bridge: it writes the same portable bundle that the served site serves
(internal CDN) and that partner sites load from a CDN URL (external CDN). `dev`
only runs this project in isolation.
Copy `app-profiles/template-bundle/`, change exactly four things (folder name, tag in
`index.js`, tag/profile strings in `index.html`, `.vue` filenames), build everything inside
your folder from `@map` blocks, `bun run build`, embed the snippet. That's the whole job.

113
README.md
View file

@ -1,71 +1,42 @@
# doxa-maps
**A bundler that turns your maps into small embeddable pieces you can drop onto any website.**
**A bundler that turns your maps into embeddable pieces you can drop onto any website.**
> ### ✨ New here, and you don't write code?
> You can build real, embeddable maps by **describing them to an AI coding agent** — no
> programming required. Start with the friendly, zero-jargon guide:
```
app-profiles/<map>/ bun run build where they run
┌──────────────────┐ ┌──────────────────┐ ┌─────────────────────────┐
│ your map screens │ │ one .js per map │ │ ▸ any partner website │
│ (.vue) + colors + │──────► │ Vue + CSS inlined │────► │ ▸ a SPA (several maps │
│ data wiring │ │ tree-shaken IIFE │ │ in one layout) │
└──────────────────┘ │ + an index.html │ │ ▸ Android/iOS app │
shared library/ ──────► │ per map page │ │ (Capacitor) │
(@map: legend, map, └──────────────────┘ └─────────────────────────┘
controls — reused) public/doxa-maps/… one build → three products
```
| The idea | In one line |
|---|---|
| **Bundle** | one self-contained `.js` = a custom HTML tag (`<my-map>`) — drop it on any page, no framework, no build step for the host |
| **Embeddable by default** | every map also gets a generated standalone page — link it or iframe it (Start.me, Notion, any CMS) |
| **Many maps, one bundle** | a bundle holds as many map screens as you want; add a `.vue`, rebuild |
| **Lean & fast** | tree-shaken, Vue + CSS inlined → **one request**, faster than loading Vue from a CDN, nothing to 404 |
| **JAMstack hot-swap** | data API, token, even the map engine (Mapbox ↔ MapLibre) are runtime settings — change them with **no rebuild** |
| **SPA** | compose several maps into one lazy-loaded layout → a full map site from the same build |
> ✨ **Don't write code?** Describe your map to an AI coding agent — start at
> **[docs/tutorial.md](./docs/tutorial.md)**.
## In plain words
**What a bundler is.** It's a tool that takes your map code and packages it into a small,
ready-to-use file. You write the map once; the bundler produces the shippable piece.
**What it outputs.** Plain `.js` files in the `public/` folder — one per bundle. That's it.
No server, no build step on anyone else's side. Serve the folder from anywhere and the maps work.
**What a bundle is.** A self-contained web component — a custom HTML tag like `<my-map>`. You add
one `<script>` and drop the tag on a page. It carries everything it needs, so it runs on any site,
in any framework, or in none.
**One bundle can hold several maps.** A single bundle can define more than one element
(`<my-map>`, `<coverage-map>`, …). One file, many maps — fewer things to load and manage.
**The bundles are lean.** The build is tree-shaken: only the code a map actually uses gets
included, unused code is dropped. Small files, fast loads — which matters when a map embeds on
someone else's page.
**Embed on other people's websites.** Because each map is just a tag, you can put your maps on a
partner's or customer's site — or in any tool that accepts an embed, like Start.me, Notion, a CMS,
a landing page. Same map, many places. Write it once, embed it everywhere.
**Build a whole app (SPA), not just single maps.** Bundle several elements into one layout and you
get a single-page app made of your maps — lazy-loaded so it stays fast. So the same pieces are both
standalone embeds *and* the building blocks of a full site.
**They're JAMstack, so the API is hot-swappable.** The map is static; the data/service it talks to
is a runtime setting, not baked in. You can point a map at a different API — or swap the map engine
itself (Mapbox ↔ MapLibre) — by changing config, no rebuild. Portable and future-proof.
## Why it's fast (one self-contained file)
Everything a map needs is built into **one file** — the Vue runtime, the components, and the CSS
are all inlined. The browser makes **one request** and the map runs. There's no waterfall of extra
downloads and no dependency on a separate Vue CDN being online or version-matched.
- **One output file.** The JS carries its own CSS (`cssCodeSplit: false`) and its own Vue, built as
a single IIFE. Nothing to wire up, nothing that can 404 on its own.
- **Faster than loading Vue from a CDN.** A CDN copy of Vue is an extra request plus a version you
have to keep in sync; inlining removes both. Fewer moving parts, less technical debt.
- **Lean.** Tree-shaking drops unused code, so the single file stays small even though it's complete.
(The one deliberate exception is the heavy PDF-export library — loaded from a CDN only if someone
actually exports — so the everyday map load never pays for it.)
## Quickstart
Bun is required (`bun.lock` is the committed lockfile — install from <https://bun.sh>).
Bun required ([bun.sh](https://bun.sh); `bun.lock` is committed).
```bash
bun install
bun run build # builds every app-profiles/<bundle>/ → public/doxa-maps/<bundle>/<bundle>.js
bun run dev # HMR staging at http://localhost:5173/ (does NOT update a built public/)
bun run build # every app-profiles/<bundle>/ → public/doxa-maps/<bundle>/<bundle>.js
bun run dev # live playground at http://localhost:5173/ — every map, clickable,
# with a copy-paste embed snippet for each
```
Built output lands in **`public/`**, the committed, served web-root. Serve that folder from
anywhere and the bundles are embeddable from it.
## Embed a map
```html
@ -77,26 +48,24 @@ anywhere and the bundles are embeddable from it.
<my-map profile-config='{"profile":"my-map","tk":"pk.eyJ..."}'></my-map>
```
The bundle is a plain `<script>` web component — the host page needs no build step and no
framework. `profile-config` is read at runtime, so the same bundle re-points at different data,
colors, or API with no rebuild.
Or skip the script tag entirely — iframe the map's generated page:
`/doxa-maps/<bundle>/<profile>/index.html`. Both snippets are pre-generated per map in
`public/doxa-maps/manifest.json`.
## Docs
- **[Why doxa-maps](./docs/why.md)** — the problem, the "map islands" idea, one diagram.
- **[Tutorial](./docs/tutorial.md)** — build your first map, step by step.
- **[How-to](./docs/how-to.md)** — task recipes: embed, add a profile, choose a CDN, set tokens.
- **[Reference](./docs/reference.md)** — file map, config fields, contracts, the `@map` library index.
- **[Explanation](./docs/explanation.md)** — the short "what it is".
- **[Architecture](./docs/architecture/)** — the deep end: contracts, catalogs, wiring.
| Read this | When |
|---|---|
| **[why.md](./docs/why.md)** | 2 min — the problem, the "map islands" idea, one diagram |
| **[tutorial.md](./docs/tutorial.md)** | build your first map, step by step |
| **[how-to.md](./docs/how-to.md)** | task recipes — embed, iframe, SPA, colors, mobile, tokens |
| **[reference.md](./docs/reference.md)** | lookup — config fields, build scripts, contracts |
| **[architecture/](./docs/architecture/)** | the deep end — full contracts and catalogs, when you want them |
**Try it live:** `bun run dev` opens the staging playground — every map, clickable, with a
paste-ready embed snippet for each.
Contributors: see **[CONTRIBUTING.md](./CONTRIBUTING.md)**.
Contributors: **[CONTRIBUTING.md](./CONTRIBUTING.md)**.
## License
Built on the **doxa-maps template (Apache-2.0)** — the shared `library/` and build tooling are a
permissive base everyone shares. Your maps under `app-profiles/<bundle>/` are yours: your color
strategies, API connections, and i18n. Full text: **[LICENSE](./LICENSE)**.
The **doxa-maps template** (`library/` + build tooling) is **Apache-2.0** — a permissive base
everyone shares. Your maps under `app-profiles/<bundle>/` are **yours**: your colors, API
connections, and i18n. Full text: **[LICENSE](./LICENSE)**.

18
docs/README.md Normal file
View file

@ -0,0 +1,18 @@
# docs/ — pick your path
```
"what is this?" "teach me" "how do I …?" "look it up" "all the detail"
│ │ │ │ │
why.md ──────────► tutorial.md ────► how-to.md ────► reference.md ───► architecture/
(2 min, 1 diagram) (first map) (task recipes) (tables) (contracts)
```
- **[why.md](./why.md)** — the problem and the "map islands" idea. Start here.
- **[tutorial.md](./tutorial.md)** — build your first map, no jargon.
- **[how-to.md](./how-to.md)** — recipes: embed, iframe, SPA, colors, CDN, mobile, tokens.
- **[reference.md](./reference.md)** — config fields, build scripts, the `profile-config` contract.
- **[explanation.md](./explanation.md)** — the model in 15 lines.
- **[architecture/](./architecture/)** — the deep end: full contracts, the per-file catalog.
Nothing on the simple path is longer than a few minutes; everything complex lives in
`architecture/`, linked from where you'd need it.

View file

@ -13,6 +13,7 @@ when a simple doc says *"full contract in architecture/…"*, this is where it p
| [embeddability.md](./embeddability.md) | the generated per-map pages + manifest that make every map embeddable by default |
| [app-wrappers.md](./app-wrappers.md) | the SPA shell: `nav.js` + `PROFILES` contracts, `bun run shell` pipeline |
| [poster-generation.md](./poster-generation.md) | the poster/preview generator pipeline |
| [patterns-and-provenance.md](./patterns-and-provenance.md) | the two multi-map patterns in depth + where the design comes from |
Simple docs: [../why.md](../why.md) · [../tutorial.md](../tutorial.md) · [../how-to.md](../how-to.md) ·
[../reference.md](../reference.md) · [../explanation.md](../explanation.md)

View file

@ -0,0 +1,99 @@
# Architecture — provenance & the two multi-map patterns
> Contributor front door: [../../CONTRIBUTING.md](../../CONTRIBUTING.md).
## Where this comes from — the migration story
These maps were built **modular-first**, as standalone embeddable widgets, while
the host site still ran on **WordPress**. Each map was a `<script src>` + a custom
element you could drop onto any page — never tied to WordPress internals, themes,
or plugins. It was just a self-contained bundle that booted itself.
When the host site **migrated from WordPress to Nuxt**, the maps came across
**unchanged**. Nothing in this project had to be ported, rewritten, or adapted to
the new framework. The Nuxt host simply loads the same IIFE bundle as a static
asset the same way the WordPress page did — `<script src>` then the custom
element. The build target moved from one site's static directory to another's;
the bundles themselves did not change a line.
That is the payoff of the modular-first design: **the maps are framework-agnostic
build outputs, not framework components.** A future migration to yet another host
would be the same story — re-point the build output, embed the script tag, done.
---
## Two patterns for multiple maps from one bundle
A single bundle can present **several different maps**. There are **two ways** to
do it, and they solve different problems. Neither is "more correct" — pick the one
that matches *why* your maps differ.
> **Vocabulary.** A **bundle** is one folder under `app-profiles/<name>/` → one
> `<name>.js` script. A **profile** is a `.vue` file at that folder's root,
> discovered by `import.meta.glob('./*.vue')` in the bundle's `index.js`. The host
> picks which profile to mount with the `profile` field of `profile-config`; the
> rest of `profile-config` (token, data source, colors, instanceId…) are
> **parameters** passed to that profile.
### Pattern A — Parameterized element (one profile, many instances)
**Use when:** the maps are *the same map* differing only by **configuration** —
data source, color set, starting tab, labels.
One bundle, **one** `.vue` profile. You mount the element as many times as you
like, each with a different `profile-config` — same `profile`, different
parameters. The bundle's script loads **once**; each element instance is
independent (its own Pinia + i18n).
**Example — `doxa-simple-map`:** a single `doxa-simple-map.vue` profile drives
three maps (Engagement / Prayer / Adoption). All three slots use
`profile: 'doxa-simple-map'`; only the surrounding config differs.
```html
<script src="/doxa-maps/doxa-simple-map/doxa-simple-map.js"></script>
<doxa-map profile-config='{"profile":"doxa-simple-map","tk":"pk…","dataSource":"engagement"}'></doxa-map>
<doxa-map profile-config='{"profile":"doxa-simple-map","tk":"pk…","dataSource":"prayer"}'></doxa-map>
```
### Pattern B — Multi-profile bundle (several profiles, selected by name)
**Use when:** the maps are **genuinely different map types** (different `.vue`
logic/layout), but you want them to **share one build** because they import the
same slice of the template.
One bundle, **multiple** `.vue` profiles at the folder root. The host chooses which
profile to mount via the `profile` field — `ProfileLoader` resolves it against the
bundle's `import.meta.glob('./*.vue')` registry (key `./<profile>.vue`). Still
**one script tag**; you select the map per element with `profile`.
**Example — `my-upg-100-list`:** two profiles in one bundle —
`upg-100-list.vue` and `research-map-clone.vue` — both served by the single
`my-upg-100-list-map` element.
```html
<script src="/doxa-maps/my-upg-100-list/my-upg-100-list.js"></script>
<my-upg-100-list-map profile-config='{"profile":"upg-100-list","tk":"pk…"}'></my-upg-100-list-map>
<my-upg-100-list-map profile-config='{"profile":"research-map-clone","tk":"pk…"}'></my-upg-100-list-map>
```
> **Note — profiles are selected by `profile`, not by separate script tags.** A
> bundle ships exactly one `<name>.js`. Whether it has one profile (Pattern A) or
> many (Pattern B), you load that one script and pick the profile per element via
> `profile-config`. (A bundle *may* additionally register more than one element
> **tag** — e.g. `doxa-simple-map` registers both `<doxa-map>` and
> `<doxa-simple-map>` for back-compat — but that's a tag-aliasing concern, separate
> from how many maps the bundle offers.)
### Which to pick
| | Pattern A — parameterized | Pattern B — multi-profile |
|---|---|---|
| `.vue` profiles in the bundle | one | several |
| Maps differ by | config **parameters** | distinct `.vue` **logic** |
| Select a map with | different `profile-config` (same `profile`) | the `profile` field |
| Script tags | one | one |
| Example | `doxa-simple-map` (3 instances) | `my-upg-100-list` (2 profiles) |
---

View file

@ -7,6 +7,14 @@ first, then use this page to look up a task. For contracts and field tables, see
The three zones govern everything: **reuse `library/` (`@map`), build in
`app-profiles/<your-map>/`, never touch `internal/`.**
**Pick your task:**
[embed on a site](#embed-a-map-on-a-partner-site) · [link/iframe directly](#link-or-iframe-a-map-directly-zero-effort-embed) ·
[add a map](#add-a-new-map-profile) · [imitate a profile](#imitate-an-existing-profile-dont-re-derive-it) ·
[change colors](#change-colors) · [choose a CDN](#where-do-the-maps-get-served-from) ·
[build a SPA](#compose-maps-into-a-spa-the-app-shell) · [mobile app](#build-the-android--ios-app-optional) ·
[API + token](#set-the-api--mapbox-token) · [agent guardrail](#give-your-agent-the-guardrail) ·
[troubleshooting](#troubleshooting)
---
## Embed a map on a partner site

View file

@ -221,6 +221,7 @@ Simple lookups live here; the full contracts, catalogs, and wiring detail live i
| [embeddability.md](./architecture/embeddability.md) | generated per-map pages + manifest — embeddable by default |
| [app-wrappers.md](./architecture/app-wrappers.md) | the SPA shell: `nav.js` + `PROFILES` contracts |
| [poster-generation.md](./architecture/poster-generation.md) | the poster/preview generator pipeline |
| [patterns-and-provenance.md](./architecture/patterns-and-provenance.md) | the two multi-map patterns + provenance |
The **Semantic Tree Legend** component (a reusable hierarchical legend) is documented in
[file-map.md](./architecture/file-map.md#the-semantic-tree-legend-mapcomponentslegendssemantictreelegendvue).