Addresses the actual root cause behind the Denmark 2026 corrupted-coordinate bug: there was no supported way to set an entry's location to somewhere other than the current GPS position, forcing hand-typed/pasted raw coordinates through Admin2's fragile text field. Backend sanitization (cache-on-save) already guards against silent corruption; this spec adds a frontend way to avoid needing that path at all.
8.5 KiB
Post form: location override (search + map + drag)
Status: 📋 Not started
Problem
The post form's lat/lng fields exist in the blueprint (user/pages/02.post/post-form.md) as plain type: text fields, but a theme CSS rule (user/themes/intotheeast/css/style.css:893-895) hides them, and the only way to populate them is the 📍 Get Location button, which reads the browser's live GPS position via navigator.geolocation.
This breaks down whenever an entry describes a place the traveller isn't physically standing in when they write it up — the common case for journal entries written at the end of a day, from a shelter/hostel/train, about somewhere visited earlier. There is currently no supported way to set a coordinate for anywhere other than "here, right now."
The only workaround has been logging into Admin2 and hand-typing/pasting raw decimal coordinates directly into the page's frontmatter field. This is what produced the Denmark 2026 bug: a coordinate pasted from an external source carried an invisible Unicode bidi mark (U+200E), which PHP's (float) cast silently coerced to 0.0, placing the entry's map marker at (0, 0) with no error or warning anywhere in the pipeline.
Backend sanitization has already been added (user/plugins/cache-on-save/cache-on-save.php: cleanCoordinate(), wired into both onFormValidationProcessed for the public form and onAdminSave for Admin2/API saves) to strip invisible characters and range-validate lat/lng before they ever reach a page's frontmatter. That fix is necessary but not sufficient: it prevents silent corruption of whatever gets typed, but does nothing to prevent the underlying problem — a fragile, invisible-to-the-eye, paste-prone raw text field is still the only way to set an arbitrary location, and there's no way to visually confirm the result before submitting. This spec addresses that gap directly, on the frontend post form, so the Admin2 round-trip is no longer needed for this at all.
Goals
- Give the traveller a reliable, visual way to set an entry's coordinates for a location other than their current GPS position, without touching Admin2.
- Let any coordinate-setting mistake be caught before submit, via a live map preview, rather than relying solely on backend validation to catch it after the fact.
- Keep the common case (GPS, writing about where you currently are) exactly as fast and simple as it is today — no added friction for the 📍 Get Location button.
Non-goals
- No changes to Admin2 or the
apiplugin. 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/lngfloats 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.
Design
Placement
- 📍 Get Location (GPS): unchanged. Stays in its current top-level
.form-action-row, primary/always-visible action for "I'm posting from where I am right now." - City / Country: unchanged position and behavior in the main field flow (still plain, always-visible text fields, still auto-filled by GPS reverse-geocode only when blank).
- 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 unrelatedpublished/force_connect/featuredtoggles). Closed by default. Contains:- A "🔍 Look up coordinates" button.
- A small MapLibre preview map with a single, draggable marker.
- The raw
lat/lngtext fields, relocated here from their current CSS-hidden position in the main flow (thedisplay: none !importantrule instyle.cssis removed; the fields simply live inside this disclosure instead).
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. - Explicit click, not live-as-you-type — matches the deliberate, single-action feel of the existing GPS button.
- The lookup only reads City/Country — it never writes back to them. A geocode result sets
lat/lngand 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/lngand 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 asmaplibre-utils.js, no new API key), with one draggable marker. maplibre-glis 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 inpost-form.js, so the ~200KB library is never fetched for ordinary GPS-only submissions.- Four ways to set a coordinate, all kept in sync with each other:
- GPS button (main flow) — if "More location details" is later opened, the map/pin reflect whatever
lat/lngcurrently hold. - Search-result click — sets fields, moves/creates pin.
- Dragging the pin — on
dragend, reads the marker'slngLat, writes back into thelat/lngtext fields (rounded to 6 decimal places, matching the GPS button's existing precision). - 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().
- GPS button (main flow) — if "More location details" is later opened, the map/pin reflect whatever
- If the map is opened with no
lat/lngset yet, no pin is shown until one of the four paths above sets a value.
Error handling
- No search results: inline message under the search box, map/pin untouched.
- Search network failure: silent-ish degrade (consistent with existing weather/reverse-geocode error handling in
post-form.js), fields untouched. - Invalid manual
lat/lngtext: no client-side hard block (the map preview and eventual server-sidecleanCoordinate()are the safety nets); this UI's whole point is to make that failure mode rare in practice, not to duplicate the backend validator client-side. - Geolocation permission denied: unchanged existing behavior (
#location-statuserror message).
Out of scope / explicitly deferred
- No changes to
user/plugins/admin2/oruser/plugins/api/— confirmed and intentional. - No removal of the existing backend
cleanCoordinate()sanitization (onFormValidationProcessed+onAdminSaveincache-on-save.php) — it remains as defense-in-depth, especially for the still-possible Admin2 edit path. - Automated Playwright coverage for the new search→pin→submit flow is desirable but currently blocked by a pre-existing, unrelated
make test-accountMakefile quoting bug — flagged as a follow-up, not a blocker for shipping this feature. Manual in-browser QA (per CLAUDE.md's UI-change testing guidance) is required before considering this done.
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 expectedlat/lng. - Exercise the ambiguous-search case (a place name with multiple matches) and the no-match case.
- 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.