Building a Camping Map with Astro and Leaflet
Why Astro and Leaflet?
Astro is a modern static site generator that ships zero JavaScript by default. Leaflet is the leading open-source JavaScript library for interactive maps. Together they form a powerful combination for building fast, interactive map applications.
Why this matters for your career:
- Astro is one of the fastest-growing web frameworks (2023-2025)
- Leaflet is the most popular open-source mapping library (used by 80%+ of open-source map projects)
- Mapping applications are a common freelance project category
- Performance matters — Astro's zero-JS-by-default approach makes maps load fast
What Is Astro?
Astro is a static site builder designed for content-focused websites. It delivers lightning-fast performance by sending minimal JavaScript to the browser.
Key Features
| Feature | Benefit | |---------|---------| | Zero JS by default | Pages load without JavaScript unless you add it | | Island architecture | Interactive components load independently | | Multi-framework | Use React, Vue, Svelte, or vanilla JS components | | Markdown support | Write content in Markdown with frontmatter | | File-based routing | Pages are automatically routed based on file structure | | Image optimization | Automatic WebP conversion and responsive images | | SSG + SSR | Static generation or server-side rendering |
What Is Leaflet?
Leaflet is a lightweight, open-source JavaScript library for interactive maps. It is designed with simplicity, performance, and usability in mind.
Key Features
| Feature | Leaflet | Google Maps | Mapbox | |---------|---------|-------------|--------| | License | Open source (BSD) | Proprietary | Proprietary | | Cost | Free | Pay per usage | Pay per usage | | Bundle size | ~40 KB | ~200+ KB | ~150+ KB | | Tile source | Any OSM-compatible tiles | Map tiles only | Mapbox tiles only | | Offline support | Yes (with cached tiles) | No | No | | Customization | Full | Limited | Limited |
Project Setup
# Create a new Astro project
npm create astro@latest camping-map -- --template basics
cd camping-map
# Install Leaflet and its TypeScript types
npm install leaflet
npm install -D @types/leaflet
# Install leaflet.markercluster for clustering
npm install leaflet.markercluster
# Start development server
npm run dev
Adding Leaflet to Astro
Component Approach
Create a Map component that encapsulates all Leaflet logic:
---
// src/components/Map.astro
---
<div id="map" class="map-container"></div>
<script>
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
// Initialize map when component mounts
const map = L.map('map').setView([23.5, 121.0], 8);
// Add OpenStreetMap tiles
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="https://openstreetmap.org">OpenStreetMap</a>'
}).addTo(map);
// Add a campsite marker
const marker = L.marker([25.033, 121.565])
.bindPopup('<b>Taipei Campground</b><br>Nearby hiking trails available.')
.addTo(map);
</script>
<style>
.map-container {
width: 100%;
height: 600px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
</style>
Using the Map Component
---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro';
import Map from '../components/Map.astro';
---
<Layout title="Taiwan Camping Map">
<main>
<h1>Find Your Next Campsite</h1>
<p>Explore campsites across Taiwan with our interactive map.</p>
<Map client:load />
</main>
</Layout>
The client:load directive tells Astro to hydrate the Map component immediately on page load.
Client Directives
| Directive | When JS Loads | Use Case |
|-----------|--------------|----------|
| client:load | Immediately on page load | Map must be ready on first paint |
| client:idle | When browser is idle | Map below the fold |
| client:visible | When element is visible | Map in a tab or accordion |
| client:media | At a specific breakpoint | Desktop-only map features |
| client:only | Framework-specific rendering | Map that can't be server-rendered |
For a map that should be immediately visible, use client:load. For maps in tabs or below the fold, use client:visible.
Loading Markers from Supabase
---
// src/components/CampsiteMap.astro
import { supabase } from '../lib/supabase';
---
<div id="map" class="map-container"></div>
<script>
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
// Fetch campsites from Supabase API
async function loadCampsites() {
const response = await fetch('/api/campsites.json');
const campsites = await response.json();
return campsites;
}
async function initMap() {
const map = L.map('map').setView([23.5, 121.0], 8);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="https://openstreetmap.org">OSM</a>'
}).addTo(map);
const campsites = await loadCampsites();
campsites.forEach(site => {
L.marker([site.latitude, site.longitude])
.bindPopup(`<b>${site.name}</b><br>${site.description}`)
.addTo(map);
});
}
initMap();
</script>
API Route for Campsites
---
// src/pages/api/campsites.json.ts
import { supabase } from '../../lib/supabase';
export async function GET() {
const { data, error } = await supabase
.from('campsites')
.select('*')
.limit(500);
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(JSON.stringify(data), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
Performance Optimization
| Technique | Implementation |
|-----------|---------------|
| Load Leaflet only when needed | client:visible directive |
| Lazy-load markers | Fetch markers after map is initialized |
| Use CDN for Leaflet CSS/JS | Faster download from edge cache |
| Preconnect to tile server | <link rel="preconnect" href="https://tile.openstreetmap.org"> |
| Cache tile layers | Browser caches tiles automatically |
| Compress marker data | Return only needed fields from API |
| Use WebP for marker icons | 30% smaller than PNG |
| Limit initial markers | Show only nearest, load more on pan |
Astro + Leaflet Checklist
| Item | Done |
|------|------|
| npm create astro@latest and configure | ⬜ |
| Install leaflet and @types/leaflet | ⬜ |
| Create Map component with Leaflet | ⬜ |
| Add OpenStreetMap tile layer | ⬜ |
| Set up Supabase client | ⬜ |
| Create API route for campsites | ⬜ |
| Display markers on the map | ⬜ |
| Add marker clustering | ⬜ |
| Handle loading states and errors | ⬜ |
| Test on mobile devices | ⬜ |
| Deploy to Vercel or Netlify | ⬜ |
Summary
Astro and Leaflet are a powerful combination for building fast, interactive map applications. Astro provides the static site framework with zero-JS-by-default performance, while Leaflet provides the interactive mapping capabilities. Together they create a responsive, performant camping map.
Key takeaways:
- Astro sends zero JavaScript by default — only interactive components load JS
- Leaflet is open-source, lightweight (~40 KB), and free
- Use
client:loadfor visible maps,client:visiblefor below-fold maps - Create Map components in Astro and load markers from Supabase API
- Use Astro API routes (
src/pages/api/*.ts) for backend endpoints - Preconnect to tile servers for faster map loading
- Lazy-load markers and add clustering for performance
What's Next: Supabase Spatial Data
The next chapter covers setting up Supabase with PostGIS spatial data — creating tables with geography columns, spatial indexes, and geo-spatial queries.