Content model
Everything a template reads lives on one tree: site.brand, site.nav, site.socials, site.locale and the typed collections under site.content.*. Model version 1.0.
Four rules the model never breaks, so neither should your template:
- Bounded collections. Every list documents its cap and carries a
moreHref, link onward, never assume you have everything. - Open enums. Type fields grow; branch on the values you style and fall back for the rest (the validator proves your template survives unknown values).
- Explicit nullability. Optional fields are documented nullable; always branch.
- Add-only. Nothing you read today is removed within the major; deprecated means it stops being documented, never that it stops being served.
Your template’s content footprint (which paths it reads) and its minimum model version are computed at publish from your sources, you never declare either. Dynamic indexing into site.content is refused so that stays decidable. During development, preview-content.json beside your manifest replaces collection DATA wholesale (shape is schema-checked; the file is hot-reloaded, never packaged, refused at upload); the kit’s dev server renders this same reference with live example data at /model.
site.content.services
Section titled “site.content.services”The organisation’s published content pages (the services CMS), newest curation order first.
Bounded at 24 · more at /services · since 1.0
| Field | Type | Notes | Since |
|---|---|---|---|
id |
string | 1.0 | |
title |
string | max 200 chars | 1.0 |
slug |
string | 1.0 | |
summary |
string | nullable, always branch · max 500 chars | 1.0 |
imageUrl |
string | nullable, always branch | 1.0 |
href |
string | 1.0 |
Example (the canonical fixture):
{ "id": "66666666-6666-6666-6666-666666666661", "title": "Food Bank", "slug": "food-bank", "summary": "Emergency parcels, no referral needed, from the Foundry Lane depot every weekday morning.", "imageUrl": "p60fixture:service/foodbank", "href": "/services?service=food-bank"}site.content.events
Section titled “site.content.events”Published events, soonest first. registrationMode is an OPEN enum: INFO, RSVP and TICKETED exist today and new modes will arrive; branch on the ones you style and fall back for the rest.
Bounded at 12 · more at /events · since 1.0
| Field | Type | Notes | Since |
|---|---|---|---|
id |
string | 1.0 | |
name |
string | max 200 chars | 1.0 |
description |
string | nullable, always branch · max 200 chars | 1.0 |
registrationMode |
string | open enum: INFO, RSVP, TICKETED today | 1.0 |
startsAt |
string | 1.0 | |
endsAt |
string | nullable, always branch | 1.0 |
venueName |
string | nullable, always branch · max 200 chars | 1.0 |
online |
boolean | 1.0 | |
imageUrl |
string | nullable, always branch | 1.0 |
onSale |
boolean | 1.0 | |
soldOut |
boolean | 1.0 | |
priceFrom |
number | nullable, always branch | 1.0 |
currency |
string | nullable, always branch | 1.0 |
detailHref |
string | 1.0 |
Example (the canonical fixture):
{ "id": "22222222-2222-2222-2222-222222222222", "name": "The Lantern Gala", "description": "Our biggest night of the year: supper, music from the Alderford Community Choir, and the annual lantern walk along the river to close.", "registrationMode": "TICKETED", "startsAt": "2030-09-20T19:00:00Z", "endsAt": null, "venueName": "The Maltings, Alderford", "online": false, "imageUrl": "p60fixture:event/gala", "onSale": true, "soldOut": false, "priceFrom": 25, "currency": "GBP", "detailHref": "/events?event=22222222-2222-2222-2222-222222222222"}site.content.articles
Section titled “site.content.articles”Published articles, newest first. Detail bodies stay editor-owned sanitised richtext on the article page BY DESIGN; the card model carries everything a listing needs.
Bounded at 12 · more at /articles/all · since 1.0
| Field | Type | Notes | Since |
|---|---|---|---|
id |
string | 1.0 | |
title |
string | max 200 chars | 1.0 |
slug |
string | 1.0 | |
excerpt |
string | nullable, always branch · max 500 chars | 1.0 |
imageUrl |
string | nullable, always branch | 1.0 |
authorName |
string | nullable, always branch · max 120 chars | 1.0 |
authorRole |
string | nullable, always branch · max 120 chars | 1.0 |
categories |
string[] | 1.0 | |
publishedAt |
string | 1.0 | |
readingMinutes |
number | nullable, always branch | 1.0 |
href |
string | 1.0 |
Example (the canonical fixture):
{ "id": "88888888-8888-8888-8888-888888888888", "title": "The Community Garden Opens Its Gates", "slug": "community-garden-opens", "excerpt": "Two years of digging, fundraising and Saturday mornings in the rain: the Foundry Lane garden is finally open to everyone.", "imageUrl": "p60fixture:article/garden", "authorName": "Amina Khan", "authorRole": "Community Editor", "categories": [ "News" ], "publishedAt": "2030-04-12T09:00:00Z", "readingMinutes": 5, "href": "/articles/community-garden-opens"}site.content.campaigns
Section titled “site.content.campaigns”Live campaigns, curation order.
Bounded at 12 · more at /campaigns · since 1.0
| Field | Type | Notes | Since |
|---|---|---|---|
id |
string | 1.0 | |
title |
string | max 200 chars | 1.0 |
href |
string | 1.0 | |
summary |
string | nullable, always branch · max 500 chars | 1.0 |
imageUrl |
string | nullable, always branch | 1.0 |
Example (the canonical fixture):
{ "id": "99999999-9999-9999-9999-999999999991", "title": "Keep the 27 Bus", "href": "/campaigns/keep-the-27-bus", "summary": "The only bus serving the Fenwick estate is set to be cut. Add your name before the consultation closes.", "imageUrl": "p60fixture:campaign/bus"}site.content.causes
Section titled “site.content.causes”Causes currently accepting donations, quick-give first. paymentOptions mirrors the public cause contract.
Bounded at 12 · more at /donate · since 1.0
| Field | Type | Notes | Since |
|---|---|---|---|
id |
string | 1.0 | |
title |
string | max 200 chars | 1.0 |
slug |
string | 1.0 | |
description |
string | nullable, always branch · max 500 chars | 1.0 |
imageUrl |
string | nullable, always branch | 1.0 |
targetAmount |
number | nullable, always branch | 1.0 |
raisedAmount |
number | nullable, always branch | 1.0 |
progressPercentage |
number | nullable, always branch | 1.0 |
href |
string | 1.0 | |
paymentOptions |
object | nullable, always branch | 1.0 |
Example (the canonical fixture):
{ "id": "55555555-5555-5555-5555-555555555550", "title": "Flood Response Appeal", "slug": "flood-response", "description": "The Aldern broke its banks on Sunday. Thirty households need dry bedding, dehumidifiers and somewhere warm now.", "imageUrl": "p60fixture:cause/flood", "targetAmount": 50000, "raisedAmount": 18750, "progressPercentage": 38, "href": "/donate/flood-response", "paymentOptions": { "oneOff": true, "recurringCard": true, "directDebit": true }}site.content.courses
Section titled “site.content.courses”Enrollable courses. The long-form About body is sanitised richtext on the course page, never in cards.
Bounded at 12 · more at /courses · since 1.0
| Field | Type | Notes | Since |
|---|---|---|---|
id |
string | 1.0 | |
title |
string | max 200 chars | 1.0 |
description |
string | nullable, always branch · max 500 chars | 1.0 |
imageUrl |
string | nullable, always branch | 1.0 |
venue |
string | nullable, always branch · max 200 chars | 1.0 |
audience |
string | open enum: ADULT, CHILD, FAMILY today | 1.0 |
priceFrom |
number | nullable, always branch | 1.0 |
currency |
string | nullable, always branch | 1.0 |
soldOut |
boolean | 1.0 | |
href |
string | 1.0 |
Example (the canonical fixture):
{ "id": "33333333-3333-3333-3333-333333333333", "title": "Junior Athletics", "description": "Weekly athletics for children aged 7 to 12, coached by qualified volunteers at the King's Park track.", "imageUrl": "p60fixture:course/athletics", "venue": "King's Park track", "audience": "CHILD", "priceFrom": 15, "currency": "GBP", "soldOut": false, "href": "/courses"}site.content.resources
Section titled “site.content.resources”Published documents, grouped by folder.
Bounded at 24 · more at /resources · since 1.0
| Field | Type | Notes | Since |
|---|---|---|---|
id |
string | 1.0 | |
title |
string | max 200 chars | 1.0 |
folder |
string | nullable, always branch | 1.0 |
url |
string | 1.0 | |
contentType |
string | nullable, always branch | 1.0 |
bytes |
number | nullable, always branch | 1.0 |
publishedAt |
string | nullable, always branch | 1.0 |
Example (the canonical fixture):
{ "id": "88888888-8888-8888-8888-888888888881", "title": "Annual report 2029.pdf", "folder": "Reports", "url": "/fixture/annual-report-2029.pdf", "contentType": "application/pdf", "bytes": 1245760, "publishedAt": "2030-02-01T10:00:00Z"}site.content.locations
Section titled “site.content.locations”The organisation’s physical places, primary first.
Bounded at 12 · more at /locations · since 1.0
| Field | Type | Notes | Since |
|---|---|---|---|
id |
string | 1.0 | |
name |
string | max 200 chars | 1.0 |
address |
string | nullable, always branch · max 500 chars | 1.0 |
latitude |
number | nullable, always branch | 1.0 |
longitude |
number | nullable, always branch | 1.0 |
primary |
boolean | 1.0 | |
directionsHref |
string | nullable, always branch | 1.0 |
Example (the canonical fixture):
{ "id": "99999999-9999-9999-9999-999999999901", "name": "Foundry Lane Hub", "address": "14 Foundry Lane, Alderford AL2 4RS", "latitude": 53.8008, "longitude": -1.5491, "primary": true, "directionsHref": "https://www.google.com/maps/dir/?api=1&destination=Foundry%20Lane%20Hub"}site.content.schedules
Section titled “site.content.schedules”TYPE-DERIVED recurring schedules. type is an OPEN enum; ‘worship’ is the first (its shape is the layout worship contract: times, labels, today, next). Other institutions arrive as new types; render the types you know, ignore the rest.
Bounded at 4 · since 1.0
| Field | Type | Notes | Since |
|---|---|---|---|
type |
string | open enum: worship today | 1.0 |
content |
object | 1.0 |
Example (the canonical fixture):
{ "type": "worship", "content": { "times": [ { "name": "Fajr", "begins": "03:20", "congregation": "04:00" }, { "name": "Zuhr", "begins": "13:11", "congregation": "13:30" }, { "name": "Asr", "begins": "18:30", "congregation": "19:00" }, { "name": "Maghrib", "begins": "21:04", "congregation": "21:04" }, { "name": "Isha", "begins": "22:12", "congregation": "22:30" } ], "beginsLabel": "Begins", "congregationLabel": "Jama'at", "note": "Jum'a 13:15 & 14:15", "href": "/services?service=prayer-times", "today": "24 July 2026", "todayAlt": "11 Safar 1448", "next": { "name": "Asr", "begins": "18:30", "congregation": "19:00", "target": "2030-07-24T19:00:00+01:00" }, "jumah": [ { "name": "Jum'a 1", "begins": null, "congregation": "13:15" }, { "name": "Jum'a 2", "begins": null, "congregation": "14:15" } ], "hijri": { "display": "11 Safar 1448" }, "observances": [ { "name": "Community iftar", "date": "2 March 2030", "href": "/events" } ] }}site.content.about
Section titled “site.content.about”The admin-authored page composition: the ordered sections of the home/about surface. type is the section catalogue’s OPEN enum; content is that section type’s fields. This is what section.* re-addresses; a mega-page template can walk it directly.
Bounded at 24 · since 1.0
| Field | Type | Notes | Since |
|---|---|---|---|
type |
string | open enum | 1.0 |
content |
object | 1.0 |

