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:
parent
f0ceefa6db
commit
19e1213e71
7 changed files with 208 additions and 435 deletions
403
CONTRIBUTING.md
403
CONTRIBUTING.md
|
|
@ -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
113
README.md
|
|
@ -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
18
docs/README.md
Normal 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.
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
99
docs/architecture/patterns-and-provenance.md
Normal file
99
docs/architecture/patterns-and-provenance.md
Normal 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) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Reference in a new issue