Skip to content

Collection Integration

This guide shows how to embed maps in Astro collection item posts using content collection schemas and map builder utilities. Whether you’re creating a travel blog, restaurant reviews, or hiking guides, maplibre-yaml provides simple patterns for adding geographic context.

The collection item integration approach uses three key concepts:

  1. Content Collection Schemas - Pre-built Zod schemas that add location/region/route fields to your collection item frontmatter
  2. Map Builder Utilities - Functions that convert simple geographic data into full map configurations
  3. Global Configuration - Site-wide map style settings that individual posts can inherit

Choose the schema that matches your content type:

src/content.config.ts
import { defineCollection } from "astro:content";
import { glob } from "astro/loaders";
import { getCollectionItemWithLocationSchema } from "@maplibre-yaml/astro/utils";
export const collections = {
posts: defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/collections/posts" }),
schema: getCollectionItemWithLocationSchema(),
}),
};

A glob() loader works on Astro 5, 6 and 7 as written. On Astro 4 it needs experimental: { contentLayer: true }, the config file at src/content/config.ts, and a base outside src/content/ (see Supported versions). Loader entries have an id (the file name without its extension) and render with render() from astro:content; the legacy type: "content" API (post.slug, post.render()) is gone in Astro 6.

src/collections/posts/my-trip.md
---
title: "My Trip to Paris"
pubDate: 2024-03-15
location:
coordinates: [2.3522, 48.8566]
name: "Paris, France"
zoom: 12
---
We arrived in Paris on a crisp spring morning...
src/pages/posts/[...slug].astro
---
import { getCollection, render } from 'astro:content';
import { Map, buildPointMapConfig } from '@maplibre-yaml/astro';
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
const posts = await getCollection('posts');
return posts.map(post => ({
params: { slug: post.id },
props: { post }
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
// Build map config if location exists
const mapConfig = post.data.location
? buildPointMapConfig({
location: post.data.location,
mapStyle: 'https://demotiles.maplibre.org/style.json'
})
: null;
---
<BaseLayout title={post.data.title}>
<article>
<h1>{post.data.title}</h1>
{mapConfig && (
<Map config={mapConfig} height="300px" />
)}
<Content />
</article>
</BaseLayout>

Best for: Restaurant reviews, place spotlights, single-destination content.

import { getCollectionItemWithLocationSchema } from "@maplibre-yaml/astro/utils";
const schema = getCollectionItemWithLocationSchema();

Frontmatter example:

---
title: "Best Coffee in Brooklyn"
pubDate: 2024-03-15
location:
coordinates: [-73.9857, 40.6892]
name: "Devoción Coffee"
zoom: 16
markerColor: "#8B4513"
---

Best for: Travel itineraries, city guides, comparison posts.

import { getCollectionItemWithLocationsSchema } from "@maplibre-yaml/astro/utils";
const schema = getCollectionItemWithLocationsSchema();

Frontmatter example:

---
title: "10 Days in Japan"
pubDate: 2024-03-15
locations:
- coordinates: [139.6917, 35.6895]
name: "Tokyo"
description: "Days 1-4"
- coordinates: [135.7681, 35.0116]
name: "Kyoto"
description: "Days 5-7"
- coordinates: [135.5023, 34.6937]
name: "Osaka"
description: "Days 8-10"
---

Best for: Neighborhood guides, property coverage areas, territorial content.

import { getCollectionItemWithRegionSchema } from "@maplibre-yaml/astro/utils";
const schema = getCollectionItemWithRegionSchema();

Frontmatter example:

---
title: "Guide to Brooklyn Heights"
pubDate: 2024-03-15
region:
name: "Brooklyn Heights"
coordinates:
- [
[-73.998, 40.698],
[-73.985, 40.698],
[-73.985, 40.692],
[-73.998, 40.692],
[-73.998, 40.698],
]
fillColor: "#3388ff"
fillOpacity: 0.3
---

Best for: Hiking guides, road trips, cycling routes.

import { getCollectionItemWithRouteSchema } from "@maplibre-yaml/astro/utils";
const schema = getCollectionItemWithRouteSchema();

Frontmatter example:

---
title: "Pacific Coast Highway Road Trip"
pubDate: 2024-03-15
route:
name: "SF to LA via PCH"
coordinates:
- [-122.4194, 37.7749] # San Francisco
- [-122.0322, 36.9741] # Santa Cruz
- [-121.8947, 36.6002] # Monterey
- [-120.6596, 35.2828] # San Luis Obispo
- [-119.6982, 34.4208] # Santa Barbara
- [-118.2437, 34.0522] # Los Angeles
color: "#e74c3c"
width: 4
---

For collection items with mixed content types, use the comprehensive schema:

import { z } from "astro/zod";
import { getCollectionItemWithGeoSchema } from "@maplibre-yaml/astro/utils";
const schema = getCollectionItemWithGeoSchema({
// Add your custom fields
author: z.string(),
category: z.enum(["travel", "hiking", "food", "city-guide"]),
tags: z.array(z.string()).default([]),
});

Creates a map centered on a single location with a marker:

import { buildPointMapConfig } from "@maplibre-yaml/astro";
const config = buildPointMapConfig({
location: {
coordinates: [-74.006, 40.7128],
name: "New York City",
description: "The Big Apple",
},
mapStyle: "https://demotiles.maplibre.org/style.json",
zoom: 12, // optional, defaults to location.zoom or 12
interactive: true, // optional, defaults to true
});

Creates a map showing multiple locations with automatic bounds fitting:

import { buildMultiPointMapConfig } from "@maplibre-yaml/astro";
const config = buildMultiPointMapConfig({
locations: [
{ coordinates: [-74.006, 40.7128], name: "NYC" },
{ coordinates: [-118.2437, 34.0522], name: "LA" },
{ coordinates: [-87.6298, 41.8781], name: "Chicago" },
],
mapStyle: "https://demotiles.maplibre.org/style.json",
padding: 50, // pixels of padding around bounds
});

Creates a map displaying a polygon region:

import { buildPolygonMapConfig } from "@maplibre-yaml/astro";
const config = buildPolygonMapConfig({
region: {
coordinates: [
[
[-74.0, 40.7],
[-73.9, 40.7],
[-73.9, 40.8],
[-74.0, 40.8],
[-74.0, 40.7], // Close the polygon
],
],
name: "Manhattan",
fillColor: "#ff6b6b",
fillOpacity: 0.4,
},
mapStyle: "https://demotiles.maplibre.org/style.json",
});

Creates a map showing a line/route with start/end markers:

import { buildRouteMapConfig } from "@maplibre-yaml/astro";
const config = buildRouteMapConfig({
route: {
coordinates: [
[-122.4194, 37.7749],
[-121.8947, 36.6002],
[-118.2437, 34.0522],
],
name: "California Road Trip",
color: "#e74c3c",
width: 4,
},
mapStyle: "https://demotiles.maplibre.org/style.json",
});

Instead of specifying mapStyle on every map, create a shared configuration. For a validated, YAML-backed version that every builder accepts as its second argument, see loadGlobalMapConfig in Astro Integration → Global Configuration. A plain object works too:

src/lib/map-config.ts
export const globalMapConfig = {
mapStyle: "https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY",
// Add any other defaults you want to share
};

Then use it in your pages:

---
import { Map, buildPointMapConfig } from '@maplibre-yaml/astro';
import { globalMapConfig } from '../lib/map-config';
const { location } = Astro.props.post.data;
const mapConfig = location
? buildPointMapConfig({
location,
mapStyle: globalMapConfig.mapStyle
})
: null;
---
{mapConfig && <Map config={mapConfig} height="300px" />}

Here’s a full example of a collection item post template with conditional map rendering:

src/pages/posts/[...slug].astro
---
import { getCollection, render } from 'astro:content';
import {
Map,
buildPointMapConfig,
buildMultiPointMapConfig,
buildPolygonMapConfig,
buildRouteMapConfig
} from '@maplibre-yaml/astro';
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
const posts = await getCollection('posts');
return posts.map(post => ({
params: { slug: post.id },
props: { post }
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
const { location, locations, region, route } = post.data;
const MAP_STYLE = 'https://demotiles.maplibre.org/style.json';
// Build appropriate map config based on available data
function getMapConfig() {
if (location) {
return buildPointMapConfig({ location, mapStyle: MAP_STYLE });
}
if (locations && locations.length > 0) {
return buildMultiPointMapConfig({ locations, mapStyle: MAP_STYLE });
}
if (region) {
return buildPolygonMapConfig({ region, mapStyle: MAP_STYLE });
}
if (route) {
return buildRouteMapConfig({ route, mapStyle: MAP_STYLE });
}
return null;
}
const mapConfig = getMapConfig();
---
<BaseLayout title={post.data.title}>
<article class="post">
<header>
<h1>{post.data.title}</h1>
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toLocaleDateString()}
</time>
</header>
{mapConfig && (
<div class="post-map">
<Map config={mapConfig} height="350px" />
</div>
)}
<div class="post-content">
<Content />
</div>
</article>
</BaseLayout>
<style>
.post-map {
margin: 2rem 0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
</style>

Calculate the center point of multiple coordinates:

import { calculateCenter } from "@maplibre-yaml/astro";
const center = calculateCenter([
[-74.006, 40.7128],
[-73.935, 40.73],
[-73.867, 40.752],
]);
// Returns approximately [-73.936, 40.732]

Calculate the bounding box of coordinates:

import { calculateBounds } from "@maplibre-yaml/astro";
const bounds = calculateBounds([
[-74.006, 40.7128],
[-73.867, 40.752],
]);
// Returns [[-74.006, 40.7128], [-73.867, 40.752]]

For blog posts, simpler maps are often better. A single marker showing “where this happened” is more effective than a complex multi-layer visualization.

  • City overview: zoom 10-12
  • Neighborhood: zoom 13-15
  • Specific building/location: zoom 16-18

For purely illustrative maps, disable interactivity to reduce visual noise:

const config = buildPointMapConfig({
location,
mapStyle: MAP_STYLE,
interactive: false,
});

If your map isn’t immediately visible, consider lazy loading:

{mapConfig && (
<div class="post-map" data-map-config={JSON.stringify(mapConfig)}>
<!-- Placeholder, hydrate with JS when visible -->
</div>
)}
  • Learn about layers for more complex visualizations
  • Explore live data for real-time updates
  • See examples for more patterns