Files
intotheeast-com/docs/working/specs/2026-07-23-post-form-location-override-design.md
T
m038 2ab6575e4b docs(working): spec — post form location override (search + map + drag pin)
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.
2026-07-23 20:39:30 +02:00

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 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.

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 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).

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/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.
  • 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.
  • 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.
    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().
  • 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.

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/lng text: no client-side hard block (the map preview and eventual server-side cleanCoordinate() 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-status error message).

Out of scope / explicitly deferred

  • No changes to user/plugins/admin2/ or user/plugins/api/ — confirmed and intentional.
  • No removal of the existing backend cleanCoordinate() sanitization (onFormValidationProcessed + onAdminSave in cache-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-account Makefile 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 expected lat/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.