Jamstack Architecture
Modern advocacy mapping platforms should use Jamstack (JavaScript, APIs, Markup):
| Component | Approach |
|---|---|
| Site generator | 11ty (Eleventy) |
| Map rendering | Client-side (Leaflet/MapLibre) |
| Data storage | Static JSON/GeoJSON |
| Hosting | CDN (Netlify, Cloudflare) |
| Database | None (static files) |
Benefits:
- No server-side vulnerabilities
- Extremely fast
- Easy to cache
- Low hosting costs
- High resilience
11ty Integration
Project Structure
src/
├── _data/
│ ├── facilities.json
│ ├── checkpoints.json
│ └── site.json
├── _includes/
│ ├── layouts/
│ │ └── map.njk
│ └── components/
│ └── map-embed.njk
├── resources/
│ └── mapping-tools/
│ └── index.md
├── assets/
│ ├── js/
│ │ └── map.js
│ └── css/
│ └── map.css
└── data/
├── facilities.geojson
└── 100-mile-zone.geojson
Data Files
// src/_data/facilities.json
{
"lastUpdated": "2026-03-25",
"source": "FOIA-2026-ICE-1234",
"count": 47,
"facilities": [
{
"id": "ADELANTO",
"name": "Adelanto ICE Processing Center",
"lat": 34.5794,
"lng": -117.4089,
"type": "CDF",
"capacity": 1940
}
]
}
Map Layout Template
{# src/_includes/layouts/map.njk #}
---
layout: layouts/base.njk
---
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<div class="map-page">
<div class="map-sidebar">
{{ content | safe }}
</div>
<div class="map-container">
<div id="map" aria-label="Interactive facility map"></div>
</div>
</div>
<script defer src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script defer src="/assets/js/map.js"></script>
Passing Data to JavaScript
{# In your map page #}
<script>
window.mapData = {
facilities: {{ facilities | dump | safe }},
checkpoints: {{ checkpoints | dump | safe }},
lastUpdated: "{{ facilities.lastUpdated }}"
};
</script>
Lazy Loading Maps
The Problem
Map libraries are heavy. Loading them on every page hurts performance.
IntersectionObserver Solution
// Only load map when scrolled into view
const mapContainer = document.getElementById('map-container');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMap();
observer.disconnect();
}
});
}, {
rootMargin: '100px' // Start loading 100px before visible
});
observer.observe(mapContainer);
async function loadMap() {
// Show loading state
mapContainer.innerHTML = '<div class="loading">Loading map...</div>';
// Load Leaflet dynamically
await loadScript('https://unpkg.com/leaflet@1.9.4/dist/leaflet.js');
await loadCSS('https://unpkg.com/leaflet@1.9.4/dist/leaflet.css');
// Initialize map
initializeMap();
}
function loadScript(src) {
return new Promise((resolve) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
document.head.appendChild(script);
});
}
Automated Data Pipeline
GitHub Actions Workflow
# .github/workflows/update-data.yml
name: Update Map Data
on:
schedule:
- cron: '0 6 * * *' # Daily at 6 AM UTC
workflow_dispatch: # Manual trigger
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Fetch latest data
run: node scripts/fetch-data.js
env:
DATA_API_KEY: ${{ secrets.DATA_API_KEY }}
- name: Validate GeoJSON
run: npx geojsonhint src/data/*.geojson
- name: Build site
run: npm run build
- name: Deploy
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./_site
Data Fetch Script
// scripts/fetch-data.js
const fs = require('fs');
const path = require('path');
async function fetchFacilityData() {
// Fetch from your data source
const response = await fetch(process.env.FACILITY_API_URL);
const data = await response.json();
// Convert to GeoJSON
const geojson = {
type: 'FeatureCollection',
features: data.facilities.map(f => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [f.longitude, f.latitude]
},
properties: {
id: f.id,
name: f.name,
type: f.facility_type,
capacity: f.capacity,
population: f.current_population
}
}))
};
// Write to data directory
fs.writeFileSync(
path.join(__dirname, '../src/data/facilities.geojson'),
JSON.stringify(geojson, null, 2)
);
console.log(`Updated ${geojson.features.length} facilities`);
}
fetchFacilityData().catch(console.error);
CSV to GeoJSON Conversion
// scripts/csv-to-geojson.js
const csv = require('csv-parser');
const fs = require('fs');
const features = [];
fs.createReadStream('data/checkpoints.csv')
.pipe(csv())
.on('data', (row) => {
features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [
parseFloat(row.longitude),
parseFloat(row.latitude)
]
},
properties: {
name: row.name,
type: row.type,
highway: row.highway
}
});
})
.on('end', () => {
const geojson = {
type: 'FeatureCollection',
features
};
fs.writeFileSync(
'src/data/checkpoints.geojson',
JSON.stringify(geojson, null, 2)
);
});
Performance Optimization
Bundle Analysis
// eleventy.config.js
module.exports = function(eleventyConfig) {
// Pass through map assets
eleventyConfig.addPassthroughCopy('src/assets/js');
eleventyConfig.addPassthroughCopy('src/data');
// Minify JS in production
if (process.env.NODE_ENV === 'production') {
eleventyConfig.addTransform('minify-js', async (content, outputPath) => {
if (outputPath.endsWith('.js')) {
const { minify } = await import('terser');
const result = await minify(content);
return result.code;
}
return content;
});
}
};
GeoJSON Optimization
// Simplify GeoJSON at build time
const simplify = require('@turf/simplify');
const simplified = simplify(geojson, {
tolerance: 0.001,
highQuality: true
});
// Reduce coordinate precision
const truncated = JSON.stringify(simplified, (key, value) => {
if (typeof value === 'number') {
return Math.round(value * 100000) / 100000; // 5 decimal places
}
return value;
});
Caching Headers
# netlify.toml
[[headers]]
for = "/data/*.geojson"
[headers.values]
Cache-Control = "public, max-age=3600, stale-while-revalidate=86400"
[[headers]]
for = "/assets/js/*.js"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
CDN Deployment
Netlify Configuration
# netlify.toml
[build]
command = "npm run build"
publish = "_site"
[build.environment]
NODE_VERSION = "20"
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
Cloudflare Pages
# .cloudflare/config.yaml
build:
command: npm run build
directory: _site
env:
NODE_VERSION: "20"
Geographic Distribution
CDN edges ensure fast loading worldwide:
| Region | Typical Latency |
|---|---|
| US (origin) | < 50ms |
| Mexico | < 100ms |
| Central America | < 150ms |
| Europe | < 100ms |
Monitoring
Uptime Monitoring
// Simple health check endpoint
// netlify/functions/health.js
exports.handler = async () => {
return {
statusCode: 200,
body: JSON.stringify({
status: 'healthy',
timestamp: new Date().toISOString()
})
};
};
Error Tracking
// Client-side error reporting (privacy-preserving)
window.onerror = function(msg, url, line) {
// Send to self-hosted error tracker
fetch('/api/errors', {
method: 'POST',
body: JSON.stringify({
message: msg,
// No user data
page: location.pathname,
timestamp: Date.now()
})
});
};
Testing
GeoJSON Validation
// tests/geojson.test.js
const geojsonhint = require('@mapbox/geojsonhint');
const fs = require('fs');
const path = require('path');
describe('GeoJSON files', () => {
const dataDir = path.join(__dirname, '../src/data');
const files = fs.readdirSync(dataDir).filter(f => f.endsWith('.geojson'));
files.forEach(file => {
test(`${file} is valid GeoJSON`, () => {
const content = fs.readFileSync(path.join(dataDir, file), 'utf8');
const errors = geojsonhint.hint(content);
expect(errors).toHaveLength(0);
});
});
});
Map Rendering
// tests/map.test.js
const puppeteer = require('puppeteer');
describe('Map page', () => {
let browser, page;
beforeAll(async () => {
browser = await puppeteer.launch();
page = await browser.newPage();
});
afterAll(() => browser.close());
test('map container renders', async () => {
await page.goto('http://localhost:8080/map/');
const map = await page.$('#map');
expect(map).not.toBeNull();
});
test('facilities load', async () => {
await page.waitForSelector('.leaflet-marker-icon');
const markers = await page.$$('.leaflet-marker-icon');
expect(markers.length).toBeGreaterThan(0);
});
});
Deployment Checklist
Pre-Deploy
- [ ] GeoJSON validates
- [ ] All tests pass
- [ ] Map loads on mobile
- [ ] Accessibility audit passes
- [ ] No third-party trackers
- [ ] HTTPS configured
- [ ] CSP headers set
Post-Deploy
- [ ] Site loads from CDN
- [ ] Map data is current
- [ ] Health check passes
- [ ] Error monitoring active
- [ ] Cache headers correct
Related Resources
- Libraries Comparison - Framework selection
- Privacy Features - Security configuration
- QGIS Workflows - Data preparation
- Technical SEO - 11ty optimization