Appearance
To publish a historical web map with Leaflet, georeference and tile your old map to XYZ tiles in EPSG:3857, drop those tiles and your GeoJSON onto a static host, and load them with a few lines of Leaflet. No server is required for the common case. The two things that trip people up are tile-scheme mismatches (TMS vs XYZ) and oversized GeoJSON; both are easy to avoid once you know to look. Here is the end-to-end path.
Step 1: Tile the georeferenced raster
Assume you already have a georeferenced GeoTIFF of your historical map. Reproject it to Web Mercator and cut it into tiles:
bash
# reproject to web mercator
gdalwarp -t_srs EPSG:3857 old_map.tif old_map_3857.tif
# generate XYZ tiles for zoom levels 8–16
gdal2tiles.py --xyz -z 8-16 old_map_3857.tif tiles/The --xyz flag matters: without it gdal2tiles emits TMS tiles, whose y-axis is flipped relative to what Leaflet expects by default.
Step 2: Add the basic map and overlay
Leaflet needs a div, a basemap and your tile layer:
html
<div id="map" style="height:600px"></div>
<script>
const map = L.map('map').setView([51.75, -1.26], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
{ attribution: '© OpenStreetMap', maxZoom: 19 }).addTo(map);
const historic = L.tileLayer('tiles/{z}/{x}/{y}.png',
{ opacity: 0.8, maxZoom: 16, attribution: 'Old map, 1880' }).addTo(map);
</script>If you generated TMS tiles instead, add tms: true to the historical layer's options to fix the flip.
Why is my overlay misaligned?
Two causes account for nearly every misalignment:
| Symptom | Cause | Fix |
|---|---|---|
| Overlay flipped top-to-bottom | TMS vs XYZ scheme | --xyz flag, or tms: true |
| Overlay shifted sideways | Not reprojected to 3857 | gdalwarp -t_srs EPSG:3857 |
| Edges fuzzy / cut off | Wrong zoom range tiled | widen -z range |
Get the scheme and the projection right and the old map snaps into place over the modern one.
Step 3: Let users compare old and new
The feature historical-map users want most is a fade between layers. An opacity slider is two lines:
html
<input type="range" min="0" max="1" step="0.05" value="0.8"
oninput="historic.setOpacity(this.value)">For a side-by-side swipe instead, the leaflet-side-by-side plugin splits the viewport between two layers with a draggable handle.
How do I keep vector data fast?
A single multi-megabyte GeoJSON will stall the browser. Three defences:
- Simplify geometry with
mapshaper -simplify 15% keep-shapes. - Cluster dense points using
Leaflet.markercluster. - For very large layers, serve vector tiles rather than one file.
Aim to keep any GeoJSON you load directly under a few megabytes; beyond that, tile it.
Step 4: Deploy as static files
Because pre-rendered tiles and GeoJSON are just files, deployment is a copy:
bash
# the whole map is static — push to any static host
rsync -av tiles/ index.html data.geojson user@host:/var/www/oldmap/GitHub Pages, Cloudflare Pages and Netlify all serve this with no backend, free tier included. Add proper attribution for both the basemap and the historical source — it is a courtesy and often a licence requirement.
Key Takeaways
- Tile the georeferenced raster to XYZ tiles in EPSG:3857 with
gdal2tiles.py --xyz. - The whole map is static files — no server needed for the common case.
- Misalignment is almost always a TMS/XYZ flip or a missing reprojection.
- An opacity slider (
setOpacity) gives the old-vs-new fade users want. - Keep direct-loaded GeoJSON small; simplify, cluster, or use vector tiles.
- Leaflet defaults to Web Mercator (EPSG:3857) — match your tiles to it.
- Always credit both the basemap and the historical map source.
Frequently Asked Questions
How do I add a georeferenced historical map to Leaflet?
Export the georeferenced raster to web tiles (XYZ or TMS) with gdal2tiles.py, then load them with L.tileLayer pointing at the tile folder URL template. Leaflet overlays them on a modern basemap so users can compare.
Do I need a server to publish a Leaflet map?
No. Pre-rendered XYZ tiles plus GeoJSON are static files, so any static host — GitHub Pages, Cloudflare Pages, Netlify — works without a backend. A server is only needed for very large dynamic datasets.
Why is my historical overlay slightly misaligned?
Usually a TMS-versus-XYZ y-axis flip or a CRS mismatch. gdal2tiles produces TMS by default; set tms: true in L.tileLayer or generate XYZ tiles, and make sure everything is in EPSG:3857 for the web.
How do I keep a big GeoJSON from freezing the browser?
Simplify the geometry (mapshaper), serve vector tiles instead of one huge file, and cluster point layers with Leaflet.markercluster. Aim to keep any single GeoJSON under a few megabytes.
Can users fade between the old and modern map?
Yes — add an opacity slider that calls setOpacity on the historical tile layer, or use the Leaflet.Sync / side-by-side plugin for a swipe comparison. Both are common in historical web maps.
Which projection do Leaflet web maps use?
Web Mercator, EPSG:3857, by default. Reproject and tile your historical raster to 3857 unless you deliberately configure a custom CRS, which is advanced and rarely needed.