docs(working): plan — post form location override; doc-review fixes to spec
Adds the implementation plan for the location-override feature and folds in ce-doc-review findings: a panel-open sync gap (pin didn't render on reopen with pre-existing coordinates), keyboard/ARIA accessibility gaps in the search-results list and mismatch flag, a shared MAP_STYLE module to remove duplication drift risk, and a corrected Open-Meteo risk/mitigation split. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ Backend sanitization has already been added (`user/plugins/cache-on-save/cache-o
|
||||
- No changes to Admin2 or the `api` plugin. The backend sanitization already shipped there stays as-is, as defense-in-depth for the Admin2 edit path (which this spec doesn't touch).
|
||||
- No change to how coordinates are stored (still plain `lat`/`lng` floats in frontmatter).
|
||||
- No offline/self-hosted geocoding — this reuses free, no-key, CORS-enabled public APIs, consistent with the form's existing BigDataCloud (reverse geocode) and Open-Meteo (weather) integrations.
|
||||
- No additional integrity verification (certificate pinning, response signing, etc.) for the geocoding/tile third-party responses beyond HTTPS. A compromised or MITM'd response could theoretically feed bogus coordinates or map tiles into the preview, but this is accepted as low-probability and already bounded by the unchanged server-side `cleanCoordinate()` range validator, which gates what actually reaches frontmatter regardless of what the preview displays.
|
||||
|
||||
## Design
|
||||
|
||||
@@ -33,27 +34,32 @@ Backend sanitization has already been added (`user/plugins/cache-on-save/cache-o
|
||||
- **New "More location details" disclosure**, placed directly below the City/Country fields (a separate `<details>` block from the existing "More options" advanced-fields disclosure, which stays scoped to the unrelated `published`/`force_connect`/`featured` toggles). Closed by default. Contains:
|
||||
- A **"🔍 Look up coordinates"** button.
|
||||
- A small MapLibre preview map with a single, draggable marker.
|
||||
- The raw `lat`/`lng` text fields, relocated here from their current CSS-hidden position in the main flow (the `display: none !important` rule in `style.css` is removed; the fields simply live inside this disclosure instead).
|
||||
- The raw `lat`/`lng` text fields, relocated here from their current CSS-hidden position in the main flow (the `display: none !important` rule in `user/themes/intotheeast/css/style.css:894-895`, which targets `input[name="data[lat]"]`/`input[name="data[lng]"]`, is removed; the fields simply live inside this disclosure instead). This is a pure DOM relocation — the `name="data[lat]"`/`name="data[lng]"` attributes are unchanged, so `cache-on-save.php`'s `sanitizeCoordinates()` (which keys off those exact field names) and `post-form.js`'s existing `field('lat')`/`field('lng')` helper both keep working unmodified. Checked the theme for other references to that CSS rule or those field names — none found outside `style.css:894-895` and `post-form.js`'s own read/write of the fields — so removing the rule has no other side effects.
|
||||
|
||||
### Search mechanics
|
||||
|
||||
- The lookup button geocodes the **current values of the City and Country fields** (e.g. `"Jerup, Denmark"`) via Open-Meteo's free geocoding endpoint (`https://geocoding-api.open-meteo.com/v1/search?name=...&count=5&language=en&format=json`) — same provider the form already trusts for weather (`api.open-meteo.com`), no API key required.
|
||||
- The lookup button geocodes the **City field alone** via Open-Meteo's free geocoding endpoint (`https://geocoding-api.open-meteo.com/v1/search?name=<city>&count=10&language=en&format=json`) — same provider the form already trusts for weather (`api.open-meteo.com`), no API key required. CORS is confirmed open on this endpoint independent of the weather endpoint (`access-control-allow-origin: *`, verified directly against `geocoding-api.open-meteo.com`).
|
||||
- **The Country field is not concatenated into the query string.** Verified against the live API: a combined query like `name=Paris%2C%20Texas` or `name=Jerup%2C%20Denmark` either returns zero results or silently degrades to matching only the part before the comma — Open-Meteo's `name` param does fuzzy/substring matching on the place name, not a "name, country" filter syntax. Concatenating would silently break the lookup for exactly the disambiguation case (e.g. "Paris, Texas") this feature exists to handle.
|
||||
- Instead: query by City name alone (returns all same-named places, e.g. all five "Paris" results worldwide), then — if the Country field is non-blank — rank results client-side by matching Country against each result's `country` field (case-insensitive substring), matching entries first. All results still render in the list below, just reordered.
|
||||
- Explicit click, not live-as-you-type — matches the deliberate, single-action feel of the existing GPS button.
|
||||
- While a lookup request is in flight, the button shows a brief "Searching…" state (disabled, consistent with how other in-flight actions in `post-form.js` guard against double-submission); it re-enables on response, whether that's results, no-match, or network failure.
|
||||
- Clicking "🔍 Look up coordinates" with both City and Country empty is treated the same as a no-match: inline hint to fill in a city or country first, no request is sent.
|
||||
- **The lookup only reads City/Country — it never writes back to them.** A geocode result sets `lat`/`lng` and moves the pin only. This avoids the earlier concern of an ambiguous or slightly-off match silently overwriting a name the traveller deliberately typed.
|
||||
- Multiple matches → rendered as a small clickable list (place name, admin region, country) so the traveller can disambiguate (e.g. "Paris, Île-de-France, France" vs "Paris, Texas, United States"). Clicking an entry sets `lat`/`lng` and moves the pin; the list is not shown again until the next lookup.
|
||||
- Multiple matches → rendered as a small clickable list (place name, admin region, country), Country-matches ranked first per above, so the traveller can disambiguate (e.g. "Paris, Île-de-France, France" vs "Paris, Texas, United States"). Each list item is built via `document.createElement` + `.textContent` — the same convention used everywhere else in `post-form.js` for dynamic content (no `innerHTML` string-building exists in the file today) — since these are untrusted, API-sourced strings. Clicking an entry sets `lat`/`lng` and moves the pin; the list is not shown again until the next lookup.
|
||||
- No matches → inline hint: try adding a country, or drag the pin manually.
|
||||
- Network failure → degrades the same way the existing reverse-geocode/weather calls do: silent-ish failure, fields untouched, traveller can still fall back to manual entry or the pin.
|
||||
|
||||
### Map preview + sync
|
||||
|
||||
- Single MapLibre GL map instance, reusing the site's existing style (`https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json` — same as `maplibre-utils.js`, no new API key), with one draggable marker.
|
||||
- `maplibre-gl` is dynamically imported (`import('maplibre-gl')`) only when the "More location details" `<details>` is opened for the first time — mirrors the existing HEIC-conversion lazy-chunk pattern in `post-form.js`, so the ~200KB library is never fetched for ordinary GPS-only submissions.
|
||||
- Single MapLibre GL map instance, reusing the site's existing style (`https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json` — same as `maplibre-utils.js`, no new API key), with one draggable marker sized to at least a ~44×44px touch target (matching standard iOS/Android touch-target guidance), since this is a mobile-first form.
|
||||
- `maplibre-gl`'s JS is dynamically imported (`import('maplibre-gl')`) only when the "More location details" `<details>` is opened for the first time — mirrors the existing HEIC-conversion lazy-chunk pattern in `post-form.js`, so the ~200KB library is never fetched for ordinary GPS-only submissions. Its CSS (`maplibre-gl/dist/maplibre-gl.css`, ~8KB minified) is imported statically at the top of `post-form.js` instead, bundled unconditionally into the always-loaded `css-compiled/post-form.css` — unlike the JS, the CSS chunk can't be split off a dynamic import without esbuild orphaning it (no `<link>` reference is ever emitted for a code-split CSS chunk), so only the JS half of the HEIC lazy-chunk pattern applies here.
|
||||
- Four ways to set a coordinate, all kept in sync with each other:
|
||||
1. **GPS button** (main flow) — if "More location details" is later opened, the map/pin reflect whatever `lat`/`lng` currently hold.
|
||||
1. **GPS button** (main flow) — writes `lat`/`lng` directly. If "More location details" is closed, the map/pin simply reflect the new values whenever the panel is next opened. If the panel is already open when GPS resolves, the same field→pin sync used by path 4 (typing) fires immediately, so the pin jumps to the new position live instead of requiring a re-open.
|
||||
2. **Search-result click** — sets fields, moves/creates pin.
|
||||
3. **Dragging the pin** — on `dragend`, reads the marker's `lngLat`, writes back into the `lat`/`lng` text fields (rounded to 6 decimal places, matching the GPS button's existing precision).
|
||||
4. **Typing directly into lat/lng** — on blur/debounced input, if both values parse as valid finite numbers within range, move (or create) the pin. Invalid/unparseable input just leaves the pin where it was — this is a visual aid, not a blocking validator; final enforcement stays server-side in `cleanCoordinate()`.
|
||||
4. **Typing directly into lat/lng** — on blur/debounced input, if both values parse as valid finite numbers within range, move (or create) the pin. Invalid/unparseable input leaves the pin where it was, but visually flags the field (e.g. a red outline plus an inline "not reflected on map" note) so the traveller can tell the text and the pin disagree — this is a visual aid, not a blocking validator; final enforcement stays server-side in `cleanCoordinate()`. The flag clears once the field's value parses and the pin catches up.
|
||||
- If the map is opened with no `lat`/`lng` set yet, no pin is shown until one of the four paths above sets a value.
|
||||
- The map instance is created once, the first time "More location details" is opened, and held in module scope; reopening the `<details>` later reuses that instance rather than constructing a duplicate. Repeat `import('maplibre-gl')` calls resolve from the ES module cache with no extra network fetch — the same behavior the existing `heic-to` lazy import already relies on. Because the container sits under `display: none` while the `<details>` is closed, MapLibre initializes with a zero-size canvas the first time; the map calls `.resize()` on every subsequent open to pick up the container's real dimensions.
|
||||
|
||||
### Error handling
|
||||
|
||||
@@ -71,5 +77,6 @@ Backend sanitization has already been added (`user/plugins/cache-on-save/cache-o
|
||||
## Testing plan
|
||||
|
||||
- Manual QA in the dev browser: open `/post`, expand "More location details," exercise all four coordinate-setting paths (GPS, search + pick a result, drag the pin, type raw numbers) and confirm the pin and fields stay in sync in both directions. Submit and confirm the saved entry's frontmatter has the expected `lat`/`lng`.
|
||||
- Exercise the ambiguous-search case (a place name with multiple matches) and the no-match case.
|
||||
- Exercise the ambiguous-search case: City "Paris" with Country "Texas" and confirm the Texas result ranks first over the France/Tennessee/Kentucky/Illinois matches — this is the specific case the City-only-query + client-side-rank fix targets, since concatenating "Paris, Texas" into a single query string returns zero results from Open-Meteo. Also exercise the no-match case.
|
||||
- Reopen "More location details" a second time in the same session and confirm the map doesn't duplicate (still one canvas, correctly sized) and the pin still reflects the current `lat`/`lng`.
|
||||
- Exercise the "type garbage into lat/lng" case and confirm the map simply doesn't move the pin (no crash), while a submit still round-trips through the existing backend `cleanCoordinate()` validation.
|
||||
|
||||
Reference in New Issue
Block a user