Skip to content

Commit 2d91501

Browse files
authored
feat(maps): add Leaflet and MapLibre integrations (#833)
1 parent 3725922 commit 2d91501

63 files changed

Lines changed: 6528 additions & 1632 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
---
2+
title: "Example: Pickup Locator"
3+
description: Build a production-shaped store locator with accessible selection, map fallbacks, popups, and a delivery zone.
4+
---
5+
6+
This real-world pickup locator pairs a location list with the map. The list keeps addresses and opening hours available when tiles fail, and it gives keyboard users a direct way to choose a location.
7+
8+
It demonstrates reactive selection, accessible location buttons, explicit OpenStreetMap attribution, marker popups, error fallback content, and a GeoJSON delivery zone.
9+
10+
::code-group
11+
12+
:leaflet-pickup-locator-demo{label="Demo"}
13+
14+
```vue [Source]
15+
<script setup lang="ts">
16+
import type { Feature, Polygon } from 'geojson'
17+
import type { LatLngExpression, LatLngTuple } from 'leaflet'
18+
19+
interface PickupLocation {
20+
id: string
21+
name: string
22+
address: string
23+
hours: string
24+
position: LatLngTuple
25+
}
26+
27+
const locations: PickupLocation[] = [
28+
{
29+
id: 'flinders-lane',
30+
name: 'Flinders Lane',
31+
address: 'Centre Place, Melbourne VIC',
32+
hours: 'Open today until 6 pm',
33+
position: [-37.8164, 144.9656],
34+
},
35+
{
36+
id: 'queen-victoria-market',
37+
name: 'Queen Victoria Market',
38+
address: 'Queen Street, Melbourne VIC',
39+
hours: 'Open today until 3 pm',
40+
position: [-37.8076, 144.9568],
41+
},
42+
{
43+
id: 'southbank',
44+
name: 'Southbank',
45+
address: 'Southbank Promenade, Southbank VIC',
46+
hours: 'Open today until 8 pm',
47+
position: [-37.8202, 144.9655],
48+
},
49+
]
50+
51+
const selectedId = ref(locations[0]!.id)
52+
const center = ref<LatLngExpression>(locations[0]!.position)
53+
const zoom = ref(14)
54+
55+
const deliveryZone: Feature<Polygon> = {
56+
type: 'Feature',
57+
properties: { name: 'Same-day delivery zone' },
58+
geometry: {
59+
type: 'Polygon',
60+
coordinates: [[
61+
[144.946, -37.802],
62+
[144.982, -37.802],
63+
[144.986, -37.828],
64+
[144.951, -37.832],
65+
[144.946, -37.802],
66+
]],
67+
},
68+
}
69+
70+
function selectLocation(location: PickupLocation) {
71+
selectedId.value = location.id
72+
center.value = location.position
73+
zoom.value = 16
74+
}
75+
</script>
76+
77+
<template>
78+
<div class="pickup-locator">
79+
<aside aria-labelledby="pickup-title">
80+
<h2 id="pickup-title">
81+
Pickup in Melbourne
82+
</h2>
83+
84+
<ol>
85+
<li v-for="location in locations" :key="location.id">
86+
<button
87+
type="button"
88+
:aria-pressed="selectedId === location.id"
89+
@click="selectLocation(location)"
90+
>
91+
<strong>{{ location.name }}</strong>
92+
<span>{{ location.address }}</span>
93+
<small>{{ location.hours }}</small>
94+
</button>
95+
</li>
96+
</ol>
97+
</aside>
98+
99+
<ScriptLeafletMap
100+
v-model:center="center"
101+
v-model:zoom="zoom"
102+
width="100%"
103+
:height="520"
104+
aria-label="Pickup locations and delivery zone in central Melbourne"
105+
>
106+
<template #error>
107+
<p role="alert">
108+
The map is unavailable. Choose a pickup location from the list.
109+
</p>
110+
</template>
111+
112+
<ScriptLeafletTileLayer
113+
url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
114+
:options="{
115+
maxZoom: 19,
116+
attribution: '&copy; <a href=&quot;https://www.openstreetmap.org/copyright&quot;>OpenStreetMap contributors</a>',
117+
}"
118+
/>
119+
120+
<ScriptLeafletGeoJson
121+
:data="deliveryZone"
122+
:options="{
123+
style: {
124+
color: '#15803d',
125+
fillColor: '#86efac',
126+
fillOpacity: 0.18,
127+
weight: 2,
128+
},
129+
}"
130+
/>
131+
132+
<ScriptLeafletMarker
133+
v-for="location in locations"
134+
:key="location.id"
135+
:position="location.position"
136+
:alt="`${location.name} pickup location`"
137+
:title="location.name"
138+
@click="selectLocation(location)"
139+
>
140+
<ScriptLeafletPopup
141+
:open="selectedId === location.id"
142+
:options="{ minWidth: 180 }"
143+
>
144+
<strong>{{ location.name }}</strong><br>
145+
{{ location.address }}<br>
146+
{{ location.hours }}
147+
</ScriptLeafletPopup>
148+
</ScriptLeafletMarker>
149+
</ScriptLeafletMap>
150+
</div>
151+
</template>
152+
```
153+
154+
::
155+
156+
Use CSS Grid to place the list beside the map on wide screens and above it on small screens. Keep the list in the document even when the map is the main visual.
157+
158+
GeoJSON coordinates use `[longitude, latitude]`; Leaflet marker positions use `[latitude, longitude]`.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
title: Tiles & Attribution
3+
---
4+
5+
Leaflet renders and controls the map, but it does not supply map imagery. Add a `ScriptLeafletTileLayer` for each raster tile service you use.
6+
7+
## Choose a Tile Service
8+
9+
A tile URL usually contains `{z}`, `{x}`, and `{y}` placeholders:
10+
11+
```vue
12+
<ScriptLeafletTileLayer
13+
url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
14+
:options="{
15+
maxZoom: 19,
16+
attribution: '&copy; <a href=&quot;https://www.openstreetmap.org/copyright&quot;>OpenStreetMap contributors</a>',
17+
}"
18+
/>
19+
```
20+
21+
For production, check the service's traffic limits, caching rules, allowed use cases, privacy policy, and availability commitments. Some providers require an API key or account. Others offer downloadable tiles for self-hosting.
22+
23+
Keep the URL configurable so you can change providers without rewriting the map component:
24+
25+
```ts [nuxt.config.ts]
26+
exportdefaultdefineNuxtConfig({
27+
runtimeConfig: {
28+
public: {
29+
mapTileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
30+
},
31+
},
32+
})
33+
```
34+
35+
```vue
36+
<script setup lang="ts">
37+
const config = useRuntimeConfig()
38+
const tileAttribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>'
39+
</script>
40+
41+
<template>
42+
<ScriptLeafletTileLayer
43+
:url="config.public.mapTileUrl"
44+
:options="{ attribution: tileAttribution }"
45+
/>
46+
</template>
47+
```
48+
49+
## OpenStreetMap's Standard Tiles
50+
51+
OpenStreetMap data is open, but the standard tile servers are a donation-funded public service. They have no SLA and may block clients that break the [tile usage policy](https://operations.osmfoundation.org/policies/tiles/).
52+
53+
When using `tile.openstreetmap.org`:
54+
55+
- Keep `© OpenStreetMap contributors` visible on the map.
56+
- Use the official HTTPS tile URL and allow the browser to send its normal referrer.
57+
- Respect HTTP caching headers. Do not force tiles to bypass the browser cache.
58+
- Do not bulk download, prefetch areas, or add offline download features.
59+
60+
Normal browser-based map viewing follows the technical caching and identification requirements. A proxy, native client, or offline feature needs additional work and may need a different provider.
61+
62+
## Attribution
63+
64+
Pass the provider's required attribution through the tile layer `options`. Leaflet combines attribution from active layers in its attribution control.
65+
66+
Do not cover the control with page UI or remove it. If your map uses several data or imagery sources, include the attribution required by each source.
67+
68+
## Failure Fallback
69+
70+
Tile services can reject requests or become unavailable while the rest of the page still works. Keep essential locations in HTML and provide a useful error state:
71+
72+
```vue
73+
<ScriptLeafletMap :center="[-37.8136, 144.9631]" :zoom="13">
74+
<template #error>
75+
<p role="alert">
76+
The map is unavailable. Use the location list to choose a store.
77+
</p>
78+
</template>
79+
</ScriptLeafletMap>
80+
```
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
title: Performance & Accessibility
3+
---
4+
5+
`ScriptLeafletMap` loads near the viewport by default. It also reserves the configured dimensions during SSR, which prevents the page from shifting when Leaflet starts.
6+
7+
## Loading Strategies
8+
9+
### Visible by Default
10+
11+
The default `visible` trigger delays the SDK, stylesheet, and tile requests until the map approaches the viewport:
12+
13+
```vue
14+
<ScriptLeafletMap :center="[-37.8136, 144.9631]" :zoom="13" />
15+
```
16+
17+
### Immediate Loading
18+
19+
Load immediately when the map is the page's primary content:
20+
21+
```vue
22+
<ScriptLeafletMap
23+
trigger="immediate"
24+
:center="[-37.8136, 144.9631]"
25+
:zoom="13"
26+
/>
27+
```
28+
29+
You can also use an [element event trigger](/docs/guides/script-triggers#element-event-triggers) such as `mousedown` when the map should load only after explicit interaction.
30+
31+
## Stable Layout and Loading States
32+
33+
Always give the map a height. A number is treated as pixels; a CSS length works well for responsive layouts:
34+
35+
```vue
36+
<ScriptLeafletMap
37+
width="100%"
38+
height="clamp(24rem, 60vw, 36rem)"
39+
:center="[-37.8136, 144.9631]"
40+
/>
41+
```
42+
43+
Use the `placeholder`, `loading`, and `error` slots when the default states do not fit your page. Put addresses, opening hours, and other essential details outside the map so they remain available before loading and after an error.
44+
45+
## Interactive Maps
46+
47+
Give the map a specific `aria-label` and each marker a unique `alt`. Leaflet's keyboard controls remain available after the map loads.
48+
49+
```vue
50+
<ScriptLeafletMap
51+
:center="[-37.8136, 144.9631]"
52+
:zoom="13"
53+
aria-label="Pickup locations in central Melbourne"
54+
>
55+
<ScriptLeafletMarker
56+
:position="[-37.8164, 144.9656]"
57+
alt="Flinders Lane pickup location"
58+
/>
59+
</ScriptLeafletMap>
60+
```
61+
62+
A text list should duplicate any location a user must discover or select. A visual marker alone is not enough for store selection, delivery status, or directions.
63+
64+
## Decorative Maps
65+
66+
Set `:interactive="false"` when the map is only decoration. The component disables map input, hides it from assistive technology, and makes child controls inert.
67+
68+
```vue
69+
<ScriptLeafletMap
70+
:center="[-37.8136, 144.9631]"
71+
:zoom="13"
72+
:interactive="false"
73+
/>
74+
```
75+
76+
## Custom Styles
77+
78+
Nuxt Scripts injects its embedded Leaflet stylesheet when the SDK starts loading. If your app already provides Leaflet CSS, disable the embedded copy:
79+
80+
```ts [nuxt.config.ts]
81+
exportdefaultdefineNuxtConfig({
82+
css: ['leaflet/dist/leaflet.css'],
83+
})
84+
```
85+
86+
```vue
87+
<ScriptLeafletMap
88+
:center="[-37.8136, 144.9631]"
89+
:inject-styles="false"
90+
/>
91+
```
92+
93+
Use the same approach when an inline-style Content Security Policy prevents the default stylesheet injection.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
title: <ScriptLeafletMap>
3+
---
4+
5+
The map facade reserves layout space during SSR, loads Leaflet when triggered, creates the `L.Map`, and provides it to child components.
6+
7+
::script-types{script-key="leaflet"filter="ScriptLeafletMap"}
8+
::
9+
10+
`center` and `zoom` are reactive. Use `v-model:center` or `v-model:zoom` when you also want to receive user pan and zoom changes.
11+
12+
```vue
13+
<ScriptLeafletMap
14+
v-model:center="center"
15+
v-model:zoom="zoom"
16+
width="100%"
17+
height="28rem"
18+
@ready="({ map }) => console.log(map.value)"
19+
/>
20+
```
21+
22+
The `placeholder`, `awaitingLoad`, `loading`, and `error` slots customize each loading state. The default error state is visible and announced with `role="alert"`.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
title: <ScriptLeafletTileLayer>
3+
---
4+
5+
Adds an `L.TileLayer` to the nearest parent map. Nuxt Scripts does not choose a tile provider for you.
6+
7+
::script-types{script-key="leaflet"filter="ScriptLeafletTileLayer"}
8+
::
9+
10+
Always follow the selected provider's terms and pass its required attribution through `options.attribution`.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
title: <ScriptLeafletMarker>
3+
---
4+
5+
Adds a reactive `L.Marker`. Supply a unique `alt` so the generated marker image has a useful accessible name.
6+
7+
::script-types{script-key="leaflet"filter="ScriptLeafletMarker"}
8+
::
9+
10+
Nest a [`<ScriptLeafletPopup>`{lang="html"}](/scripts/leaflet/api/popup) in the default slot to bind it to the marker.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: <ScriptLeafletPopup>
3+
---
4+
5+
Renders slotted HTML in an `L.Popup`. Nest it inside a marker, or pass `position` for a standalone popup. Use the reactive `open` prop to control visibility.
6+
7+
::script-types{script-key="leaflet"filter="ScriptLeafletPopup"}
8+
::
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
title: <ScriptLeafletGeoJson>
3+
---
4+
5+
Adds inline GeoJSON through `L.geoJSON`. Replacing `data` clears and repopulates the existing layer; changing `options.style` updates its path style.
6+
7+
::script-types{script-key="leaflet"filter="ScriptLeafletGeoJson"}
8+
::
9+
10+
Remote fetching is intentionally outside the component. Fetch and validate data with `useFetch`, then pass the resulting object to `data` so loading and failure states remain explicit.

0 commit comments

Comments
 (0)