Post

How to Add Interactive Maps to Jekyll Using Leaflet and OpenStreetMap

Embed interactive Leaflet maps with OpenStreetMap in Jekyll using front matter config and a Liquid tag.

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.html is 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:
        '&copy; <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:
        '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/">CARTO</a>',
    },
    light: {
      url: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
      attribution:
        '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/">CARTO</a>',
    },
    satellite: {
      url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
      attribution: '&copy; <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 in map-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:

  1. Open Google Maps
  2. Right-click on any point
  3. Click the coordinates that appear — they’re copied to your clipboard
  4. 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
This post is licensed under CC BY 4.0 by the author.