Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion .architecture/build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,43 @@ requirement, used after the switch.
| PHP Compat | `php-compat.yml` | Push/PR |
| WP Smoke | `wordpress-smoke.yml` | Push/PR |
| Deploy WP.org | `deploy-wordpress-org.yml` | Tag push |
| Docs | `docs-site.yml` | Push main |
| Site (landing + docs) → gh-pages | `deploy-site.yml` | Push master |

## Project Website (site/)

Two independent builds published together under one origin (`jeowp.org`):

- **Landing** — React + Vite, source in `landing-page/`, built to `site/` (Vite `outDir`; `LANDING_OUT_DIR` overrides it). `LANDING_BASE=/subpath` produces a homolog build under a subpath.
- **Docs** — VitePress, source in `docs/`, built to `site/docs/`.

Cross-navigation: the landing links into the docs via `landing-page/src/links.js` (`DOCS_URL = '/docs/'`); the docs navbar logo links back to the landing (`themeConfig.logoLink`).

### Site commands

| Command | Purpose |
|---|---|
| `npm run dev:site` | Both dev servers under one origin: http://localhost:5173 (landing, HMR) + `/docs/` (VitePress dev, proxied by the landing dev server) |
| `npm run build:site` | Installs deps and builds both parts into `site/` (landing, then docs) |
| `npm run preview:site` | Serves the built `site/` at http://localhost:4174 |
| `npm --prefix docs run docs:dev` | Docs dev server only |
| `npm --prefix docs run docs:build` | Docs production build only (into `site/docs/`) |
| `LANDING_BASE=/novo npm --prefix landing-page run build` | Landing build under a subpath (homolog) |

> Caveat: `preview:site` uses Python's `http.server`, which does not resolve extensionless URLs — `/docs/concepts` 404s locally while GitHub Pages serves it from `concepts.html`. Verify extensionless docs URLs on the deployed site (or request the `.html` locally).

### Documentation conventions (docs/)

- **Stack**: VitePress with `base: '/docs/'`, `cleanUrls: true`, `srcExclude: ['README.md']` (repo-only index). Output goes straight to `site/docs/` (`outDir`, `emptyOutDir`).
- **Structure**: flat source pages — `docs/*.md` and `docs/dev/*.md`; the home is `docs/index.md`. No rewrites: every page builds to flat `x.html` and is served at the extensionless URL `/docs/x` (GitHub Pages resolves `/x` → `x.html`). Legacy trailing-slash URLs (`/docs/x/`, MkDocs era) are not served.
- **Internal links**: relative `.md` anchors in natural source-space form — the same path you would use to open the target file from the linking page:
- root → root: `[AI Settings](ai-settings.md)`
- root → dev: `[Geocoders](dev/geocoders.md)`
- dev → dev sibling: `[Geocoders](geocoders.md)`
- dev → root: `[AI Settings](../ai-settings.md)`
- home → page: `[Read documentation](index-plugin.md)`
- **Why relative `.md`**: VitePress fails the build on dead links for `.md`/`.html` targets only — absolute links (`/page/`) bypass the check. Relative anchors also resolve when browsing the repo on GitHub.
- **Images**: shared `docs/img/` directory, referenced relatively (`![alt](img/foo.png)`) — valid because pages stay flat.
- **Publishing**: `deploy-site.yml` builds the complete site (landing + docs) on every push to `master` and deploys it to the `gh-pages` branch. `site/` is gitignored. Builds are deterministic (content-hashed assets are stable across builds).

## Node.js

Expand Down
56 changes: 50 additions & 6 deletions .architecture/composed-styles/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ rendering path for `mapbox`-type layers (shipped in 3.1.0).
| File | Role |
|------|------|
| `src/includes/maps/class-map-style-composer.php` | `Jeo\Map_Style_Composer` — backend engine (Singleton) |
| `src/includes/maps/class-style-token-migrator.php` | `Jeo\Style_Token_Migrator` — converts legacy `{token}` strings to expressions (MapLibre compat) |
| `src/includes/loaders.php` | Accessor `jeo_map_style_composer()` |
| `src/includes/class-jeo.php` | Instantiation (`::init`) + `jeoMapVars` localization |
| `src/js/src/shared/composed-style-data.js` | Frontend/editor data loader (`loadComposedStyleData`) |
Expand Down Expand Up @@ -45,7 +46,7 @@ Registered in `Jeo::init()` (`class-jeo.php:49`).
| Constant | Value | Purpose |
|----------|-------|---------|
| `CACHE_DIR` | `jeo-mapbox-composed-styles` | Subdirectory under `wp_upload_dir()['basedir']` |
| `CACHE_VERSION` | `15` | Bump to invalidate all artifacts; embedded in hash & metadata |
| `CACHE_VERSION` | `18` | Bump to invalidate all artifacts; embedded in hash & metadata |
| `TOKEN_PLACEHOLDER` | `__JEO_MAPBOX_ACCESS_TOKEN__` | Replaces raw `access_token=...` in stored style JSON |
| `DEFAULT_FALLBACK_SPRITE` | `mapbox://sprites/mapbox/standard` | Used when source sprites lack icons |
| `VIRTUAL_SCOPE_PREVIEW` | `preview` | Editor preview scope (requires `edit_posts`) |
Expand All @@ -60,16 +61,25 @@ build_context(map_id) / build_virtual_context(payload)
├── Jeo::fetch_mapbox_style() // GET api.mapbox.com/styles/v1/{styleId} per mapbox ref
│ // shared helper, transient-cached (jeo_mapbox_style_json_*)
│ // purged on layer save + forced refresh; TTL filter jeo_mapbox_style_cache_ttl (default 1h)
│ // transient stores the RAW body; decode + legacy {token} normalization
│ // (Style_Token_Migrator) run on every read — cache never depends on the code
├── migrate_legacy_style_functions() // convert pre-expressions {base, stops} objects → interpolate/step/match expressions (MapLibre compat; see below)
├── Style_Token_Migrator::migrate_style() // rewrite "{token}" strings in text-field/icon-image → get/concat expressions,
│ // including tokens nested inside expression values (legacy function outputs); MapLibre 3+
│ // renders unmigrated tokens literally (the "{name_en}" bug)
├── build_composite_sprite()// GD: merge sprites (1x + @2x), prefix image names, pack 2048px canvas
│ // raw sprite JSON+PNG transient-cached (jeo_sprite_*, TTL filter jeo_sprite_cache_ttl, default 1d);
│ // forced refresh bypasses the sprite cache
├── select_glyphs() // first bundle with text-font, else first glyphs URL
├── merge root properties // projection, light, terrain, fog (warns on conflicts)
├── copy + remap layers // source remap, ref-layer remap, visibility by `default`, image-prefix rewrite
├── normalize_unsupported_expressions() // replace Mapbox-only pitch/distance-from-center → 0 (MapLibre compat)
├── is_style_layer_enabled()// honor per-layer style_layers[].show
├── transform_interactions()// remap click/mouseover targets → composite layer IDs
└── build_direct_layer() // append mapbox-tileset-*, mvt, tilelayer as plain sources+layers
→ write_json_file() × 3 // style.json, manifest.json, report.json (pretty-printed)
→ write_json_file() × 3 // style.json, manifest.json, report.json (pretty-printed);
// write failures are logged ([JEO] prefix) and returned as WP_Error
// (a silent failure would force recomposition on every request)
```

### Artifacts (per composition)
Expand Down Expand Up @@ -109,12 +119,12 @@ All under namespace `jeo/v1`. See also [`rest-api/README.md`](../rest-api/README
| Route | Method | Permission | Description |
|-------|--------|------------|-------------|
| `/map-style/{id}` | GET | `can_read_map` (published OR `edit_post`) | Metadata (`enabled`, `style`/`manifest` URLs, `warnings`). Arg `refresh` forces regeneration. |
| `/map-style/{id}/style` | GET | `can_read_map` | Serves `style.json` (`Cache-Control: public, max-age=300`) |
| `/map-style/{id}/manifest` | GET | `can_read_map` | Serves `manifest.json` (`Cache-Control: public, max-age=300`) |
| `/map-style/{id}/style` | GET | `can_read_map` | Serves `style.json`. With a valid `?hash=` matching an existing artifact: `Cache-Control: public, max-age=31536000, immutable` (content-addressed); otherwise current artifact with `max-age=300` |
| `/map-style/{id}/manifest` | GET | `can_read_map` | Serves `manifest.json` with the same hash-aware caching as `/style` |
| `/map-style/compose` | POST | `can_create_virtual` | Compose from a JSON payload (scope/kind/postId/layers/center/zoom). `onetime`=public, `preview`=logged-in + `edit_posts` |
| `/map-style/layer/{id}/refresh` | POST | `can_refresh_layer` (`map-layer` + `edit_post`) | Force-regenerate all maps referencing a layer. Returns `{mapIds[], refreshed, failed}` |
| `/map-style/{scope:preview\|onetime}/{hash}/style` | GET | `can_read_virtual` | Virtual artifact `style.json` (`onetime`=open, `preview`=logged-in + `edit_posts`) |
| `/map-style/{scope:preview\|onetime}/{hash}/manifest` | GET | `can_read_virtual` | Virtual artifact `manifest.json` |
| `/map-style/{scope:preview\|onetime}/{hash}/style` | GET | `can_read_virtual` | Virtual artifact `style.json` (`onetime`=open, `preview`=logged-in + `edit_posts`; `max-age=31536000, immutable`) |
| `/map-style/{scope:preview\|onetime}/{hash}/manifest` | GET | `can_read_virtual` | Virtual artifact `manifest.json` (`max-age=31536000, immutable`) |

> **Preview nonce (3.1.1):** for `preview` scope with a logged-in user, the metadata
> response appends `_wpnonce=wp_create_nonce('wp_rest')` to style/manifest URLs so
Expand Down Expand Up @@ -154,6 +164,12 @@ run `compose_context`, write all three JSON files, persist post meta.
- `save_post_map` → deletes the three meta keys above (next read regenerates).
- `save_post_map-layer` → finds all maps using the layer (`get_map_ids_for_layer`,
serialized-meta LIKE query) and invalidates each.
- Forced refresh (`?refresh=true`, layer refresh endpoint) → purges the shared style JSON
transients (`purge_ref_style_caches`) **and** bypasses the sprite transients so
recomposition goes back to the sources.
- Artifact write failures are surfaced (`_jeo_mapbox_composed_style_error` + `[JEO]` log
line) instead of being silently ignored — a failing write would otherwise make every
request recompose from scratch.

### Virtual Cache Cleanup

Expand All @@ -171,6 +187,7 @@ than the TTL (`jeo_mapbox_composed_virtual_cache_ttl`).
| `jeo_mapbox_composed_style_fallback_sprite` | `mapbox://sprites/mapbox/standard` | Fallback sprite root (`''` disables) |
| `jeo_mapbox_composed_virtual_cache_ttl` | `30 * DAY_IN_SECONDS` (onetime) / `DAY_IN_SECONDS` (preview) (arg: `$scope`) | Virtual cache TTL |
| `jeo_mapbox_composed_style_default_glyphs` | `mapbox://fonts/mapbox/{fontstack}/{range}.pbf` | Default glyphs passed to frontend (in `class-jeo.php`) |
| `jeo_sprite_cache_ttl` | `DAY_IN_SECONDS` (arg: `$url`) | TTL for the raw sprite JSON/PNG transients (`jeo_sprite_*`); sprites are immutable per style version |

## Frontend Localization (`jeoMapVars`)

Expand Down Expand Up @@ -299,6 +316,33 @@ token is set, the existing sanitize-to-placeholder behavior is preserved.
- **MapLibre compatibility**: `normalize_unsupported_expressions` replaces Mapbox-only
expression operators (`pitch`, `distance-from-center`) with `0` so composed styles render
under MapLibre.
- **Legacy token strings**: Pre-expressions Mapbox styles (streets v7/v8-era) declare
`text-field`/`icon-image` as `"{name_en}"` token strings. MapLibre GL JS 3+ removed token
interpolation, so unmigrated values render the literal `{name_en}` text on the map.
`Jeo\Style_Token_Migrator` (also applied in `Jeo::fetch_mapbox_style()` /
`Jeo::fetch_style_json()` before caching) rewrites them:
- a single `"{name}"` → `["get", "name"]`
- mixed text → `["concat", "text ", ["get", "a"], ...]`
- tokens nested inside expression values (e.g. `["step", ["zoom"], "{abbr}", 6, "{name_en}"]`,
produced when legacy stop-function outputs were tokens) are rewritten in place
- `{{`/`}}` literal-brace escapes are honored; strings that only produce literal text are
left untouched so the migration stays idempotent
- sources are never visited — tile URL templates (`{z}/{x}/{y}`) are safe
- **ordering matters**: the migrator never descends into JSON objects (PHP decoded both
objects and lists as arrays; `array_is_list()` distinguishes them). Raw legacy stop
functions (`{stops: [...]}`) are objects whose outputs must stay raw until
`migrate_legacy_style_functions()` converts them — the composer-side token pass then
converts the token strings that survive inside the resulting step/interpolate
expressions. Descending into the object pre-conversion caused the composer to
literal-wrap the migrated expressions (`["literal", ["get","abbr"]]`), which MapLibre
rejects with `Expected formatted but found array<string, 2> instead.`
- **scope invariant**: only strings containing `{}` are ever rewritten — token-free
values (font stacks, offsets, colors) are untouched, and `wrap_function_output()`
keeps its original unconditional literal-wrapping for array outputs. The style
transients cache the raw response body and derive (decode + normalize) on every
read, so changing normalization rules never invalidates cache entries and entries
from the regression window (derived arrays) are simply cache misses
Conversions are counted per style and reported as a composer warning.
- **Legacy stop functions**: Old Mapbox styles (pre-expressions era, e.g. streets v7-based
styles) declare paint/layout values as `{base, stops}` function objects, which MapLibre
rejects with `Bare objects invalid. Use ["literal", {...}]`. Right after fetching each
Expand Down
1 change: 1 addition & 0 deletions .architecture/maps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
|------|------|
| `src/includes/maps/class-maps.php` | `Jeo\Maps` class — CPT, meta, shortcode |
| `src/includes/maps/class-map-style-composer.php` | `Jeo\Map_Style_Composer` — merges Mapbox styles into cached composite Style JSON (see [`composed-styles/README.md`](../composed-styles/README.md)) |
| `src/includes/maps/class-style-token-migrator.php` | `Jeo\Style_Token_Migrator` — converts legacy `{token}` strings to style-spec expressions (MapLibre compat) |
| `src/js/src/jeo-map/class-jeo-map.js` | `JeoMap` class — frontend rendering |
| `src/js/src/jeo-map/index.js` | Entry point, DOM scan |
| `src/js/src/maps-sidebar/` | Gutenberg sidebar for editing |
Expand Down
68 changes: 68 additions & 0 deletions .github/workflows/deploy-site.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Deploy Site

on:
push:
branches:
- master
paths:
- docs/**
- landing-page/**
- package.json
- .github/workflows/deploy-site.yml
workflow_dispatch:

permissions:
contents: write

concurrency:
group: deploy-site
cancel-in-progress: true

jobs:
deploy-site:
name: Build and deploy to gh-pages
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
cache-dependency-path: |
docs/package-lock.json
landing-page/package-lock.json

- name: Build site (landing + docs)
run: npm run build:site

- name: Deploy to gh-pages
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

# Reuse the existing gh-pages history when present; fall back to an
# orphan branch on first deploy (e.g. fresh forks).
if git rev-parse --verify -q origin/gh-pages >/dev/null; then
git worktree add deploy origin/gh-pages
else
git worktree add --orphan -b gh-pages deploy
fi

# Swap in the fresh build (the worktree's .git file stays put) and
# guarantee the GitHub Pages control files.
find deploy -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +
cp -a site/. deploy/
touch deploy/.nojekyll
echo 'jeowp.org' > deploy/CNAME

git -C deploy add -A
if [ -z "$(git -C deploy status --porcelain)" ]; then
echo "gh-pages is already up to date."
exit 0
fi
git -C deploy commit -m "deploy: publish site build of ${GITHUB_SHA:0:7}"
git -C deploy push origin HEAD:gh-pages
49 changes: 0 additions & 49 deletions .github/workflows/docs-site.yml

This file was deleted.

6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,9 @@ dist/
docs/node_modules/
docs/.vitepress/cache/
docs/.vitepress/dist/

# Generated site output — deployed to gh-pages by CI (deploy-site.yml)
/site/

# Legacy landing workspace (figma references, local notes)
/site-novo/
Loading
Loading