Emergency Hotline: Call 1-844-363-1423 (United We Dream Hotline)
ICE Encounter

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