How to Add Interactive Maps to Jekyll Using Leaflet and OpenStreetMap
This guide shows how to integrate interactive Leaflet maps with OpenStreetMap tiles into a Jekyll static site — using only front matter configuration and a single Liquid tag. No npm, no gems, no build changes required.
Here’s what the result looks like:
How It Works
The integration uses three files working together:
| File | Role |
|---|---|
_plugins/posts-map-tag.rb |
Registers {% map %} as a custom Liquid tag |
_includes/metadata-hook.html |
Conditionally loads Leaflet CSS/JS only on pages with a map |
assets/js/map-renderer.js |
Reads data attributes from the HTML and initialises the map |
The data flow is straightforward:
1
Front matter (YAML) → Plugin outputs <div> with data-* attributes → JS reads attributes → Leaflet renders map
Posts without map: in their front matter load zero additional assets.
Step 1 — Create the Plugin
Create _plugins/posts-map-tag.rb:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
require 'json'
module Jekyll
class MapTag < Liquid::Tag
def initialize(tag_name, markup, tokens)
super
end
def render(context)
page = context.registers[:page]
map_data = page['map']
return '' unless map_data
center = map_data['center']
zoom = map_data['zoom'] || 13
height = map_data['height'] || '450px'
width = map_data['width'] || '100%'
style = map_data['style'] || 'default'
markers = map_data['markers'] || []
<<~HTML
<div class="leaflet-map"
data-center="#{center.join(',')}"
data-zoom="#{zoom}"
data-style="#{style}"
data-markers='#{JSON.generate(markers)}'
style="height: #{height}; width: #{width}; border-radius: 8px;">
</div>
HTML
end
end
end
Liquid::Template.register_tag('map', Jekyll::MapTag)
This plugin reads the map: block from the post’s front matter and outputs an HTML <div> with all the configuration stored as data-* attributes. The div itself contains no JavaScript — it’s pure HTML. If no map: key exists in the front matter, it outputs nothing.
Step 2 — Edit the Metadata Hook
Edit _includes/metadata-hook.html to conditionally load Leaflet:
1
2
3
4
5
6
7
8
{% if page.map %}
<link
rel="stylesheet"
href="https://unpkg.com/[email protected]/dist/leaflet.css"
/>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<script src="/assets/js/map-renderer.js" defer></script>
{% endif %}
If you’re using the Chirpy theme,
metadata-hook.htmlis automatically injected into the<head>of every page. Other themes may use different hook files — check your theme’s documentation.
The {% if page.map %} conditional ensures Leaflet’s CSS (~40KB), JS (~42KB), and your renderer script only load on pages that actually use a map.
Step 3 — Create the Map Renderer
Create assets/js/map-renderer.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
(function () {
var tileProviders = {
default: {
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
},
dark: {
url: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/">CARTO</a>',
},
light: {
url: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/">CARTO</a>',
},
satellite: {
url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
attribution: '© <a href="https://www.esri.com/">Esri</a>',
},
};
document.querySelectorAll(".leaflet-map").forEach(function (el) {
var center = el.dataset.center.split(",").map(Number);
var zoom = parseInt(el.dataset.zoom);
var style = el.dataset.style || "default";
var markers = JSON.parse(el.dataset.markers || "[]");
var provider = tileProviders[style] || tileProviders["default"];
var map = L.map(el).setView(center, zoom);
L.tileLayer(provider.url, {
attribution: provider.attribution,
maxZoom: 19,
}).addTo(map);
markers.forEach(function (m) {
var marker = L.marker(m.coords).addTo(map);
if (m.popup) {
marker.bindPopup(m.popup);
if (m.open) {
marker.openPopup();
}
}
});
});
})();
The renderer finds all .leaflet-map divs, reads their data-* attributes, and calls the Leaflet API to render each map. Four tile providers are included — all free, no API keys required.
Usage
Front Matter Options
1
2
3
4
5
6
7
8
9
10
map:
center: [1.3521, 103.8198] # required — [latitude, longitude]
zoom: 12 # optional — 1 (world) to 18 (street), default: 13
height: 500px # optional — any CSS value, default: 450px
width: 100% # optional — any CSS value, default: 100%
style: default # optional — default | dark | light | satellite
markers: # optional — array of markers
- coords: [1.3521, 103.8198] # required — [latitude, longitude]
popup: "Marina Bay Sands" # optional — popup text on click
open: true # optional — popup opens on load
Post Body
Place the map anywhere in your markdown:
1
{% map %}
Tile Styles
| Value | Source | Best for |
|---|---|---|
default |
OpenStreetMap | General use |
dark |
CartoDB Dark Matter | Dark-themed sites |
light |
CartoDB Positron | Minimal, clean look |
satellite |
ESRI World Imagery | Aerial/geographic context |
Why This Approach
There were several ways to integrate maps into Jekyll. Here’s why this design was chosen:
- Data in front matter — map configuration stays with the post metadata, not buried in the content
-
No inline JavaScript in markdown — the
{% map %}tag outputs only an HTML div; all JS lives inmap-renderer.js - Conditional loading — Leaflet assets only load on pages that use maps
-
Plugin over include —
{% map %}is cleaner than{% include leaflet-map.html %}, and the rendering logic lives in Ruby rather than Liquid templates generating JavaScript
Finding Coordinates
To get the latitude and longitude for any location:
- Open Google Maps
- Right-click on any point
- Click the coordinates that appear — they’re copied to your clipboard
- Paste into your front matter as
[latitude, longitude]
Notes
-
Restart Jekyll after creating the plugin — new files in
_plugins/require a server restart - GitHub Pages does not support custom plugins with its default build. If you deploy via GitHub Actions (which Chirpy uses), plugins work fine
- Leaflet is loaded from unpkg CDN — no local files to manage or update
