# Property listings

An estate agency site. The main page lists properties for sale; each
listing has its own page with photos, address, price, and details.
Listings can be marked as sold without deleting them — they vanish
from the public list but the URL still works for anyone who bookmarked
it.

This is the pattern for any site that's essentially "a list of things
with detail pages": properties, products, jobs, courses, events,
team members. The mechanics are always the same:

1. Each item is a sub-page with its data in metadata
2. The main page fetches `cross_page_metadata.json` and renders cards from the
   metadata of all items
3. A `status` field on the metadata controls which items appear in
   the public list
4. Bulk creation is just a Python loop

## The script

```python
import requests
import time

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

slug = "smith-estate-agency"

# 1. Create the project
requests.post(f"{API}/projects", headers=H, json={
    "slug": slug,
    "context": "An estate agency in Springfield with residential listings.",
})
time.sleep(2)

# 2. Brand it — navy and gold, traditional estate agency feel
requests.put(f"{API}/projects/{slug}/assets/tailwind-config.js", headers=H, json={
    "content": (
        'tailwind.config = { theme: { extend: { '
        'colors: { brand: "#1e3a5f", accent: "#e2a84b" }, '
        'fontFamily: { serif: ["Lora", "serif"] } '
        '} } }'
    ),
})

requests.put(f"{API}/projects/{slug}/assets/styles.css", headers=H, json={
    "content": "@import url('https://fonts.googleapis.com/css2?family=Lora:wght@400;500;600;700&display=swap');",
})

# 3. Bulk-create the listings. In real life this might come from a
# spreadsheet, a database, or the user pasting them in.
properties = [
    {
        "slug": "123-oak-street",
        "title": "3 bed semi-detached",
        "address": "123 Oak Street, Springfield",
        "price": 250000,
        "bedrooms": 3,
        "bathrooms": 2,
        "sqm": 110,
        "features": ["South-facing garden", "Off-street parking", "Period features"],
    },
    {
        "slug": "456-elm-avenue",
        "title": "4 bed detached with garden",
        "address": "456 Elm Avenue, Springfield",
        "price": 375000,
        "bedrooms": 4,
        "bathrooms": 3,
        "sqm": 160,
        "features": ["Large garden", "Garage", "Recently extended"],
    },
    {
        "slug": "789-pine-road",
        "title": "2 bed flat, city centre",
        "address": "789 Pine Road, Springfield",
        "price": 145000,
        "bedrooms": 2,
        "bathrooms": 1,
        "sqm": 65,
        "features": ["Top floor", "Lift", "Walking distance to station"],
    },
]

for p in properties:
    # Create the page
    requests.post(f"{API}/projects/{slug}/pages", headers=H, json={
        "slug": p["slug"],
        "notes": p["title"],
    })
    time.sleep(0.3)

    # Set its metadata. The "status" field controls visibility.
    requests.put(
        f"{API}/projects/{slug}/pages/{p['slug']}/metadata",
        headers=H,
        json={
            "address": p["address"],
            "price": p["price"],
            "bedrooms": p["bedrooms"],
            "bathrooms": p["bathrooms"],
            "sqm": p["sqm"],
            "features": p["features"],
            "status": "active",
        },
    )
    time.sleep(0.3)

# 4. The main page reads cross_page_metadata.json and renders cards for active listings.
main_html = """<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <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">
</head>
<body class="font-serif bg-stone-50 text-stone-900"
      x-data="{ listings: [] }"
      x-init="
        fetch('/cross_page_metadata.json').then(r => r.json())
          .then(d => listings = d.pages.filter(p => p.metadata.status === 'active'))
      ">

  <header class="bg-brand text-white py-16 px-6 text-center">
    <h1 class="text-5xl font-bold mb-2">Smith Estate Agency</h1>
    <p class="text-blue-200 text-lg">Property specialists in Springfield since 1987</p>
  </header>

  <main class="max-w-5xl mx-auto px-6 py-16">
    <h2 class="text-3xl font-bold text-brand mb-8">Properties for sale</h2>

    <div class="grid md:grid-cols-2 gap-6">
      <template x-for="listing in listings" :key="listing.slug">
        <a :href="listing.url"
           class="block bg-white rounded-lg shadow hover:shadow-xl transition-all duration-200 hover:-translate-y-1 overflow-hidden">
          <div class="p-6">
            <div class="flex justify-between items-start mb-3">
              <h3 class="text-xl font-bold text-brand" x-text="listing.notes"></h3>
              <span class="text-xl font-bold text-accent whitespace-nowrap"
                    x-text="'£' + listing.metadata.price.toLocaleString()"></span>
            </div>
            <p class="text-stone-500 mb-4" x-text="listing.metadata.address"></p>
            <div class="flex gap-4 text-sm text-stone-600 mb-4">
              <span x-text="listing.metadata.bedrooms + ' beds'"></span>
              <span>&middot;</span>
              <span x-text="listing.metadata.bathrooms + ' baths'"></span>
              <span>&middot;</span>
              <span x-text="listing.metadata.sqm + ' m&sup2;'"></span>
            </div>
            <div class="flex flex-wrap gap-2">
              <template x-for="feature in listing.metadata.features.slice(0, 3)">
                <span class="bg-blue-50 text-brand text-xs px-2 py-1 rounded-full font-sans"
                      x-text="feature"></span>
              </template>
            </div>
          </div>
        </a>
      </template>
    </div>

    <template x-if="listings.length === 0">
      <p class="text-center text-stone-500 py-16">No active listings right now.</p>
    </template>
  </main>

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

  <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},
)

# 5. The detail page template — each listing reads its own metadata.
# The __PAGE_SLUG__ placeholder is replaced per-listing in the upload loop.
listing_html = """<!DOCTYPE html>
<html lang="en">
<head>
  <base href="/__PAGE_SLUG__/">
  <meta charset="UTF-8">
  <title>Property Details — Smith Estate Agency</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="/assets/tailwind-config.js"></script>
  <link rel="stylesheet" href="/assets/styles.css">
</head>
<body class="font-serif bg-stone-50 text-stone-900"
      x-data="{ p: null }"
      x-init="
        const slug = window.location.pathname.replace(/^\\/|\\/$/g, '');
        fetch('/cross_page_metadata.json').then(r => r.json())
          .then(d => p = d.pages.find(pg => pg.slug === slug))
      ">

  <header class="bg-brand text-white py-6 px-6">
    <a href="/" class="text-2xl font-bold hover:text-blue-200">Smith Estate Agency</a>
  </header>

  <template x-if="p && p.metadata">
    <main class="max-w-3xl mx-auto px-6 py-12">
      <a href="/" class="text-brand hover:underline text-sm mb-6 block">&larr; Back to listings</a>

      <h1 class="text-4xl font-bold text-brand mb-2" x-text="p.notes"></h1>
      <p class="text-stone-500 mb-4" x-text="p.metadata.address"></p>
      <div class="text-3xl font-bold text-accent mb-8"
           x-text="'£' + p.metadata.price.toLocaleString()"></div>

      <div class="grid grid-cols-3 gap-4 mb-8">
        <div class="bg-white rounded-lg shadow p-4 text-center">
          <p class="text-2xl font-bold text-brand" x-text="p.metadata.bedrooms"></p>
          <p class="text-stone-500 text-sm">Bedrooms</p>
        </div>
        <div class="bg-white rounded-lg shadow p-4 text-center">
          <p class="text-2xl font-bold text-brand" x-text="p.metadata.bathrooms"></p>
          <p class="text-stone-500 text-sm">Bathrooms</p>
        </div>
        <div class="bg-white rounded-lg shadow p-4 text-center">
          <p class="text-2xl font-bold text-brand" x-text="p.metadata.sqm + ' m&sup2;'"></p>
          <p class="text-stone-500 text-sm">Floor area</p>
        </div>
      </div>

      <section class="bg-white rounded-lg shadow p-8">
        <h2 class="font-bold text-brand mb-3">Features</h2>
        <div class="flex flex-wrap gap-2">
          <template x-for="f in p.metadata.features">
            <span class="bg-blue-50 text-brand text-sm px-3 py-1 rounded-full" x-text="f"></span>
          </template>
        </div>
      </section>
    </main>
  </template>

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

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

# Upload the template to each listing page, replacing the base href
# placeholder with the actual page slug.
for p in properties:
    requests.put(
        f"{API}/projects/{slug}/pages/{p['slug']}/files/index.html",
        headers=H,
        json={"content": listing_html.replace("__PAGE_SLUG__", p["slug"])},
    )
    time.sleep(0.2)

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

## Marking a listing as sold

The metadata's `status` flag controls visibility. Flip it to `sold`
and the listing disappears from the main page (because the filter is
`p.metadata.status === 'active'`), but the URL still works for anyone
who bookmarked it.

```python
import requests

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

# Read current metadata, modify, write back (PUT replaces the whole object)
page = requests.get(
    f"{API}/projects/smith-estate-agency/pages/123-oak-street",
    headers=H,
).json()
metadata = page["metadata"]

metadata["status"] = "sold"
metadata["sold_date"] = "2026-04-15"

requests.put(
    f"{API}/projects/smith-estate-agency/pages/123-oak-street/metadata",
    headers=H,
    json=metadata,
)
```

You could also extend the listing page HTML to show a big "SOLD"
banner when `status === 'sold'`, so people who land on the URL still
see something useful.

## Variations

- **Add hero photos** for each listing — see `photo-gallery.md`.
- **Show listings on a map** instead of a grid — see `map-with-pins.md`.
- **Filter by bedrooms/price** with Alpine.js — add `<select>` controls
  bound to the listings array.
- **Add a contact form per listing** — see `contact-form.md`.
