- library/components/WARNING.md, library/dashboards/README.md and documentation/contributing/README.md taught "graduate into the library at the second consumer". They now teach the plugin-designer skill's rule (section 3): a component that could serve more than one map is built once in library/, driven by props, and landed through the build owner; one-off or experimental pieces stay in a map's folder; a new pattern is a pilot, then approved, then rolled out. The contributing guide's "full reasoning" link pointed to an index without it; it now points to the skill. - The app shell doxa-maps-spa is labelled Deep Research. Ten docs still called it the Research Gallery (15 mentions): DEPLOY.md, the desktop how-to, embeddability, develop-a-plugin, reference-universal-props, reference.md, the shell's EMBED.md, and the plugin-designer skill with two of its references. Identifiers (doxa-maps-spa, branch and route names) are unchanged; README.md is not touched.
9.3 KiB
Develop a plugin and see it live — the golden path
Author a map plugin, build it once, prove it mounts, and see it live — in six steps. Each step links to the doc that teaches that piece in depth.
| Step | You do | Command | Done when |
|---|---|---|---|
| 1. Pick a capability | choose how much your plugin nests | — | you know your tier (1–5) |
| 2. Author | write 3 files in plugin-designs/<your-plugin>/ |
— | the folder has .vue + index.js + index.html |
| 3. See it in the studio | iterate with hot reload, no build | bun run dev |
your plugin appears at http://localhost:5173/ |
| 4. Build | produce the deployable tree | BUILD_ALLOW=1 bun run build |
exit 0, all guards PASS |
| 5. Prove it mounts | open every plugin in a real browser | bun run smoke:embeds |
N/N pages mount cleanly |
| 6. Publish and see it live | copy the tree to your host, embed it | see Step 6 | the live URL shows it |
Everything you build is a compiled Vue custom element: one folder → one bundle → one <tag>. A static
HTML page is never a plugin (it can only be iframed).
Step 1 — Pick a capability
A plugin is one of five tiers, simplest → most composable:
single element → prop-table → multi-view → composite (nested elements) → app shell (nested SPAs).
Composite tiers declare themselves in <bundle>.meta.json ({"type": …}). Imitate the nearest reference
plugin: reference-capability-exemplars.md names one per tier.
Step 2 — Author: three files are enough
In plugin-designs/<your-plugin>/:
| File | Required? | What it is |
|---|---|---|
<your-plugin>.vue |
required | the root Vue SFC — your plugin's UI (styles are shadow-scoped automatically) |
index.js |
required | defineCustomElement(RootVue) → customElements.define('<your-tag>', El) — register ONLY your own tag |
index.html |
required | the staging entry page that mounts <your-tag> — the build fails without it |
<your-plugin>.meta.json |
composite tiers only | {"type": "<machine key>", "composes": […], "label": "…"} |
<your-plugin>.instances.json |
optional | preset instances — supplying one makes the bundle prop-table (cap 2) |
src/ |
optional | private components used only by this plugin |
plugin-designs/ is your free zone. Contribute to the shared library/ (@map) only when a component
is genuinely reusable and parameterized — a library edit reaches every plugin. Walkthrough:
tutorial.md.
Step 3 — See it in the studio (seconds, no build)
bun run dev # vite on :5173; `/` is the Plugin Studio, every plugin in one place
bun run smoke:studio # ~5s: asserts the studio mounts and every plugin is a real subtree
This is the loop to live in while authoring: about 5 seconds, against a full build of 1–4 minutes. The build is for publishing, not for looking.
Step 4 — Build the deployable tree
BUILD_ALLOW=1 bun run build # discovers plugin-designs/*, bundles each, writes public/dx-maps/<plugin>/<plugin>.js
Exit 0 + every guard PASS = a good build. Your plugin now exists under public/dx-maps/<your-plugin>/
with a standalone leaf page and a manifest.json entry. BUILD_ALLOW=1 is deliberate: a full build first
wipes public/dx-maps/, so it refuses (exit 75) in a shared checkout unless you mean it. To compile only
your plugin: BUNDLE=<your-plugin> bun x vite build. Recipes: how-to.md.
Step 5 — Prove it mounts in a real browser
A green build is a static result: it proves files exist, parse and carry correct hashes. It never opens your element, and an element that throws on setup renders nothing while the build stays green.
MAPBOX_TOKEN=$(python3 -c "import json;print(json.load(open('vite.doxa-maps-build-config.json'))['token'])")
MAPBOX_TOKEN=$MAPBOX_TOKEN bun run smoke:embeds # every plugin
MAPBOX_TOKEN=$MAPBOX_TOKEN node internal/embed-smoke.mjs --grep <your-plugin> # just yours
Expect N/N pages mount cleanly. The token matters: without it every map plugin fails to mount while
every non-map plugin passes, which looks like many broken plugins and is not. Take the token from the
build config, as above (why).
Step 6 — Publish and see it live
Copy the built tree to any static host, then embed from it. The tree is public/dx-maps/; every
plugin's ready-made snippets are in its manifest.json.
set -e # first line: a failing check must stop the publish
bun run check:settled # never publish a tree a build is rewriting
bun run check:sri # every leaf's integrity hash matches its bundle
bun run check:embed-targets # every embed snippet points at a real file
rsync -a --delete public/dx-maps/ <your-host-checkout>/dx-maps/
# commit and push the host checkout; for a large tree: git -c http.postBuffer=524288000 push …
| # | Deployment method | How | Status |
|---|---|---|---|
| 1 | iframe | point an <iframe> at the plugin's leaf page |
✅ production |
| 2 | mobile app | bun run phone (Capacitor) — same tree, no new code |
✅ built |
| 3 | JavaScript embed | <script src=".../<plugin>.js"> + <your-tag> (SRI-pinned) |
✅ production |
| 4 | WordPress / Disciple Tools | deployment/wordpress/doxa-maps-embed/ emits the embed snippet |
🧪 proof of concept |
| 5 | desktop (Tauri) | bun run tauri:build — a binary and .deb wrapping the same tree |
🧪 proof of concept |
Done = the live site shows it, not a green build. To check the whole pipe at once, open the
golden-path-check plugin on your host: it reads the manifest beside it and reports how many bundles
it can see and whether it finds itself. To check that what you published is current, run
bun run check:catalog-currency.
Your plugin surfaces itself. Deep Research (doxa-maps-spa) reads the built manifest.json
and puts every bundle it doesn't already curate into its folded Experimental shelf, with the label and
description from the manifest — no code edit. Promoting it to a curated tab is an optional edit to
TAB_DEFS and NAV_MODEL in plugin-designs/doxa-maps-spa/. One row per bundle: a bundle's extra
profiles are views inside its pane.
The whole path: author in plugin-designs/ → see it in the studio → BUILD_ALLOW=1 bun run build
→ prove it mounts (smoke:embeds) → publish → it appears on the Experimental shelf. Deep dives:
tutorial.md · how-to.md · reference.md · why.md.
Notes and history
The measurements behind the steps above, kept for whoever needs the reasoning.
- Step 2 — how many files a plugin really has (measured 2026-09-21). Across 18 bundles a folder held
3 to 8 files.
all-people-groupsis the true minimum at 3 (root.vue+index.js+index.html), and 9 of 18 had no.meta.jsonbecause single and prop-table tiers are inferred. Removingindex.htmlfromdemo-map/made the build error and dropped the bundle (18 → 17 buildable). - Step 3 — studio vs build timing (measured 2026-09-22).
smoke:studiopassed in 4.7 s (29 tabs, 0 failed requests); a full build took 43 s–4 min on the same box. The studio also catches a bug class the built tree cannot: the dev manifest hands rooted refs (/plugin-designs/…), the built one relative refs (./bundle/bundle.js), so a base-join bug is invisible in one and fatal in the other. - Step 5 — why the browser check exists (measured 2026-09-21). A
throwinjected intotemplate-map-a.vue'ssetup()still built (exit 0), passedcheck:parseandcheck:drop, andsmoke:embedsreported 0/2 mounted. Without a token, 27/40 mount — every map profile fails. A composite also needswindow.MAP_TOKEN, because?tk=reaches only the top-level element; the smoke sets both. The browser check is not in CI (no browser binary there), so it is a local gate you run on purpose. - Step 5 — which token (measured 2026-09-23). The
https://doxa.life/api/maps/tokenendpoint serves a token restricted todoxa.life: a tile request without that Referer returns 403, so basemaps render blank while pins draw. The build-configtokenreturns 200 from any origin. It is apk.public key, restricted by URL rather than kept secret;check:public-hygieneallowspk.and fails onsk./tk.. - Step 6 — why
set -ecomes first. An earlier publish took the live catalog down with a 404 twice over: the verify was chained so a failing check fell through to the push, andrsync --deleteran from a tree a concurrent build had just emptied, mirroring the emptiness to the host. Hencecheck:settledas a precondition and a verify that can abort. - Step 6 —
http.postBuffer=524288000. The tree is ~39 MB; git's 1 MB default cannot stream it, and the failure (The remote end hung up unexpectedly) reads like a server rejection when it is a local buffer limit. - Step 6 — deployment status wording. "Proof of concept" means the code exists and its contract check
passes, but it is not production-ready: method 4 has no capture on a live WordPress install, and method 5
emits a real binary and
.deb(~67 MB and ~60 MB) that has not been launched on a desktop.