Astro
@maplibre-yaml/astro puts maps on an Astro site. Write each map as a YAML
file, then show it with <Map> (a map in the page), <FullPageMap> (a map
that fills the window) or <Scrollytelling> (a story map). The components
load MapLibre for you and work on any static host, GitHub Pages included.
Quick start
Section titled “Quick start”Install the package, then show a map from a YAML file.
Install
Section titled “Install”In an Astro project (create one with npm create astro@latest -- --template minimal), install the component package, the core library and MapLibre:
npm install @maplibre-yaml/astro @maplibre-yaml/core maplibre-gl@^6pnpm add @maplibre-yaml/astro @maplibre-yaml/core maplibre-gl@^6yarn add @maplibre-yaml/astro @maplibre-yaml/core maplibre-gl@^6Use the package manager your project already uses; mixing them leaves you
with two lockfiles. @maplibre-yaml/astro 0.7 needs @maplibre-yaml/core 0.9
or later, so install and upgrade the two together. maplibre-gl 4, 5 and 6
(6.4.1 or later) all work; 6 is recommended. Astro 4 to 7 are supported
(Supported versions).
Show a map from a YAML file
Section titled “Show a map from a YAML file”Keep each map as a YAML file in public/ and point <Map src> at it. This is
the recommended setup: the YAML stays a plain file you can edit, and the same
file works locally and on GitHub Pages.
-
Create
public/configs/hero-map.yaml:public/configs/hero-map.yaml type: mapid: hero-mapconfig:center: [-74.006, 40.7128]zoom: 12mapStyle: "https://tiles.openfreemap.org/styles/liberty"layers:- id: markertype: circlesource:type: geojsondata:type: FeatureCollectionfeatures:- type: Featuregeometry:type: Pointcoordinates: [-74.006, 40.7128]properties:name: "New York City"paint:circle-radius: 12circle-color: "#3b82f6"circle-stroke-width: 2circle-stroke-color: "#ffffff" -
Show it on a page, for example
src/pages/index.astro:src/pages/index.astro ---import { Map } from '@maplibre-yaml/astro';---<html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>My map</title></head><body><h1>Welcome</h1><Map src="/configs/hero-map.yaml" height="500px" /></body></html> -
Run
npm run devand open the address it prints (usuallyhttp://localhost:4321). You should see a street map of New York with a blue dot. A map that takes more than a second shows “Loading map…” until the basemap draws.
How it fits together:
srcis the file’s address on the site, not its path on disk. Files inpublic/are served from the site root, sopublic/configs/hero-map.yamlis/configs/hero-map.yaml.- Paths inside the YAML start at
public/too. Put your data next to it (public/data/places.geojson, loaded withurl: "/data/places.geojson"). Large files (PMTiles, FlatGeobuf, GeoParquet, COG) work frompublic/:astro dev,astro previewand GitHub Pages serve the parts of a file the map asks for (Large data). mapStyleis the basemap. Any MapLibre style URL works, or a Protomaps.pmtilesfile you keep inpublic/(mapStyle: { pmtiles: data/city.pmtiles, flavor: light }), which needs no API key (Host your own basemap).- The YAML is checked in the browser. If it has a mistake, the map shows an error card naming the field and line instead. To catch mistakes when the site builds, load the YAML at build time.
- Everything a map document can do works here:
markers:,images:,parameters:(the params panel),legend:, popups,classify:,title:/description:andlocale:(Accessibility & languages), the GeoJSON shorthands (location:,route:, …) and format v2 documents. The component hands the whole document to the<ml-map>element, so there is no Astro-specific setup.
height defaults to 400px; class and style style the map’s box. Every
prop is listed under Map component API.
FullPageMap component
Section titled “FullPageMap component”<FullPageMap> fills the whole window and adds zoom and reset buttons. Use it
for a page that is only a map.
Basic usage
Section titled “Basic usage”Save the map as public/configs/world-map.yaml:
type: mapid: world-mapconfig: center: [10, 25] zoom: 1.5 mapStyle: "https://tiles.openfreemap.org/styles/liberty"layers: - id: capitals type: circle source: type: geojson data: type: FeatureCollection features: - type: Feature geometry: { type: Point, coordinates: [-77.04, 38.91] } properties: { name: Washington } - type: Feature geometry: { type: Point, coordinates: [2.35, 48.86] } properties: { name: Paris } - type: Feature geometry: { type: Point, coordinates: [139.69, 35.69] } properties: { name: Tokyo } paint: circle-radius: 8 circle-color: "#e63946" circle-stroke-width: 2 circle-stroke-color: "#ffffff"and give it a page of its own, such as src/pages/map.astro:
---import { FullPageMap } from '@maplibre-yaml/astro';---<html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>World map</title> </head> <body style="margin: 0;"> <FullPageMap src="/configs/world-map.yaml" /> </body></html>Keep the viewport <meta>: without it, phones lay the page out about 980px
wide and shrink it, so the map and its buttons come out tiny (astro dev
warns in the browser console when it is missing). The shorter snippets on this
page show only the component; put them on a page like this one or in a
layout.
Under a site header
Section titled “Under a site header”FullPageMap is position: fixed and covers the whole window, header
included. Start it below the header with style (here the header is 64px
tall):
<header style="height: 64px;">…</header><FullPageMap src="/configs/world-map.yaml" style="top: 64px; height: calc(100vh - 64px);"/>Don’t raise its z-index instead: the map would then cover the header.
Controls
Section titled “Controls”The buttons zoom in, zoom out, and reset the view to the one the map opened
with (including a fitTo framing of the data). They sit in the map’s
top-right corner, and anything else placed there (a legend, the params panel,
your own corner slot) stacks below them. Their labels come
from the document’s locale: table (NavigationControl.ZoomIn,
NavigationControl.ZoomOut, maplibre-yaml.FullPageMap.ResetView).
If the document has its own zoom buttons (controls: { navigation: true }),
FullPageMap adds only the reset button, so one YAML file can serve both
<Map> and <FullPageMap>. showControls={false} removes all of them.
Legend
Section titled “Legend”A layer with a legend: entry (or a classify:
block) gets a legend in the top-left corner, as on any map. To move it, add a
legend: block with a position to the YAML, or pass showLegend and
legendPosition when the document has no legend: block of its own.
showLegend needs the document at build time, so it works with config, not
src (the component warns at build time if you pass both). legend: false
at the top of the document turns the legend off.
layers: - id: parks type: fill legend: { label: Parks, color: "#2e7d32", shape: square }Legend entries come only from the layers’ legend: fields, the legend
block’s items: and classified layers, never from paint colours. When no
layer has one, no legend is shown. The layer reference
lists the entry’s fields.
Story maps and content collections
Section titled “Story maps and content collections”- Story maps in Astro:
<Scrollytelling>shows a story map (also called scrollytelling), where chapters of text scroll over a map that moves with them. - Astro content collections: a map on every page of a collection, built from a location, region or route in each entry’s frontmatter, or whole map documents kept as validated entries.
Deploying to GitHub Pages (or any subpath)
Section titled “Deploying to GitHub Pages (or any subpath)”A static Astro build works on any static host. A GitHub Pages project site
is served from a subpath (https://<user>.github.io/<repo>/), so set site
and base in astro.config.mjs, as Astro’s
GitHub Pages guide
describes:
import { defineConfig } from "astro/config";
export default defineConfig({ site: "https://<user>.github.io", base: "/<repo>",});Change nothing else. Map, FullPageMap, Scrollytelling and Chapter
pass the base to the map, so src="/configs/hero-map.yaml" and the paths
inside your YAML load from public/ under it. Check before you push with
npm run build && npm run preview, at http://localhost:4321/<repo>/.
Publishing & subpaths has the details
(which paths the base applies to, base={false}, large files on GitHub
Pages), and the Astro quick start
has the GitHub Actions workflow.
Common tasks
Section titled “Common tasks”Editing in astro dev
Section titled “Editing in astro dev”What you see after saving a file under astro dev depends on how the page
loads it:
| You edit | What happens |
|---|---|
A YAML file in public/ (<Map src>) | Reload the page to see it. |
A file read by loadMapConfig / loadScrollytellingConfig, or a feature_ref GeoJSON | Reload the page to see it. |
| A content-collection YAML or Markdown file | The page updates by itself. |
The loadGlobalMapConfig file | Restart astro dev: the module that reads it ran once and kept the old file. |
Add the optional integration and every row becomes “the page reloads by
itself”. It watches the files the loaders read and the YAML, JSON, TOML and
GeoJSON under public/:
import { defineConfig } from "astro/config";import maplibreYaml from "@maplibre-yaml/astro/integration";
export default defineConfig({ integrations: [maplibreYaml()],});It also pre-bundles maplibre-gl and @maplibre-yaml/core when the dev server
starts. Without that, the first page with a map makes Vite find them late:
the terminal says new dependencies optimized … reloading, the console shows
504 (Outdated Optimize Dep), and the page reloads once. The integration acts
only under astro dev; builds are the same with or without it. It needs
@maplibre-yaml/astro 0.6 or later.
Load the YAML at build time
Section titled “Load the YAML at build time”With src, a mistake in the YAML shows up as an error card in the browser.
Load the file at build time instead and pass it as config, and the same
mistake fails astro build and shows in the astro dev error overlay, so a
broken map never reaches your site. Keep these files outside public/, for
example in src/configs/:
---import { Map, loadMapConfig } from '@maplibre-yaml/astro';
// The path starts at the project root (where `astro build` runs).const config = await loadMapConfig('./src/configs/map.yaml');---<Map config={config} height="500px" />The error names the file, each failing field with its line, and the parser’s warnings, which often explain the error:
Scrollytelling configuration validation failed for ./src/stories/trip.yaml: error Chapter 3 ("summit"): Unknown key "centre" — did you mean "center"? ("center" is required here.) (line 81, column 5)A deployed site picks up a change to these files only when it is rebuilt.
If you pass both config and src, config wins and the build warns.
To report the problems your own way before failing, catch YAMLLoadError:
---import { Map, loadMapConfig, YAMLLoadError } from '@maplibre-yaml/astro';
let config;try { config = await loadMapConfig('./src/configs/map.yaml');} catch (error) { if (error instanceof YAMLLoadError) { // `errors` is set for validation failures; a missing file or a YAML // syntax error has only `message` (and the original error as `cause`). console.error(`${error.filePath}: ${error.message}`); error.errors?.forEach((e) => console.error(` ${e.message}`)); // also e.path, e.line, e.reason } throw error; // fail the build}---<Map config={config} />To load every map in a folder, use loadFromGlob with Vite’s
import.meta.glob:
---import { loadFromGlob, Map } from '@maplibre-yaml/astro';import { YAMLParser } from '@maplibre-yaml/core';
// A leading "/" is the project root. A "./" pattern is relative to THIS// file, so './src/configs/*.yaml' from src/pages/ matches nothing.const files = import.meta.glob<string>('/src/configs/*.yaml', { query: '?raw', import: 'default',});const maps = await loadFromGlob(files, (yaml) => YAMLParser.safeParseMapBlock(yaml));---{maps.map(({ path, config }) => ( <section> <h2>{config.id} <small>{path}</small></h2> <Map config={config} height="400px" /> </section>))}If any file fails validation, loadFromGlob throws one YAMLLoadError
listing every failing file. A pattern that matches nothing returns an empty
array. The <string> type argument tells TypeScript what a ?raw import
holds; the older as: 'raw' option still works but Vite warns about it on
every build.
For a story, loadScrollytellingConfig(path) does the same
(Story maps in Astro). loadYAML(path, schema?) reads any YAML file and, given a zod schema, validates it.
Corner slots
Section titled “Corner slots”Put your own content over the map: a caption, a link, a credit. Children
with slot="top-left", "top-right", "bottom-left" or "bottom-right" go
in that corner, stacked with the built-in legend and params panel. A
slot="legend" child replaces the built-in legend. Map, FullPageMap and
Scrollytelling all take them:
<Map config={ringOfFire} height="480px"> <p slot="top-left" class="caption" data-testid="hero-caption"> <strong>16 volcanoes, one plate boundary</strong> Hover a dot for its name and height. </p> <a slot="bottom-right" class="caption" href="/explore">Explore full screen →</a></Map>A corner with no content takes no space. On FullPageMap, your top-right
content stacks below the zoom and reset buttons.
Inside a {condition && (...)} expression, Astro’s compiler reads a literal
slot="top-right" as its own named-slot directive and drops it. Write
{...{ slot: "top-right" }} there instead; a literal slot outside an
expression is passed through unchanged.
Named sources
Section titled “Named sources”When several layers draw the same data (a fill and its outline, say), define
the source once under sources: and name it from each layer:
type: mapid: neighborhood-mapconfig: center: [-73.985, 40.674] zoom: 14 mapStyle: "https://tiles.openfreemap.org/styles/liberty"sources: boundary: type: geojson url: "/data/boundary.geojson"layers: - id: boundary-fill type: fill source: boundary paint: fill-color: "#3388ff" fill-opacity: 0.2 - id: boundary-outline type: line source: boundary paint: line-color: "#3388ff" line-width: 2Named and inline sources can be mixed in one document. Every source type and its options are in the sources reference.
Effects
Section titled “Effects”Effects (effect: on a layer, such as the tonal hatch)
need the effects package on the page with the map. Install
@maplibre-yaml/effects and import its register module in a <script>:
---import { Map } from '@maplibre-yaml/astro';---<Map src="/configs/hatched.yaml" height="500px" />
<script> import '@maplibre-yaml/effects/register';</script>View transitions
Section titled “View transitions”The components work with Astro’s client-side router (<ClientRouter /> in
your layout’s <head>; <ViewTransitions /> on Astro 4). Each navigation
builds the new page’s maps and removes the old ones, and FullPageMap’s
buttons work on every visit. This needs @maplibre-yaml/astro 0.6 or later.
Your own page scripts run once per full page load, not on each
navigation. Do per-page work in an astro:page-load listener, which fires
after the first load and after every navigation:
<script> document.addEventListener('astro:page-load', () => { for (const mapEl of document.querySelectorAll('ml-map')) { // wire this page's maps here } });</script>Server output (SSR)
Section titled “Server output (SSR)”The default static output is not affected by this section.
With output: 'server', pages render on request on the deployed server. The
components, src, content collections, the builders and loadFromGlob all
work there. loadMapConfig, loadScrollytellingConfig, loadGlobalMapConfig
and feature_ref don’t: they read files from your project folder when the
page renders, and the deployed server has the built site but not your src/
folder. Such pages fail with a 500 and ENOENT in the server log.
Prerender the pages that use them, so they run once at build time:
---export const prerender = true;
import { Map, loadMapConfig } from '@maplibre-yaml/astro';const config = await loadMapConfig('./src/configs/map.yaml');---<Map config={config} />Or bundle the YAML into the server with an import, which works on request:
---import { Map } from '@maplibre-yaml/astro';import { YAMLParser } from '@maplibre-yaml/core';import mapYaml from '../configs/map.yaml?raw';
const config = YAMLParser.parseMapBlock(mapYaml); // throws on an invalid document---<Map config={config} />Global configuration
Section titled “Global configuration”On a site with many maps built from code (see the map builders), set the basemap and default view once:
title: "My map site"defaultMapStyle: "https://tiles.openfreemap.org/styles/liberty"defaultCenter: [-73.985, 40.674]defaultZoom: 14Load it in a shared module, then pass it to any builder as its second argument:
import { loadGlobalMapConfig } from "@maplibre-yaml/astro";
export const globalMapConfig = await loadGlobalMapConfig("./src/config/maps.yaml");---import { Map, buildPointMapConfig } from "@maplibre-yaml/astro";import { globalMapConfig } from "../lib/map-config";
const mapConfig = buildPointMapConfig( { location: { coordinates: [-73.983, 40.676], name: "Project site" } }, globalMapConfig, // the basemap and zoom come from here);---<Map config={mapConfig} height="400px" />Values you pass to the builder win; the global file fills in the rest. The
center always comes from the geometry, so defaultCenter is used only when a
map has none (such as the fallback of
buildMapConfigFromEntry).
In astro dev the shared module runs once, so restart the server after
editing maps.yaml, or add the integration.
| Field | Type | Description |
|---|---|---|
defaultMapStyle | style URL, or { pmtiles, flavor?, lang? } | The basemap for every map: a style URL, or a Protomaps basemap from a .pmtiles file in public/ |
defaultCenter | [lng, lat] | The center when a map has no geometry |
defaultZoom | number (0–24) | The zoom when the builder isn’t given one |
title, description | string | Your site’s title and description, for your own use |
Maps in MDX pages
Section titled “Maps in MDX pages”In an .mdx page, import the component and use it like in an .astro page.
MDX needs the @astrojs/mdx integration (npx astro add mdx); Starlight
sites already have it.
---layout: ../layouts/Layout.astro---import { Map } from "@maplibre-yaml/astro";
# Recent earthquakes
<Map src="/configs/hero-map.yaml" height="400px" />The layout gives the page its <head>, with the viewport <meta> phones
need; use your site’s own layout. (A layout used by an MDX page receives the
page’s frontmatter as Astro.props.frontmatter.)
React to the map from a script
Section titled “React to the map from a script”Each component renders an <ml-map> element on the server, with no client
directive; the element upgrades in the browser once the register module
loads, and from then on every method and event works as on a plain HTML
page. A page script can reach the MapLibre map with the element’s
mapReady() and listen to its events (ml-map:load, ml-map:error,
ml-map:layer-click, …):
---import { Map } from '@maplibre-yaml/astro';---<Map src="/configs/hero-map.yaml" height="400px" /><p id="status">Loading…</p>
<script> import type { MLMap } from '@maplibre-yaml/core/components';
customElements.whenDefined('ml-map').then(async () => { const mapEl = document.querySelector<MLMap>('ml-map')!; const status = document.getElementById('status')!;
const map = await mapEl.mapReady(); // the MapLibre map, once loaded const showZoom = () => (status.textContent = `Zoom ${map.getZoom().toFixed(1)}`); showZoom(); map.on('moveend', showZoom); });</script>To change the map after it has loaded, give the component an id (it lands
on the <ml-map> in place of the generated one) and pass the element a new
document with update(): the running map changes in place, keeping its
camera, and ml-map:update-applied reports what changed. An invalid
document is refused with ml-map:update-rejected and the map kept:
---import { Map } from '@maplibre-yaml/astro';---<Map id="quakes" src="/configs/hero-map.yaml" height="400px" /><button id="recolour" type="button">Recolour the first layer</button>
<script> import type { MLMap } from '@maplibre-yaml/core/components';
customElements.whenDefined('ml-map').then(() => { const mapEl = document.getElementById('quakes') as MLMap; mapEl.addEventListener('ml-map:update-applied', ({ detail }) => console.log('changed', detail.summary.propertiesSet) ); document.getElementById('recolour')!.addEventListener('click', async () => { const doc = structuredClone(mapEl.getDocument()!); // the document the map shows doc.layers![0].paint = { ...doc.layers![0].paint, 'circle-color': '#2563eb' }; const { outcome } = await mapEl.update(doc); // "applied"; the camera stays console.log(outcome); }); });</script>getState() and setState() read and write the document’s state: values
the same way (State and parameters).
For a page whose readers pan the map themselves while its document changes,
<Map camera="host"> keeps an update from ever moving the camera. Every
event, method and property is in the
<ml-map> reference; the update rules are
under Updating the document.
Upgrading an existing project
Section titled “Upgrading an existing project”A project keeps the versions it was created with. Before 1.0, a range such as
"@maplibre-yaml/core": "^0.8.1" stays within 0.8.x, so features documented
here for a later release are missing until you upgrade. Upgrade core and the
Astro package together, then restart astro dev:
npm install @maplibre-yaml/core@latest @maplibre-yaml/astro@latestpnpm add @maplibre-yaml/core@latest @maplibre-yaml/astro@latestyarn add @maplibre-yaml/core@latest @maplibre-yaml/astro@latestWhat changed in each release is in Upgrading to 0.10 (and its older releases notes) and in the packages’ changelogs on npm.
Reference
Section titled “Reference”Map component API
Section titled “Map component API”interface MapProps { src?: string; // The YAML file's address on the site (a file in public/) config?: MapBlock; // Or a document loaded at build time. One of the two is required id?: string; // The <ml-map>'s id, for a page script to reach it (default: generated) camera?: 'document' | 'host'; // Whether a changed document may move the camera (default 'document') height?: string; // Default "400px" class?: string; style?: string; base?: string | false; // Where the document's paths start. Default: your site's base // (import.meta.env.BASE_URL); false = relative to the page quiet?: boolean; // Hide the on-map notices (default false) debug?: boolean; // Show author hints wherever the page is served (default false)}id and camera are for pages that change the map after it loads
(React to the map from a script): id
lands on the rendered <ml-map> so document.getElementById(id) reaches
its methods and events; camera="host" passes through as the element’s
camera attribute, so an update never moves the camera. Both need
@maplibre-yaml/astro 0.8 and @maplibre-yaml/core 0.10 or later.
quiet hides the “Part of this map couldn’t load” notice (a data file
that 404’d, a layer that failed) and the zero-height hint, for a finished site
whose visitors shouldn’t see them. The console still logs every problem, and a
fatal one (a document that doesn’t validate, a basemap that never loads)
still shows its error card. “Loading map…” is not a notice; hide it with
ml-map .ml-map-loading { display: none; }.
debug concerns author hints: a “did you mean”, a key nested under
config:, coordinates that look written [latitude, longitude], a value
starting with the reserved =. They show only while you develop (in astro dev, on any address, and when the site is served from your computer, as with
astro preview on localhost). debug shows them on a deployed page too,
while you check it; quiet still wins. The full rules are in
Reading what the map tells you.
FullPageMap and Scrollytelling take the same src, config, base,
quiet and debug; FullPageMap takes id and camera too.
FullPageMap component API
Section titled “FullPageMap component API”interface FullPageMapProps { src?: string; config?: MapBlock; id?: string; camera?: 'document' | 'host'; showControls?: boolean; // Zoom and reset buttons (default true) showLegend?: boolean; // A legend at legendPosition, when the document has no // legend: block (default false; needs config) legendPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; // Default 'top-right' class?: string; style?: string; base?: string | false; quiet?: boolean; debug?: boolean;}<Scrollytelling> and <Chapter> are on
Story maps in Astro.
Other exports
Section titled “Other exports”- Loaders:
loadMapConfig,loadScrollytellingConfig,loadFromGlob,loadYAML,loadGlobalMapConfig, and theYAMLLoadErrorthey throw. - Builders for maps from coordinates:
buildPointMapConfigand the others on Astro content collections. - Collection schemas from
@maplibre-yaml/astro/utils, also on Astro content collections. - Types for your own components:
MapProps,FullPageMapProps,ScrollytellingProps,ChapterProps, the frontmatter shapesLocationPoint,RegionPolygon,RouteLineandFeatureRef, and the builder options (PointMapOptions,RouteMapOptions, …).
import type { MapProps, LocationPoint } from '@maplibre-yaml/astro';The @maplibre-yaml/astro README
lists every export, and the TypeScript API reference is
generated from the source.
Supported versions
Section titled “Supported versions”| Astro | Supported | Notes |
|---|---|---|
| 7 | Yes, tested in CI | Needs the Node.js version Astro 7 asks for (22.23.4 or later for Astro 7.4.2; npm install says so if yours is older). |
| 6 | Yes, tested in CI | Build collection schemas with z from astro/zod. |
| 5 | Yes, tested in CI | |
| 4 | Yes, tested in CI | Content-layer collections need three changes, below. |
Astro 6 and 7 need @maplibre-yaml/astro 0.5 or later (earlier versions
declare astro: ^4 || ^5, so npm install refuses them). The maplibre-gl
peer range is ^4 || ^5 || ^6.4.1. CI builds the
example site on every major against the published
packages, with astro check, astro build and astro preview, drives it in
a browser, and runs a smoke test under astro dev.
Collections and zod. Astro 4 and 5 ship zod 3; Astro 6 and 7 ship zod 4,
and the two can’t be mixed. Import z from astro/zod, which works on all
four majors; the /utils schema helpers are built from the same module, so
they nest in your z.object() on every major. (z from astro:content is
the same object, but Astro 6 deprecated that export.) Since
@maplibre-yaml/astro 0.6, the getCollectionItemWith*Schema() helpers type
entry.data fully on every major.
Astro 4 only. The glob() collection loaders on these pages need three
changes:
- Turn on
experimental: { contentLayer: true }inastro.config.mjs. - Keep the content config in
src/content/config.ts; Astro 4 does not readsrc/content.config.ts. A one-lineexport { collections } from "../content.config";there serves every major. - Point the loader’s
baseoutsidesrc/content/. Astro 4’sglob()skips files there, and the folder becomes a legacy collection whose ids keep the extension (park.md).
Also on Astro 4, render pages through a layout with a <head>: a page with
no <head> drops a component’s <script> when the component has no
<style>.
Runnable example
Section titled “Runnable example”examples/astro/site
is a small site, Ring of Fire, that uses every pattern on these pages. CI
builds it and drives it in a browser on Astro 4, 5, 6 and 7. Snippets titled
with one of its file names are copied from it, and a test fails if they
drift.
| Page | Shows |
|---|---|
/ | <Map config> from loadMapConfig, <Map src>, corner slots |
/explore | <FullPageMap> with controls, showLegend, markers, a params panel, hover popups |
/story | <Scrollytelling config>: chapters move the camera, toggle layers, run every action type |
/story-runtime | <Scrollytelling src>, loaded in the browser |
/volcanoes | A Markdown collection (glob() + LocationPointSchema), one page per entry via buildMapConfigFromEntry |
/gallery | A YAML collection of whole map documents (extendSchema(getMapSchema(), …)) |
/field-notes/… | Dated posts on getCollectionItemWithLocationSchema(), one point map each via buildPointMapConfig |
/transitions | <FullPageMap> and <Map> under <ClientRouter /> view transitions |
Run it from a clone with pnpm install && pnpm build, then
pnpm --filter @maplibre-yaml/example-astro-site dev. To run the CI check for
one major: node scripts/astro-matrix.mjs --majors 7.
Troubleshooting
Section titled “Troubleshooting”Problems every kind of page shares are in Troubleshooting,
among them a map with no height,
Worker failed to load
and maps that 404 once published
(on GitHub Pages: set base).
These are specific to Astro.
Incorrect src path for public files
Section titled “Incorrect src path for public files”You see a Map file not found card, such as Couldn't load src="public/configs/map.yaml": 404 Not Found., followed by the address the
browser asked for.
src is the address on the site, not a path on disk. Leave out public/:
public/configs/map.yaml is src="/configs/map.yaml". Also check that the
file is in public/ (not src/) and that its name and extension match. On a
site with a base, the card adds a hint when the address looks like it is
missing the subpath.
A saved YAML change doesn’t show
Section titled “A saved YAML change doesn’t show”Reload the page, or restart astro dev for the loadGlobalMapConfig file;
the integration makes every case reload by itself.
Using client:only or client:load on Astro components
Section titled “Using client:only or client:load on Astro components”You see astro build fail while bundling for the browser (for example
"readFile" is not exported by "__vite-browser-external"), or the component
doesn’t render.
Map, FullPageMap and Scrollytelling are Astro components, not React,
Vue or Svelte components, so client directives don’t apply. Remove them:
<Map src="/configs/map.yaml" />.
ReferenceError: HTMLElement is not defined
Section titled “ReferenceError: HTMLElement is not defined”With @maplibre-yaml/core 0.9.0 or earlier, this happens when a page or your
own component imports @maplibre-yaml/core/register in its frontmatter (the
--- block): the frontmatter runs on the server while the page builds, where
browser elements don’t exist. Later versions don’t throw there, but the import
still does nothing in the frontmatter, because the frontmatter never runs in
the browser, so the map stays empty. Either way, import it in a <script>,
which runs only in the browser (example). The
@maplibre-yaml/astro components do this for you.
Some chunks are larger than 500 kB
Section titled “Some chunks are larger than 500 kB”Every build of a site with a map prints this yellow warning. That chunk is
the map engine (maplibre-gl plus maplibre-yaml, about 1.5 MB before
compression, loaded only by pages with a map). It is a size notice, not an
error, and the build succeeded. To quiet it, raise Vite’s limit in
astro.config.mjs: vite: { build: { chunkSizeWarningLimit: 2000 } }.
Alternatives
Section titled “Alternatives”The components with src or config, above, are the recommended path. Use
these routes only when you need something they don’t do.
Without the Astro package: <ml-map> directly
Section titled “Without the Astro package: <ml-map> directly”If you write your own component around the <ml-map> element, it has to do
what the package’s components do for you: load MapLibre’s stylesheet, point
maplibre-gl 6 at its worker, register the element, and pass your site’s base.
---import 'maplibre-gl/dist/maplibre-gl.css'; // page CSS: here, not in the scriptinterface Props { src: string; height?: string;}const { src, height = '400px' } = Astro.props;---
<ml-map src={src} base={import.meta.env.BASE_URL} style={`height: ${height}; display: block;`}></ml-map>
<script> // maplibre-gl 6 loads its worker from a separate file, which it can't find // inside a Vite bundle by itself. import { setWorkerUrl } from 'maplibre-gl'; import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url'; import '@maplibre-yaml/core/register';
setWorkerUrl(workerUrl);</script>Use it like <Map>: <MyMap src="/configs/hero-map.yaml" />. Why each line
is where it is:
- The register import goes in the
<script>, never the frontmatter (HTMLElement is not defined). If<ml-map>stays an unknown element, this script didn’t run. - The stylesheet goes in the frontmatter. There it is page CSS, which
Astro puts in the page’s
<head>in dev and build alike. Imported from the<script>,astro devinjects it with JavaScript, and a view transition drops it. - The worker lines are for maplibre-gl 6. Without them the map reports
Worker failed to load. On maplibre-gl 4 or 5, delete them (there is no
worker file to import, and the build fails if you keep them). Their order
doesn’t matter: since
@maplibre-yaml/core0.7.1,<ml-map>waits for the page’s scripts before it builds its map. basemakessrcand the paths in the YAML start at your site’s base, as the subpath section needs.
The element’s attributes, events and methods are in the
<ml-map> reference.
Build-time parsing without the Astro package
Section titled “Build-time parsing without the Astro package”To validate at build time with core alone, read the files with
import.meta.glob and pass the parsed document to <ml-map config> as JSON.
A mistake then fails the build. This is what loadMapConfig does for you.
---import 'maplibre-gl/dist/maplibre-gl.css';import { YAMLParser } from '@maplibre-yaml/core';
interface Props { configPath: string; // e.g. "/src/configs/hero-map.yaml" height?: string;}const { configPath, height = '400px' } = Astro.props;
const files = import.meta.glob<string>('/src/configs/**/*.yaml', { query: '?raw', import: 'default', eager: true,});const yaml = files[configPath];if (!yaml) throw new Error(`Config not found: ${configPath}`);
const result = YAMLParser.safeParseMapBlock(yaml);if (!result.success) { throw new Error(`Invalid config ${configPath}: ${result.errors[0].message}`);}---
<ml-map config={JSON.stringify(result.data)} base={import.meta.env.BASE_URL} style={`height: ${height}; display: block;`}></ml-map>
<script> import { setWorkerUrl } from 'maplibre-gl'; import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url'; import '@maplibre-yaml/core/register';
setWorkerUrl(workerUrl);</script>Use it as <BuildTimeMap configPath="/src/configs/hero-map.yaml" />. In
astro dev, Vite reloads the page when you save one of these files.
Inline configuration
Section titled “Inline configuration”You can write the YAML inside a page, in a template literal, and parse it with
YAMLParser.safeParseMapBlock. Avoid it: indentation inside a template
literal is easy to break, and the error (“Expected object, got null”) doesn’t
point at the cause. If you do, start the YAML right after the opening
backtick:
---import { Map } from '@maplibre-yaml/astro';import { YAMLParser } from '@maplibre-yaml/core';
const yaml = `type: mapid: simple-mapconfig: center: [-74.006, 40.7128] zoom: 12 mapStyle: "https://tiles.openfreemap.org/styles/liberty"layers: []`;const config = YAMLParser.parseMapBlock(yaml); // throws on an invalid document---<Map config={config} height="300px" />Moved sections
Section titled “Moved sections”These sections now live on other pages. The headings stay so that old links still land here.
Scrollytelling component
Section titled “Scrollytelling component”<Scrollytelling>: Story maps in Astro, with
its component API.
Chapter component
Section titled “Chapter component”<Chapter>: Story maps in Astro: Chapter component.
Adding geographic data to content collections
Section titled “Adding geographic data to content collections”Astro content collections: Adding geographic data to content collections.
GeoJSON feature references
Section titled “GeoJSON feature references”feature_ref: Astro content collections: GeoJSON feature references.
Advanced: extending schemas
Section titled “Advanced: extending schemas”extendSchema(): Astro content collections: Map documents as collection entries.