Development
Contributor workflow and verification protocol for Waymark JS.
Contributor workflow
- Install dependencies:
npm install - Start dev server:
npm run dev - Run formatting:
npm run format - Verify formatting:
npm run format:check - Regenerate
docs/llms-full.txtand run docs/API sync checks:npm run docs:sync(test:cifirst runsnpm run check:llms, which fails if the committed bundle is stale) - Preview and build the docs site:
npm run docs:dev(Nuxt dev server; defaults tohttp://localhost:3000) andnpm run docs:generate(builds the deployable static site todocs/.output/public) - Run unit tests:
npm test - Build and verify the dist output:
npm run build && npm run check:dist - Run functional browser tests:
npm run test:browser - Regenerate the committed screenshots after an intentional visual change:
npm run screenshots; verify byte-identity without touching the committed set:npm run screenshots:verify. Refresh the vendored basemap fixture — a deliberate, network-touching act — withnpm run fixtures:refresh(see Screenshots). - Run the full CI gate locally:
npm run test:ci(format check → llms bundle check → docs sync → build → dist check → docs generate → unit tests → browser tests).
npm run test:browser boots a dedicated Playwright Vite server on 127.0.0.1:4173 with --strictPort, so browser tests are deterministic even when your normal dev port is occupied. Browser tests import src/entry.js directly — the built bundle is verified separately by npm run check:dist and the Vitest dist smoke test. playwright.config.js pins workers: 1 and retries: 1 because the suite includes WebGL/shader map tests that flake under parallel workers; single-worker execution was verified fully green. npm run test:browser runs the functional project only — committed pixels are captured by the separate screenshots project (see Screenshots) — and both projects run fully offline against the vendored basemap fixture.
npm run docs:sync chains node scripts/llms-full.mjs, npm run docs:contract, and npm run docs:api-sync (which runs api:contract first). The generator step rebuilds docs/llms-full.txt — the full content of every doc linked in docs/llms.txt, concatenated into one file for single-fetch consumption. Both docs/llms.txt and docs/llms-full.txt are also published as tracked, byte-for-byte copies under docs/public/ for repository indexing; docs:sync refreshes those copies and npm run check:llms verifies them. test:ci runs the generator in check mode first (npm run check:llms), so a stale committed bundle fails the gate before docs:sync regenerates it. No root SKILL.md is generated; consumer-facing agent docs follow the llms.txt convention and are distributed with the docs GitHub Pages deploy (see Docs site).
Vite config is split by role: vite.config.dev.js for local dev server, vite.config.lib.js for library builds, and vitest.config.js for tests.
vite.config.lib.js defines process.env.NODE_ENV as a build-time replacement for the dist bundle, preventing browser runtime errors like process is not defined. It also keeps maplibre-gl external (rollupOptions.external), so the build emits a single ESM bundle — dist/waymark.js (~31.5 kB, ~8.8 kB gzip) with a bare maplibre-gl import, no CSS and no inlined worker. npm run check:dist guards that output shape.
Conventions and definitions
- Instance: public object returned by
createInstance({ map, document? }). - Map injection: the consumer creates and owns the MapLibre map; Waymark duck-types and mounts its overlay onto it.
- Runtime core: internal lifecycle object in
src/runtime/createInstanceCore.js. - Runtime registry: internal
WeakMapfrom map → core insrc/runtime/runtimeRegistry.js(one runtime per map). - InstanceDocument: canonical serialisable payload from
instance.toJSON()(thedocumentoption). - GeoJSON: map data format and symbol naming (
createGeoJSONModule,geoJSON). - Data layers: canonical shape is
data.layers: Array<{ type?: "geojson", data: object }>. - Feature interaction: click/hover/leave event resolution in
src/geojson/createFeatureInteractionModule.js; popups and cursor styling are consumer-owned. - Type icons:
config.types[].iconis a name hook for a consumer-registered map image (map.addImage); Waymark ships no icon assets and never registers images itself. The dev playground registers an inlinephoto-markerPNG this way and maps it to thephototype.
Keep runtime internals internal. Consumer docs and tests should target public API behaviour.
Naming conventions
Use camelCase identifiers, with acronyms kept uppercase.
instanceDocumentInstanceDocumentgeoJSON
Use instanceDocument as the canonical variable/parameter name for the document payload, and map for the injected MapLibre map.
Prefer these canonical forms in source, tests, docs, and fixtures.
For data-layer ordering, use these canonical rules in docs/tests/fixtures:
data.layersis the canonical data envelope.- layer input shape is
{ type?: "geojson", data }and runtimeinstance.data.addLayer(...)uses the same contract. - data-layer stack order is top-first:
layers[0]is visually on top. - insertion position is below the first
symbollayer already present in the injected map style (appended when none exists).
For full data/GeoJSON contract details, prefer docs/5.data.md rather than duplicating rules in multiple docs.
Cross-module sync pattern (runtime-state owned)
Use this pattern when runtime modules must stay in sync.
- Runtime state owns mutable state
- Keep authoritative mutable state in
src/runtime/state/createInstanceState.js. createInstanceCore(...)owns lifecycle orchestration and commands, not duplicate mutable state copies.- Modules and adapters consume snapshots and never become a second source of truth.
- Keep authoritative mutable state in
- Dispatch named commands
- Route mutations through explicit dispatch paths (for example
types.visibility.set,types.visibility.reset,debug.set). - Runtime commands dispatch state changes, then adapters apply map-side effects.
- Route mutations through explicit dispatch paths (for example
- Emit canonical state events
- Every real state mutation emits
waymark:state.changedplus a scoped state event (waymark:state.debug.changed,waymark:state.types.visibility.changed). - No-op dispatches emit nothing.
- Every real state mutation emits
- Emit data-layer events only where useful
- The GeoJSON module emits
waymark:data.layer.added,waymark:data.layer.mounted, andwaymark:data.layer.errorfor layer lifecycle observability.
- The GeoJSON module emits
- Emit feature interaction events from layer observation
- The feature interaction module emits
waymark:feature.click,waymark:feature.hover, andwaymark:feature.leavewith{ id, feature, layerId, lngLat, originalEvent }. - These are observability events only: they do not mutate runtime state, the map cursor, or the camera.
- The feature interaction module emits
Guidance for future sync features:
- keep mutation logic in runtime core, not in map adapters
- keep commands idempotent and no-op on invalid/unchanged input
- emit events only after state mutation succeeds
- prefer one aggregate event per mutation path over multiple partial events
- ensure
instance.toJSON()reflects the same post-mutation state as emitted events
Testing strategy
tests/docs/*.test.js(Vitest + jsdom): API contract tests without WebGL.tests/docs/runtime/createInstanceState.test.js: runtime-state module contract (getSnapshot, command dispatch, scoped/global state events, no-op behaviour).tests/docs/dist.smoke.test.js: imports the builtdist/waymark.jswithmaplibre-glmocked, assertingcreateInstanceexists and constructsLngLatBoundsthrough the mock. It skips cleanly whendist/is absent and runs for real intest:ci, which builds first.tests/browser/*.test.js(Playwright,functionalproject): browser API and dev-page functional smoke.1.api.test.jscovers feature interaction events (click/hover/leave detail, hover suppression, leave semantics, destroy cleanup) and theScreenshot scenesassertions, which mount the capture scenes and assert render evidence (feature counts, polygon framing, per-family filters) without writing pixels; its specs fetch the shared fixtures (/geojson/markers.json,/geojson/track-port-hardy-cape-scott.geojson,/geojson/shapes.json,/geojson/sample-point.geojson) from the dev server.2.development.test.jscovers the dev page: initial render (including ODbL attribution visibility), adding a layer, the consumer-side property-inspector popup (title, image, property rows), the marker/line/shape image fallbacks (thumbnail → medium → large), line and shape popup anchoring at the click location, and type visibility toggling.tests/browser/screenshots.spec.js(Playwright,screenshotsproject): the seven committed captures, rendered under the fixed-point capture protocol — see Screenshots.tests/browser/helpers/scenes.js: shared page-driven scene builders used by both the functional specs and the screenshots spec, so assertions and committed pixels exercise identical scenes.tests/browser/helpers/test.js: Playwrighttest/expectextended with the external-request guard — every test context blocks non-localhost http(s) requests and fails the test naming the offending URL.tests/browser/helpers/screenshot.js: fixed-point capture helper (screenshot(page, name, selector?)), the settle gate (waitForSettled), the safe-output guard andSCREENSHOT_OUT_DIRredirection — see Screenshots.tests/docs/basemap/*.test.js: unit tests pinning the vendored basemap fixture — tile enumeration, generator host policy, manifest integrity, and the serving middleware contract.
npm run check:dist statically asserts the built output: dist/ contains only waymark.js, the bundle imports maplibre-gl as a bare external, and there is no base64 worker blob, no CSS and no Vue markers. test:ci runs it after npm run build.
Use jsdom tests to validate API contract behaviour, not as proof of non-browser runtime support. Browser tests are the runtime source of truth for instance behaviour.
Dev page setup
The development playground lives under dev/ as a vanilla ES module app (dev/index.html, dev/dev.js). Its data is the shared fixture set under tests/fixtures/ — the single source of truth for the dev harness and the browser tests. vite.config.dev.js points publicDir at tests/fixtures, so the dev server serves the north Vancouver Island demo fixtures at /: route.json (config) and geojson/ data. dev/dev.js imports maplibre-gl/dist/maplibre-gl.css (MapLibre requires its stylesheet for popup positioning), creates its own MapLibre map on #map (the vendored OpenGIS basemap style served at /basemap/style.json, initial centre on Holberg, fadeDuration: 0), and injects it via createInstance({ map, document }), booting one instance from /route.json plus four GeoJSON layers (track-north-island.geojson, track-port-hardy-cape-scott.geojson, markers.json, shapes.json) from /geojson/. It also fetches /geojson/sample-point.geojson for the add-layer control. The vendored style and the /__basemap__/** middleware make the dev harness fully offline — no live basemap requests (see Screenshots).
Before createInstance(...), the harness registers an inline PNG data URL as the photo-marker image (map.loadImage + map.addImage), so the photo type's symbol layers resolve the icon from first mount. It also demonstrates the consumer-owned feature interaction pattern: a Popup property inspector on waymark:feature.click, and cursor changes on waymark:feature.hover / waymark:feature.leave.
The click inspector renders a title heading, a single <img class="dev-popup__image"> when any image URL property is present, and the remaining properties as key/value rows. The image URL is picked in preference order image_thumbnail_url → image_medium_url → image_large_url, the image alt is the feature title (or Feature image), and all three image_*_url keys are omitted from the rows whenever an image is displayed. The popup anchors at feature.geometry.coordinates for Point geometry, and at the click lngLat for everything else. The harness emits no [waymark:dev] diagnostic logs during normal operation — interaction feedback lives in the inspector.
Vite dependency pre-bundling breaks MapLibre's worker: MapLibre resolves its worker relative to its own module URL, and Vite's pre-bundled copy under node_modules/.vite/deps/ has no worker file alongside it, so GeoJSON source processing silently stalls (map.loaded() never becomes true). vite.config.dev.js sets optimizeDeps.exclude: ["maplibre-gl"] so the package is served as-is. Vite consumers of Waymark should do the same in their own config.
dev/index.html renders a restrained toolbar — the Waymark JS title plus a "click a feature to inspect its properties" cue — with four grouped controls:
#dev-add-layercallsinstance.data.addLayer({ data: samplePoint }, { fitBounds: true });samplePointcarrieswaymarkType: "beer"#dev-fit-boundscallsinstance.data.fitBounds()#dev-toggle-beertogglesinstance.types.setVisibility("beer", ...)#dev-debug(checkbox) togglesinstance.debug.setEnabled(...)console logging; it initialises checked fromconfig.debuginroute.json(true)
Fixture notes:
tests/fixtures/route.jsongives every type atitle, maps thephototype toicon: "photo-marker"withiconSize, defines theareafill type (fill-color: "#2f855a"), and enables debug logging.- The GeoJSON fixtures under
tests/fixtures/geojson/(markers.json,track-north-island.geojson,track-port-hardy-cape-scott.geojson,shapes.json,sample-point.geojson) carrywaymarkTypeonly — the legacy duplicatedtypeproperty has been removed. shapes.jsonholds a singlewaymarkType: "area"Polygon ("Cape Scott trail area") that carries onlyimage_medium_url, exercising the medium fallback in the popup.sample-point.geojsonsits in-scene at[-127.998, 50.661]; the add-layer browser test asserts that centre afterfitBounds.- Holberg (the pub "The Scarlet Ibis" at
[-128.009734, 50.653923]) is the demo-closeup anchor, and the big route line istrack-port-hardy-cape-scott.
The page exposes window.waymarkInstance, window.waymarkInstances and window.waymarkMaps (both keyed by the dev map key map), and window.createWaymarkInstance for manual console testing.
This gives a stable baseline for browser smoke coverage in tests/browser/2.development.test.js:
- the toolbar controls,
#map, and the map canvas render with zero console errors - adding a layer increases
data.layersand fits bounds to the sample point - clicking a marker opens the property-inspector popup (title, image, property rows) and loads the
image_thumbnail_url - clicking a track line opens the popup anchored at the click location and loads the
image_large_url - clicking a shape opens the popup with its properties and loads the
image_medium_url - image URL keys never render as raw URL text rows in any popup
- toggling the "beer" type hides and restores typed visibility
Run:
npm run format:check
npm run docs:sync
npm run docs:generate
npm test
npm run build && npm run check:dist
npm run test:browser
Docs ↔ tests sync protocol
Treat docs and tests as one contract. Change both in the same slice.
| Docs page | Unit tests | Browser tests |
|---|---|---|
docs/1.api.md | tests/docs/1.api.test.js | tests/browser/1.api.test.js |
docs/3.instances.md | Internal-boundary assertions in tests/docs/1.api.test.js | Dev smoke in tests/browser/2.development.test.js |
docs/4.map.md | Map injection assertions in tests/docs/1.api.test.js | Map API coverage in tests/browser/1.api.test.js |
docs/5.data.md | Data-layer contract assertions in tests/docs/1.api.test.js | Data-layer API coverage in tests/browser/1.api.test.js |
Automation guards:
npm run docs:contractchecks required docs headings and mapped testdescribe(...)blocks.npm run docs:api-syncchecks API sections indocs/1.api.mdagainst generated source contract data.
Keep docs/llms.txt's links and descriptions in step when the docs set changes; docs/llms-full.txt regenerates from them via npm run docs:sync and is never hand-edited. After editing any doc linked from docs/llms.txt, run npm run docs:sync so the committed bundle stays fresh — otherwise npm run check:llms (and therefore test:ci) fails.
Hard sync workflow (docs ↔ source ↔ tests)
npm run docs:sync is intentionally strict and should be treated as a hard gate before tests. CI runs npm run check:llms before regenerating, so a stale committed docs/llms-full.txt fails the gate instead of being silently overwritten.
Execution order:
node scripts/llms-full.mjs- Reads the
.mdlinks fromdocs/llms.txtin order and regeneratesdocs/llms-full.txt(stripping frontmatter and api-contract markers).
- Reads the
npm run docs:contract- Reads
scripts/doc-test-contract.json. - Verifies required headings in tracked docs pages (
docs/1.api.md,docs/4.map.md). - Verifies mapped
describe(...)block names intests/docs/1.api.test.jsandtests/browser/1.api.test.js.
- Reads
npm run docs:api-sync- Runs
npm run api:contractfirst, generatingdocs/.generated/api-contract.jsonfromsrc/runtime/createInstanceEvents.js(event constants) andsrc/config/defaults.js(defaultConfig), together with the fixedcreateInstance({ map, document? })signature. - Compares marker blocks in
docs/1.api.md(signature,defaults,lifecycle-events,data-layer-events,feature-events) against generated contract values.
- Runs
If this gate fails, fix docs headings/marker blocks and corresponding test describe(...) names in the same change.
Adding or changing API sections safely
Use this convention whenever you add, rename, or remove a section in docs/1.api.md.
API section naming
- Use stable heading text in sentence case (for example,
## InstanceDocument shape). - Treat each heading name as automation-backed contract text, not editorial copy.
One-to-one heading and describe(...) mapping
- Each API section heading in
docs/1.api.mdmust have matchingdescribe(...)blocks in:tests/docs/1.api.test.jstests/browser/1.api.test.js
- The
describe(...)names must be identical to the heading text.
Manifest updates
- When adding or renaming an API section, update
scripts/doc-test-contract.jsonin the same change:requiredHeadingsdescribeMappings
Marker blocks for source-derived facts
- Source-derived API facts in
docs/1.api.mdmust be wrapped in marker blocks:
...
- Keep
<name>stable once introduced.
Extending source-derived contract fields
- If you add a new source-derived contract field, update all three in the same change:
scripts/generate-api-contract.mjsscripts/check-api-doc-sync.mjs- The matching marker block in
docs/1.api.md
Practical checklist
- Add or rename the API section heading in
docs/1.api.md(stable sentence case). - Add or rename matching
describe(...)blocks in both API test files with identical names. - Update
scripts/doc-test-contract.json(requiredHeadingsanddescribeMappings). - If the section includes source-derived facts, add or update the
api-contractmarker block. - If you introduced a new source-derived field, update both sync scripts and the marker block together.
- Run
npm run format:checkandnpm run docs:syncbefore handing off.
Sync checklist:
- Update the relevant docs headings and examples.
- Update matching
describe(...)blocks and assertions. - Run
npm run docs:sync,npm test, andnpm run test:browser. - Ensure old filenames/headings are removed.
Screenshots
The browser suite commits seven PNG screenshots as visual evidence — three API-page captures and four dev-page captures — under screenshots/ at the repo root (tracked in git and published with the docs site). Playwright splits them from the functional suite with two projects (playwright.config.js):
| Project | Scope |
|---|---|
functional | All assertions (1.api.test.js, 2.development.test.js); renders no committed pixels |
screenshots | The seven committed captures (screenshots.spec.js) under the pinned capture protocol |
npm run test:browserruns thefunctionalproject only.npm run screenshotsregenerates the committed set intoscreenshots/.npm run screenshots:verifyregenerates into the throwawaytest-results/screenshots-verify/, SHA-256-compares every file against the committed set (writingreport.json), and exits non-zero on any mismatch, keeping the captures for inspection. It is deliberately not part oftest:ci.
A safe-output guard in tests/browser/helpers/screenshot.js refuses to write into the tracked screenshots/ directory from a headed run or the wrong project; setting SCREENSHOT_OUT_DIR redirects captures elsewhere (the verify script uses this).
Capture protocol
Capture is fixed-point, not timed. screenshot(page, name, selector?) first awaits the shared waitForSettled(page) gate — bounded waits for network idle, every registered window.waymarkMaps map to be idle with tiles loaded, every <img> complete and decoded, document.fonts.ready, and two animation frames. It then accepts a frame only when three consecutive lossless PNG frames are byte-identical. Each round forces a render (map.redraw() plus the render event, then two animation frames) and toggles a full-viewport magenta probe overlay: a frame that fails to reflect the probe proves the compositor served a stale surface, and the round is discarded. Rounds are bounded; on exhaustion the helper throws rather than writing an unstable image — no tolerance and no fallback.
The scene builders in tests/browser/helpers/scenes.js gate capture readiness: the dev page waits until every mounted Waymark symbol layer's icon-image is registered, and the API scenes poll until their features have painted and the map is idle. Both the functional specs and the screenshots spec reuse the same builders, so committed pixels and functional assertions cannot drift apart.
Deterministic rendering
The screenshots project pins the render path in playwright.config.js: bundled-Chromium SwiftShader launch flags, locale en-CA, timezone America/Vancouver, colorScheme: "light", reducedMotion: "no-preference", a 1280×720 viewport and deviceScaleFactor: 1. Captured maps pass fadeDuration: 0 so symbol/icon fade cannot settle mid-transition. MapLibre popups are stabilised against a compositor promotion quirk — the stylesheet's will-change: transform promotes the popup to its own compositing layer at a fractional offset, which resamples non-deterministically — by overriding it to auto before capture. Re-verify this on any maplibre-gl upgrade.
A single-thread SwiftShader.ini pin is written by the Playwright global setup and removed by the teardown for invocations that include the screenshots project; functional-only runs never receive it. Keep it as a precaution, but note it was empirically inert on the baseline machine: byte stability comes from the capture protocol and harness fixes above, not from the pin.
Offline test environment (vendored basemap)
The dev harness and both browser suites run fully offline. tests/fixtures/basemap/ holds a generated, origin-free slice of the OpenGIS basemap: a canonical style.json, a site.manifest.json recording per-file provenance (source, bytes, sha256, licence note), and the site/** asset tree. The style is served by the existing publicDir mapping at /basemap/style.json; tiles, glyphs and sprite are served from /__basemap__/** by a small Vite middleware — a missing tile or glyph returns an empty 200 so MapLibre stays quiet, while a missing sprite returns a loud 404.
npm run fixtures:refresh regenerates the fixture as a deliberate act: it fetches the live style from the canonical host, allow-lists committable hosts (aborting loudly on anything else), rewrites asset URLs to root-relative local form, inlines the tile template and zoom range (no runtime descriptor fetch), vendors glyph range 0-255 for the declared fontstacks plus the 1× sprite, records provenance in the manifest, and prunes stale files. The full z0–14 pyramid is kept by explicit decision, bounded by the demo-area envelope (the seven capture viewports plus one tile of padding) and a MAX_TILE_COUNT = 5000 generator guard; the tree is around 22 MB. Attribution is preserved verbatim and carried from the TileJSON onto the inlined source — it must remain visible in rendered captures.
The suite enforces the guarantee: every test context installs a context-level route (tests/browser/helpers/test.js) that blocks every non-localhost http(s) request and fails the test naming the offending URL, so a live basemap dependency cannot creep back in.
Byte-identity scope and re-baselining
Regeneration is byte-identical for the same macOS major version, CPU architecture and pinned configuration. Cross-platform (Linux) identity is explicitly not targeted; the macOS identity workflow is the environment-drift canary (see CI/CD automation).
Re-baselining is a deliberate act: run npm run screenshots, review the image diff, then run npm run screenshots:verify. A macOS major upgrade, a SwiftShader-affecting browser change, or a move of the identity workflow to another architecture is a re-baseline event, not a bug.
Capture set
API captures (/browser-api.html) load the shared fixtures over the dev server (markers.json, track-port-hardy-cape-scott.geojson, shapes.json) and frame a Holberg closeup (centre [-128.0145, 50.6525], zoom 14) on the 640×360 #map element so the point, line, and polygon all sit in frame.
| Screenshot | What it shows |
|---|---|
api-quick-start | Injected map with a GeoJSON layer (demo markers) rendered on the vendored basemap |
api-geojson-families | Circle (point), line, and fill (polygon) geometry families rendered |
api-geojson-mixed | Point, line, and polygon sublayers from one mixed FeatureCollection |
Dev-page captures (/) render the full 1280×720 viewport:
| Screenshot | What it shows |
|---|---|
dev-page | Default dev page state — toolbar, map, and the four initial fixture layers |
dev-layer-added | GeoJSON layer added from the toolbar, bounds fitted to [-127.998, 50.661] |
dev-popup | Feature inspector popup open after clicking the "The Scarlet Ibis" marker |
dev-popup-line | Line popup anchored at the click location, with the feature's image |
Docs site
The docs site is a Nuxt static-site app that lives entirely under docs/: docs/nuxt.config.js configures it, source lives in docs/app/, the build module in docs/modules/docs-pages.js, and the internal /api/docs/* routes in docs/server/. npm run docs:dev (nuxt dev docs) serves it locally for previewing prose changes; npm run docs:generate (nuxt generate docs) prerenders every route and writes the deployable site to the generated docs/.output/public.
At build time the module packages the site: it copies the tracked screenshots/ directory, llms.txt, llms-full.txt and docs/assets/waymark-logo.svg (as the site favicon) into the generated docs/public/ (served verbatim), then renders each page once with docs/app/lib/docs-content.mjs into JSON assets under docs/server/assets/docs-pages/, which the /api/docs/nav and /api/docs/[slug] routes read. The docs/public/llms.txt and docs/public/llms-full.txt copies are tracked in version control for repository indexing; npm run docs:sync refreshes them and npm run check:llms verifies they match their docs/ sources.
Pages, routes and the prev/next pagers follow the markdown links in docs/llms.txt: README.md is the home page, and the numbered docs keep their reading order.
- Pages render with
marked(GitHub-flavoured markdown) andhighlight.jsat build time; content headings get self-links and GitHub alerts become branded callouts. - Embedded screenshots render as semantic
<figure>elements with descriptive captions — the markdown image title when present, otherwise the alt text. - Fenced code blocks are syntax-highlighted at build time and get a client-side copy button; wide tables scroll inside their own wrappers.
- Relative
.mdlinks become site routes; other repo-relative links are rewritten tohttps://github.com/OpenGIS/Waymark-JS/blob/master/...; llms bundle links point at the site root. - Layout and theming come from the hosted OpenGIS branding stylesheet plus the
background-map.jsheader module; the favicon and header mark come fromdocs/assets/waymark-logo.svg.
Site configuration is via environment variables, documented in docs/.env.example; copy it to docs/.env (gitignored) to override — values are read from the app root (docs/) when the dev server starts or the site is generated, so restart or regenerate after changing them. NUXT_PUBLIC_PRODUCTION_URL is the single deployment value: it sets both the absolute site URL used for <link rel="canonical"> and the asset base path (derived from its pathname; default https://www.ogis.org/waymark-js, and the root path when unset). NUXT_PUBLIC_BRANDING_URL (default https://www.ogis.org/branding) sets the two branding asset links in the head — the theme stylesheet and the background-map.js module — and nothing else: the logo is a local asset and the Waymark v1 dependencies are resolved inside the hosted module.
docs/assets/ holds the hand-authored logo SVG. SVG assets are formatted via @prettier/plugin-xml, registered in .prettierrc.json, so the logo is covered by npm run format:check.
The Deploy docs workflow (.github/workflows/docs.yml) publishes the site: pushes to master touching docs/**, screenshots/**, scripts/llms-full.mjs, package.json/package-lock.json or the workflow itself — plus manual workflow_dispatch — run npm run check:llms, configure Pages (whose base_url output supplies the deploy target), then npm run docs:generate on Node 24 (.nvmrc) with NUXT_PUBLIC_PRODUCTION_URL derived at deploy time from that output — it drives both the canonical URLs and the asset paths — and upload docs/.output/public via GitHub Pages. One-time setup: Settings → Pages → Source = "GitHub Actions". Published at https://www.ogis.org/waymark-js/.
CI/CD automation
Four GitHub Actions workflows automate the release pipeline:
- CI (
.github/workflows/ci.yml) — runsnpm run test:cion PRs and pushes tomaster. The browser leg is thefunctionalproject only: it renders no committed pixels and the suite has no live basemap dependency. - Release (
.github/workflows/release.yml) — runs on every push tomaster; when the currentpackage.jsonversion has no existing git tag, it creates the tag and a GitHub Release with auto-generated release notes. - Deploy docs (
.github/workflows/docs.yml) — builds the Nuxt docs site and deploys it to GitHub Pages onmasterpushes that touch docs inputs (see Docs site). - Screenshots identity (
.github/workflows/screenshots.yml) — runsnpm run screenshots:verifyonmacos-15-intel(the baseline machine's architecture) for path-filtered pushes tomasterand manual dispatch, and uploadstest-results/screenshots-verify/on failure. It is the environment-drift canary: if the report shows all files differing, suspect environment drift; a subset differing more likely indicates a genuine rendering change.screenshots:verifyis deliberately not part oftest:ci. Note the runner caveat: GitHub's x86_64 macOS images are deprecated upstream and retire in August 2027; moving the job to arm64 will be a deliberate re-baseline event (see Byte-identity scope and re-baselining).