Driver 2026-09-21: 'a folder called deployment which will store the capacitor
folder, wordpress folder and tauri folder', and 'a folder called documentation
to store the contributing folder so docs are in one place'.
docs/ + contributing/ + CONTRIBUTING.md -> documentation/ (three doc homes into
one). capacitor/ + wordpress/ + src-tauri/ -> deployment/.
The public/ -> dist/ rename the Driver asked about is NOT here: after seeing the
measured blast radius (855 tracked files, 54 source refs, and public/ is the
served web-root four consumers point at) the Driver withdrew it - 'keep the
public folder the same'.
Path references were rewritten mechanically (258 across 87 files), but three
classes needed hand fixes because a prefix regex cannot see them:
- join(ROOT,'docs',...) in 10 internal scripts - a quoted SEGMENT, not a prefix
- tauri frontendDist '../public' -> '../../public', since src-tauri is now one
level deeper; check:tauri caught this exactly as the card predicted
- the WordPress contract test's dirname(__DIR__,2) -> 3, same reason
Also reverted one over-reach: the rewrite renamed a FINDING-CATEGORY label
'docs' in public-hygiene-check.mjs, which is a rule name in its own header, not
a path.
Verified: full build ALL GUARDS PASS, SRI consistent; check:tauri, check:mobile
(cap sync + webDir OK), check:wp-embed, quoted-scripts, capability-citations,
capability-exemplars, library-index, public-hygiene, parse and drop all pass.
11 KiB
Develop a plugin and see it live — the golden path
The one thing that matters: author a map plugin, run one build, and see it live on the platform. This page THREADS the existing docs into that read-order — it does not duplicate them. Follow the six steps; each links to the doc that teaches that piece in depth. Brand-neutral: any team on any platform inherits this same path.
The path at a glance
author build publish see it live
plugin-designs/ → bun run build → merge to catalog → https://<platform>/plugin-assets/…
(one folder) (one step) (the runbook) (a tab, an iframe, a <script>)
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). See reference.md for the capability model.
Step 1 — Understand the 5 capabilities (30 sec)
A plugin is one of five tiers, simplest → most composable:
single-element → prop-table → monolithic → nested-element SPA → nested-SPA (app-shell).
You declare the tier in <bundle>.meta.json ({"type": …}) for the composite tiers. Read
reference.md §capabilities; imitate the nearest reference plugin in plugin-designs/.
Step 2 — Author the plugin (the 4-file pattern)
In plugin-designs/<your-plugin>/ create these files (imitate the nearest reference plugin, never author
a plugin as a page). Three are required; the rest are opt-in:
| 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 |
Measured 2026-09-21, so you can calibrate. Across the 18 shipped bundles the folder holds 3 to 8 files — not one has "exactly four."
all-people-groupsis the true minimum at 3 (root.vue+index.js+index.html) and is a fully working, manifest-listed plugin. 9 of 18 carry no.meta.jsonat all, because single and prop-table tiers are inferred rather than declared. Start at three and add a file only when you need what it does.
index.htmlreally is load-bearing: moving it out ofdemo-map/made the build error and dropped the bundle from the buildable list (18 → 17). Restoring it returned a clean build.
plugin-designs/ is your FREE ZONE — build freely here. Only contribute a component to the shared
library/ (@map) if it is genuinely reusable + parameterized — a library edit reaches every plugin, so
it's a HIGH BAR (see reference.md §governance). Full walkthrough: tutorial.md.
Step 3 — Build (one step)
From doxa-maps-plugin-studio/:
bun run build # discovers plugin-designs/*, bundles each, writes public/doxa-maps/<plugin>/<plugin>.js
Exit 0 + all DX guards PASS + inline-parse-check PASS = a good build. Your plugin now exists as a real
custom-element bundle under public/doxa-maps/<your-plugin>/, with a standalone leaf page and a manifest
entry. Recipes: how-to.md.
Step 3b — Prove it MOUNTS in a browser (before you publish)
A green build is a static result: the guards check that files exist, parse, carry a correct SRI hash and
resolve. None of them opens your element. A bundle can pass every guard above and still render nothing —
a thrown setup() removes the subtree silently, and the build stays green.
This was measured, not assumed (2026-09-21). A
throwwas injected intotemplate-map-a.vue'ssetup()and the bundle rebuilt. Result: the build succeeded (206 kB emitted, exit 0),check:parsePASS,check:dropPASS — andsmoke:embedsreported 0/2 pages mount cleanly. One deliberate defect, invisible to every static guard, caught only in a browser. That gap is the entire reason this step exists.
# a token is required or map profiles cannot mount (see below)
MAPBOX_TOKEN=$(curl -s -A "Mozilla/5.0" https://doxa.life/api/maps/token \
| python3 -c "import json,sys;print(json.load(sys.stdin)['token'])")
MAPBOX_TOKEN=$MAPBOX_TOKEN bun run smoke:embeds # every profile in the manifest
MAPBOX_TOKEN=$MAPBOX_TOKEN node internal/embed-smoke.mjs --grep <your-plugin> # just yours
smoke:embeds serves public/ and opens every bundle × profile × preconfigured instance from
manifest.json, asserting each custom element defines, attaches a shadow root and fills it. Expect
N/N pages mount cleanly. Measured 2026-09-21: 40/40.
The token is not optional, and its absence is loud. Without
MAPBOX_TOKENevery map profile fails its mount assertion while every DOM profile passes — 27/40, which reads like 13 broken plugins and is not. The run prints a two-line warning when the token is missing; believe the warning, not the failures.?tk=alone is also not enough for a composite: it reaches only the top-level element'sprofile-config, while nested children readwindow.MAP_TOKEN. The smoke sets both.
This step is not in CI. CI is the single authoritative builder but installs no browser binary (no
playwright install step), so the mount check is a LOCAL gate an author runs deliberately. That is exactly
why it belongs here in the golden path rather than being left to be remembered.
Step 4 — Publish to the live catalog
The platform serves from the catalog repo (platform/mfe-index), NOT from where you develop. Publishing
= merging your built bundles into the catalog. The tested procedure, in one block:
# 1. clone the catalog fresh (fetch-first, never force)
git clone --depth 1 <catalog-repo-url> /tmp/catalog
# 2. merge your built bundles into its doxa-maps subtree (--delete mirrors removals)
rsync -a --delete public/doxa-maps/ /tmp/catalog/app/doxa-maps/
# 3. VERIFY every manifest bundle's <name>/<name>.js resolves BEFORE push; confirm the diff is scoped
# to app/doxa-maps only (never touch a sibling team's bundles)
# 4. commit (product-only message), then push with a RAISED BUFFER (see below):
git -C /tmp/catalog -c http.postBuffer=524288000 push origin HEAD:main
★
http.postBuffer=524288000is load-bearing, not optional. The tree is ~39 MB and git's 1 MB default cannot stream it. Without the flag the push fails withThe remote end hung up unexpectedly— which reads as a SERVER rejection (permissions? branch protection? repo full?) and sends you debugging the wrong machine entirely. It is a local buffer limit. This one line is the difference between step 4 working and step 4 looking like an access problem.
The platform serves the catalog directly, so a pushed change is live at once — verify at the URL in Step 5. (The fuller runbook with every gotcha lives in the project's deploy-cycle notes; the block above is the load-bearing sequence.)
(The end-state is the Forgejo CI build-gate — push source, CI builds + publishes automatically. Until a runner
is registered, the runbook is the manual path. Same build:unlocked seam.)
Step 5 — See it live (the 5 deployment methods)
Your one element deploys five ways (only an element supports all five — a page supports iframe only):
| # | Method | How | Status today |
|---|---|---|---|
| 1 | iframe | point an <iframe> at the plugin's leaf page |
✅ proven |
| 3 | JavaScript embed | <script src=".../<plugin>.js"> + <your-tag> (SRI-hashed) |
✅ proven |
| 2 | mobile | bun run phone (Capacitor) — build step, no new code |
🔶 built, not shot |
| 4 | WordPress plugin | the deployment/wordpress/doxa-maps-embed/ PHP wrapper emits the embed snippet |
🔶 built, not shot |
| 5 | desktop (Tauri) | bun run tauri:build → a real binary + .deb wrapping the same public/ tree |
🔶 built, not shot |
Say "proven" ONLY for 1 & 3 (2, 4 and 5 are scaffolded but no binary/install has been shot). Verify at https://<platform>/plugin-assets/doxa-maps/<plugin>/<plugin>/index.html.
Done = the live site shows it, not a green build.
Step 6 — Surface it (make it discoverable)
A plugin reachable only by direct URL isn't really "seen live." Good news: you usually don't have to do anything.
It is already surfaced — automatically. The cap-5 SPA reads the built manifest.json at runtime and
auto-discovers every bundle that is neither already placed in a curated section nor on the small
NOT_EXPERIMENTAL exclusion list (the two shells, the template, and bundles that are nested inside another
tab). Your plugin appears in the collapsible Experimental folder at the bottom of the rail, marked 🧪, with
its label and description read from the manifest. No TAB_DEFS entry, no NAV_MODEL row, no code edit.
Verified 2026-09-21 in a browser: the rail rendered 11 rows — 6 curated, then 5 auto-discovered (
all-people-groups,country-posters,demo-map,doxa-vision-card,stack-demo), each carrying the 🧪 marker. Discovery is fail-soft: if the manifest is unreachable the folder stays empty and the curated rail is unaffected — a discovery failure never takes down the app.
Promote it to a curated tab (optional). The curated sections encode a deliberate teaching order, so a
plugin graduates out of Experimental only when it earns a place in that order. That is the edit Step 6 used to
describe as mandatory: add a TAB_DEFS entry plus a NAV_MODEL row in plugin-designs/doxa-maps-spa/, then
rebuild + republish.
One rail row per BUNDLE, not per profile. A bundle's profiles (gospel-resources ships four) are alternate views reached inside the pane. The rail is an onboarding ramp, not an inventory.
That's the whole path: author in plugin-designs/ → bun run build → prove it MOUNTS (smoke:embeds) →
publish via the runbook → see it live → and it surfaces itself in the SPA's Experimental folder. Only two
of those six steps are edits you write: the plugin, and the optional promotion out of Experimental. Deep
dives: tutorial.md · how-to.md · reference.md · why.md.