# Placeholders — the substitution contract

`template.html` and `index.html` are pure placeholder templates: every property-specific value is a `{{ token }}` token. There is no exemplar copy left to accidentally inherit. After rendering, `grep "{{" output/brochure.html` (and `output/web/index.html`) MUST return **zero matches**. Any remaining `{{ x }}` is a substitution bug.

This file is the substitution contract — the exhaustive list of every token used across both templates, what fills it, what format it takes, and what voice the copy tokens need. Style and composition guidance lives in `copy.md` (and is referenced inline from each `<!-- REPLACE: ... -->` comment in the templates). The two files are complementary: this one says **what** each token is; `copy.md` says **how** the copy should read.

Worked examples below quote the validated Sparrows Farm reference brochure at `/Users/neo/estate-agents/muvin/properties/henham-road-debden-green-hamperden-end-cb11-3lz-949931/output/brochure.html` — that brochure is the canonical "what good looks like." Read it whenever a placeholder's intent is unclear.

## How to use this file

1. Open the seller brief at `<property_dir>/seller-brief.md` and `<property_dir>/property.json` side-by-side.
2. Walk the categories below top-to-bottom — common tokens first (identity, brand, agent, listing, specs), then chapter-by-chapter editorial copy.
3. For each token, write the substitution into a single dict / map keyed by token name. The substitution step at the end of `property-brochure` reads the map and rewrites both files in one pass.
4. Run the **completion check**: `grep -c "{{" output/brochure.html output/web/index.html`. Both files must return `0`. If any token is unsubstituted, it is a defect — the brochure is not shippable until every `{{ x }}` is gone.

## Category 1 — Property identity

These tokens are used everywhere — cover, opener, folio strips, footer, alt text, image filenames.

| Token | Source | Format / length | Notes |
|---|---|---|---|
| `{{ property_name }}` | `property.json -> property_name`, or `address.line_1` if name is null | Title Case, ~10-30 chars | Distinct house name preferred ("Sparrows Farm", "Agincourt House"); falls back to the street ("12 Henham Road") when the property has no separate name. CSS renders it ALL-CAPS on the cover — source is Title Case. |
| `{{ property_slug }}` | `property.json -> source.url` (slug part) | lowercase-kebab | Used in every image filename pattern: `images/{{ property_slug }}-NN.webp`. Examples: `sparrows-farm`, `agincourt-house`. |
| `{{ address_line_1 }}` | `property.json -> address.line_1` | Title Case | Street or first address line ("Henham Road", "Llanddewi Rhydderch"). |
| `{{ locality }}` | `property.json -> address.locality` | Title Case | Village / town district. |
| `{{ town }}` | `property.json -> address.town` | Title Case | Nearest town. |
| `{{ county }}` | `property.json -> address.county` | Title Case | County. |
| `{{ postcode }}` | `property.json -> address.postcode` | UK format, with `&nbsp;` between out-code and in-code on the cover and in the brochure ("NP7&nbsp;9TS"). Plain in flat strings ("NP7 9TS"). | |
| `{{ address_subtitle }}` | Composed from the above | `<line_1> <em>·</em> <locality> <em>·</em> <county> <em>·</em> <postcode>`, single line | Cover subtitle. The `<em>·</em>` separators are gold-coloured via CSS. |
| `{{ folio_strip }}` | Composed | `{{ property_name }} · {{ locality }}` | Repeats on every interior page's left folio slot. |
| `{{ folio_mid_NN }}` | Per-page chapter title | "Contents" / "Of place & pedigree" / "A home at ease" / etc. | Used in `<em>` of folio-mid. Match the chapter the page belongs to (intermissions inherit their parent chapter's mid-label). |

Worked example (Sparrows Farm):
- `property_name` = "Sparrows Farm"
- `property_slug` = "sparrows-farm"
- `address_subtitle` = `Henham Road <em>·</em> Hamperden End <em>·</em> Essex <em>·</em> CB11&nbsp;3LZ`
- `folio_strip` = "Sparrows Farm · Hamperden End"

## Category 2 — Brand pack

From the brand pack at `<brand_dir>/{description.md, DESIGN.md}` and `property.json -> agent`.

| Token | Source | Format | Notes |
|---|---|---|---|
| `{{ brand_slug }}` | `<brand_dir>` basename | lowercase-kebab | Used in logo filenames: `images/{{ brand_slug }}-logo-light.png` (dark surfaces) and `images/{{ brand_slug }}-logo-dark.png` (light surfaces). Naming convention is the **colour of the art**, not the surface — see `images.md` → "Brand logo". |
| `{{ brand_name }}` | `description.md` first line | Title Case | "Muvin", "Beacons Real Estate". |
| `{{ brand_town }}` | `description.md` officials block | Title Case | Branch town. |
| `{{ brand_county }}` | `description.md` officials block | Title Case | |
| `{{ brand_year }}` | Current year | YYYY | Used in the index.html footer copyright. |
| `{{ office_address_line_1 }}` | `description.md` registered office line | "25 Bell Street" | Street, first line of the postal address. |
| `{{ office_address_line_2 }}` | `description.md` registered office line | "Sawbridgeworth, Hertfordshire CM21 9AR" | Locality + county + postcode. |
| `{{ office_phone }}` | `property.json -> agent.phone` | Display format | "01873 377 575". |
| `{{ office_phone_tel }}` | Composed | `tel:` link format | "+441873377575" (no spaces, with country code). |
| `{{ office_email }}` | `property.json -> agent.email` | lowercase | "sales@beaconsrealestate.com". |
| `{{ email_subject_enquiry }}` | Composed | `{{ property_name }} — enquiry` | Used in mailto: subject param (URL-encode spaces if needed). |
| `{{ email_subject_viewing }}` | Composed | `{{ property_name }} — viewing request` | Same as above. |

## Category 3 — Named agent

From `property.json -> agent`.

| Token | Source | Format | Notes |
|---|---|---|---|
| `{{ agent_first_name }}` | First name | Title Case | Goes in `<em>` on the back page contact cell. |
| `{{ agent_surname }}` | Last name (possibly hyphenated) | Title Case | Plain weight after the first name. |

Worked example (Sparrows Farm): `{{ agent_first_name }}` = "Adam", `{{ agent_surname }}` = "Mackay".

## Category 4 — Listing references

From `property.json -> source` and `property.json -> media`.

| Token | Source | Format | Notes |
|---|---|---|---|
| `{{ listing_url }}` | `source.url` | full URL | Used in QR + listing CTAs. |
| `{{ listing_id }}` | `source.listing_id` | numeric | |
| `{{ listing_display }}` | Composed | "{{ brand domain }} &middot; listing {{ listing_id }}" | Short, scannable text for QR caption (e.g. "beaconsrealestate.co.uk · listing 1424919"). |
| `{{ video_url }}` | `media.video_tours[0]` | full URL | Empty string if no video; in that case drop the video QR per the back-page rules. |
| `{{ video_short_url }}` | derived | "youtu.be/<id>" or equivalent | Visible URL under the video QR. |

## Category 5 — Price

From `property.json -> price`.

| Token | Source | Format | Notes |
|---|---|---|---|
| `{{ price_qualifier }}` | `price.qualifier` | "Offers Over" / "Guide Price" / "Asking Price" / "POA" | Use VERBATIM from property.json — don't paraphrase. |
| `{{ price_formatted }}` | `price.amount` | "£1,750,000" | UK-formatted with thousands separators. |
| `{{ price_line }}` | Composed | `{{ price_qualifier }} {{ price_formatted }}` | Used inline in the opener spec line. |

## Category 6 — Specs (visible on opener, hero info-card, particulars)

From `property.json -> specifications`.

| Token | Source | Format | Notes |
|---|---|---|---|
| `{{ tenure }}` | `specifications.tenure` | "Freehold" / "Leasehold" / "Share of Freehold" | |
| `{{ year_built }}` | `specifications.year_built` | YYYY or descriptive | The hero info-card uses "c.&nbsp;YYYY". |
| `{{ epc_rating }}` | `specifications.epc_rating` | Single letter A-G or "TBC" | |
| `{{ spec_beds }}` | derived | "5 Bed" / "5 Bedrooms" | Page.html hero info-card uses short form ("5 Bed"). |
| `{{ spec_baths }}` | derived | "4 Bath" | |
| `{{ spec_area }}` | derived | "4,521 sq ft" | |
| `{{ spec_plot }}` | derived | "2.20 acres" or drop the cell if no plot | |
| `{{ opener_meta_N_label }}` / `{{ opener_meta_N_value }}` | derived | Five fixed cells | Typical labels: Bedrooms, Bathrooms, Receptions, Internal, Plot. Drop a cell (don't fill with TBC) if the value is genuinely unknown. The row MUST NOT wrap — see copy.md "Stats rows". |

## Category 7 — TOC chapter list (page 2)

Eight chapters in the canonical folio. Each entry has three tokens.

| Token group | Format | Notes |
|---|---|---|
| `{{ toc_chapter_N_title }}` | Title Case, short | "Of place & pedigree", "A home at ease", "Hearth & hall", "Bedrooms & baths", "Terrace & outdoor kitchen", "Garden & ground", "Plan & particulars", "Material information". Adapt to the property — never use vague nouns ("garden complex"); name the actual features. |
| `{{ toc_chapter_N_sub }}` | sentence case, ~6-12 words | One-line "what's in this chapter" — appears under the title in tracked-caps small slate. |
| `{{ toc_chapter_N_page }}` | "03", "04", "06", "08", "10", "12", "14", "15" by default | MUST match the actual rendered pages. Intermissions (pages 5, 7, 11) and the site-plan page (13) are not numbered in the TOC. |
| `{{ toc_caption }}` | italic sentence, 15-30 words | Elevator pitch, sits over the TOC hero image. Distilled from seller brief Q5 ("the feeling on entry") + Q19 ("three words"). |
| `{{ toc_credit }}` | two-line block | Italic photography note + "Marketed by {{ brand_name }}, {{ brand_town }}, {{ brand_county }}." See template inline for exact format. |

## Category 8 — Chapter editorial copy

Each chapter's narrative tokens. Length and voice rules below; the full register guidance is in `copy.md`.

### Chapter I — Opener (page 3)

| Token | Length / voice | Source |
|---|---|---|
| `{{ chapter1_eyebrow }}` | "Chapter I · <chapter1 title>" | Match the TOC chapter-1 title. |
| `{{ opener_title_main }}` | The property name as h2 glyph | One word or compound noun. CSS renders italic. |
| `{{ opener_title_sub }}` | `<locality>, <county>` | Tracked caps via CSS. |
| `{{ opener_spec_line }}` | `<strong>{{ address_line_1 }}</strong> · {{ postcode }} &nbsp;·&nbsp; <em>{{ price_line }}</em>` | Single line. |
| `{{ opener_para_1 }}` | 4-6 sentences, dropcap. Place + age + tenure context. | Distil seller brief Q1 + Q3 + Q11. |
| `{{ opener_para_2 }}` | 3-4 sentences. Period + craft + modern comfort. End on an italic phrase. | Distil seller brief Q3 + Q10. |
| `{{ opener_image_alt }}` / `{{ opener_image_caption }}` | One factual + one italic sentence | The principal elevation or strong aerial. |

### Chapter II — A home at ease (page 4) + intermission (page 5)

| Token | Length / voice |
|---|---|
| `{{ chapter2_eyebrow }}` | "Chapter II" |
| `{{ chapter2_heading }}` | h3, italic close — "A home <em>at ease</em>" |
| `{{ chapter2_deck }}` | one italic sentence, 12-25 words |
| `{{ chapter2_para_1 }}` | 4-6 sentences — entrance through reception to central living |
| `{{ chapter2_para_2 }}` | 3-5 sentences — kitchen specifics + utility/outdoor connection |
| `{{ chapter2_strip_N_caption_label }}` | bold lead-in (e.g. `"The kitchen."`) |
| `{{ chapter2_strip_N_caption_text }}` | one descriptive sentence |
| `{{ intermission_05_caption }}` | italic anchor sentence, 10-20 words, starts with `<em>The [room]</em>,` |

### Chapter III — Hearth & hall / equivalent (page 6) + intermission (page 7)

Same shape as Chapter II — one eyebrow, one heading, one deck, one body paragraph (longer: 6-9 sentences walking through secondary reception rooms), one big-image alt, two stacked-image alts.

### Chapter IV — Bedrooms & baths (page 8)

| Token | Length / voice |
|---|---|
| `{{ chapter4_eyebrow }}` | "Chapter IV" |
| `{{ chapter4_heading }}` | "Bedrooms<br>&amp; <em>baths</em>" |
| `{{ chapter4_deck }}` | italic, 15-25 words. Count + principal-suite signature. |
| `{{ chapter4_para_1 }}` | 3-5 sentences walking the sleeping floor |
| `{{ chapter4_stat_N_value }}` / `{{ chapter4_stat_N_label }}` | Big number (.v) + all-caps label (.l). Typical: 5 bedrooms / 3 bathrooms / first-floor sq ft. |
| `{{ chapter4_para_2 }}` | 2-3 sentence closer on the principal suite |
| 5 gallery cells | Mix principal-bedroom (tall feature cell) + secondary bedrooms + family bathroom. Verify pixel content matches the principal in the tall cell. |

### Chapter V — Feature page (page 10) + intermission (page 11)

For properties with a true garden complex (pool, summerhouse, outdoor kitchen, paddocks) this is the leisure chapter. For others, repurpose to the property's most distinctive outdoor or auxiliary feature (annexe, studio, terrace, pavilion).

| Token | Length / voice |
|---|---|
| `{{ chapter5_eyebrow }}` | "Chapter V · <feature title>" |
| `{{ chapter5_heading }}` | italic editorial headline, two-line |
| `{{ chapter5_deck }}` | italic, 18-30 words |
| `{{ chapter5_para }}` | 5-7 sentences. **Name each feature exactly** — outdoor kitchen ≠ summerhouse ≠ orangery; see structure.md "Distinguishing features clearly". |
| `{{ chapter5_attr_N_label }}` / `{{ chapter5_attr_N_value }}` | Four crisp facts — plot, signature feature, count, material. |
| `{{ intermission_11_caption }}` | italic anchor sentence |

### Chapter VI — Garden & ground (page 12) + site plan (page 13)

| Token | Length / voice |
|---|---|
| `{{ chapter6_eyebrow }}` | "Chapter VI" |
| `{{ chapter6_heading }}` | italic two-line |
| `{{ chapter6_panel_N_heading }}` / `{{ chapter6_panel_N_body }}` | Four panels each: italic h4 + 2-3 sentence body. Typical four: rear garden, paddocks/plot, front/approach, ancillary outbuildings. Re-title per property. |
| `{{ siteplan_eyebrow }}` | "Continued, Chapter VI · The plot" |
| `{{ siteplan_heading }}` | the plot's headline fact ("In excess of <em>2.20 acres</em>", "The <em>plot &amp; outlook</em>") |
| `{{ siteplan_panel_N_heading }}` / `{{ siteplan_panel_N_body }}` | Three panels: plot, boundary, garden complex / outbuildings. |

### Chapter VII — Plan & particulars (page 14)

| Token | Length / voice |
|---|---|
| `{{ chapter7_eyebrow }}` | "Chapter VII · Plan &amp; particulars" |
| `{{ chapter7_heading }}` | "All floors <em>at a glance</em>" |
| `{{ floorplan_lead }}` | italic — "Total internal area · <em>X sq ft</em> · Y m&sup2;" |
| `{{ floorplan_sub }}` | Floor-by-floor breakdown + ancillary footprints, joined by &nbsp;·&nbsp;. **List outbuildings separately** — never silently roll detached gymnasium / garage / annexe sq ft into the principal home figure. See structure.md "Floor area". |
| `{{ particular_N }}` × 9 | Numbered particulars — each a single short sentence with `<strong>` on the named element. Sequence: architectural fact → character feature → kitchen specifics → heating/services → signature internal → signature outdoor → ancillary buildings → plot → outlook. Drop unused slots; never go over 9 (column overflows). |
| `{{ epc_current_a..g }}` | The matching band gets ` current` (leading space → CSS class). All others empty string. Exactly one is non-empty. |
| `{{ epc_caption }}` | 1-2 sentences on heating + what the band reflects. |

### Chapter VIII — Material information (page 15)

Every `{{ mi_* }}` token corresponds to a Material Information row. Mark unknowns "TBC" verbatim — **never invent**. Drop a row only if the field is genuinely not applicable (e.g. Coastal Erosion for an inland home). `{{ mi_part_b_extra_label }}` / `{{ mi_part_b_extra_value }}` is a per-property slot for a distinctive fact (e.g. "Pool", "Annexe", "Outdoor"). See structure.md "Material Information".

### Back page (page 16)

| Token | Length / voice |
|---|---|
| `{{ backpage_bg_src }}` / `{{ backpage_bg_alt }}` | Atmospheric exterior at golden hour (slot 26 or back-equivalent). |
| `{{ backpage_headline }}` | "Arrange a viewing of <em>{{ property_name }}</em>" — canonical phrasing. |
| `{{ backpage_tagline }}` | 25-45 words. Mirrors but does not duplicate the cover/opener. Close with "Best understood in person." or equivalent clincher. |

## Category 9 — index.html (landing) editorial copy

The landing page mirrors the brochure's content but in a continuous-scroll form. Tokens are prefixed `landing_*` to avoid name collisions with the brochure tokens. Sections:

| Section | Tokens | Notes |
|---|---|---|
| Story | `{{ landing_story_eyebrow }}`, `{{ landing_story_headline }}`, `{{ landing_story_para_1 }}`, `{{ landing_story_para_2 }}` | Same source as Chapter I opener but scaled for screen — para 1 short (3-4 sentences) for dropcap legibility; para 2 longer (4-6). |
| Home | `{{ landing_home_eyebrow }}`, `{{ landing_home_headline }}`, `{{ landing_home_para_1..3 }}` | Three paragraphs: entrance/flow, kitchen-dining, reception/orangery. |
| Bedrooms | `{{ landing_bedrooms_eyebrow }}`, `{{ landing_bedrooms_headline }}`, `{{ landing_bedrooms_para_1..2 }}` | Two paragraphs. |
| Garden | `{{ landing_garden_eyebrow }}`, `{{ landing_garden_headline }}`, `{{ landing_garden_para_1..2 }}` | Two paragraphs. |
| Area | `{{ landing_area_eyebrow }}`, `{{ landing_area_headline }}`, `{{ landing_area_para_1..2 }}` | Two paragraphs — admin/transport + qualitative setting. |
| Photo grids 1-6 | `{{ landing_grid_N_src_a..d }}` / `{{ landing_grid_N_alt_a..d }}` | Four cells per grid; image filenames reference the same {{ property_slug }}-NN.webp slots used in the brochure. |
| Floor plan section | `{{ landing_floorplan_heading }}`, `{{ landing_floorplan_lead }}`, `{{ landing_floorplan_row_N_k }}` / `{{ landing_floorplan_row_N_v }}` × 5 | Up to 5 rows + an EPC row that reuses `{{ epc_rating }}`. Drop a row if the property doesn't have that footprint. |
| Key facts | reuses `{{ mi_* }}` tokens from the brochure + `{{ landing_flood_summary }}` | The landing-page "Key facts" compresses Part A into 15 rows. |
| CTA strip | `{{ landing_cta_headline }}` | "Interested? Let's talk." or property-specific. |
| Nav anchors | `{{ nav_section_1..4 }}` | Defaults: "Story", "The Home", "The Garden", "The Area". Rename only if the property's narrative arc differs. |
| `{{ hero_tag }}` | "For Sale · New" / "For Sale" | The small tracked-caps tag on the hero info-card. |
| `{{ hero_N_src }}` / `{{ hero_N_alt }}` × 3 | Three Ken-Burns hero slides. THREE DIFFERENT MOMENTS — not three angles. |
| `{{ page_title }}` | "<Property name> · <street>, <locality> · For Sale" | Browser tab. |
| `{{ meta_description }}` | Same single-sentence pitch as `{{ backpage_tagline }}`. |

## Category 10 — Structured data (JSON-LD)

One token appears in `<head>` on both `brochure.html` and `web/index.html`. It carries the schema.org projection of the property so answer engines (Claude, ChatGPT, Perplexity, Gemini) can read the listing without parsing prose. The substitution step calls the `aeo-emit-jsonld` MCP tool and pastes the returned `scriptBlock` into the token; no manual JSON is written.

| Token | Source | Format / length | Notes |
|---|---|---|---|
| `{{ jsonld_script_block }}` | `aeo-emit-jsonld` MCP tool, inline mode | The tool returns `{ scriptBlock: "<script type=\"application/ld+json\">…</script>" }` — paste verbatim. | Required on every brochure. Empty string is a defect — answer engines will fail `jsonld-present`. |

**How to call the tool** — once per brochure, after `property.json` is loaded:

```
aeo-emit-jsonld({
  accountId: <current account id>,
  label: "RealEstateListing",
  properties: {
    name: property.property_name || property.address.line_1,
    description: property.meta_description || property.backpage_tagline,
    price: property.price.amount,           // number, not formatted string
    priceCurrency: property.price.currency, // "GBP"
    streetAddress: property.address.line_1,
    addressLocality: property.address.locality,
    postalCode: property.address.postcode,
    url: property.listing_url,              // canonical public URL
    image: property.hero_image_url,         // absolute URL to hero
  },
})
```

The tool returns `{ scriptBlock }`. Substitute the string verbatim into `{{ jsonld_script_block }}`. If the call throws `MissingPropertyError`, the property record is incomplete — fix the source data, not the brochure.

**Why inline mode (label + properties), not entityId** — brochures are produced from `property.json` on disk, not from a Neo4j entity. The tool accepts both surfaces; inline is the right one for this pipeline. If a brochure is ever produced from a graph entity directly, switch to `entityId` and skip the property bag.

## Completion check

The substitution step that runs `property-brochure` should end with this verification gate:

```bash
PROP=<property_dir>
remaining=$(grep -c '{{' "$PROP/output/brochure.html" "$PROP/output/web/index.html" 2>/dev/null | awk -F: '{s+=$2} END {print s}')
if [ "$remaining" -gt 0 ]; then
  echo "DEFECT: $remaining unsubstituted {{ tokens }} remain — see grep '{{' output/"
  exit 1
fi
echo "Substitution clean."
```

A brochure with any `{{ x }}` remaining in the rendered output is shipped broken — the verification gate is mandatory.

## Adding a new token

If a property has a feature the canonical placeholder set doesn't cover (e.g. an annexe with separate utility, a wine cellar, a moat — they exist), do not invent a new token in the templates without also adding the entry here. The substitution contract is what catches the bug; an unlisted token gets silently ignored by the substitution step and the brochure ships with `{{ wine_cellar }}` literal in the body. Either reuse an existing token, or add the new one to this file AND to template.html/index.html before rendering.
