Skip to content

Astro Integration

Astro is a natural fit for maplibre-yaml - both embrace configuration-driven approaches. This guide covers integrating maps into Astro sites, from simple page embeds to reusable components.

Get a map in your Astro site in 5 minutes.

  1. Install dependencies

    Terminal window
    pnpm add @maplibre-yaml/core maplibre-gl
  2. Create a map configuration file

    Create public/configs/hero-map.yaml:

    type: map
    id: hero-map
    config:
    center: [-74.006, 40.7128]
    zoom: 12
    mapStyle: "https://demotiles.maplibre.org/style.json"
    layers:
    - id: marker
    type: circle
    source:
    type: geojson
    data:
    type: FeatureCollection
    features:
    - type: Feature
    geometry:
    type: Point
    coordinates: [-74.006, 40.7128]
    properties:
    name: "New York City"
    paint:
    circle-radius: 12
    circle-color: "#3b82f6"
    circle-stroke-width: 2
    circle-stroke-color: "#ffffff"
  3. Create a Map component

    Create src/components/Map.astro:

    ---
    interface Props {
    src: string;
    height?: string;
    class?: string;
    }
    const { src, height = '400px', class: className } = Astro.props;
    ---
    <ml-map
    src={src}
    style={`height: ${height}; display: block;`}
    class={className}
    />
    <script>
    import '@maplibre-yaml/core/register';
    import 'maplibre-gl/dist/maplibre-gl.css';
    </script>
    <style>
    ml-map {
    width: 100%;
    border-radius: 8px;
    overflow: hidden;
    }
    </style>
  4. Use in a page

    src/pages/index.astro
    ---
    import Layout from '../layouts/Layout.astro';
    import Map from '../components/Map.astro';
    ---
    <Layout title="Home">
    <h1>Welcome</h1>
    <Map src="/configs/hero-map.yaml" height="500px" />
    </Layout>

For Astro projects, we recommend using the dedicated @maplibre-yaml/astro package instead of the core web components directly. This package provides:

  • Native Astro Components - Designed specifically for Astro’s architecture
  • Build-Time Loading - Load and validate YAML at build time for better performance
  • Runtime Loading - Or load from /public for dynamic configs
  • TypeScript Support - Full type safety with MapBlock types
  • Error Handling - Beautiful error messages with validation details
  • Content Collections - Schema helpers for Astro Content Collections
Terminal window
pnpm add @maplibre-yaml/astro @maplibre-yaml/core maplibre-gl

The Map component is the simplest way to add a map to your Astro site:

Load YAML from /public directory:

src/pages/index.astro
---
import { Map } from '@maplibre-yaml/astro';
---
<Map
src="/configs/map.yaml"
height="500px"
class="my-map"
/>

Place your YAML file in /public/configs/map.yaml:

type: map
id: my-map
config:
center: [-74.006, 40.7128]
zoom: 12
mapStyle: "https://demotiles.maplibre.org/style.json"
layers:
- id: points
type: circle
source:
type: geojson
url: "https://example.com/data.geojson"
paint:
circle-radius: 6
circle-color: "#007cbf"
interface MapProps {
// Data source (one required)
src?: string; // Path to YAML file in /public
config?: MapBlock; // Pre-loaded configuration
// Styling
height?: string; // Container height (default: "400px")
class?: string; // CSS class name
style?: string; // Inline styles
}

Examples:

<!-- Minimal -->
<Map src="/maps/simple.yaml" />
<!-- With custom height -->
<Map src="/maps/dashboard.yaml" height="100vh" />
<!-- With styling -->
<Map
src="/maps/styled.yaml"
height="500px"
class="shadow-lg rounded-lg"
style="border: 1px solid #ccc;"
/>
<!-- Build-time loaded -->
---
import { Map, loadMapConfig } from '@maplibre-yaml/astro';
const earthquakes = await loadMapConfig('./src/maps/earthquakes.yaml');
---
<Map config={earthquakes} height="600px" />

The package provides utilities for loading YAML configurations:

Load and validate a map configuration from a file:

---
import { loadMapConfig, Map } from '@maplibre-yaml/astro';
try {
const config = await loadMapConfig('./src/configs/my-map.yaml');
// config is fully typed as MapBlock
} catch (error) {
console.error('Failed to load map config:', error);
}
---
<Map config={config} />

Load multiple configurations at once:

---
import { loadFromGlob, Map } from '@maplibre-yaml/astro';
import { YAMLParser } from '@maplibre-yaml/core';
const configFiles = import.meta.glob('./src/configs/*.yaml', { as: 'raw' });
const maps = await loadFromGlob(
configFiles,
(yaml) => YAMLParser.safeParseMapBlock(yaml)
);
---
<div class="map-gallery">
{maps.map(({ path, config }) => (
<div>
<h2>{config.id}</h2>
<Map config={config} height="400px" />
</div>
))}
</div>

Use Astro Content Collections for managing map configurations:

1. Define collection schema:

src/content/config.ts
import { defineCollection } from 'astro:content';
import { getMapSchema } from '@maplibre-yaml/astro';
export const collections = {
maps: defineCollection({
type: 'data',
schema: getMapSchema()
})
};

2. Add YAML files:

src/content/maps/earthquake-map.yaml
type: map
id: earthquake-map
config:
center: [-120, 35]
zoom: 6
mapStyle: "https://demotiles.maplibre.org/style.json"
layers:
- id: earthquakes
type: circle
source:
type: geojson
url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson"
paint:
circle-radius: 8
circle-color: "#ff4444"

3. Query and use in pages:

---
import { getCollection } from 'astro:content';
import { Map } from '@maplibre-yaml/astro';
const maps = await getCollection('maps');
const earthquakeMap = maps.find(m => m.id === 'earthquake-map');
---
<Map config={earthquakeMap.data} height="600px" />

The Map component displays user-friendly errors when configurations fail to load or validate:

<!-- Invalid YAML or network error will show error message -->
<Map src="/configs/broken.yaml" />

Error display includes:

  • YAML Syntax Errors: Shows parsing error with details
  • Validation Errors: Lists all validation failures with paths
  • Network Errors: Shows fetch failure messages

You can also handle errors programmatically with build-time loading:

---
import { loadMapConfig, YAMLLoadError } from '@maplibre-yaml/astro';
let config;
try {
config = await loadMapConfig('./src/configs/map.yaml');
} catch (error) {
if (error instanceof YAMLLoadError) {
console.error('Validation errors:');
error.errors.forEach(e => {
console.error(` ${e.path}: ${e.message}`);
});
}
throw error; // Fail build on invalid config
}
---
<Map config={config} />

Add custom metadata to your map configurations:

src/content/config.ts
import { defineCollection } from 'astro:content';
import { extendSchema, getMapSchema } from '@maplibre-yaml/astro';
import { z } from 'zod';
export const collections = {
maps: defineCollection({
type: 'data',
schema: extendSchema(getMapSchema(), {
author: z.string(),
publishDate: z.date(),
tags: z.array(z.string()),
featured: z.boolean().default(false)
})
})
};
src/content/maps/featured-map.yaml
author: "Jane Doe"
publishDate: 2024-01-15
tags: ["earthquakes", "geoscience"]
featured: true
type: map
id: featured-map
config:
center: [0, 0]
zoom: 2
mapStyle: "https://demotiles.maplibre.org/style.json"
---
import { getCollection } from 'astro:content';
const featuredMaps = await getCollection('maps', ({ data }) => data.featured);
---
{featuredMaps.map(entry => (
<div>
<h2>{entry.data.id}</h2>
<p>By {entry.data.author} on {entry.data.publishDate}</p>
<Map config={entry.data} />
</div>
))}

The FullPageMap component creates a full-viewport map with built-in controls and an optional legend. It’s perfect for landing pages, dashboards, or dedicated map views.

  • Full Viewport: Fixed positioning that fills the entire viewport (100vw × 100vh)
  • Built-in Controls: Zoom in/out and reset view buttons
  • Optional Legend: Auto-generated legend with configurable positioning
  • Same Loading Options: Supports both src (runtime) and config (build-time) loading
  • Responsive Design: Optimized layouts for mobile and desktop
---
import { FullPageMap } from '@maplibre-yaml/astro';
---
<FullPageMap src="/configs/world-map.yaml" />
interface FullPageMapProps {
// Map source (one required)
src?: string; // Path to YAML file in /public
config?: MapBlock; // Pre-loaded configuration object
// Control options
showControls?: boolean; // Show zoom/reset controls (default: true)
showLegend?: boolean; // Show auto-generated legend (default: false)
legendPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
// Legend position (default: 'top-right')
// Styling (optional - component is fixed viewport by default)
class?: string; // Additional CSS classes
style?: string; // Additional inline styles
}
---
import { FullPageMap } from '@maplibre-yaml/astro';
---
<!DOCTYPE html>
<html>
<head>
<title>Interactive Map Dashboard</title>
</head>
<body style="margin: 0; padding: 0;">
<FullPageMap
src="/configs/dashboard-map.yaml"
showControls
showLegend
legendPosition="bottom-left"
/>
</body>
</html>
<FullPageMap
src="/configs/presentation-map.yaml"
showControls={false}
/>
<FullPageMap
src="/configs/branded-map.yaml"
showLegend
legendPosition="top-right"
style="z-index: 100;"
class="dashboard-map"
/>

Build-time loaded with complete configuration

Section titled “Build-time loaded with complete configuration”
---
import { FullPageMap, loadMapConfig } from '@maplibre-yaml/astro';
const mapConfig = await loadMapConfig('./src/configs/geospatial-data.yaml');
---
<FullPageMap
config={mapConfig}
showControls
showLegend
legendPosition="bottom-right"
/>

When showLegend is enabled, the component automatically generates a legend based on your map’s layers:

  • Fill layers: Colored squares with fill color and opacity
  • Line layers: Horizontal lines with stroke color
  • Circle layers: Circular symbols with fill color

The legend automatically:

  • Groups layers by source and type
  • Extracts paint properties for visualization
  • Filters out MapLibre internal layers
  • Displays human-readable layer names

The built-in controls provide:

  • Zoom In (+): Increases zoom level by 1
  • Zoom Out (−): Decreases zoom level by 1
  • Reset View (↻): Returns to initial center, zoom, bearing, and pitch

Controls are accessible with ARIA labels and keyboard navigation.

The Chapter component displays individual narrative sections within scrollytelling experiences. It’s typically used internally by the Scrollytelling component but can be used independently for custom implementations.

  • Flexible Content: Title, HTML description, images, and videos
  • Multiple Alignments: left, right, center, or full-width layouts
  • Theme Support: Light and dark themes with CSS custom properties
  • Hidden Mode: Map-only chapters without visible content
  • Active States: Visual feedback based on scroll position
  • Accessibility: ARIA labels, semantic HTML, keyboard navigation
  • Responsive Design: Mobile-optimized with multiple breakpoints
  • Reduced Motion: Respects user’s motion preferences
interface ChapterProps {
id: string; // Unique chapter identifier (required)
title: string; // Chapter title (required)
description?: string; // HTML description content
image?: string; // Image URL (absolute or relative)
video?: string; // Video URL (absolute or relative)
alignment?: 'left' | 'right' | 'center' | 'full'; // Content position (default: 'center')
hidden?: boolean; // Hide content for map-only chapters
isActive?: boolean; // Active state (controlled by parent)
theme?: 'light' | 'dark'; // Visual theme (default: 'light')
}
---
import { Chapter } from '@maplibre-yaml/astro';
---
<Chapter
id="intro"
title="Introduction"
description="<p>Welcome to our story about climate data.</p>"
alignment="center"
/>
<Chapter
id="hero"
title="The Journey Begins"
description="<p>Exploring the landscape of our data.</p>"
image="/images/hero-landscape.jpg"
alignment="left"
theme="dark"
/>
<Chapter
id="demo"
title="How It Works"
description="<p>Watch this short demonstration.</p>"
video="/videos/tutorial.mp4"
alignment="center"
/>
<Chapter
id="transition"
title="Transition View"
hidden
/>
<Chapter
id="finale"
title="Conclusion"
description="<p>Thank you for following this journey through the data.</p>"
alignment="full"
theme="dark"
/>
  • left: Content positioned on the left side (good for right-aligned maps)
  • right: Content positioned on the right side (good for left-aligned maps)
  • center: Content centered (default, works with most layouts)
  • full: Full-width content, centered text (impactful for conclusions)

Light theme (default):

  • White/translucent background (rgba(255, 255, 255, 0.95))
  • Dark text (#1f2937)
  • Blue links (#3b82f6)

Dark theme:

  • Dark gray/translucent background (rgba(31, 41, 55, 0.95))
  • Light text (#f9fafb)
  • Light blue links (#60a5fa)

The isActive prop controls visual emphasis:

  • Active chapters: Full opacity (1.0), normal scale
  • Inactive chapters: Reduced opacity (0.4), slightly scaled down (0.98)

This is typically managed by the parent Scrollytelling component based on scroll position.

  • Semantic <section> with role="region"
  • Unique aria-labelledby for each chapter
  • Lazy-loaded images with alt text
  • Videos with controls and fallback text
  • High contrast mode support
  • Reduced motion support

The Scrollytelling component creates immersive narrative experiences where the map transitions between different views as users scroll through story chapters. Perfect for data journalism, interactive reports, and guided data tours.

  • Scroll-Driven Navigation: Map automatically transitions as you scroll through chapters
  • Smooth Camera Transitions: Choose from flyTo, easeTo, or jumpTo animations
  • Layer Control: Show/hide map layers per chapter for progressive disclosure
  • Chapter Actions: Execute custom MapLibre actions on chapter enter/exit
  • Optional Markers: Visual navigation dots for quick chapter access
  • Dual Loading: Build-time or runtime YAML loading
  • Debug Mode: Visual indicators to help during development
  • Accessible: Full keyboard navigation and screen reader support
interface ScrollytellingProps {
src?: string; // Path to YAML config file (in public directory)
config?: ScrollytellingBlock; // Pre-loaded scrollytelling configuration
class?: string; // Additional CSS class for container
debug?: boolean; // Debug mode - shows chapter boundaries
}
---
import { Scrollytelling } from '@maplibre-yaml/astro';
---
<Scrollytelling src="/stories/earthquake-history.yaml" />

A complete scrollytelling story includes base map configuration, data sources, layers, and chapters:

version: "1.0"
id: earthquake-story
theme: dark # 'light' or 'dark'
showMarkers: true # Show chapter navigation dots
markerColor: "#3FB1CE" # Custom marker color
# Base map configuration
config:
style: "https://demotiles.maplibre.org/style.json"
center: [-118.2437, 34.0522]
zoom: 9
pitch: 0
bearing: 0
# Data sources
sources:
earthquakes:
type: geojson
data: "/data/earthquakes.geojson"
# Map layers
layers:
- id: earthquake-circles
type: circle
source: earthquakes
paint:
circle-radius: 8
circle-color: "#ff0000"
# Story chapters
chapters:
- id: intro
title: "Los Angeles Earthquakes"
description: "<p>Exploring seismic activity in Southern California.</p>"
center: [-118.2437, 34.0522]
zoom: 9
alignment: center
- id: detail
title: "Major Fault Lines"
description: "<p>The San Andreas Fault runs through this region.</p>"
image: "/images/fault-lines.jpg"
center: [-118.0, 34.0]
zoom: 11
pitch: 45
bearing: 30
animation: flyTo
speed: 0.8
alignment: left
layers:
show: [earthquake-circles]
hide: []
- id: conclusion
title: "Preparing for the Future"
description: "<p>Understanding earthquake patterns helps us prepare.</p>"
center: [-118.2437, 34.0522]
zoom: 9
alignment: full
footer: "<p>Data source: USGS</p><p>&copy; 2024</p>"
---
import { Scrollytelling } from '@maplibre-yaml/astro';
---
<Scrollytelling src="/stories/city-tour.yaml" />
---
import { Scrollytelling, loadScrollytellingConfig } from '@maplibre-yaml/astro';
const story = await loadScrollytellingConfig('./src/stories/development.yaml');
---
<Scrollytelling config={story} debug />
<Scrollytelling
src="/stories/brand-story.yaml"
class="branded-scrollytelling"
/>
<style>
.branded-scrollytelling {
font-family: 'Custom Font', sans-serif;
}
</style>

Each chapter supports extensive configuration options:

Position & Camera:

  • center: [longitude, latitude] coordinates
  • zoom: Zoom level (0-22)
  • pitch: Camera tilt (0-60 degrees)
  • bearing: Camera rotation (0-360 degrees)

Animation:

  • animation: “flyTo” (smooth arc), “easeTo” (direct), or “jumpTo” (instant)
  • speed: Animation speed (0.1-2.0, default 0.6)
  • curve: Fly curve intensity (0.1-2.0, default 1.0)

Content:

  • title: Chapter heading (required)
  • description: HTML content
  • image: Image URL for visual context
  • video: Video URL with inline player
  • alignment: “left”, “right”, “center”, or “full”
  • hidden: Hide content for map-only transitions

Interactivity:

  • layers.show: Array of layer IDs to make visible
  • layers.hide: Array of layer IDs to hide
  • onChapterEnter: Actions to execute when entering chapter
  • onChapterExit: Actions to execute when leaving chapter

Execute MapLibre actions when users enter or exit chapters:

chapters:
- id: filtered-view
title: "Major Earthquakes Only"
center: [-118.2, 34.0]
zoom: 10
onChapterEnter:
- action: setFilter
layer: earthquake-circles
filter: [">=", "magnitude", 5.0]
- action: setPaintProperty
layer: earthquake-circles
property: circle-color
value: "#ff0000"
onChapterExit:
- action: setFilter
layer: earthquake-circles
filter: null

Available Actions:

  • setFilter: Filter features in a layer
  • setPaintProperty: Change layer paint properties
  • setLayoutProperty: Change layer layout properties

When showMarkers: true, navigation dots appear on the right side of the viewport:

  • Click markers to jump to specific chapters
  • Active marker is highlighted
  • Customizable color via markerColor
  • Accessible with keyboard navigation
  • Automatically positioned and scaled

Light theme (default):

  • Light chapter cards on map background
  • Suitable for bright map styles
  • Light footer background

Dark theme:

  • Dark semi-transparent chapter cards
  • Works well with satellite/dark base maps
  • Dark footer background

Enable debug prop to see:

  • Red dashed outlines around inactive chapters
  • Green outlines around the active chapter
  • Helpful for fine-tuning scroll trigger points
<Scrollytelling src="/stories/test.yaml" debug />

The component uses IntersectionObserver with these settings:

  • Trigger zone: Middle 20% of viewport (40% margin top/bottom)
  • Activates when chapter enters the trigger zone
  • Smooth map transitions between chapters
  • Progressive chapter content fade in/out
  • Semantic HTML with proper ARIA labels
  • Keyboard navigation for chapter markers
  • Screen reader friendly chapter titles
  • Respects reduced motion preferences
  • High contrast mode support
  1. Use build-time loading for better initial page load
  2. Optimize images: Compress chapter images and use appropriate sizes
  3. Limit chapters: 5-10 chapters for optimal scrolling experience
  4. Simplify transitions: Use jumpTo for instant transitions on slower devices
  5. Lazy load media: Images and videos load only when needed
src/pages/earthquake-story.astro
---
import { Scrollytelling, loadScrollytellingConfig } from '@maplibre-yaml/astro';
const story = await loadScrollytellingConfig('./src/stories/earthquakes.yaml');
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Earthquake History Story</title>
</head>
<body>
<Scrollytelling config={story} />
</body>
</html>
<style is:global>
body {
margin: 0;
padding: 0;
font-family: system-ui, sans-serif;
}
</style>
Section titled “Pattern 1: External YAML with src Attribute (Recommended)”

The simplest pattern - let the web component handle everything. Place YAML files in the /public directory and reference them with the src attribute.

File structure:

public/
├── configs/
│ ├── hero-map.yaml
│ ├── contact-map.yaml
│ └── locations-map.yaml
src/
├── components/
│ └── Map.astro
└── pages/
└── index.astro

Map.astro:

---
interface Props {
src: string;
height?: string;
}
const { src, height = '400px' } = Astro.props;
---
<ml-map src={src} style={`height: ${height}; display: block;`} />
<script>
import '@maplibre-yaml/core/register';
import 'maplibre-gl/dist/maplibre-gl.css';
</script>

Usage:

<Map src="/configs/hero-map.yaml" />
<Map src="/configs/contact-map.yaml" height="300px" />

Benefits:

  • ✅ Simplest possible implementation
  • ✅ No build-time parsing needed
  • ✅ YAML files are easy to edit
  • ✅ Clear separation of configuration and presentation
  • ✅ Works with any static file server
  • ✅ No template literal whitespace issues

Pattern 2: External YAML with Build-Time Parsing

Section titled “Pattern 2: External YAML with Build-Time Parsing”

For projects that want build-time validation, you can parse YAML during the build:

File structure:

src/
├── components/
│ └── Map.astro
├── configs/
│ ├── hero-map.yaml
│ ├── contact-map.yaml
│ └── locations-map.yaml
└── pages/
└── index.astro

Map.astro:

---
import { YAMLParser } from '@maplibre-yaml/core';
interface Props {
configPath: string;
height?: string;
}
const { configPath, height = '400px' } = Astro.props;
// Import all YAML files at build time
const yamlFiles = import.meta.glob('/src/configs/**/*.yaml', {
as: 'raw',
eager: true
});
const yamlContent = yamlFiles[configPath];
if (!yamlContent) {
const available = Object.keys(yamlFiles).join(', ');
throw new Error(`Config not found: ${configPath}. Available: ${available}`);
}
const result = YAMLParser.safeParseMapBlock(yamlContent);
if (!result.success) {
console.error('YAML parse errors:', result.errors);
throw new Error(`Invalid config ${configPath}: ${result.errors[0].message}`);
}
const configJson = JSON.stringify(result.data);
---
<ml-map style={`height: ${height}; display: block;`} config={configJson} />
<script>
import '@maplibre-yaml/core/register';
import 'maplibre-gl/dist/maplibre-gl.css';
</script>

Usage:

<Map configPath="/src/configs/hero-map.yaml" />
<Map configPath="/src/configs/contact-map.yaml" height="300px" />

Benefits:

  • ✅ Build-time parsing catches errors early
  • ✅ YAML validation during development
  • ✅ Configuration bundled with the build

Tradeoffs:

  • ⚠️ More complex component code
  • ⚠️ Requires rebuild to see YAML changes
Section titled “Pattern 3: Inline Configuration (Not Recommended)”

For simple, one-off maps, you can inline configuration. However, this approach has significant drawbacks and is generally not recommended.

InlineMap.astro:

---
import { YAMLParser } from '@maplibre-yaml/core';
interface Props {
yaml: string;
height?: string;
}
const { yaml, height = '400px' } = Astro.props;
const result = YAMLParser.safeParseMapBlock(yaml);
if (!result.success) {
console.error('Parse errors:', result.errors);
}
const configJson = result.success ? JSON.stringify(result.data) : null;
const error = result.success ? null : result.errors;
---
{error ? (
<div class="map-error">
<strong>Configuration Error</strong>
<pre>{JSON.stringify(error, null, 2)}</pre>
</div>
) : (
<ml-map style={`height: ${height}; display: block;`} config={configJson} />
)}
<script>
import '@maplibre-yaml/core/register';
import 'maplibre-gl/dist/maplibre-gl.css';
</script>
<style>
.map-error {
padding: 20px;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
color: #dc2626;
}
.map-error pre {
margin-top: 10px;
font-size: 12px;
overflow-x: auto;
}
</style>

Usage in page:

---
import InlineMap from '../components/InlineMap.astro';
// ⚠️ MUST start content immediately after backtick
const simpleMapYaml = `type: map
id: simple-map
config:
center: [-74.006, 40.7128]
zoom: 12
mapStyle: "https://demotiles.maplibre.org/style.json"
layers: []`;
---
<InlineMap yaml={simpleMapYaml} height="300px" />

In MDX files (like documentation), use the src attribute pattern for the cleanest approach:

guide.mdx:

---
title: Map Guide
---
import Map from "../../components/Map.astro";
## Live Earthquake Data
Here's a map showing recent earthquakes:
<Map src="/configs/earthquake-map.yaml" height="400px" />
The data refreshes from the USGS API.

public/configs/earthquake-map.yaml:

type: map
id: earthquake-demo
config:
center: [-120, 37]
zoom: 4
mapStyle: "https://demotiles.maplibre.org/style.json"
layers:
- id: quakes
type: circle
source:
type: geojson
url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_week.geojson"
paint:
circle-radius: 6
circle-color: "#ef4444"

A full-featured component with events and controls using the src pattern:

InteractiveMap.astro:

---
interface Props {
src: string;
height?: string;
showControls?: boolean;
onLayerClick?: string; // JavaScript function name to call
}
const {
src,
height = '400px',
showControls = false,
onLayerClick
} = Astro.props;
const mapId = `map-${Math.random().toString(36).slice(2, 9)}`;
---
<div class="interactive-map" id={mapId}>
{showControls && (
<div class="map-controls">
<button class="control-btn" data-action="zoom-in" title="Zoom In">+</button>
<button class="control-btn" data-action="zoom-out" title="Zoom Out"></button>
<button class="control-btn" data-action="reset" title="Reset View"></button>
</div>
)}
<ml-map
src={src}
style={`height: ${height}; display: block;`}
data-click-handler={onLayerClick}
/>
<div class="map-status"></div>
</div>
<script>
import '@maplibre-yaml/core/register';
import 'maplibre-gl/dist/maplibre-gl.css';
// Initialize all interactive maps
document.querySelectorAll('.interactive-map').forEach(container => {
const mapEl = container.querySelector('ml-map');
const statusEl = container.querySelector('.map-status');
const controls = container.querySelector('.map-controls');
if (!mapEl) return;
let initialCenter: [number, number] | null = null;
let initialZoom: number | null = null;
// Status updates
mapEl.addEventListener('ml-map:load', () => {
const map = mapEl.getMap();
initialCenter = map.getCenter().toArray() as [number, number];
initialZoom = map.getZoom();
statusEl.textContent = 'Map ready';
statusEl.className = 'map-status ready';
});
mapEl.addEventListener('ml-map:layer-loading', (e: CustomEvent) => {
statusEl.textContent = `Loading ${e.detail.layerId}...`;
statusEl.className = 'map-status loading';
});
mapEl.addEventListener('ml-map:layer-loaded', (e: CustomEvent) => {
statusEl.textContent = `${e.detail.featureCount} features loaded`;
statusEl.className = 'map-status ready';
});
mapEl.addEventListener('ml-map:error', (e: CustomEvent) => {
statusEl.textContent = `Error: ${e.detail.error.message}`;
statusEl.className = 'map-status error';
});
// Custom click handler
const clickHandler = mapEl.dataset.clickHandler;
if (clickHandler && typeof window[clickHandler] === 'function') {
mapEl.addEventListener('ml-map:layer-click', (e: CustomEvent) => {
window[clickHandler](e.detail);
});
}
// Control buttons
if (controls) {
controls.addEventListener('click', (e) => {
const btn = (e.target as HTMLElement).closest('[data-action]');
if (!btn) return;
const map = mapEl.getMap();
if (!map) return;
const action = btn.dataset.action;
switch (action) {
case 'zoom-in':
map.zoomIn();
break;
case 'zoom-out':
map.zoomOut();
break;
case 'reset':
if (initialCenter && initialZoom !== null) {
map.flyTo({ center: initialCenter, zoom: initialZoom });
}
break;
}
});
}
});
</script>
<style>
.interactive-map {
position: relative;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e5e7eb;
}
.map-controls {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
display: flex;
flex-direction: column;
gap: 4px;
}
.control-btn {
width: 32px;
height: 32px;
border: none;
background: white;
border-radius: 4px;
font-size: 18px;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: center;
}
.control-btn:hover {
background: #f3f4f6;
}
.map-status {
position: absolute;
bottom: 10px;
left: 10px;
padding: 4px 8px;
background: rgba(255,255,255,0.9);
border-radius: 4px;
font-size: 12px;
z-index: 10;
}
.map-status.loading { color: #d97706; }
.map-status.ready { color: #059669; }
.map-status.error { color: #dc2626; }
</style>

Content Collections Integration (Optional/Advanced)

Section titled “Content Collections Integration (Optional/Advanced)”

For sites with many maps that need build-time validation, you can use Astro Content Collections. Note that this is more complex than the recommended src attribute pattern:

src/content/config.ts:

import { defineCollection, z } from "astro:content";
const maps = defineCollection({
type: "data",
schema: z.object({
type: z.literal("map"),
id: z.string(),
config: z.object({
center: z.tuple([z.number(), z.number()]),
zoom: z.number(),
mapStyle: z.string(),
pitch: z.number().optional(),
bearing: z.number().optional(),
}),
layers: z.array(z.any()),
}),
});
export const collections = { maps };

src/content/maps/headquarters.yaml:

type: map
id: headquarters
config:
center: [-122.4194, 37.7749]
zoom: 15
mapStyle: "https://demotiles.maplibre.org/style.json"
layers:
- id: office
type: circle
source:
type: geojson
data:
type: Feature
geometry:
type: Point
coordinates: [-122.4194, 37.7749]
properties:
name: "Headquarters"
paint:
circle-radius: 12
circle-color: "#3b82f6"

CollectionMap.astro:

---
import { getEntry } from 'astro:content';
interface Props {
slug: string;
height?: string;
}
const { slug, height = '400px' } = Astro.props;
const mapEntry = await getEntry('maps', slug);
if (!mapEntry) {
throw new Error(`Map not found: ${slug}`);
}
// Content collection already validates against schema
const configJson = JSON.stringify(mapEntry.data);
---
<ml-map style={`height: ${height}; display: block;`} config={configJson} />
<script>
import '@maplibre-yaml/core/register';
import 'maplibre-gl/dist/maplibre-gl.css';
</script>

Usage:

<CollectionMap slug="headquarters" height="500px" />

”Expected object, got null” or Similar Schema Errors

Section titled “”Expected object, got null” or Similar Schema Errors”

Cause: YAML indentation was destroyed by template literal processing, or the file couldn’t be loaded.

Solutions:

  1. Use the src attribute pattern (recommended)

    <Map src="/configs/my-map.yaml" />

    Place your YAML file in public/configs/my-map.yaml

  2. Check browser console for loading errors

    The web component will log errors if it can’t fetch or parse the YAML file.

  3. Verify file path is correct

    Files in /public are served from the root, so /public/configs/map.yaml becomes src="/configs/map.yaml"

Symptoms: Nothing visible, or map is 0px tall.

Cause: CSS height not set properly.

Solution:

<ml-map src="/configs/map.yaml" style="height: 400px; display: block;" />

Both height and display: block are required.

Symptoms: Error: Config not found or network 404 error

Cause: Incorrect path or file not in public directory.

Solutions:

  1. Ensure file is in /public directory (not /src)
  2. Use root-relative paths: src="/configs/map.yaml" for /public/configs/map.yaml
  3. Check browser network tab for 404 errors
  4. Verify file name and extension are correct

Symptoms: Changes to YAML files in /public don’t appear immediately.

Cause: Browser caching or dev server needs refresh.

Workaround:

  • Hard refresh the browser (Cmd+Shift+R or Ctrl+Shift+R)
  • Or restart the dev server
  • Note: Files in /public are served statically, so they update immediately without rebuild

Solution: Add to src/env.d.ts:

/// <reference types="astro/client" />
declare module "*.yaml" {
const content: string;
export default content;
}

Symptoms: <ml-map> renders as unknown element.

Cause: Registration script didn’t run.

Solution: Ensure script is in component:

<script>
import '@maplibre-yaml/core/register';
import 'maplibre-gl/dist/maplibre-gl.css';
</script>

The script must be inside the component, not in the frontmatter.

Use src Attribute (Recommended)

Place YAML files in /public/configs/ and use <ml-map src="/configs/map.yaml">. Simplest and most maintainable approach.

Set Explicit Dimensions

Always set height and display: block on <ml-map> elements.

Keep Config Separate

Avoid inline YAML in components. External files are easier to edit and maintain.

Use Build-Time Validation (Optional)

For critical applications, parse YAML at build time (Pattern 2) to catch errors early.