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: '&copy; <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: '&copy; <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:load for visible maps, client:visible for 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.

Member Exclusive Free Tutorial

This chapter is free exclusive content for registered members! Please login or register to unlock immediately.

Login / Register Now