One modern, responsive web frontend that unifies BlueMap (3D) and squaremap (2D) maps behind a single, config-driven map selector.
It reuses each plugin's own renderer instead of re-implementing rendering, so it keeps working across plugin updates with minimal adjustment:
- squaremap maps are drawn natively with Leaflet, reusing squaremap's exact tile scheme and CRS.Simple
projection (
src/backends/squaremap). - BlueMap maps are shown by embedding BlueMap's shipped webapp in an iframe (
src/backends/bluemap), deep-linked to the selected map. BlueMap's 3D WebGL engine is not published as a library, so embedding is the update-proof path: upgrade BlueMap → drop in its newweb/build → no frontend changes.
Shared across both backends: the map selector, responsive chrome (desktop rail / mobile bottom-sheet), a unified online players list, a points-of-interest panel, deep-linkable URLs, and theming/branding.
React 19 · Vite · TypeScript · Tailwind CSS v4 · Leaflet · zod. Package manager: bun.
bun install
bun run dev # http://localhost:5173 — opens the offline "Demo (mock)" 2D mapThe default public/config.json ships a demo map backed by mock data in
public/mock/squaremap, so the app renders a working 2D map (tiles, two players, two POIs)
with no server. Point the other entries at your real plugin outputs (below) and remove the demo when ready.
bun run build # typecheck + production build to dist/
bun run preview # serve the production build
bun test # unit tests (config validation + squaremap projection)Everything is driven by public/config.json, validated at startup against
public/config.schema.json. It is read at runtime — edit it in a deployed build to
change maps/branding without rebuilding. An invalid config shows an on-screen error explaining what's wrong.
Per-map fields:
| field | backend | meaning |
|---|---|---|
url |
both | Web root of the plugin output. squaremap: the dir containing tiles/. BlueMap: the dir containing settings.json + maps/. |
world |
squaremap | World name as in tiles/settings.json → worlds[].name. |
map |
bluemap | BlueMap map id (used as the iframe URL hash, e.g. #world). |
All user-facing UI copy lives in public/strings.json (validated against
public/strings.schema.json) and is loaded at runtime — edit it in a deployed build
to translate or reword the UI without rebuilding. Every key is optional; omitted keys fall back to the built-in
English default, and a missing/invalid file just uses all defaults (the app never fails over text). A few values take
{placeholders} (e.g. loadingMap, mapLoadError support {map}). Branding (title, subtitle, …) stays in
config.json.
// public/strings.json — override only what you want
{ "playersHeading": "Spieler", "noPlayers": "Keine Spieler auf dieser Karte.", "loadingMap": "Lade {map}…" }-
BlueMap (iframe) works cross-origin with no special setup.
-
squaremap (native fetch) loads
settings.json, tiles andplayers.jsonwithfetch/<img>from this app's origin. The clean setup is a reverse proxy that serves the plugin outputs same-origin under this app, e.g.location /maps/squaremap/ { proxy_pass http://127.0.0.1:8080/; } # squaremap integrated server location /maps/bluemap/ { proxy_pass http://127.0.0.1:8100/; } # BlueMap integrated server
Then all
urls are same-origin and everything (including the BlueMap iframe) is CORS-clean. Alternatively, enable CORS on squaremap's integrated HTTP server and use absoluteurls.
For local dev, uncomment the server.proxy block in vite.config.ts to point /maps/* at your
running servers.
Because this app already provides the selector/title/player list, you can hide BlueMap's duplicate chrome inside the
iframe. Two routes, set via bluemap.hideUi in config:
-
css-addon(default, update-safe): Copypublic/bluemap-embed.cssandpublic/bluemap-embed.jsinto BlueMap's web root and reference them from BlueMap'ssettings.json— this is BlueMap's intended extension hook (BlueMapApp.jsinjectsstyles[]/scripts[]):// bluemap/web/settings.json "scripts": ["bluemap-embed.js"], "styles": ["bluemap-embed.css"]
The script adds an
embeddedclass only when BlueMap runs inside our iframe, so opening BlueMap directly is unaffected. The CSS hides the chrome. No BlueMap fork, survives upgrades. -
inject: The parent injects the CSS into the iframe at load. Requires BlueMap served same-origin (reverse proxy). More direct but couples to BlueMap's CSS class names. -
none: Leave BlueMap's UI as-is.
- BlueMap update: replace its served
web/build. No changes here. If hiding its UI and a new version renames a chrome element, adjust selectors inpublic/bluemap-embed.cssonly. - squaremap update: only relevant if squaremap changes its data contract (
tiles/layout,settings.jsonshape). All of that is isolated tosrc/backends/squaremap/squaremapClient.ts,SquaremapTileLayer.tsandprojection.ts.
src/
config/ loadConfig + zod schema (runtime-validated config.json)
backends/
types.ts MapBackendAdapter interface + normalized Player/Marker
createAdapter.ts factory by backend
squaremap/ native Leaflet renderer (client, tile layer, projection, adapter)
bluemap/ iframe embed (adapter) + live-data client
components/ Sidebar, MapSelector, PlayerList, MarkerList, BrandingHeader, MobileDrawer, Viewport
state/ useHashRoute (deep-link router: #/<mapId>?x=&z=&zoom=)
App.tsx shell orchestration
public/
config.json, config.schema.json
bluemap-embed.css, bluemap-embed.js (copy into BlueMap's web root)
mock/squaremap/ offline demo dataset
Each backend implements MapBackendAdapter (mount/unmount/focus/getView/
getPlayers/getMarkers). Viewport owns the active adapter's lifecycle, polls live
data into the shared panels, and writes the current view into the URL for shareable deep-links. squaremap renders its
own player markers on the Leaflet map; BlueMap renders players/markers inside its iframe — in both cases the same React
sidebar lists them.
{ "branding": { "title": "Server Maps", "subtitle": "Live world maps", "logo": null, // URL/path, or null for an auto initial badge "themeColor": "#3b82f6", // accent color (any CSS color) "defaultMapId": "overworld" // map opened on first load }, "bluemap": { "hideUi": "css-addon" }, // see "Hiding BlueMap's UI" below "maps": [ { "id": "overworld", "label": "Overworld", "icon": "🌍", "backend": "squaremap", "url": "/maps/squaremap", "world": "minecraft_overworld" }, { "id": "world-3d", "label": "Overworld (3D)", "icon": "🧊", "backend": "bluemap", "url": "/maps/bluemap", "map": "world" } ] }