# Main page with a map and pins

A main page that shows all sub-pages on an interactive map. Each pin
links to the page's detail view. Hover over a card and the map pin
highlights; click a pin and the card scrolls into view.

This is a killer pattern for anything geographic:

- Property listings (this example)
- Restaurants in a guide / food crawl
- Tour stops
- Multi-location businesses
- Event venues
- Job listings filtered by city

It uses **Leaflet** (free, open-source, no API key) for the map and
**OpenStreetMap** tiles. The pins come from each sub-page's
`metadata.coordinates` field — set those once when you create the
listings, and the map builds itself.

## Required metadata shape

Each sub-page needs `metadata.coordinates` like this:

```json
{
  "address": "123 Oak Street, Springfield",
  "price": 250000,
  "coordinates": { "lat": 39.78, "lng": -89.65 }
}
```

You set this when you create or update the page metadata. See
`property-listings.md` for the bulk-create pattern.

## The script

This builds on the property listings example. It assumes you've
already created a project with several sub-pages, each with
`coordinates` in their metadata.

```python
import requests

API = "https://api.uat-beam.page"
TOKEN = "<your-token>"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}

slug = "smith-estate-agency"

main_html = """<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Smith Estate Agency — Springfield Property</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="/assets/tailwind-config.js"></script>
  <link rel="stylesheet" href="/assets/styles.css">

  <!-- Leaflet for the map -->
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>

  <style>
    .leaflet-popup-content-wrapper { border-radius: 8px; }
    .pin-active .leaflet-marker-icon { filter: hue-rotate(180deg) brightness(1.2); }
    html { scroll-behavior: smooth; }
  </style>
</head>
<body class="font-serif bg-stone-50 text-stone-900"
      x-data="map"
      x-init="init()">

  <header class="bg-brand text-white py-12 px-6 text-center">
    <h1 class="text-4xl font-bold mb-2">Smith Estate Agency</h1>
    <p class="text-blue-200">Springfield property &middot; click a pin or a card</p>
  </header>

  <!-- The map -->
  <div id="map" class="h-96 w-full"></div>

  <!-- The cards, side-by-side with the map on desktop -->
  <main class="max-w-5xl mx-auto px-6 py-12">
    <h2 class="text-2xl font-bold text-brand mb-6">All listings</h2>

    <div class="grid md:grid-cols-2 gap-6">
      <template x-for="listing in listings" :key="listing.slug">
        <a :href="listing.url"
           :id="'card-' + listing.slug"
           @mouseenter="highlight(listing.slug)"
           @mouseleave="unhighlight()"
           class="block bg-white rounded-lg shadow hover:shadow-xl transition-all duration-200 overflow-hidden">
          <div class="p-6">
            <div class="flex justify-between items-start mb-2">
              <h3 class="text-lg font-bold text-brand" x-text="listing.notes"></h3>
              <span class="text-lg font-bold text-accent whitespace-nowrap"
                    x-text="'£' + listing.metadata.price.toLocaleString()"></span>
            </div>
            <p class="text-stone-500 text-sm mb-2" x-text="listing.metadata.address"></p>
            <div class="text-xs text-stone-500">
              <span x-text="listing.metadata.bedrooms + ' beds'"></span> &middot;
              <span x-text="listing.metadata.bathrooms + ' baths'"></span> &middot;
              <span x-text="listing.metadata.sqm + ' m&sup2;'"></span>
            </div>
          </div>
        </a>
      </template>
    </div>
  </main>

  <footer class="text-center py-8 text-stone-400 text-sm">
    Smith Estate Agency &middot; Springfield
  </footer>

  <script>
    document.addEventListener('alpine:init', () => {
      Alpine.data('map', () => ({
        listings: [],
        leafletMap: null,
        markers: {},

        async init() {
          // 1. Fetch all listings from cross_page_metadata.json
          const data = await fetch('/cross_page_metadata.json').then(r => r.json());
          this.listings = data.pages.filter(p =>
            p.metadata && p.metadata.coordinates && p.metadata.status === 'active'
          );

          // 2. Build the map
          this.leafletMap = L.map('map');
          L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '&copy; OpenStreetMap contributors',
            maxZoom: 19,
          }).addTo(this.leafletMap);

          // 3. Add a pin for each listing
          const bounds = [];
          this.listings.forEach(listing => {
            const c = listing.metadata.coordinates;
            const latlng = [c.lat, c.lng];
            bounds.push(latlng);

            const marker = L.marker(latlng).addTo(this.leafletMap);
            marker.bindPopup(`
              <strong>${listing.notes}</strong><br>
              £${listing.metadata.price.toLocaleString()}<br>
              <a href="${listing.url}" class="text-blue-600 underline">View details</a>
            `);

            // Click pin → scroll the matching card into view
            marker.on('click', () => {
              const card = document.getElementById('card-' + listing.slug);
              if (card) card.scrollIntoView({ block: 'center' });
            });

            this.markers[listing.slug] = marker;
          });

          // 4. Auto-fit the map to all pins
          if (bounds.length > 0) {
            this.leafletMap.fitBounds(bounds, { padding: [40, 40] });
          } else {
            this.leafletMap.setView([39.78, -89.65], 13);
          }
        },

        highlight(slug) {
          const marker = this.markers[slug];
          if (marker) marker.openPopup();
        },

        unhighlight() {
          this.leafletMap.closePopup();
        },
      }));
    });
  </script>

  <script src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js" defer></script>
</body>
</html>
"""

requests.put(
    f"{API}/projects/{slug}/pages/_root/files/index.html",
    headers=H,
    json={"content": main_html},
)

print(f"Done. Map is live at https://{slug}.uat-beam.page")
```

## How it works

1. **`/cross_page_metadata.json`** is auto-generated by the platform. It contains all
   pages with their metadata, so all the coordinates are in one place.
2. **Leaflet** loads from a CDN — no API key, no signup, no cost.
3. **The Alpine `map` component** fetches `/cross_page_metadata.json` on init, builds
   the map, and drops a pin for each listing with coordinates.
4. **`fitBounds`** auto-zooms the map to include every pin.
5. **Hover/click sync:** mousing over a card opens the corresponding
   pin's popup; clicking a pin scrolls its card into view. Both
   directions of interaction without any backend.

## Variations

- **Custom pin colours by category** — use `L.divIcon` with inline
  HTML/CSS, colour by `metadata.type` or `metadata.price` bracket.
- **Filter visible pins** — add a `<select>` for "max price" and
  hide markers + cards whose metadata doesn't match.
- **Cluster pins for dense data** — load `leaflet.markercluster` from
  CDN; one extra script tag.
- **Use Mapbox or MapTiler tiles** if you want a fancier look — they
  have free tiers but require an API key. OpenStreetMap is free
  and unlimited.

## Use this with any geographic site

Restaurant guide? Sub-page per restaurant, `coordinates` in metadata,
same map code. Walking tour? Sub-page per stop. Conference venues?
Sub-page per location. The HTML never changes — just create more
sub-pages with coordinates and the map redraws on the next visit.
