Live demo — MapLibre GL JS. Fake data for a fake version of Moonlight Garden Supply. Everything shown here (map, sidebar, popups, code) is editable by the person who builds it.

← Back to overview
How this map is built — annotated code

The whole thing is about 200 lines of JavaScript. Here's the core logic with comments.

1. Initialize the map

// MapLibre GL JS — free, open-source, no API key for dev
// In production: use a free tile key from Stadia Maps or Maptiler
const map = new maplibregl.Map({
  container: 'map',    // the div id
  style: {
    version: 8,
    sources: {
      carto: {
        type: 'raster',
        // CartoDB Positron — clean, warm, free with attribution
        tiles: ['https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png'],
        tileSize: 256,
        attribution: '© OpenStreetMap contributors © CARTO'
      }
    },
    layers: [{ id: 'base', type: 'raster', source: 'carto' }]
  },
  center: [-111.5, 39.5],  // Utah center [lng, lat]
  zoom: 6.5
});

2. Location data lives in a JSON object (or a separate file you fetch)

const installations = [
  {
    id: 'cache-valley',
    name: 'Cache Valley Aquaponics',
    city: 'Logan, UT',
    lat: 41.735, lng: -111.834,
    type: 'Aquaponics',
    detail: 'Tilapia + lettuce in a converted dairy barn.',
    year: 2024,
    color: '#3d6b44'   // each type gets a color
  },
  // ... more locations
];

3. Add a marker for each location

installations.forEach(site => {
  // Custom colored circle marker
  const el = document.createElement('div');
  el.className = 'custom-marker';
  el.style.background = site.color;

  // Popup content — plain HTML, fully styleable
  const popup = new maplibregl.Popup({ offset: 20 })
    .setHTML(`
      <div class="popup-body">
        <h3>${site.name}</h3>
        <p class="popup-meta">${site.type} · ${site.city}</p>
        <a class="popup-view-btn" href="#detail">View installation →</a>
      </div>
    `);

  new maplibregl.Marker({ element: el })
    .setLngLat([site.lng, site.lat])
    .setPopup(popup)
    .addTo(map);
});

4. Filtering — hide/show markers by type

// Each marker element gets a data-type attribute
el.dataset.type = site.type;

// Filter button logic
filterButtons.forEach(btn => {
  btn.addEventListener('click', () => {
    const selected = btn.dataset.type;
    markers.forEach(({ el, type }) => {
      el.style.display =
        (selected === 'all' || type === selected) ? 'block' : 'none';
    });
  });
});

The landing pages for each installation are separate HTML files — regular web pages with whatever layout and content makes sense. The map just links to them.

What to learn + in what order

You don't need to master all of this before starting — but this is the rough order things build on each other.

  1. HTML structure — what a web page is made of. MDN HTML. A few hours.
  2. CSS basics — colors, layout, fonts. MDN CSS. A few hours.
  3. JavaScript fundamentals — variables, functions, events. MDN JS Guide. A few days.
  4. JSON format — how location data is structured. Takes 20 minutes.
  5. MapLibre GL JS — the mapping library itself. maplibre.org/docs. The examples section is excellent.
  6. Git + Cloudflare Pages — version control and free deployment. An afternoon.

Alternative: Claude Code builds the whole thing. You learn it by asking it to explain what it wrote. The site you're reading was built that way.