diff --git a/.architecture/build/README.md b/.architecture/build/README.md index 50ccac10b..0cd27d44b 100644 --- a/.architecture/build/README.md +++ b/.architecture/build/README.md @@ -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 diff --git a/.architecture/composed-styles/README.md b/.architecture/composed-styles/README.md index 767a253a1..aec545679 100644 --- a/.architecture/composed-styles/README.md +++ b/.architecture/composed-styles/README.md @@ -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`) | @@ -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`) | @@ -60,8 +61,15 @@ 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 @@ -69,7 +77,9 @@ build_context(map_id) / build_virtual_context(payload) ├── 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) @@ -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 @@ -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 @@ -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`) @@ -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 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 diff --git a/.architecture/maps/README.md b/.architecture/maps/README.md index b8f1e108c..2bcf50373 100644 --- a/.architecture/maps/README.md +++ b/.architecture/maps/README.md @@ -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 | diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml new file mode 100644 index 000000000..47f2c3047 --- /dev/null +++ b/.github/workflows/deploy-site.yml @@ -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 diff --git a/.github/workflows/docs-site.yml b/.github/workflows/docs-site.yml deleted file mode 100644 index 7d9a5f36a..000000000 --- a/.github/workflows/docs-site.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Sync Docs Site - -on: - push: - branches: - - master - paths: - - docs/** - - landing-page/** - - package.json - - .github/workflows/docs-site.yml - workflow_dispatch: - -permissions: - contents: write - -jobs: - sync-site: - name: Build and sync site/ - 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: Commit generated site - run: | - if [ -z "$(git status --porcelain -- site)" ]; then - echo "site/ is already up to date." - exit 0 - fi - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A site - git commit -m "docs: sync generated site" - git push diff --git a/.gitignore b/.gitignore index c3480f093..e78241b29 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/AGENTS.md b/AGENTS.md index 3c870a9e7..6a7155ec8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,7 @@ npm run test:unit -- path/to/file.test.js # single file npm run test:unit -- -t "name of the test case" # single test by name npm run sync:version # Sync version across src/jeo.php, package.json, package-lock.json, and translation catalogs -# Docs (VitePress — source docs/, output committed to site/docs/) +# Docs (VitePress — source docs/, local build output in site/docs/, gitignored) npm --prefix docs run docs:dev # Dev server with live reload npm --prefix docs run docs:build # Production build into site/docs/ @@ -113,7 +113,7 @@ bash .docker/switch-php.sh # Switch Docker PHP version for local testing - **Minimap layer provenance**: Each layer object carries an optional `provenance` field (`'manual'` | `'ai'`). Layers added via the layer library modal are `'manual'`; AI-generated layers are `'ai'`. During chat refinement, `preserve_manual_layers()` guarantees that manual layers dropped by the AI are merged back — unless the AI declares them in `Minimap_Output::$removed_layer_ids` (a structured-output field the AI populates when the user explicitly asks to remove a layer; language-agnostic, no keyword lists). On regenerate, manual layers are NOT preserved (fresh start). `tag_layer_provenance()` propagates the field across responses. `build_state_context()` annotates manual layers with `[manually added]` in the AI prompt. - **Minimap render order**: `Minimap::normalize_layer_render_order()` stable-sorts layers bottom-to-top as raster → vector (base layer is separate and always below). Raster group: `tilelayer`, `mapbox-tileset-raster`, plus `mapbox` styles whose style JSON contains raster sub-layers (fetched via shared `Jeo::fetch_mapbox_style()`, transient-cached; fetch failure = raster, conservatively). Vector group: `mvt`, `mapbox-tileset-vector`, vector-only `mapbox` styles. All render paths stack by array order, so this covers editor, frontend, and composed styles. Applied in `run_agent()` (all AI paths) and legacy `api_setup()` (RAG). - **Minimap version history**: `_jeo_minimap_versions_{conversation_id}` post meta (capped at `Minimap::VERSIONS_LIMIT = 20`, oldest dropped) stores a snapshot per successful AI turn (post-normalization layers/base/center/zoom/pins + label + timestamp). "Return to previous version" works deterministically: the AI declares `Minimap_Output::$restore_version` (1-based, only on explicit user request — see the version list in `build_state_context()`), and `apply_version_restore()` replaces the output with the stored snapshot. Restore turns SKIP `tag_layer_provenance`, `apply_diff_guard`, and `preserve_manual_layers` (restores are exact snapshots — manual layers added later are dropped). Restores append a new version entry (git-revert semantics). Legacy conversations have no history: invalid/absent versions fall back to the AI reconstruction with a notice — never crashes. `to_rest_response()` includes `restored_version` (additive). -- **Mapbox style fetch helper**: `Jeo::fetch_mapbox_style( $style_id, $token, $args )` (class-jeo.php) is the single Mapbox Styles API client, used by the composer and the minimap. Successful fetches are transient-cached (`jeo_mapbox_style_json_*`, TTL filter `jeo_mapbox_style_cache_ttl`, default 1h; `bypass_cache` arg to skip). The cache is purged by the composer on `save_post_map-layer` and on forced refresh (`refresh` REST param / layer cache-refresh endpoint) via `Jeo::delete_mapbox_style_cache()`. Never fetch Mapbox styles inline — reuse this helper. Its generic keyless counterpart `Jeo::fetch_style_json( $style_url, $args )` (same caching pattern, `jeo_style_json_*` / `jeo_style_json_cache_ttl`, purge via `Jeo::delete_style_json_cache()`) serves the `style-json` layer type and any other keyless style provider. +- **Mapbox style fetch helper**: `Jeo::fetch_mapbox_style( $style_id, $token, $args )` (class-jeo.php) is the single Mapbox Styles API client, used by the composer and the minimap. Successful fetches are transient-cached (`jeo_mapbox_style_json_*`, TTL filter `jeo_mapbox_style_cache_ttl`, default 1h; `bypass_cache` arg to skip). The cache is purged by the composer on `save_post_map-layer` and on forced refresh (`refresh` REST param / layer cache-refresh endpoint) via `Jeo::delete_mapbox_style_cache()`. Never fetch Mapbox styles inline — reuse this helper. Both fetch helpers cache the **raw response body** and derive on every read (`derive_style_from_body`): decode + legacy `{token}` normalization (`text-field`/`icon-image`) via `Jeo\Style_Token_Migrator` — MapLibre GL JS 3+ renders unmigrated tokens literally, and because nothing derived is cached, normalization changes never need cache invalidation. Its generic keyless counterpart `Jeo::fetch_style_json( $style_url, $args )` (same caching pattern, `jeo_style_json_*` / `jeo_style_json_cache_ttl`, purge via `Jeo::delete_style_json_cache()`) serves the `style-json` layer type and any other keyless style provider. - **Style layer types (`style-json`)**: `mapbox` and `style-json` are style types — they load a whole MapLibre GL style that becomes the map's base style, never individual GL layers. The contract is declared per type: JS `isStyle: true` on `JeoLayerTypes.registerLayerType()` (+ optional `getStyleUrl`/`getInlineStyle`/`getStyle`), PHP `is_style => true` on `Layer_Types::register_layer_type()` (`Layer_Types::is_style()`, filterable via `jeo_layer_type_is_style`). Consumers must key off the flag, never the type slug. The `style-json` schema mirrors `geojson`: `style_url` (public style JSON URL) or `inline_style` (raw JSON text, takes precedence), no API key required. The composer merges any `is_style` type with `style_url`/`inline_style` as a bottom bundle (fetch via `fetch_style_json()`); on keyless sites the frontend applies the base via `map.setStyle()` (`class-jeo-map.js::applyBaseStyleLayer`, mirrored in `storymap-display.js`). A future provider needs only the two registrations. - **Minimap base layers**: defaults are runtime-aware (`get_default_base_layers()`): sites with a Mapbox key get `mapbox` styles, keyless sites get `style-json` (OpenFreeMap Dark) for `dark`, OSM tiles for `light`, and Esri for `satellite` — the old CARTO dark tilelayer no longer works (API key now required). Base CPTs matching a known legacy signature (CARTO dark raster) are rewritten in place with the current default by `repair_base_layer_if_legacy()` — lazily on every base lookup and eagerly via `maybe_migrate_base_layers()` on `admin_init` (versioned by `Minimap::BASE_LAYERS_MIGRATION_VERSION`; bump when defaults change). User-customized bases are never touched. Filterable via `jeo_minimap_base_layers`. - **Boundary layer generation**: `Place_Polygon_Service` (`class-place-polygon-service.php`) resolves a place name into an authoritative polygon using adapter-specific public sources: IBGE for Brazilian municipalities/states (honors the `entity_type` hint and treats a non-Brazilian context as "not applicable" — e.g. "França" with `entity_type=state` must not accent-match Franca/SP), FUNAI WFS for indigenous lands (endpoint `geoserver.funai.gov.br`, feature `Funai:tis_poligonais`, name attribute `terrai_nome`, OGC `PropertyIsLike` filter with `matchCase="false"` — the old `catalogo.funai.gov.br` CQL/`strToLower` endpoint is dead), and Nominatim (`polygon_geojson=1`, geometry decoded as stdClass for memory) with Overpass (POST, mirrors overpass-api.de → overpass.osm.ch → maps.mail.ru, 25s/mirror) as fallback for international boundaries. Results are cached per place, deduplicated by content hash, and published as WordPress attachment GeoJSON files for stable public URLs. Adapter `null` = "not applicable" (try next); adapter `WP_Error` = that source failed, but the remaining adapters still run (last error returned only if all fail). Boundary layers are created with type `geojson` (`layer_type_options.data` = attachment URL; the boundary fill colors are stored in the `default_style` post meta, not in `layer_type_options`) and rendered **client-side** by the `geojson` layer type (`layer-types/geojson.js`): a single `fill` GL layer whose outline comes from the `fill-outline-color` paint prop (~1px antialiasing halo, no configurable width/opacity — the old `{layer_id}__outline` companion line layer was replaced; the legacy toggle in `class-jeo-map.js::changeLayerVisibitly` remains only for layers created before the switch). No Mapbox style publishing is involved: the Mapbox Styles API rejects `geojson` sources in hosted styles (HTTP 422). The `geojson` type is also supported by the editor preview (`map-preview-layer.js`) and the composer (direct-type inlining, one manifest `compositeLayers` entry per GL layer). The type accepts either `data` (public GeoJSON URL) or `inline_geojson` (raw JSON text, takes precedence), and its render type is keyed by `layer_type_options.type` — only `fill` is exposed in the schema for now, with the renderers structured as switches ready for future types (line, circle, …). Styling lives **on the map layer instance** (`layers[i].style`, the same flat `{ use_default, paint, layout }` shape used by `mvt`/`mapbox-tileset-vector`), never on the layer CPT: when the instance sets `use_default`, the layer's `default_style` meta takes over. All renderers resolve the effective style identically — editor preview, frontend runtime (`class-jeo-map.js` forwards `style`/`default_style` to `JeoLayer`), and composer (`resolve_effective_layer_style()` in `class-map-style-composer.php`). Paint defaults are shared with the rest of the plugin via `JeoLayerTypes.getFallbackPaint('fill')` (`layer-types/JeoLayerTypes.js`, mirrored in the composer's PHP default paint — keep both in sync), and instance opacity multiplies numeric opacity props of the merged paint. It uses only style-spec v8 core APIs, so it is compatible with both the MapLibre GL JS and Mapbox GL JS runtimes. Tool failures are logged to the PHP error log with the `[JEO]` prefix. @@ -210,7 +210,7 @@ GitHub Actions run via **9 workflows** in `.github/workflows/` plus a shared com - `phpcs-wpcs.yml` — WPCS lint - `node-frontend.yml` — Node build + Jest tests - `dependency-review.yml` — Dependency security review -- `docs-site.yml` — Complete site build (landing + docs, committed to `site/`) +- `deploy-site.yml` — Builds the complete site (landing + docs) on push to master and publishes it to the `gh-pages` branch (GitHub Pages at jeowp.org, guarantees `.nojekyll` + `CNAME`) ### WordPress.org Compliance Rules diff --git a/README.md b/README.md index 0849b7d02..05f99d2d5 100644 --- a/README.md +++ b/README.md @@ -215,5 +215,6 @@ To build the complete site locally: npm run build:site # landing → site/, docs → site/docs/ ``` -Changes to `docs/` or `site-novo/www/` on `master` automatically regenerate -and commit `site/` through `.github/workflows/docs-site.yml`. +Changes to `docs/` or `landing-page/` on `master` are built and published to +the `gh-pages` branch (GitHub Pages at jeowp.org) through +`.github/workflows/deploy-site.yml`. diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index badd6c522..b58db7557 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,52 +1,5 @@ import { defineConfig } from 'vitepress' -// Route map: source file → URL. Every content page is served as a directory -// route (`/page/`) to keep URL parity with the previous MkDocs build -// (`site_dir: site/docs`, directory URLs). Content pages keep relative -// `img/...` references (resolved against the source file), but internal -// links must use absolute routes (`/page/`, `/dev/page/`) because relative -// `.md` links are resolved against the rewritten path and fail dead-link -// checks. -const pages = [ - 'ai-bulk-geolocation', - 'ai-context-assistant', - 'ai-georeferencing', - 'ai-in-jeo', - 'ai-settings', - 'concepts', - 'discovery', - 'getting-started', - 'geolocating-posts', - 'index-plugin', - 'layer-post', - 'map-block', - 'map-embed', - 'map-post', - 'map-shortcode', - 'minimap', - 'one-time-map-block', - 'stories-near-you', - 'story-map', -] - -const devPages = [ - 'dependency-maintenance', - 'geocoders', - 'geo-information', - 'layer-types', - 'layer-types-api', - 'migration', - 'php-compatibility', -] - -const rewrites: Record = {} -for (const page of pages) { - rewrites[`${page}.md`] = `${page}/index.md` -} -for (const page of devPages) { - rewrites[`dev/${page}.md`] = `dev/${page}/index.md` -} - export default defineConfig({ lang: 'en', title: 'JEO Maps', @@ -57,7 +10,6 @@ export default defineConfig({ base: '/docs/', cleanUrls: true, srcExclude: ['README.md'], - rewrites, // Build straight into the committed site/ tree (same contract as MkDocs) outDir: '../site/docs', @@ -70,6 +22,8 @@ export default defineConfig({ light: { src: '/logo-light.svg', alt: 'JEO Maps' }, dark: { src: '/logo-dark.svg', alt: 'JEO Maps' }, }, + // Logo click goes to the landing page, not the docs home. + logoLink: 'https://jeowp.org/', // The logo SVG already carries the "JEO Maps" wordmark — showing the // site title next to it would duplicate the brand. The page-level @@ -77,11 +31,7 @@ export default defineConfig({ siteTitle: false, nav: [ - // The landing lives at the site root, outside of /docs/ — an absolute - // URL is required because VitePress prefixes internal links with the - // base. `target: '_self'` keeps the default external-link "_blank" - // from applying. - { text: 'Home', link: 'https://jeowp.org/', target: '_self' }, + { text: 'Home', link: '/' }, { text: 'Plugin', items: [ @@ -127,7 +77,7 @@ export default defineConfig({ ], sidebar: [ - { text: 'Home', link: 'https://jeowp.org/', target: '_self' }, + { text: 'Home', link: '/' }, { text: 'Plugin', items: [ diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index 6a69fdb52..deda91d4e 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -40,6 +40,14 @@ --vp-c-bg-soft: #13223a; } +html:not(.dark) .dark-only { + display: none; +} + +html.dark .light-only { + display: none; +} + @media (min-width: 960px) { :root { --vp-nav-logo-height: 50px; diff --git a/docs/README.md b/docs/README.md index 14717e2bb..8800d706f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,7 +17,7 @@ JEO wants to help journalists and NGOs to improve storytelling with maps. Creati - Geocoding WordPress posts using OpenStreetMap (Nominatim), with extensibility for additional geocoders via hook. - Customizable marker icons that can be associated to categories, custom taxonomies or posts directly. - Map markers query integrated to posts query. -- Support [WPML](https://wpml.org/pt-br/) and [Polylang](https://br.wordpress.org/plugins/polylang/) multilingual plugins +- Support [WPML](https://wpml.org/pt-br/) and [Polylang](https://wordpress.org/plugins/polylang/) multilingual plugins - AI-powered georeferencing with multiple AI providers - AI-assisted map generation (Minimap) with chat refinement - Stories Near You block with geolocation-based post discovery diff --git a/docs/ai-bulk-geolocation.md b/docs/ai-bulk-geolocation.md index 79a043908..d2fadec90 100644 --- a/docs/ai-bulk-geolocation.md +++ b/docs/ai-bulk-geolocation.md @@ -8,7 +8,7 @@ The Bulk Geolocation system processes posts in batches using WordPress cron (WP- ## Setting up -1. Configure an AI provider in **Jeo → AI** (see [AI Settings](/ai-settings/)). +1. Configure an AI provider in **Jeo → AI** (see [AI Settings](ai-settings.md)). 2. Go to the **Bulk Geolocation** tab in **Jeo → AI**. 3. Select the post types to process, set the confidence threshold, and configure the batch size. 4. Start the bulk process. diff --git a/docs/ai-context-assistant.md b/docs/ai-context-assistant.md index bda60ea5c..247b95321 100644 --- a/docs/ai-context-assistant.md +++ b/docs/ai-context-assistant.md @@ -2,7 +2,7 @@ The AI Context Assistant is a Gutenberg sidebar panel that helps you enrich your articles with AI-suggested paragraphs and references to related content from your site's knowledge base. -See [AI in JEO](/ai-in-jeo/) to learn how RAG, agents, and conversation memory power this feature. +See [AI in JEO](ai-in-jeo.md) to learn how RAG, agents, and conversation memory power this feature. ## Opening the panel @@ -81,6 +81,6 @@ When unchecked, the tab shows the default prompt in a read-only field for refere ## Requirements -- An AI provider must be configured in **JEO → AI** (see [AI Settings](/ai-settings/)). +- An AI provider must be configured in **JEO → AI** (see [AI Settings](ai-settings.md)). - For best results, index your posts in the Knowledge Base (**JEO → AI → Knowledge Base** tab). The assistant uses RAG to find related articles from the vectorized archive. - The article must have at least 100 characters of content before the AI can generate suggestions. If the article is too short, the assistant will ask you to write more or specify what you want. diff --git a/docs/ai-georeferencing.md b/docs/ai-georeferencing.md index a164f5b79..fe9815b17 100644 --- a/docs/ai-georeferencing.md +++ b/docs/ai-georeferencing.md @@ -2,7 +2,7 @@ JEO can automatically georeference your posts using AI. Instead of manually searching and adding geolocation points, the AI analyzes your post content and suggests relevant locations with coordinates and confidence scores. -See [AI in JEO](/ai-in-jeo/) for an overview of how the AI system works. +See [AI in JEO](ai-in-jeo.md) for an overview of how the AI system works. ## How it works @@ -51,5 +51,5 @@ The conversation history is preserved per post, so you can continue refining in ## Requirements -- An AI provider must be configured in **Jeo → AI** (see [AI Settings](/ai-settings/)). +- An AI provider must be configured in **Jeo → AI** (see [AI Settings](ai-settings.md)). - The post must have a title and/or content for the AI to analyze. diff --git a/docs/ai-in-jeo.md b/docs/ai-in-jeo.md index 5cca106ab..945c630b9 100644 --- a/docs/ai-in-jeo.md +++ b/docs/ai-in-jeo.md @@ -6,7 +6,7 @@ JEO includes built-in AI capabilities for georeferencing posts, generating maps, JEO's AI features are powered by a configurable external AI provider. The plugin never runs its own AI model — instead, it sends requests to the provider you choose (such as OpenAI, Google Gemini, or a local Ollama instance) and processes the results. -All AI features are **optional**. They activate only after you configure a provider in **Jeo → AI** (see [AI Settings](/ai-settings/)). +All AI features are **optional**. They activate only after you configure a provider in **Jeo → AI** (see [AI Settings](ai-settings.md)). The key idea is that each AI feature is a specialized **agent** — an AI assistant with a specific role, a set of **tools** it can use, and access to your site's content through a **knowledge base**. @@ -26,8 +26,8 @@ JEO maintains **two separate knowledge bases** (called *vector stores*): | Knowledge base | Content | Used by | |----------------|---------|---------| -| **Posts** (`jeo_knowledge`) | Your published posts — titles and body text | [Context Assistant](/ai-context-assistant/) | -| **Layers** (`jeo_layers_knowledge`) | Your map layers — titles, types, legends, and descriptions | [Minimap](/minimap/) | +| **Posts** (`jeo_knowledge`) | Your published posts — titles and body text | [Context Assistant](ai-context-assistant.md) | +| **Layers** (`jeo_layers_knowledge`) | Your map layers — titles, types, legends, and descriptions | [Minimap](minimap.md) | Each item is converted into a numerical representation (an *embedding*) and stored in a local file-based index. When an AI feature needs context, it: @@ -59,10 +59,10 @@ The **topK** setting (in Knowledge Base tab, range 1–50, default 10) controls | Feature | Uses RAG? | Knowledge base | Purpose | |---------|-----------|----------------|---------| -| [AI Georeferencing](/ai-georeferencing/) | Optional | — | Can include taxonomy context; does not use the vector store directly | -| [Minimap](/minimap/) | Yes | Layers (`jeo_layers_knowledge`) | Searches for relevant map layers to include in the generated map | -| [Context Assistant](/ai-context-assistant/) | Yes | Posts (`jeo_knowledge`) | Finds related articles to reference in suggestions | -| [AI Bulk Geolocation](/ai-bulk-geolocation/) | No | — | Processes posts individually without RAG | +| [AI Georeferencing](ai-georeferencing.md) | Optional | — | Can include taxonomy context; does not use the vector store directly | +| [Minimap](minimap.md) | Yes | Layers (`jeo_layers_knowledge`) | Searches for relevant map layers to include in the generated map | +| [Context Assistant](ai-context-assistant.md) | Yes | Posts (`jeo_knowledge`) | Finds related articles to reference in suggestions | +| [AI Bulk Geolocation](ai-bulk-geolocation.md) | No | — | Processes posts individually without RAG | | Minilayer | No | — | Uses MCP instead (see below) | ## Structured output @@ -151,18 +151,18 @@ A quick reference for which concepts apply to each feature: | | Provider | RAG | Structured output | Tools | Conversation memory | MCP | |---|:---:|:---:|:---:|:---:|:---:|:---:| -| [AI Georeferencing](/ai-georeferencing/) | ✓ | — | ✓ | — | ✓ | — | -| [AI Bulk Geolocation](/ai-bulk-geolocation/) | ✓ | — | ✓ | — | — | — | -| [Minimap](/minimap/) | ✓ | ✓ | ✓ | ✓ | ✓ | — | -| [Context Assistant](/ai-context-assistant/) | ✓ | ✓ | ✓ | ✓ | ✓ | — | +| [AI Georeferencing](ai-georeferencing.md) | ✓ | — | ✓ | — | ✓ | — | +| [AI Bulk Geolocation](ai-bulk-geolocation.md) | ✓ | — | ✓ | — | — | — | +| [Minimap](minimap.md) | ✓ | ✓ | ✓ | ✓ | ✓ | — | +| [Context Assistant](ai-context-assistant.md) | ✓ | ✓ | ✓ | ✓ | ✓ | — | | Minilayer | ✓ | — | — | — | — | ✓ | ## Next steps -- **Configure a provider**: [AI Settings](/ai-settings/) +- **Configure a provider**: [AI Settings](ai-settings.md) - **Index your content**: Knowledge Base tab in **Jeo → AI** - **Start using features**: - - [AI Georeferencing](/ai-georeferencing/) — auto-tag posts with locations - - [Minimap](/minimap/) — generate maps from text or post content - - [Context Assistant](/ai-context-assistant/) — get AI-powered editorial suggestions - - [AI Bulk Geolocation](/ai-bulk-geolocation/) — process many posts at once + - [AI Georeferencing](ai-georeferencing.md) — auto-tag posts with locations + - [Minimap](minimap.md) — generate maps from text or post content + - [Context Assistant](ai-context-assistant.md) — get AI-powered editorial suggestions + - [AI Bulk Geolocation](ai-bulk-geolocation.md) — process many posts at once diff --git a/docs/ai-settings.md b/docs/ai-settings.md index 88ec559ff..799304230 100644 --- a/docs/ai-settings.md +++ b/docs/ai-settings.md @@ -2,7 +2,7 @@ JEO includes built-in AI capabilities for georeferencing posts, generating maps, and creating custom map layers. Before using these features, you need to configure an AI provider. -See [AI in JEO](/ai-in-jeo/) for an overview of the AI architecture and how each piece works. +See [AI in JEO](ai-in-jeo.md) for an overview of the AI architecture and how each piece works. ## Configuring an AI provider @@ -48,11 +48,11 @@ JEO ships with built-in Brazilian geographic dictionaries (biomes, conservation ### Bulk Geolocation tab -Configure batch geolocation settings for processing many posts at once. See [AI Bulk Geolocation](/ai-bulk-geolocation/) for details. +Configure batch geolocation settings for processing many posts at once. See [AI Bulk Geolocation](ai-bulk-geolocation.md) for details. ### Context Assistant tab -Customize the system prompt used by the [AI Context Assistant](/ai-context-assistant/) — the editorial suggestions sidebar in the Gutenberg editor. +Customize the system prompt used by the [AI Context Assistant](ai-context-assistant.md) — the editorial suggestions sidebar in the Gutenberg editor. - **Use custom prompt**: Check this box to override the built-in default prompt with your own. - **Custom prompt**: Editable textarea for your custom system prompt. When unchecked, the tab shows the default prompt in a read-only field for reference. diff --git a/docs/concepts.md b/docs/concepts.md index 0f184dd98..05ff659a6 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -17,32 +17,32 @@ Additionally, JEO can generate custom layers using AI (see [AI-Generated Map](#a A map is composed of one or more layers and can be associated with related geolocated posts, that'll be rendered as markers on the map. -A map post is used for reusable map interactions, that can be embedded in any post as a [shortcode](/map-shortcode/) or [Gutenberg block](/map-block/). +A map post is used for reusable map interactions, that can be embedded in any post as a [shortcode](map-shortcode.md) or [Gutenberg block](map-block.md). ## One-time Map -Instead of creating a reusable map any time you want to add a map interaction, you can create and configure an inline single-use map via the [One-Time Map block](/one-time-map-block/) for Gutenberg. +Instead of creating a reusable map any time you want to add a map interaction, you can create and configure an inline single-use map via the [One-Time Map block](one-time-map-block.md) for Gutenberg. ## Geolocating Posts -Posts can be geolocated manually by searching and adding points in the Geolocation sidebar, or automatically using AI georeferencing — which analyzes the post content and suggests locations with confidence scores. See [Geolocating posts](/geolocating-posts/) and [AI Georeferencing](/ai-georeferencing/). +Posts can be geolocated manually by searching and adding points in the Geolocation sidebar, or automatically using AI georeferencing — which analyzes the post content and suggests locations with confidence scores. See [Geolocating posts](geolocating-posts.md) and [AI Georeferencing](ai-georeferencing.md). ## AI Features -JEO's AI features are powered by a configurable external AI provider with a built-in knowledge base (RAG), specialized agents, and structured output. See [AI in JEO](/ai-in-jeo/) for a detailed explanation of how these pieces work together. +JEO's AI features are powered by a configurable external AI provider with a built-in knowledge base (RAG), specialized agents, and structured output. See [AI in JEO](ai-in-jeo.md) for a detailed explanation of how these pieces work together. ## AI-Generated Map -An AI-assisted map block that generates contextual maps from a text prompt or post content, with a chat interface for refinement. It can also generate custom map layers when a Mapbox key is configured. See [Minimap](/minimap/). +An AI-assisted map block that generates contextual maps from a text prompt or post content, with a chat interface for refinement. It can also generate custom map layers when a Mapbox key is configured. See [Minimap](minimap.md). ## Story Map -A Story Map is a scrollytelling experience: each **slide** has a location, layers, and text. As the reader scrolls, the map transitions between slides. See [Story Map](/story-map/). +A Story Map is a scrollytelling experience: each **slide** has a location, layers, and text. As the reader scrolls, the map transitions between slides. See [Story Map](story-map.md). ## Discovery -A full-page interactive map where readers can explore geolocated stories, toggle layers from different maps, and share a customized view. See [Discovery](/discovery/). +A full-page interactive map where readers can explore geolocated stories, toggle layers from different maps, and share a customized view. See [Discovery](discovery.md). ## Stories Near You -A block that displays geolocated posts closest to the reader's location, with consent-based browser geolocation and automatic fallback. See [Stories Near You](/stories-near-you/). +A block that displays geolocated posts closest to the reader's location, with consent-based browser geolocation and automatic fallback. See [Stories Near You](stories-near-you.md). diff --git a/docs/dev/geo-information.md b/docs/dev/geo-information.md index 80441b32f..80af99f23 100644 --- a/docs/dev/geo-information.md +++ b/docs/dev/geo-information.md @@ -2,7 +2,7 @@ Each post can be related to one or more points on the map. -For each point, JEO collects geographical information such as city and country names. For complete information on this, see [Geocoders](/dev/geocoders/). +For each point, JEO collects geographical information such as city and country names. For complete information on this, see [Geocoders](geocoders.md). ## How geographical information is stored @@ -67,7 +67,7 @@ Example: ], ``` -The AI also produces a `confidence` score (0–100) during georeferencing, but this is **not** stored in `_related_point` — it is used only during the approval workflow in the bulk processor. See [AI Georeferencing](/ai-georeferencing/) and [AI Bulk Geolocation](/ai-bulk-geolocation/). +The AI also produces a `confidence` score (0–100) during georeferencing, but this is **not** stored in `_related_point` — it is used only during the approval workflow in the bulk processor. See [AI Georeferencing](../ai-georeferencing.md) and [AI Bulk Geolocation](../ai-bulk-geolocation.md). ## How to search for posts by geoinformation? (indexes) diff --git a/docs/geolocating-posts.md b/docs/geolocating-posts.md index 450a655d0..2c66237f1 100644 --- a/docs/geolocating-posts.md +++ b/docs/geolocating-posts.md @@ -16,6 +16,6 @@ Maps with geolocated posts contain markers at the location of each post. Clickin ## AI Georeferencing -Alternatively, JEO can georeference posts automatically using AI. The AI analyzes the post content and suggests geolocation points with confidence scores. You can then review and approve the suggestions. See [AI Georeferencing](/ai-georeferencing/). +Alternatively, JEO can georeference posts automatically using AI. The AI analyzes the post content and suggests geolocation points with confidence scores. You can then review and approve the suggestions. See [AI Georeferencing](ai-georeferencing.md). -For bulk processing many posts at once, see [AI Bulk Geolocation](/ai-bulk-geolocation/). +For bulk processing many posts at once, see [AI Bulk Geolocation](ai-bulk-geolocation.md). diff --git a/docs/getting-started.md b/docs/getting-started.md index f5d0b47b6..6aee6fe50 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -31,6 +31,6 @@ Under **Jeo → AI**, you can configure AI-powered features: * Set up the Knowledge Base (RAG) to index your posts and layers for smarter AI results. * Configure bulk geolocation for processing posts in batches. -See [AI Settings](/ai-settings/) for detailed instructions. +See [AI Settings](ai-settings.md) for detailed instructions. ![JEO AI Settings](img/ai-settings-provider.png) diff --git a/docs/index-plugin.md b/docs/index-plugin.md index 91e4bf195..7a2f2dfff 100644 --- a/docs/index-plugin.md +++ b/docs/index-plugin.md @@ -13,7 +13,7 @@ You can post geolocated stories and create richly designed pages for each one of - Geocoding WordPress posts using OpenStreetMap (Nominatim), supporting the post type `Post`, with extensibility for additional geocoders via hook. - Customizable marker icons that can be associated to categories, custom taxonomies or posts directly. - Map markers query integrated to posts query. -- Support [WPML](https://wpml.org/pt-br/) and [Polylang](https://br.wordpress.org/plugins/polylang/) multilingual plugins +- Support [WPML](https://wpml.org/pt-br/) and [Polylang](https://wordpress.org/plugins/polylang/) multilingual plugins - AI-powered georeferencing, bulk geolocation, and map generation (multiple AI providers) - Stories Near You block with geolocation-based post discovery - AI-assisted map block (Minimap) with chat refinement and auto-generated layers @@ -22,52 +22,52 @@ You can post geolocated stories and create richly designed pages for each one of ### Getting started -- [Concepts](/concepts/) -- [Installing and configuring the plugin](/getting-started/) +- [Concepts](concepts.md) +- [Installing and configuring the plugin](getting-started.md) ### Layers -- [Creating layers](/layer-post/) +- [Creating layers](layer-post.md) ### Maps -- [Creating maps](/map-post/) +- [Creating maps](map-post.md) ### Discovery -- [Using discovery](/discovery/) +- [Using discovery](discovery.md) ### Story Map -- [Creating and using Story Map](/story-map/) +- [Creating and using Story Map](story-map.md) ### Posts -- [Geolocating posts](/geolocating-posts/) -- [AI Georeferencing](/ai-georeferencing/) -- [AI Bulk Geolocation](/ai-bulk-geolocation/) -- [Using a map shortcode](/map-shortcode/) -- [Using a map block](/map-block/) -- [Using an one-time map block](/one-time-map-block/) -- [Using a map embed](/map-embed/) +- [Geolocating posts](geolocating-posts.md) +- [AI Georeferencing](ai-georeferencing.md) +- [AI Bulk Geolocation](ai-bulk-geolocation.md) +- [Using a map shortcode](map-shortcode.md) +- [Using a map block](map-block.md) +- [Using an one-time map block](one-time-map-block.md) +- [Using a map embed](map-embed.md) ### AI Features -- [AI Settings](/ai-settings/) -- [Minimap — AI-Assisted Map Block](/minimap/) +- [AI Settings](ai-settings.md) +- [Minimap — AI-Assisted Map Block](minimap.md) ### Stories Near You -- [Stories Near You block](/stories-near-you/) +- [Stories Near You block](stories-near-you.md) ## Developer documentation ### Tutorials -- [Adding new Layer Types](/dev/layer-types/) -- [Geographical Information of a post](/dev/geo-information/) -- [Writing a Geocoder](/dev/geocoders/) +- [Adding new Layer Types](dev/layer-types.md) +- [Geographical Information of a post](dev/geo-information.md) +- [Writing a Geocoder](dev/geocoders.md) ### Differences between the old and the present JEO -- [Migration](/dev/migration/) +- [Migration](dev/migration.md) diff --git a/docs/index.md b/docs/index.md index b0140524d..44d25c942 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,11 +1,35 @@ ---- -layout: false -head: - - - meta - - http-equiv: refresh - content: '0; url=/docs/index-plugin/' ---- - -

- JEO Maps documentation -

+[![JEO Maps](/logo-light.svg){.light-only} ![JEO Maps](/logo-dark.svg){.dark-only}](https://jeowp.org/) + +The JEO WordPress geojournalism platform allows news organizations, bloggers and NGOs to publish news stories as layers of information on digital maps. With JEO, creating the interaction between data layers and contextual information is intuitive and interactive. + +## JEO Plugin + +Features: + +- [Mapbox](https://www.mapbox.com/) and [MapLibre](https://maplibre.org/) maps +- [react-map-gl](https://visgl.github.io/react-map-gl/) library +- Custom tile layers +- Layer filtering options, allowing you to mix tile layer. +- Geocoding WordPress posts using OpenStreetMap (Nominatim), supporting the post type `Post`, with extensibility for additional geocoders via hook. +- Customizable marker icons that can be associated to categories, custom taxonomies or posts directly. +- Map markers query integrated to posts query. +- Support [WPML](https://wpml.org/pt-br/) and [Polylang](https://wordpress.org/plugins/polylang/) multilingual plugins +- AI-powered georeferencing, bulk geolocation, and map generation (multiple AI providers) +- AI-assisted map block (Minimap) with chat refinement and auto-generated layers +- Stories Near You block with geolocation-based post discovery + +Projects using JEO: + +- [Plenamata](https://plenamata.eco/) +- [InfoAmazonia](https://infoamazonia.org/) +- [Pasifika EnviroNews](https://pasifika.news/) +- [Ekuatorial](https://www.ekuatorial.com/) +- [MekongEye](https://www.mekongeye.com/) +- [Caci](http://caci.cimi.org.br/) + +[Read documentation](index-plugin.md) or [See in Wordpress store](https://wordpress.org/plugins/jeowp/) + + +## JEO WordPress Theme + +[Read the documentation](https://infoamazonia.github.io/jeo-theme/) diff --git a/docs/map-block.md b/docs/map-block.md index dece2571a..e84359a9f 100644 --- a/docs/map-block.md +++ b/docs/map-block.md @@ -1,6 +1,6 @@ # Map block -After [creating maps](/map-post/), you can display them inside any post using the **JEO Map** block. +After [creating maps](map-post.md), you can display them inside any post using the **JEO Map** block. ## Adding a map to a post @@ -42,7 +42,7 @@ Besides the alignment option, there's also a group functionality available to ar ## Layer swapping -If your map has more than one layer, you can swap them and select which one you want to see, depending on the map layer settings. [Check out more about map layers here](/layer-post/) +If your map has more than one layer, you can swap them and select which one you want to see, depending on the map layer settings. [Check out more about map layers here](layer-post.md) ![Swapping layers - Example 1](img/swapping-layers-example1.png) diff --git a/docs/map-embed.md b/docs/map-embed.md index 0502bdf7e..f1e42296d 100644 --- a/docs/map-embed.md +++ b/docs/map-embed.md @@ -2,7 +2,7 @@ JEO plugin allows a map to be inserted into a post by pasting a link on the editor. This is what is called `Embed map` and it's very easy to be done. -When editing a [Map](/map-post/), one of the setting panels is `Status & Visibility`. There you can find the `embed URL` of that specific map. +When editing a [Map](map-post.md), one of the setting panels is `Status & Visibility`. There you can find the `embed URL` of that specific map. ![Embed URL](img/embed-url.png) diff --git a/docs/map-post.md b/docs/map-post.md index 776efc63f..b924cd5be 100644 --- a/docs/map-post.md +++ b/docs/map-post.md @@ -38,7 +38,7 @@ Some layer types support custom styling. When using **Mapbox vector tiles** with ![Layer settings](img/layer-settings.png) -[Check out how to create new layers here](/layer-post/) +[Check out how to create new layers here](layer-post.md) ## Related posts diff --git a/docs/minimap.md b/docs/minimap.md index 2cd9d2f22..149e30fae 100644 --- a/docs/minimap.md +++ b/docs/minimap.md @@ -2,7 +2,7 @@ The Minimap block (`jeo/ai-minimap`) generates contextual interactive maps inside the Gutenberg editor using AI. Describe what you need in natural language, and the AI builds a map with appropriate layers, center point, zoom level, and geolocation pins. -See [AI in JEO](/ai-in-jeo/) to learn how RAG, agents, and tools power this feature. +See [AI in JEO](ai-in-jeo.md) to learn how RAG, agents, and tools power this feature. ## Inserting the block @@ -71,6 +71,6 @@ After generation, the minimap behaves like any JEO map: ## Requirements -- An AI provider must be configured in **Jeo → AI** (see [AI Settings](/ai-settings/)). +- An AI provider must be configured in **Jeo → AI** (see [AI Settings](ai-settings.md)). - For best results, index your posts and layers in the Knowledge Base (RAG tab). - For AI-generated thematic layers, a Mapbox API key is also required. Boundary layers (municipalities, states, indigenous lands) are generated as GeoJSON and work without a Mapbox key. diff --git a/docs/one-time-map-block.md b/docs/one-time-map-block.md index 3f370ae56..a93e43152 100644 --- a/docs/one-time-map-block.md +++ b/docs/one-time-map-block.md @@ -6,6 +6,6 @@ When creating a new post, note that is available a new block category: **JEO**. ![JEO block](img/JEO-block.png) -Selecting **JEO One-time Map** block, a standard map preview will appear. This type of map allow all the same customizations of a normal [JEO Map](/map-post/), such as: modifying coordinates, zoom settings, related posts, alignment options and [layers settings](/layer-post/). To know more precisely how to customize all these options, check out [JEO Map](/map-post/) page. +Selecting **JEO One-time Map** block, a standard map preview will appear. This type of map allow all the same customizations of a normal [JEO Map](map-post.md), such as: modifying coordinates, zoom settings, related posts, alignment options and [layers settings](layer-post.md). To know more precisely how to customize all these options, check out [JEO Map](map-post.md) page. -![One-time Map Example](img/one-time-map-example.png) \ No newline at end of file +![One-time Map Example](img/one-time-map-example.png) diff --git a/docs/public/logo-dark.svg b/docs/public/logo-dark.svg index 64ea521f2..8644652ba 100644 --- a/docs/public/logo-dark.svg +++ b/docs/public/logo-dark.svg @@ -1,10 +1,12 @@ - - - - - - + + + + + + + + + - diff --git a/docs/stories-near-you.md b/docs/stories-near-you.md index ec56bb0ac..6b3789a89 100644 --- a/docs/stories-near-you.md +++ b/docs/stories-near-you.md @@ -85,5 +85,5 @@ If the [Newspack Blocks](https://newspack.pub/) plugin is active, the block auto ## Requirements -- Posts must be geolocated (have `_related_point` meta data). See [Geolocating posts](/geolocating-posts/). +- Posts must be geolocated (have `_related_point` meta data). See [Geolocating posts](geolocating-posts.md). - The JEO default map center must be configured in **Jeo → Settings** (used as fallback when the user does not share their location). diff --git a/landing-page/src/assets/print-animado.mp4 b/landing-page/src/assets/print-animado.mp4 index f7a258f13..cb993f032 100644 Binary files a/landing-page/src/assets/print-animado.mp4 and b/landing-page/src/assets/print-animado.mp4 differ diff --git a/site-novo/CLAUDE.md b/site-novo/CLAUDE.md deleted file mode 100644 index 8cf6e01ad..000000000 --- a/site-novo/CLAUDE.md +++ /dev/null @@ -1,91 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## What this is - -A single-page marketing site for **JEO Maps** (geographic intelligence in WordPress). It is a faithful reproduction of the approved **Figma v3 redesign** — the source of truth for layout, spacing, and the canonical **PT-BR copy** (English is a translation kept in sync in the dictionaries). When changing text or layout, match the design rather than inventing content. - -The approved layout is the frame `jeo-maps-landing-redesign` (**node `133:1531`**, page "entrega 15/9/26", 1920×8448) in the Figma file **"JEO MAPS entregas"** (`uPSQi0186RdpE4rg2aeq7q`), rendered to `figma-ref/desktop-v3.png`. The token inventory lives in `figma-ref/redesign-v3-tokens.md`. The previous **V2** design (JEO BRAINS, `desktop-v2.png`) and all of its components, assets and Tailwind tokens were **removed** in the final cleanup — don't resurrect them from git history without a fresh decision. - -The app lives in `www/`. All commands below run from there. - -## Commands - -```bash -cd www -npm install -npm run dev # Vite dev server → http://localhost:5173/novo (base '/novo', see vite.config.js) -npm run build # production build → dist/ -npm run preview # serve the production build -``` - -There is **no test runner and no linter configured** — don't reference `npm test`/`npm run lint`. Verify changes by building and visually checking in the browser (desktop 1920 + mobile 390, PT-BR + EN). - -## Stack - -React 18 + Vite 5 (with `base: '/novo'` for homologation behind a tunnel — ngrok hosts are pre-allowed in `vite.config.js`), Tailwind CSS 3 (classic config + PostCSS), Framer Motion (animation), lottie-web (official hero Lottie player; SVG-only ESM build), lucide-react (icons), and a **dependency-free i18n** built on React context (see below). - -The whole page uses **Open Sans** for both `display` and `sans` — loaded via Google Fonts in `index.html` as the variable font **with the width axis** (`Open+Sans:wdth,wght@75..100,300..800`). Most v3 headings are **Open Sans Condensed**: apply the `.font-condensed` utility (defined in `index.css` as `font-stretch: 75%`, which maps to the wdth axis) together with `uppercase` and `font-bold`. - -## Architecture - -- **`src/App.jsx`** composes the page in fixed Figma order — `Header`, `Hero`, `ResourceSlider` (`#recursos`), `FeaturesShowcase` (`#inteligencia-artificial`), `HowItWorks`, `LiveDemo` (`#experimente`), `WorkshopsV3` (`#oficinas`), `FooterV3` — plus the global UI: a mint scroll-progress bar (`useScroll`/`useSpring`) and the `ConsentBanner`. To reorder or add a section, edit the JSX here. -- **`src/i18n/`** is the i18n system, written from scratch (no dependency — an explicit round decision): `index.jsx` holds the context/provider/hook (`useI18n` → `{ lang, setLang, t }`), the `LANGUAGES` list (`pt-BR` default, `en`) and persistence (`localStorage['jeo-lang']`, plus `document.documentElement.lang` sync); `pt-BR.js` / `en.js` are flat dictionaries namespaced per section (`nav.*`, `hero.*`, `slider.*`, `showcase.*`, `hiw.*`, `demo.*`, `workshops.*`, `footer.*`, `consent.*`). **All user-facing copy lives in the dictionaries** — adding a language = adding one dictionary file + one `LANGUAGES` entry. -- **`src/components/Header.jsx`** is a sticky top bar (`sticky top-0 z-40`, 118px on desktop) with the JEO Maps wordmark, 4 anchor links (scroll-spy active state + underline), the "Instalar Plugin" CTA, a **PT-BR/EN language switcher** (dropdown listbox; also inside the mobile menu) and the mobile hamburger menu. -- **`src/links.js`** centralizes external destinations — `DOWNLOAD_URL` (wordpress.org plugin page), `DOCS_URL` (jeowp.org), `GITHUB_URL` (the repo). All external links open in a new tab (`target="_blank" rel="noopener noreferrer"`). -- **`src/components/ConsentBanner.jsx`** (rendered last in `App.jsx`) is a bottom-left cookie/terms notice shown only on the first visit; "Aceitar" writes `localStorage['jeo-consent-v1'] = 'accepted'`. Fully v3-styled and translated (`consent.*`). On desktop it sits at `sm:bottom-24` so it clears the 64px footer bottom bar (where the terms trigger lives) instead of covering it. -- **`src/components/FooterV3.jsx`** carries the **terms-of-use modal** (button in the bottom bar). The trigger is translated (`footer.termsLead`/`termsLink`); the **modal body is placeholder PT-BR by design** (pending legal content — registered in issue #669). -- **`src/components/ui/Reveal.jsx`** is the shared scroll-in animation wrapper used by nearly every section — prefer wrapping new content in `` for consistent, reduced-motion-aware entrances. -- **`src/components/ui/Lottie.jsx`** is the lottie-web wrapper (SVG renderer) used by the Hero, the ResourceSlider tabs and the FeaturesShowcase row media: `animationData`, `loop`, `autoplay`, `title` (→ `role="img"` + `aria-label`; no title → `aria-hidden`), plus two additive props (2026-09-17): `approachMargin` (one-shot IntersectionObserver with that rootMargin — the player + JSON chunks are only fetched when the box approaches the viewport, keeping below-the-fold media out of the initial page load; still fetched under reduced motion because the settled frame needs the data) and `replayOnReenter` (every fresh entry into the viewport restarts from frame 0 — narrative scroll-navigation; in-view play-once + hold otherwise). It pauses outside the viewport (`IntersectionObserver`) and when `document.hidden`, resumes on return, disposes cleanly on unmount, and under `prefers-reduced-motion` never animates — it jumps to the final settled frame (`goToAndStop(totalFrames-1)`). -- **One component per section**; section content (tabs, features, steps) is data at the top of each file or in the i18n dictionaries, mapped into markup. - -### Conventions - -1. **Design tokens live in `www/tailwind.config.js`** — the `v3-*` palette (`v3-navy`, `v3-navy-hero`, `v3-light`, `v3-mint`, `v3-green`, `v3-green-dark`, `v3-accent`, `v3-teal`, `v3-gray` + neutral `muted`/`muted-2`), `maxWidth['v3-content']` (1600px) and `boxShadow.card`. Reusable component classes (`.section-shell-v3`, `.btn-v3-primary`, `.btn-v3-ghost`, `.btn-v3-secondary`, `.eyebrow-v3`) and base styles live in `@layer` blocks in `www/src/index.css`, plus the `.font-condensed` utility. Use these instead of hardcoding hex values or repeating button/container markup. `v3-navy-footer` is a **reserved** token (see footer decision below). -2. **`prefers-reduced-motion` is sacred.** `index.css` globally neutralizes CSS animations/transitions, and every animated component guards itself with `useReducedMotion()` (videos degrade to their poster — LiveDemo — or to the static navy backdrop — Hero; parallax/float loops are skipped). Keep new motion behind the same guard. -3. **Assets in `src/assets/`**: all `v3-*` PNGs were **exported from Figma** (not redrawn) — wordmarks (navbar + footer), tab icons, feature arts, slider art, the demo print/poster. The hero illustration is the **official Lottie export** `src/assets/animations/find-location.json` (the manual layer PNGs `v3-hero-txt/pin/search/maps/sparkle-*` were deleted with the manual recreation). Reused from V2: `hacklab-logo.svg` (footer partner logo) and `print-animado.mp4` (the Live Demo video). The favicon is `public/favicon.svg` (independent of `src/`). Known gap: the six 1× crops (features 1–5, demo print, footer wordmark) still await a 2× reexport — the Figma MCP export endpoint times out (-32001); dimensions already match the Figma rects 1:1, so they render correctly, just not retina-sharp. (`v3-feature-1.png` is back in this group: restored 2026-09-17 by cropping `figma-ref/desktop-v3.png` at the Group 17 bounds after the Lottie revert below.) The former `v3-hero-bg-pattern.png` texture (1586×960, the 2× line-art export of `hero-animate`, applied full-hero at `opacity-[0.08]`) was **reverted and deleted** (2026-09-16): it read as a "ghost thumbnail" of the animation behind the copy. The official hero background is the full fill stack declared in the 15/9 Figma delivery (frame `133:1553`), wired in 2026-09-16: the **official background video** `src/assets/hero-background.mp4` — the V2 asset provided by the human (restored byte-identical, md5 `c32c7b68…`) was **re-encoded once for performance on 2026-09-16** (see "First-fold performance" below: 9.4 MB → 2.97 MB, CRF 30 + bitrate cap, `+faststart`; the untouched source lives with the human, never re-encode from `src/assets/`) — emitted by Vite as a hashed static asset outside the JS bundle, loaded async with the first-frame poster `src/assets/hero-poster.jpg`, at `opacity-80 object-cover`, under two navy `#0A1628` solids (one `mix-blend-color`, one at 90%), topped by the vertical gradient (transparent at the bottom → opaque navy at the top). All four fills are `-z-10` inside the section's isolated stacking context (DOM order = paint order), so content and Lottie stay above. Under `prefers-reduced-motion` the `` renders. Keep dimensions/layout identical when touching it. -- **Header is sticky (not scroll-revealed like V2)** and `index.css` sets `scroll-padding-top` (5rem mobile / 7.375rem desktop at lg) so anchor jumps clear it — don't remove that rule. -- **Consent banner v3 styling + `sm:bottom-24`**: keeps the footer terms trigger clickable while the banner is visible; logic (`jeo-consent-v1`) unchanged from V2. -- **The hero illustration is the official Lottie export** (`src/assets/animations/find-location.json`, 793×480, 60fps, 4s — the `hero-animate` Figma instance), rendered by `ui/Lottie.jsx` (lottie-web) in the same geometric box the manual composite occupied (`aspect-[793/480]` inside `max-w-[793px]`). Keyframe evidence: entrances settle at frame 240 with no return keyframes → **play-once + hold**, not loop. Under `prefers-reduced-motion` the player shows the static final settled frame (`goToAndStop(totalFrames-1)`). The manual PNG-layer cascade/cycle/floats was deleted — don't recreate illustration motion by hand; re-export from Figma instead. -- **The ResourceSlider tab media**: all four tabs run the official Lottie exports in `src/assets/animations/` (`localisation.json`, `layers.json`, `map.json`, `explorar.json` — 60fps, dynamic-imported async chunks; behavior decided per file from final-keyframe inspection: play-once + hold for geo/maps/data, loop for `map.json`; evidence in `figma-ref/redesign-v3-animations.md`). Tab 2's `layers.json` never shipped its photo bitmaps; the designer's official masters arrived 2026-09-21 (`layer-1..4.png`, ~2× strips, diamond alpha baked) and now ship as `www/public/i/.jpg` — **layer-N → diamond N in animation order** (d1 bottom/first drop = `26bf3786…` ← layer-1, d2 = `08621d54…` ← layer-2, d3 = `a043a878…` ← layer-3, d4 top/last = `ecbd0b36…` ← layer-4), each **perspective-warped so its baked diamond tips coincide with the Lottie's vector alpha-matte** (drop-in misplaces the mint stroke 5–11 px), JPEG q75 4:4:4, 291 kB total. The JSON itself is untouched except the 4 asset extensions (`.png`→`.jpg`; hashes stay), wired by `remapLottieAssets` (the exports set `e:1`, so the prefix is folded into `p`). **Human decision 2026-09-18: the reversion to the `MapsLayersMedia.jsx` code composition was SUSPENDED — do not recreate it.** Every tab click remounts the media (`mediaKey`) → fresh replay. All four tabs share one fixed slot `aspect-[665/418]` (the Pain Card media box — tallest ratio; the lottie SVG renderer contains/centers each comp so the panel height never jumps). The former tab media (geo video + `v3-slider-geolocalizacao.png` poster) were removed; `print-animado.mp4` is still the LiveDemo video. -- **The ResourceSlider auto-advances every 8 s** (2026-09-18, human decision: 2 s → 6 s → 8 s), cycling 1→2→3→4→1 forever; the advance is identical to a click (panel swap + `mediaKey` replay). Manual click selects AND restarts the 8 s window (via the exposed `sync`, never a bare `schedule` — a click while off-screen/hidden must not arm the timer). Paused while the section is off-screen (IntersectionObserver) and while `document.hidden`; fresh 8 s window on return. **No auto-advance under `prefers-reduced-motion`.** Implementation: a single recursive `setTimeout` with clean reset (no loose `setInterval`), everything cleared on unmount. Measured: deltas 8101/7999/8001 ms; click → next advance 8007 ms. -- **The ResourceSlider tab row shows ALL four tabs on desktop, flush with the grid edges; scrolling is a mobile/tablet-only affordance** (2026-10-21, issue #669 R02 — @marinarcruz's QA catch: the active tab was clipped at ~1280px; human decisions: desktop = all tabs visible (grab is for mobile) AND the row must span the grid — first tab flush left, last tab flush right). Implementation: `lg:justify-between` distributes the leftover space between the buttons and the edge tabs drop their outer horizontal padding at lg+ (`lg:pl-0` on the first, `lg:pr-0` on the last — middle tabs keep symmetric padding for click area), so text/underline edges align EXACTLY with the shell's content box (verified 0px deltas at 1024/1280/1600/1920). This **supersedes the Figma insets** (64px left / 99px right measured on `figma-ref/desktop-v3.png` x 224..1661). The row is scaled down through the laptop band — type `lg:text-[24px] xl:text-[28px] min-[1600px]:text-[32px]`, gaps `lg:gap-8 xl:gap-10 min-[1600px]:gap-14`, tab padding `lg:px-3 xl:px-5 min-[1600px]:px-10` — reaching full Figma type size only at ≥1600px. Verified: `scrollWidth == clientWidth` (no scroll) and all 4 tabs fully in viewport at every width 1024–1920; the grab cursor is `lg:cursor-default`-suppressed on desktop. Below lg the row scrolls horizontally (`overflow-x-auto` + `no-scrollbar`) and the active tab is centered on every activation (`scrollIntoView inline:center`, guarded by **actual scrollability** — `scrollWidth > clientWidth`, not a breakpoint — with a debounced resize re-center). The scroll box gets `-my-1 py-1` so button focus rings aren't clipped. On mobile/tablet the row remains **mouse-draggable** (2026-09-17, issue #669 slice): pointer handlers on the `role="tablist"` container set `scrollLeft` directly past a **5px threshold** — below it the gesture is a plain click, above it the container gets `cursor-grabbing select-none` and the post-`pointerup` click is swallowed once (capture-phase `onClickCapture` + `preventDefault`/`stopPropagation`) so releasing over a neighbor tab doesn't activate it. `setPointerCapture` fires **only after the threshold** (capturing on pointerdown would retarget the click to the container per the nearest-common-ancestor rule and eat plain tab clicks) and is wrapped in try/catch for synthetic pointers. Touch is never intercepted (native pan-x wins); buttons keep keyboard focus/click and the click-to-center behavior. Resting cursor is `cursor-grab` (` was the LCP element. Re-encoded from the human's untouched source: `ffmpeg -i source -vf scale=1280:720 -c:v libx264 -crf 30 -maxrate 1150k -bufsize 2300k -preset slow -an -movflags +faststart -pix_fmt yuv420p` → **2.97 MB**, PSNR vs source **32.2 dB average** (≥ 32 dB gate for the aggressive path; CRF 28 alone measured 37.2 dB but 6.2 MB). Under the hero's overlays (`opacity-80` + `mix-blend-color` navy + navy/90) the difference is invisible. Always re-encode from the human's source file, never from `src/assets/`. -- **`hero-poster.jpg`** (76 KB, frame 0, `-q:v 5`, 1280×720): paints under the overlays the moment React mounts the video, before any video byte arrives; being frame 0 the handover to playback is seamless. This is what moved LCP: the poster (a fast image) is now the LCP element instead of the video. -- **Lottie waterfall**: both async chunks (player 241 KB + `find-location.json` 274 KB) are kicked at **module-evaluation time** in `Hero.jsx` top-level (import() dedupes by module, so `Lottie.jsx`'s own effect-time calls resolve to the in-flight promises). Rejected alternatives: a hashed `` in `index.html` (breaks on every rebuild — hashes change) and statically importing the JSON into the main bundle (~25 KB gzip added to the text's critical path for ~600 ms of animation onset — bad trade). -- **LiveDemo video deferred**: `preload="none"` + IntersectionObserver play/pause (600px margin). The 22.5 MB recording used to start downloading at ~1.4 s — before the hero's LCP — and trickle for ~36 s. -- **Fonts were already correct**: `display=swap` + preconnect to `fonts.googleapis.com`/`fonts.gstatic.com` (single variable woff2, 83 KB). No change needed. -- `prefers-reduced-motion` still renders no video at all (0 MP4 requests), and the Lottie shows its settled final frame — unchanged by any of the above. diff --git a/site-novo/figma-ref/desktop-v2.png b/site-novo/figma-ref/desktop-v2.png deleted file mode 100644 index 35ac4a4a1..000000000 Binary files a/site-novo/figma-ref/desktop-v2.png and /dev/null differ diff --git a/site-novo/figma-ref/desktop-v3.png b/site-novo/figma-ref/desktop-v3.png deleted file mode 100644 index 6d3d611f3..000000000 Binary files a/site-novo/figma-ref/desktop-v3.png and /dev/null differ diff --git a/site-novo/figma-ref/desktop.png b/site-novo/figma-ref/desktop.png deleted file mode 100644 index fa5b6df66..000000000 Binary files a/site-novo/figma-ref/desktop.png and /dev/null differ diff --git a/site-novo/figma-ref/footer-ref.png b/site-novo/figma-ref/footer-ref.png deleted file mode 100644 index d46e46dee..000000000 Binary files a/site-novo/figma-ref/footer-ref.png and /dev/null differ diff --git a/site-novo/figma-ref/redesign-v3-animations.md b/site-novo/figma-ref/redesign-v3-animations.md deleted file mode 100644 index 8052ed86d..000000000 --- a/site-novo/figma-ref/redesign-v3-animations.md +++ /dev/null @@ -1,749 +0,0 @@ -# Redesign v3 — Animation Reference (prototype × implementation) - -Reference doc for issue #669 (landing redesign, motion slice). Maps every -animation **expected by the Figma prototype** to what the code does today. - -## Method & limitations - -- **Figma desktop MCP**: `get_design_context` / `get_metadata` do **not** expose - prototype reactions/interactions, and `hero-animate` (133:418) is a plain - component (no variant set). Static file data alone therefore proves nothing - about motion. -- **Prototype ground truth**: the prototype was opened in Chrome - (`node-id=133-1531`) at 1920×914, dpr 1. Content offset measured as - **viewport_y = figma_y + 130** (x is 1:1; verified against sparkle vector - bounding boxes). Screenshots were diffed programmatically with a pure-node - PNG decoder (`/tmp/opencode/pngdiff*.mjs`, no new dependencies): per-tile - heatmaps, named-region deltas, integer-shift cross-correlation (translation - vectors), and fixed-point color series. -- **Prototype timing (2026-09-18, F3 slice)**: screenshot series and recorded - webm both fail here (round-trip latency 12–18 s; MediaRecorder webm has no - seek index → decoded seeks repeat keyframes). The reliable method: - **live frame sampling** — `canvas.captureStream(60)` of the player canvas → - `