Skip to content

Fix blank geo map under maplibre-gl 6: serve its worker from public/ - #8

Merged
Cristian-Curaba merged 2 commits into
mainfrom
fix/maplibre-resize-sync-conflict
Sep 11, 2026
Merged

Cristian-Curaba merged 2 commits into
mainfrom
fix/maplibre-resize-sync-conflict

Conversation

@Cristian-Curaba

@Cristian-Curaba Cristian-Curaba commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Root cause

maplibre-gl 6 ships ESM-only and loads tile decoding into a Web Worker. That worker imports a sibling chunk, maplibre-gl-shared.mjs, by relative path.

Next.js — in both Turbopack and --webpack mode — turns the usual new URL(..., import.meta.url) worker reference into a single hashed asset without emitting the sibling next to it. The worker throws on its first import, and, quoting the upstream docs, "the map mounts but never requests a tile".

That single fault explains every symptom reported:

Symptom Cause
"doesn't even show the map in background" no tile ever requested
"zoom/pan moves the window, not the map" the CSS-transform sync in useMapViewportSync is the only thing still moving
"stays there permanently", Δlng never converges nothing to re-anchor to

No console error, which is why it stayed hidden.

Fix

The upstream-documented Turbopack/Next.js setup (docs → Installation → Turbopack):

  • scripts/copy-maplibre-worker.mjs copies both dist files into public/maplibre/ on predev/prebuild — from node_modules, so it always matches the installed version.
  • setWorkerUrl("/maplibre/maplibre-gl-worker.mjs") at module scope in geo-map-background.tsx.
  • public/maplibre/ is gitignored (generated) and ESLint-ignored (vendor code).

Reverts the speculative trackResize: false from b83d098 — it addressed a hypothesis, not this cause.

Rest of the v5→v6 migration guide

Audited every other breaking change; none apply: no map.transform, no styleimagemissing, no JSON.parse on feature properties, no #pragma mapbox, no GeoJSONSource.setData, no require('maplibre-gl'), no instanceof on events. WebGL2-only is a browser-support change, not a code change.

Deploy path

  • Both files land in out/ (verified) → Docker/Caddy serve them.
  • Caddy's CSP already allows worker-src 'self'; the URL is same-origin.
  • @immutable caching is scoped to /_next/static/*, so /maplibre/* stays no-cache — an upgrade can't serve a stale worker.

Verification

tsc --noEmit, npm run lint (0 errors), npm test (230 passed), npm run build all green. Dev server serves both files at 200 application/javascript.

Needs a browser check on a georeferenced Canvas to confirm tiles now render.

Cristian-Curaba and others added 2 commits September 11, 2026 10:52
…map)

Regression from the 5.24.0 -> 6.4.1 bump (#5). maplibre-gl 6 added an
internal ResizeObserver on the `container` option (_setupResizeObserver,
new in this major version) that calls resize() + redraw() on its own.
We pass the SAME element as container that useMapViewportSync
CSS-transforms every frame for the zoom sync (translate + scale, to
keep the background locked to React Flow's viewport without a
jumpTo() per frame) -- and that hook already has its own ResizeObserver
on that element, calling resize() explicitly at every point that
matters: on load, on tile style change, and on a real container resize.

Two independent resize-tracking paths racing on the one CSS-transformed
element is what made zooming visibly stretch/narrow the map canvas
instead of just scaling it smoothly.

trackResize: false disables only maplibre's own new auto-resize path
(confirmed its only consumer in the library). Nothing else depends on
it -- our code already explicitly calls .resize() at every point
maplibre's internal tracking would have.

Verified: tsc 0 errors, 230/230 tests, eslint 0 new warnings, clean
build. Visual confirmation (does zoom now scale smoothly) still needs
an actual browser -- please check after pulling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
maplibre-gl 6 ships ESM-only and loads tile decoding into a Web Worker
that imports a sibling chunk (maplibre-gl-shared.mjs) by relative path.
Next.js — in both Turbopack and --webpack mode — emits that worker as a
lone hashed asset without the sibling, so the worker throws on its first
import. The map mounts and fires "load", but never requests a tile: a
blank background, no console error, and the CSS-transform viewport sync
left as the only thing moving (hence "zoom pans the window, not the map").

Follow the upstream Turbopack/Next.js recipe: copy both dist files into
public/maplibre/ at predev/prebuild time (always matching the installed
version) and point setWorkerUrl at the same-origin path. Caddy already
allows worker-src 'self' and leaves /maplibre/* on no-cache, so an
upgrade can't serve a stale worker.

Also reverts the speculative trackResize:false from the previous commit —
it addressed a hypothesis, not this cause, and maplibre's own resize
tracking is harmless.

Docs: architecture.md gains the build step and the version bump to 6.

Ref: https://maplibre.org/maplibre-gl-js/docs/ → Installation → Turbopack
Signed-off-by: Cristian Curaba <cristiancuraba00@gmail.com>
@Cristian-Curaba Cristian-Curaba changed the title Disable maplibre-gl's own resize tracking (fixes zoom stretching the map) Fix blank geo map under maplibre-gl 6: serve its worker from public/ Sep 11, 2026
@Cristian-Curaba
Cristian-Curaba merged commit 26d332e into main Sep 11, 2026
5 checks passed
@Cristian-Curaba
Cristian-Curaba deleted the fix/maplibre-resize-sync-conflict branch September 11, 2026 09:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant