在 Hugo 部落格中加入互動式旅行地圖 (Leaflet + JSON)

Posted by Wayne X.Y. on Tuesday, March 10, 2026

在 Hugo 部落格中加入互動式旅行地圖 (Leaflet + JSON)

最近想在部落格的 Travel 頁面頂部加上一個可以標記去過哪些地方的互動式地圖,幾經思考後決定使用開源且輕量的 Leaflet.js,搭配自訂的 JSON 檔案來管理景點和顏色。


🗺️ 實作目標

  1. /life/travel 的頂部固定一個地圖,顯示去過的國家與地點。
  2. 透過 JSON 檔案集中管理景點資料 (包含中英文名稱、經緯度、以及專屬顏色)。
  3. 使用 Leaflet.js 渲染地圖,並根據 JSON 自動計算邊界縮放 (fitBounds) 以容納所有標記。
  4. 將地圖封裝成 Hugo Shortcode
    方便在不同語系頁面重複使用。

💾 建立資料檔 (Data Layer)

首先,在 Hugo 專案的 data/ 目錄下建立一個 travel.json,之後如果有新增旅程,只要修改這裡就可以,不用動到程式碼:

[
  {
    "name_zh": "台北 (台灣)",
    "name_en": "Taipei (Taiwan)",
    "lat": 25.033,
    "lng": 121.5654,
    "color": "red"
  },
  {
    "name_zh": "福岡市",
    "name_en": "Fukuoka City",
    "lat": 33.5902,
    "lng": 130.4017,
    "color": "red"
  }
]

🧩 封裝 Hugo Shortcode (Presentation Layer)

接著,在 layouts/shortcodes/ 底下新增 travel-map.html。我們透過 CDN 引入 Leaflet 的 CSS 和 JS,並讀取剛剛建立的 JSON 資料。

<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" crossorigin="" />
<script src="https://unpkg.com/[email protected]/dist/leaflet.js" crossorigin=""></script>

<div id="travel-map" style="height: 400px; width: 100%; border-radius: 8px; margin-bottom: 2rem; z-index: 1;"></div>

<script>
    document.addEventListener("DOMContentLoaded", function () {
        // 修正 CDN 預設 icon 路徑問題
        delete L.Icon.Default.prototype._getIconUrl;
        L.Icon.Default.mergeOptions({
            iconRetinaUrl: 'https://unpkg.com/[email protected]/dist/images/marker-icon-2x.png',
            iconUrl: 'https://unpkg.com/[email protected]/dist/images/marker-icon.png',
            shadowUrl: 'https://unpkg.com/[email protected]/dist/images/marker-shadow.png',
        });

        // 讀取 Hugo data/travel.json 的資料
        var travelData = \{\{ site.Data.travel | jsonify | safeJS \}\};
        var lang = "\{\{ .Page.Lang \}\}";

        var map = L.map('travel-map', {
            scrollWheelZoom: true // 允許透過滾輪縮放地圖
        });

        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '&copy; OpenStreetMap contributors'
        }).addTo(map);

        var bounds = [];

        if (travelData && travelData.length > 0) {
            travelData.forEach(function (loc) {
                var name = lang === "en" ? loc.name_en : loc.name_zh;
                var markerOptions = {};
                
                // 根據 color 屬性套用不同的 Marker 顏色
                if (loc.color) {
                    markerOptions.icon = new L.Icon({
                        iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-' + loc.color + '.png',
                        shadowUrl: 'https://unpkg.com/[email protected]/dist/images/marker-shadow.png',
                        iconSize: [25, 41],
                        iconAnchor: [12, 41],
                        popupAnchor: [1, -34],
                        shadowSize: [41, 41]
                    });
                }
                
                var marker = L.marker([loc.lat, loc.lng], markerOptions).addTo(map)
                    .bindPopup("<b>" + name + "</b>");
                bounds.push([loc.lat, loc.lng]);
            });
            // 自動計算邊界並設定 50px 的 padding
            map.fitBounds(bounds, { padding: [50, 50] });
        } else {
            map.setView([23.6978, 120.9605], 5); // 預設視角
        }
    });
</script>

💡 Note: 這裡特別使用了 leaflet-color-markers 來替換預設的藍色標記,這樣就能替不同趟的旅程貼上專屬顏色的圖示!

📄 寫入頁面

完成 Shortcode 之後,只需要在任何 Markdown 檔案裡加上這行指令:

\{\{< travel-map >\}\}

自動生成互動地圖的同時還支援中英雙語系,讓 English 頁面的 popups 自動顯示英文地名!

🚀 What’s Next?

目前需要透過手動修改 travel.json 標記去過的地點,實際上使用手機拍照時,照片資訊都會紀錄位置資訊,未來說不定可以透過照片資訊,直接標註在地圖上就好~