430 lines
23 KiB
Markdown
430 lines
23 KiB
Markdown
# How-to — task recipes
|
|
|
|
Short recipes for specific tasks. New to the project? Read the **[tutorial](./tutorial.md)**
|
|
first, then use this page to look up a task. For contracts and field tables, see
|
|
**[reference.md](./reference.md)**.
|
|
|
|
The three zones govern everything: **reuse `library/` (`@map`), build in
|
|
`plugin-designs/<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) · [preset variants](#ship-n-preset-variants-of-one-screen-instances) · [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) ·
|
|
[swap engine](#swap-the-map-engine-mapbox--maplibre) · [API + token](#set-the-api--mapbox-token) · [agent guardrail](#give-your-agent-the-guardrail) ·
|
|
[troubleshooting](#troubleshooting)
|
|
|
|
---
|
|
|
|
## Embed a map on a partner site
|
|
|
|
A built bundle is a plain `<script src>` plus a custom element — no framework, no build step
|
|
on the partner's end. A partner page loads the Mapbox peer, then the bundle, then drops in the
|
|
element with its own `profile-config`.
|
|
|
|
```html
|
|
<!-- Mapbox peer (once per page, before the bundle) -->
|
|
<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>
|
|
|
|
<!-- The bundle (relative path = host serves it; absolute https = external CDN) -->
|
|
<script src="/dx-maps/my-map/my-map.js"></script>
|
|
|
|
<!-- The map. `profile` = your .vue filename. `tk` = the Mapbox token. -->
|
|
<my-map profile-config='{"profile":"my-map","tk":"pk.eyJ..."}'></my-map>
|
|
```
|
|
|
|
`profile-config` is read at **runtime**, so the same bundle re-points at different data,
|
|
colors, or language with **no rebuild** — just change the JSON on the page. Without a valid
|
|
`tk` the bundle loads but the basemap stays blank (see *Set the API + Mapbox token* below).
|
|
|
|
---
|
|
|
|
## Link or iframe a map directly (zero-effort embed)
|
|
|
|
You don't have to use the script tag. **Every profile also builds its own standalone page** —
|
|
a bare, full-screen map at:
|
|
|
|
```
|
|
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
|
|
Start.me or Notion):
|
|
|
|
```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>
|
|
```
|
|
|
|
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
|
|
copy-paste ready. Design details: [architecture/embeddability.md](./architecture/embeddability.md).
|
|
|
|
**After each release:** iframe embeds update automatically. A script embed with an `integrity`
|
|
hash pins the exact bytes, so re-copy its snippet from `manifest.json` after each release — once
|
|
the bundle changes, the old hash stops matching and the browser refuses to run the script.
|
|
|
|
---
|
|
|
|
## Add a new map profile
|
|
|
|
The fastest path is **copy `template-bundle` and rename it.** A bundle is one folder under
|
|
`plugin-designs/`; its name becomes the bundle name and the output filename. Don't prefix it
|
|
with `_` or `.` — those are skipped by the build.
|
|
|
|
1. **Copy the template.**
|
|
```bash
|
|
cp -r plugin-designs/template-bundle plugin-designs/my-new-map
|
|
```
|
|
2. **Rename the element tag.** In `plugin-designs/my-new-map/index.js`, change the
|
|
`customElements.define('…', …)` tag to your map's tag. The entry must glob the bundle's
|
|
flat profiles and hand them to the loader — it already does this; keep the shape:
|
|
```js
|
|
const profileModules = import.meta.glob('./*.vue') // FLAT — matches only this bundle's screens
|
|
// …app.provide('profileModules', profileModules) inside configureApp…
|
|
customElements.define('my-new-map', buildElement())
|
|
```
|
|
3. **Keep `index.html`.** Vite needs it to discover the bundle; it's also the dev staging
|
|
page. At minimum it ends with `<script type="module" src="./index.js"></script>`.
|
|
4. **Write your profile screen** `my-new-map.vue`, **flat** in the folder (no `profiles/`
|
|
subfolder — the glob is `./*.vue`). The filename (without `.vue`) is the profile name the
|
|
host requests via `profile-config.profile`. Compose it from `@map` blocks and read host
|
|
inputs with `inject` (`mapboxToken`, `dataSource`, `profileConfig`, `instanceId`, and the
|
|
instance-scoped Pinia stores) — never prop-drill, never call `useXxxStore()` directly.
|
|
5. **Pick reusable pieces** from the `@map` library index in **[reference.md](./reference.md)**.
|
|
Import each with the `@map/…` alias; do not copy their code into your folder.
|
|
6. **Build and verify.**
|
|
```bash
|
|
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)
|
|
```
|
|
A green build is not a working map — the build proves the bundle *compiles*; the guards
|
|
prove the data is right and the app renders it. Confirm the pane actually paints; a profile
|
|
that fails to mount looks identical to one that mounted and rendered nothing.
|
|
|
|
> **Two ways to ship several maps from one bundle:** *(A) Parameterized* — one `.vue`, mounted
|
|
> many times with different `profile-config` (same `profile`, different data/colors). *(B)
|
|
> Multi-profile* — several `.vue` files in one folder, selected by the `profile` field. Both
|
|
> ship one script tag. Pick A when maps differ only by config, B when they're genuinely
|
|
> different map types that share the same slice of the library.
|
|
|
|
---
|
|
|
|
## Ship N preset variants of one screen (instances)
|
|
|
|
One parameterized profile can emit **many standalone embed pages** — no extra `.vue`. Drop a
|
|
`<profile>.instances.json` next to the profile:
|
|
|
|
```jsonc
|
|
{
|
|
"profile": "doxa-simple-map",
|
|
"apiBaseUrl": "https://…", // optional
|
|
"instances": [
|
|
{ "id": "prayer", "title": "Prayer Map",
|
|
"props": { "instanceId": "embed-prayer", "dataSource": "pray-tools",
|
|
"tabs": [{ "id": "prayer", "colorStrategy": "prayer" }] } }
|
|
]
|
|
}
|
|
```
|
|
|
|
`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
|
|
still accepts live props). Instances *split* one profile into many render targets; `tabs`
|
|
*nest* many views into one element — different tools.
|
|
|
|
---
|
|
|
|
## Imitate an existing profile (don't re-derive it)
|
|
|
|
You have a map that works and want a second one that behaves almost the same. **Copy the
|
|
profile and change parameters in the copy** — do not build it from scratch, and do not add a
|
|
`mode` flag to the original.
|
|
|
|
> **The rule: PROFILES duplicate. COMPONENTS get promoted and parameterized.**
|
|
> A profile is cheap — a duplicated profile that reads top-to-bottom beats one profile with a
|
|
> flag threaded through 40 branches. A *component* used by two profiles is different: promote
|
|
> it to `library/components/` and give it props. Duplicating a profile duplicates
|
|
> *configuration* (the part that's supposed to differ); duplicating a component duplicates
|
|
> *behaviour* (the part that shouldn't).
|
|
|
|
Steps:
|
|
|
|
1. **Find the profile you're imitating — including nested ones.** A `.vue` profile can contain
|
|
another full profile inside a tab. Search for the *behaviour* you want, not the app name; if
|
|
you copy the wrong level you inherit the wrong thing.
|
|
2. **Copy the folder, then the profile file.** `profile` is the **filename**, not the folder.
|
|
One folder = one bundle = one element; each flat `.vue` inside is one profile.
|
|
3. **Change ONLY named parameters.** If you're typing new logic, you've stopped imitating — go
|
|
find the parameter in the original that already does what you're about to write. Count
|
|
parameter *sites* by diffing the two files, not by memory.
|
|
> **Trap: `onMounted` runs last and overwrites your default.** If the source initializes a
|
|
> value in its mount handler, setting it at declaration is not enough — one constant must
|
|
> drive both the initial `ref` and the `onMounted` path.
|
|
4. **Wire it into the shell registry** (`library/layouts/app-wrapper-material/index.html`, the
|
|
`PROFILES` object). A nav entry with no registry entry renders a silent placeholder. Take
|
|
`src` from the folder, `tag` from `customElements.define` in `index.js`, `profile` from the
|
|
`.vue` filename — never from a guess.
|
|
5. **Load data per tab with `fetch`, never `import`.** The single-file IIFE inlines every
|
|
import, so a static `import` of a data table inlines it into the bundle (one such import
|
|
took two bundles to 2.0 MB and 2.9 MB; per-tab `fetch` brought them to 308 KB and 647 KB).
|
|
6. **Verify it mounts.** Open it and confirm the pane paints — don't infer it from a clean
|
|
build.
|
|
|
|
**One-line test:** are you changing a named constant, or writing logic? Constant → you're
|
|
imitating. Logic → stop.
|
|
|
|
---
|
|
|
|
## Change colors
|
|
|
|
Colors are **values, not logic** — you edit values, the library owns the mechanism. Which of
|
|
two places a color lives follows one rule: **reused by more than one map → shared library;
|
|
only your map uses it → your profile.**
|
|
|
|
**Path A — a shared color (affects every map that uses it).** Edit
|
|
`library/colors/<name>.js` (e.g. `religion.js`) and change a hex in the `PALETTE`. The library
|
|
auto-discovers every `<name>.js` in that folder, so there's no registration step. Be
|
|
deliberate — this changes *every* map using that mode.
|
|
|
|
**Path B — a color private to your map.** Create
|
|
`plugin-designs/<your-map>/src/colors/<name>.js` (copy `template-bundle/src/colors/example-mode.js`
|
|
as a starter). Your bundle's `index.js` runs `registerStrategies(import.meta.glob('./src/colors/*.js', { eager: true }))`
|
|
at startup, merging your local files **over** the shared set. The **filename** derives the
|
|
mode key (`language-family.js` → `languageFamily`):
|
|
|
|
- **Same key as a shared strategy → your file overrides it** (for your map only).
|
|
- **A new key → your file adds a private strategy** (only your map sees it).
|
|
|
|
Because each bundle is its own self-contained IIFE with its own copy of the registry, a local
|
|
strategy can never leak into another map. The required `export default` shape (the strategy
|
|
contract) is in [architecture/color-strategies.md](./architecture/color-strategies.md);
|
|
`religion.js` is the canonical example.
|
|
|
|
---
|
|
|
|
## Where do the maps get served from?
|
|
|
|
Your built maps are just files. To show them on other websites, the files need a **public home**
|
|
on the internet that any site can pull from — that's what a "CDN" is. **You choose the home, and
|
|
there are options:**
|
|
|
|
- **Your own site (doxa-life).** Put the built maps in doxa-life's `public/` folder — its Nuxt
|
|
site serves them, so **doxa-life is the CDN**. No third party.
|
|
- **GitHub.** Push the built maps to a public GitHub repo and serve them free through
|
|
**GitHub Pages or jsDelivr** — **GitHub is the CDN**.
|
|
- **doxa-base's built-in home (Forgejo).** doxa-base ships **Forgejo as a CDN** — push the maps
|
|
there and it serves them, with no extra service to set up.
|
|
- **Anywhere else.** Any static host works — Cloudflare, an S3-style bucket, etc.
|
|
|
|
Pick one home and **every partner site embeds the same map from it** — change the map once and it
|
|
updates everywhere.
|
|
|
|
**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/dx-maps/<bundle>/`.
|
|
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
|
|
snippets (leave it empty for relative URLs that work wherever the folder is dropped).
|
|
|
|
> **`dev` never updates a built `public/`.** Run `bun run build` (or `BUNDLE=<name> bun run
|
|
> build:bundle`) and refresh to see a change on the served site. `bun run dev` is for iterating
|
|
> on a map in isolation.
|
|
|
|
---
|
|
|
|
## Compose maps into a SPA (the app shell)
|
|
|
|
Several maps + one layout = a single-page app, served from the same `public/` tree.
|
|
|
|
1. **Pick a wrapper** in `library/layouts/` (`app-wrapper-material` is the full shell: sidebar,
|
|
lazy-loading, i18n).
|
|
2. **Register your map** in two places inside the wrapper:
|
|
- `nav.js` — add a sidebar entry (`{ kind, id, label, icon, target }`; `target` controls
|
|
desktop/mobile packaging).
|
|
- the `PROFILES` object in its `index.html` — tell the shell what to mount:
|
|
`{ 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
|
|
site root IS the SPA and the bundles sit beneath it. (Also required for the mobile app.)
|
|
> Note: the build's own front page — the **Plugin Studio** — is `public/dx-maps/index.html`,
|
|
> inside the output folder, so installing the shell at `public/index.html` does not replace it.
|
|
> The next `bun run build` leaves the shell's copy alone and regenerates the output folder.
|
|
4. **Build & serve**: `bun run build`, then serve `public/` — the shell lazy-loads each bundle
|
|
the first time you open its nav entry.
|
|
|
|
Contracts and gotchas (PROFILES shape, tag-vs-bundle names, `--no-token`):
|
|
[architecture/app-wrappers.md](./architecture/app-wrappers.md).
|
|
|
|
---
|
|
|
|
## Build the Android / iOS app (optional)
|
|
|
|
**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
|
|
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
|
|
folder into a native app. You never need to build mobile to ship the web maps.
|
|
|
|
**Android** (project is committed; needs Android Studio + a JDK):
|
|
|
|
```bash
|
|
bun run apk:debug # build web → cap sync → verify → assemble a debug APK
|
|
bun run apk:install # adb install onto a connected device/emulator
|
|
```
|
|
|
|
Step by step instead: `bun run build` → `bun run cap:sync` → open `deployment/capacitor/android` in Android Studio.
|
|
|
|
**iOS** (scaffold once on a Mac): `./node_modules/.bin/cap add ios`, then `bun run build &&
|
|
./node_modules/.bin/cap sync ios`, and open `deployment/capacitor/ios` in Xcode. Same `webDir: public`.
|
|
|
|
> **Invoke the local bin, not `bunx`.** `bunx` is not on PATH in every environment (it exits 127,
|
|
> *command not found*), which is what broke `cap:sync` → `apk:debug` → `phone` at step one. The
|
|
> Capacitor CLI is installed locally, so `./node_modules/.bin/cap` always resolves. Elsewhere this
|
|
> repo uses the spaced `bun x` form for the same reason.
|
|
|
|
> `cap sync` copies `public/` into the native project's assets. A wrong `webDir` gives a **blank
|
|
> white screen** at launch (empty assets), not an error — `bun run cap:verify` guards against it.
|
|
|
|
---
|
|
|
|
## Swap the map engine (Mapbox ↔ MapLibre)
|
|
|
|
The engine is a **runtime parameter** — same bundle, no rebuild. Resolution order
|
|
(first defined wins): `profile-config.provider` → `?provider=` in the page URL →
|
|
`window.MAP_PROVIDER` → default `auto`.
|
|
|
|
`auto` picks **Mapbox when a token is available** (a `tk` in the profile-config, `window.MAP_TOKEN`,
|
|
`?tk=`, a token pasted once in the tree, the studio's ⚙ Settings, or a configured token URL) and
|
|
**MapLibre otherwise** — keyless, with no request to Mapbox. The repo ships no Mapbox token, so a
|
|
fresh checkout draws every map with MapLibre until you give it one. An explicit `provider` always wins.
|
|
|
|
```html
|
|
<!-- per embed -->
|
|
<my-map profile-config='{"profile":"my-map","provider":"maplibre"}'></my-map>
|
|
|
|
<!-- or flip any generated page from the URL -->
|
|
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
|
|
automatically); the geocoder search bar keeps working in both engines. Its **place** results come
|
|
from Mapbox geocoding, on either engine, and only with a token: without one no request goes to Mapbox,
|
|
the box says "place search needs a Mapbox token", and people-group, language, religion and country
|
|
search (local data) keep working. `mapbox` behaves exactly
|
|
as before, and an embed that passes a token keeps getting Mapbox under `auto`.
|
|
|
|
> Status: **on `main`.** Generated pages honor `provider:'maplibre'` end-to-end — the page
|
|
> loads the MapLibre peer instead of Mapbox and **skips the token ladder entirely** (no
|
|
> account, no key). Living demo: `demo-map/demo-map/maplibre/index.html` (a preconfigured
|
|
> instance); the embed smoke (`bun run smoke:embeds`) verifies it mounts with a live canvas.
|
|
|
|
---
|
|
|
|
## Show a different people-group list
|
|
|
|
Every map shows DOXA's people groups unless you ask for another list — same bundle, no rebuild.
|
|
Resolution order (first defined wins): `profile-config.list` → `?list=` in the page URL →
|
|
`window.MAP_LIST` → default `doxa`.
|
|
|
|
| `list` | also accepts | shows |
|
|
|---|---|---|
|
|
| `doxa` | `2000`, `project-2000` | Project 2000: DOXA's people groups |
|
|
| `unreached` | `7000`, `project-7000` | Project 7000: unreached people groups |
|
|
| `frontier` | `4700`, `project-4700` | Project 4700: frontier people groups |
|
|
| `frontier-500k` | `700`, `project-700` | Project 700: frontier people groups of 500,000 or more |
|
|
| `unengaged` | — | Unengaged unreached people groups |
|
|
|
|
```html
|
|
<!-- per embed -->
|
|
<my-map profile-config='{"profile":"my-map","list":"frontier-500k"}'></my-map>
|
|
|
|
<!-- or flip any generated page from the URL -->
|
|
https://<cdn>/dx-maps/<bundle>/<profile>/index.html?list=700
|
|
```
|
|
|
|
**What you get today:** the DOXA groups that belong to that list. Rows and prayer data come from the
|
|
DOXA API, so a list's groups that DOXA does not carry are not shown yet. An unknown list falls back to
|
|
`doxa` with a console warning. If a list's membership file cannot load, the map shows **no**
|
|
groups rather than DOXA's whole list under the wrong name. Composites pass `list` to every child.
|
|
The definitions live in `library/lists/lists.json`; `bun run lists` regenerates the membership files.
|
|
Full contract: [reference-universal-props.md](./reference-universal-props.md), section 5.
|
|
|
|
---
|
|
|
|
## Hide the Plugin Studio header
|
|
|
|
The Studio header is where you set the universal props (language, engine, token). **Hide header**
|
|
collapses it so the plugin takes the full height; a **Show header** button stays in the top corner
|
|
to bring it back. The choice is remembered in this browser.
|
|
|
|
---
|
|
|
|
## Embed under a strict Content-Security-Policy
|
|
|
|
No bundle needs `'unsafe-eval'` (translations compile without `eval`; `bun run check:no-eval`), and
|
|
every third-party script is pinned with an integrity hash (`library/constants/cdn.js`,
|
|
`bun run check:cdn-pins`). A page embedding the maps needs, in addition to its own sources:
|
|
|
|
| Directive | Add | Why |
|
|
|---|---|---|
|
|
| `script-src` | `https://cdn.jsdelivr.net` (and `https://api.mapbox.com` for Mapbox) | the map engine and the search geocoder |
|
|
| `style-src` | `'unsafe-inline'`, the same CDN hosts | each plugin styles its own shadow root with a `<style>` element |
|
|
| `worker-src` | `blob:` | the map engines run their tile workers from a blob URL |
|
|
| `connect-src` | the tile/style hosts you use (`https://tiles.openfreemap.org` for MapLibre, `https://*.mapbox.com` for Mapbox), your data API | tiles, styles, glyphs, data |
|
|
| `img-src` | `data:` `blob:` and the same tile hosts | sprites and raster tiles |
|
|
|
|
---
|
|
|
|
## Set the API + Mapbox token
|
|
|
|
Both the data backend and the Mapbox token are **runtime inputs**, never baked into a bundle.
|
|
|
|
**Data (the API).** Maps read data through `@map/api/`, driven by `library/api/sources.json`,
|
|
which lists every connection by id, picks one `activeSource`, and maps each source's raw field
|
|
names onto the system field names components expect (`fieldMappings`). Three connection types:
|
|
`rest-api` (the default — the live people-group API), `csv` (a local file), `api` (a single
|
|
JSON URL). To add or change a source, edit `sources.json` — components stay source-agnostic.
|
|
The base URL resolves at runtime from `window.MAP_APP_API_URL` → `VITE_API_BASE_URL` → `''`
|
|
(same-origin), so a host repoints the API with **no rebuild**.
|
|
|
|
**The Mapbox token.** Supply it per embed as `tk` in `profile-config`, or point
|
|
`window.MAP_TOKEN_URL` (or the build-config `tokenUrl`) at an endpoint that returns it. The
|
|
token stays remote — only the URL ships. Set `tokenUrl` **absolute**
|
|
(`https://…/api/maps/token`) so a CDN-hosted folder and the mobile app both work; the endpoint
|
|
must send CORS headers permitting the embedding origins. In dev, the token comes from `.env`'s
|
|
`VITE_MAPBOX_TOKEN`. A Mapbox `pk.` token is designed to be client-visible — protect it with
|
|
URL restrictions at Mapbox, not by hiding it.
|
|
|
|
---
|
|
|
|
## Give your agent the guardrail
|
|
|
|
If you point an AI agent at this repo, paste this first, before it reads any code:
|
|
|
|
> This is the doxa-maps bundler. Three zones: `library/` (`@map`) = reusable shared blocks —
|
|
> **reuse via the `@map/…` alias, never edit**. `plugin-designs/<map>/` = the sandbox for **one**
|
|
> map — **build everything here**. `internal/` = the build machine — **never touch it**.
|
|
> To build a map: copy `plugin-designs/template-bundle/`, read its README, pick reusable pieces
|
|
> from `documentation/reference.md`, then build the map's own code inside `plugin-designs/<the-map>/`.
|
|
> Change colors by editing **values** in that map's `src/colors/` folder — not logic in
|
|
> `library/`. Then run `bun run build`. If a change seems to belong in `library/`, stop and
|
|
> ask — that's an expert move that can break every other map.
|
|
|
|
Optional power-up: Mapbox publishes official agent skills (cartography, style patterns,
|
|
performance, integration) that make an agent much better at map work —
|
|
`/plugin marketplace add mapbox/mapbox-agent-skills` then `/plugin install mapbox`, or
|
|
`npx skills add mapbox/mapbox-agent-skills`.
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Cause | Fix |
|
|
|---|---|---|
|
|
| Changed a `.vue`, served site shows old map | You ran `dev`, or didn't rebuild | `bun run build`, then refresh |
|
|
| `Set BUNDLE=<name> for build` | Ran `vite build` with no bundle selected | `bun run build` (all) or `BUNDLE=<name> bun run build:bundle` |
|
|
| New bundle doesn't appear | Missing `index.html` or `index.js` in the folder | Vite discovers a bundle only when both exist |
|
|
| Old bundle still loads after a build | Browser cached the static `.js` | Hard-refresh / disable cache in devtools |
|
|
| Map is blank on a partner page | Mapbox peer not loaded, or no token | Load `mapbox-gl.js` + geocoder **before** the bundle; pass a valid `tk` |
|
|
| `Profile not found` error box | `profile` doesn't match a flat `.vue` filename in the bundle | Fix the `profile` field to match a filename |
|