Skip to content
GIS for History

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:

SymptomCauseFix
Overlay flipped top-to-bottomTMS vs XYZ scheme--xyz flag, or tms: true
Overlay shifted sidewaysNot reprojected to 3857gdalwarp -t_srs EPSG:3857
Edges fuzzy / cut offWrong zoom range tiledwiden -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.