Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions docs/adr/0044-collections-stage-3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# ADR-0044: Collections stage 3: JSON-LD presets from fields, expiry as a render-time gate, listings by field
- Status: Accepted
- Date: 2026-10-23 (decided by Halil)
- Related: RFC-0042, RFC-0041 / ADR-0043, RFC-0040 / ADR-0042, RFC-0008 / ADR-0023, RFC-0038 / ADR-0040, #71
- Sources: [Google Search gallery](https://developers.google.com/search/docs/appearance/structured-data/search-gallery) · [Event](https://developers.google.com/search/docs/appearance/structured-data/event) · [JobPosting](https://developers.google.com/search/docs/appearance/structured-data/job-posting) · [Product snippet](https://developers.google.com/search/docs/appearance/structured-data/product-snippet) · [Recipe](https://developers.google.com/search/docs/appearance/structured-data/recipe) · [Rank Math schema templates](https://rankmath.com/kb/schema-templates/) · [PublishPress Future](https://wordpress.org/plugins/post-expirator/) · [Cloudflare D1 generated columns](https://developers.cloudflare.com/d1/reference/generated-columns/)

## Context
Collections have typed fields since ADR-0043, but the fields do little on the public site:
- every entry emits a generic `BlogPosting`;
- an event stays listed after it ends;
- themes cannot list entries by a field value.

The open stage 3 items from RFC-0040 and RFC-0041 address these: structured data, date-driven expiry, field listings, index pages and per-collection feeds.

## Decision
1. **Contract:** new optional collection settings. There is no `schemaVersion` bump.
- `listFields`: up to 4 keys.
- `expiry`: `{field, action: "unlist" | "hide", afterMinutes?}`.
- `structuredData`: `{type, properties}`. Each property names its source by `kind`: `field`, `entry`, `site` or `constant`. The entry and site property names are named `$defs` (cargo-typify merges inline enums).
- `contentList` sections gain `where` (up to 4 conditions) and `orderBy`.
- Which properties each type accepts, the field types that fit them and the required ones are a closed table in code (`@cloudhal/core-ts/structured-data`).
2. **Presets:** Event, JobPosting, Product and Recipe.
- FAQPage is out, since Google deprecated FAQ rich results.
- Organization and LocalBusiness belong to site settings later.
- No rating or review properties.
- A mapping is checked when it is saved. An entry missing a required value emits no preset.
- `withStructuredData()` replaces the generic `BlogPosting`; `entryHead` is unchanged, so old themes keep today's output.
- Output goes through `jsonLdText`, capped at 32 KB.
3. **Expiry is a gate, not an automation.** It reuses ADR-0023's time gates: `min(availability.until, field date + grace)`. A `date` ends at the start of the next day in the site's zone.
- `unlist` (default): the page stays, but the entry leaves listings, feeds, the sitemap and search.
- `hide`: 404.
- No scheduler runs and nothing is rewritten.
- Publishing a change that adds a gate, or moves a live gate earlier, is approved like `schedule_unpublish`.
- Writers refuse `expiry` until the site Worker reports `gates: 2`.
4. **Listings by field:**
- A closed set of operators per field type.
- Sorting only by number, date or datetime.
- `now` is the only relative value (no offsets).
- One in-memory evaluator (`fields/query.ts`) is shared by themes (`queryEntries`), `ContentList`, the API, MCP and Studio.
- No indexes in stage 3. A side table follows only if the 10,000-entry benchmark shows more than 50 ms CPU or more than 10,000 rows read per list.
- Studio list columns and filters come from `listFields`.
5. **Index pages and feeds:**
- A collection's index page is an ordinary page made by "Create listing page", with a `contentList`; there is no `indexPath`.
- Per-collection feeds are served at `<prefix>/rss.xml` by a renderer middleware, only where the theme's routes answer 404.
6. **Later:**
- reference fields and repeaters (stage 4, own RFC);
- offsets on `now`;
- 410 for `hide`, decided with RFC-0008's 410 question;
- the Google Indexing API.

## Consequences
- Positive:
- Entries can earn rich results.
- Ended events and closed jobs leave listings without anyone acting, and without a background job.
- Themes and agents list by field with one set of semantics.
- Collections get index pages and feeds.
- Negative:
- The structured-data table must follow Google's documentation.
- Unindexed listings scan a collection in memory.
- Collection settings grow again.
- Risks:
- **JSON-LD injection:** mitigated by `jsonLdText` and plain-text constants.
- **Gates on old site Workers:** guarded by the `gates: 2` feature flag.
- **Mapping drift:** if a mapped field changes, the operations and `list_field_in_use` refuse or report it.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,4 @@ One file per decision, numbered, immutable once accepted (write a new ADR to sup
| [0041](0041-site-archives.md) | Site archives: `cloudhal export`/`import`, a deterministic `.cloudhal` tar of NDJSON parts and media with per-file SHA-256, ids kept, bootstrap/`--replace` (snapshot first)/`--dry-run`, graph only and no built-in encryption (RFC-0039) |
| [0042](0042-first-class-collections.md) | First-class collections: `Site.collections` is a registry (declared keys, collections with entries, `posts`) with no data migration; per-locale labels, icon, feed, sitemap and sort; immutable keys; a new collection's prefix checked for overlaps; agents create collections only through approved `create_collection` (RFC-0040) |
| [0043](0043-typed-custom-fields.md) | Typed custom fields: nine closed types on `Site.collections.<key>.fields` (discriminator `type`), values in `ContentEntry.fields`, one eval-free checker (`@cloudhal/core-ts/fields`) for store, API and MCP that also emits the agents' JSON Schema, `requiredToPublish` at review and publish, per-field localization, definitions on the site node with caps, renames/type changes/removals only as snapshotted, approved operations (RFC-0041) |
| [0044](0044-collections-stage-3.md) | Collections stage 3: JSON-LD presets (Event, JobPosting, Product, Recipe) mapped from fields through a closed table, expiry as a render-time gate (`unlist`/`hide`, no automation), listings filtered and sorted by field with one shared evaluator and no indexes yet, listing pages as ordinary pages and per-collection feeds through a middleware (RFC-0042) |
Loading
Loading