livemap-carto
Builds and serves versioned MapLibre cartography assets — style JSON, sprites, and PBF glyphs — as a static Caddy container. Tile data lives in Martin (separate service); this repo owns the presentation layer only.
devOverview
The repo has one hand-authored canonical style
(styles/bright/) and any number of
palette-derived variants generated from it by
scripts/recolor.mjs. Every directory under
styles/ is automatically registered at
build time — no manual wiring is needed when adding or
renaming a style.
At build time, envsubst replaces template
variables (tile server URLs, map center, version) in
every style.json and HTML file. The raw
source files are therefore not valid JSON until the
build runs.
Repo structure
bright/ canonical hand-authored style (source of truth)
night/ GENERATED — do not hand-edit
dark/ Dark Matter reference, external origin
lessdark/ lighter Dark Matter variant
scripts/
recolor.mjs palette-driven style generator
palettes/
night.json palette mapping for the Night style
substitute-vars.sh envsubst wrapper run at container build time
site/ source for /map.html, /color-editor.html, /docs.html
templates/ endpoint directory index.html
fonts/ TTF sources → compiled to PBF glyphs at build
config/
production.env canonical env values (MARTIN_DOMAIN, etc.)
Caddyfile static file server config inside the container
Justfile build and run recipes
Containerfile multi-stage container build
Local development
You need Podman or Docker. The
just run recipe builds the full static
bundle and starts a local server:
just run # build + serve at http://localhost:8080/
just run 8081 # use a different port
just stop # tear it down
After starting, the useful pages are:
- http://localhost:8080/ — endpoint directory (versions, URLs, style list)
- http://localhost:8080/map.html — live map preview with style switcher
- http://localhost:8080/color-editor.html — interactive color editor
- http://localhost:8080/docs.html — this page
styles/ and
site/ are not served directly — they go
through the container build first. After any source
change, re-run just run to see the result.
Adding a new style
New styles are created by writing a palette JSON that
maps every (CSS property, color value) pair
in the source style to a new color, then running
recolor.mjs to generate the output. The
color editor can help you find the right values
interactively before committing them to the palette.
1. Write a palette JSON
Create a file in scripts/palettes/. The
required fields:
{
"name": "Night", // becomes style.name in the output
"outName": "night", // output directory: styles/night/
"source": "bright", // read from styles/bright/style.json
"spriteFrom": "bright",// copy sprites from this style (defaults to source)
"byKey": {
"fill-color": {
"#f8f4f0": "#1e2530", // source color → target color
"#fff": "#c8d0db"
},
"line-color": {
"#fff": "#a3acba" // same hex, different role → different target
}
}
}
Color keys are scoped by CSS property name because the same hex value can legitimately need a different dark-mode target depending on its visual role — a road-fill white and a label-halo white are not interchangeable.
styles/bright/style.json,
including whitespace and case. The script normalises
them to lowercase with whitespace stripped before
comparison, so #F8F4F0 and
#f8f4f0 are the same, but the palette still
needs to cover every distinct value that appears.
2. Generate the style
node scripts/recolor.mjs scripts/palettes/your-palette.json
If any (property, color) pairs in the
source style are not covered by the palette, the script
exits non-zero and lists them. Add those entries to the
palette and rerun. When the script exits zero,
styles/your-style/style.json and its
sprites are written.
3. Iterate with the color editor
Run the container and open the color editor to review and tune your style visually:
just run
# open http://localhost:8080/color-editor.html?style=your-style
- Select your style from the dropdown in the toolbar.
- Click on any area of the map — a panel appears showing every layer rendered at that point with its current color values.
- Click a color swatch to open the color picker. The map updates live.
- When you are happy, click ↓ style.json to download the modified style.
-
Compare the downloaded file against your palette
JSON to identify which source colors need new target
values. Add those to your palette and rerun
recolor.mjs.
expr next to
properties whose values are MapLibre expressions (e.g.
road colour varying by zoom). These cannot be edited
interactively — if you need to remap them, locate the
relevant layer in
styles/bright/style.json and handle them
directly in the palette or as a post-processing step.
4. Commit
A complete new-style commit includes both the palette and the generated output:
scripts/palettes/your-palette.json # the source of truth
styles/your-style/style.json # generated output
styles/your-style/sprites/ # copied from source
Merge directly to main — no pull requests.
Editing the base style
styles/bright/style.json is the
hand-authored master. Edit it directly, then regenerate
all palette-derived styles so they stay in sync:
# after editing styles/bright/style.json
node scripts/recolor.mjs scripts/palettes/night.json
# repeat for any other palette-derived styles
If you added new color values to Bright,
recolor.mjs will report them as unmapped
and exit non-zero. Add them to each palette before
committing.
Rules & gotchas
-
Never hand-edit generated styles.
styles/night/style.json(and any other palette-derived style) will be overwritten the next timerecolor.mjsruns. Edit the palette instead. - Never commit a generated style without also committing the palette change that produced it. The two must stay in sync or the next recolor will silently undo your hand edits.
-
Raw style files are not valid JSON.
They contain bare
${VAR}placeholders (e.g.8.5417) that are replaced byenvsubstat build time. Do not try to parse them directly outside the build. - Always rerun recolor after editing Bright. Derived styles will silently diverge if you skip this step.
-
Color values in palettes must match
exactly
(case- and whitespace-insensitive). Run
recolor.mjsto surface any gaps; it will list all unmapped pairs.