Skip to content

Vitepress and leaflet.markercluster

VitePress pre-renders the app in Node.js during the production build, using Vue's Server-Side Rendering (SSR) capabilities: this fact prevents using 'leaflet.markercluster' (or its wrapper module 'vue-leaflet-markercluster') within a vitepress project. Let's say we create this simple leaflet map within a vue component imported within a vitepress project:

vue
<template>
  <div id="map">
    <l-map
      :zoom="9"
      :center="[45.1, 9.1]"
      :use-global-leaflet="true"
    >
      <l-tile-layer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" :attribution="attribution"/>
      <l-marker-cluster-group>
        <l-marker :lat-lng="[45.1, 9.1]"/>
        <l-marker :lat-lng="[45.2, 9.2]"/>
        <l-marker :lat-lng="[45.3, 9.3]"/>
      </l-marker-cluster-group>
    </l-map>
  </div>
</template>

<script setup lang="ts">
import {LMarker, LMap, LTileLayer} from "@vue-leaflet/vue-leaflet";
import L, {marker} from 'leaflet'

globalThis.L = L
import {LMarkerClusterGroup} from 'vue-leaflet-markercluster'
import {ref} from 'vue'

import 'leaflet/dist/leaflet.css'
import 'vue-leaflet-markercluster/dist/style.css'

const attribution = ref("openstreetmap contributors")
const prefix = ref("leaflet")
</script>

<style scoped>
#map {
  height: 50vh;
  width: 100%;
}
</style>

While running this code with yarn docs:dev or pnpm docs:dev (aliases of vitepress dev docs, see the vitepress documentation) it's fine, of course the leaflet.markercluster SSR requirements broke the build step. Luckily it's possible building the project using the import() syntax, commonly called dynamic import. There are different possible implementations (e.g. async components) but in this case I prefer using the classic js syntax within the onBeforeMount() hook, then we can fix the broken component this way:

vue
<template>
  <div id="map-photos"/>
</template>

<script setup lang="ts">
import "leaflet/dist/leaflet.css";
import "leaflet.markercluster/dist/MarkerCluster.css"
import "leaflet.markercluster/dist/MarkerCluster.Default.css"
import {onBeforeMount} from "vue";

onBeforeMount(async () => {
  const L = await import("leaflet")
  const {MarkerClusterGroup} = await import('leaflet.markercluster/dist/leaflet.markercluster')

  const map = L.map("map-photos") // div id
  map.setView([45.1, 9.1], 8)
  L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png").addTo(map);
  let mcluster = new MarkerClusterGroup()
  let marker1 = new L.Marker([45.1, 9.1])
  let marker2 = new L.Marker([45.2, 9.2])
  let marker3 = new L.Marker([45.3, 9.3])
  marker1.addTo(mcluster)
  marker2.addTo(mcluster)
  marker3.addTo(mcluster)
  mcluster.addTo(map)
})
</script>

<style scoped>
#map-photos {
  width: 100%;
  height: 60vh;
}
</style>

Note that onBeforeMount() must be async to make it work. Last but not least the package.json of this vitepress project contains these dependencies:

json
{
  "name": "vitepress-ssr-build",
  "version": "1.0.0",
  "scripts": {
    "docs:dev": "vitepress dev docs",
    "docs:build": "pnpm i --frozen-lockfile && vitepress build docs",
    "docs:serve": "vitepress serve docs",
    "docs:preview": "vitepress preview docs --port 3000"
  },
  "license": "MIT",
  "type": "module",
  "dependencies": {
    "@vue-leaflet/vue-leaflet": "^0.10.1",
    "leaflet": "^1.9.4",
    "leaflet.markercluster": "^1.5.3",
    "vitepress": "^1.0.0-rc.10",
    "vue": "^3.3.4"
  },
  "devDependencies": {
    "@tsconfig/recommended": "^1.0.2",
    "@types/geojson": "^7946.0.10",
    "@types/leaflet": "^1.9.4",
    "@types/leaflet.markercluster": "^1.5.2"
  }
}

Like my website? Pay me a coffee
References are available upon request. I hereby authorize the use of my personal data in compliance with the Italian D. Lgs. 196/2003, art. 13 for the purpose of making me job offers.