@bettercms-ai/mcp 0.68.2 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: "Site blueprints"
3
- description: "Route, collection and section plans per business type: portfolio, agency, corporate, SaaS/AI and ecommerce."
3
+ description: "Route, collection, form, structured-data and redirect plans per business type: portfolio, agency, corporate, SaaS/AI, ecommerce, directory, blog or publication, docs or knowledge base, local business or restaurant, events, course, job board, real estate, nonprofit and multi-brand."
4
4
  ---
5
5
 
6
6
  # Site blueprints by business type
@@ -15,6 +15,8 @@ working site's taxonomy. The platform rules behind each layer are in
15
15
  - [Common decision process](#common-decision-process)
16
16
  - [Starter rules](#starter-rules)
17
17
  - [Deliverables](#deliverables)
18
+ - [Production shapes every blueprint uses](#production-shapes-every-blueprint-uses): foundations,
19
+ field types, list limits, URL limits, structured data, forms, checklist
18
20
  - Catalogs, read only the one that matches (a mixed site can use two, but give each page family
19
21
  one route owner):
20
22
  - [Portfolio](#portfolio-website): a person or studio presents selected work, services and a
@@ -26,6 +28,18 @@ working site's taxonomy. The platform rules behind each layer are in
26
28
  solutions, industries, integrations, trust and education need separate models
27
29
  - [Large ecommerce](#large-ecommerce-website): catalog, categories, products and shopping
28
30
  journeys dominate; a Shopify-backed store also reads [shopify-headless.md](shopify-headless.md)
31
+ - [Directory](#directory-or-listings-website): hundreds of same-shaped listings with category
32
+ and location hubs
33
+ - [Blog / publication](#blog-or-publication): dated articles with bylines, taxonomy and a feed
34
+ - [Docs / knowledge base](#docs-or-knowledge-base): a navigation tree, code, versions, search
35
+ - [Local business / restaurant](#local-business-or-restaurant): address, hours, menu, booking
36
+ - [Events](#events-or-conference): dated events, agenda, speakers, venue, sponsors
37
+ - [Course](#course-or-education): courses, curriculum, instructors, cohorts
38
+ - [Job board](#job-board-or-careers): open roles, departments, an ATS
39
+ - [Real estate](#real-estate-or-property-listings): properties, agents, offices, areas
40
+ - [Nonprofit](#nonprofit): programs, impact, stories, reports, donations
41
+ - [Multi-brand](#multi-brand-and-multi-language): several brands or languages on one design
42
+ system
29
43
 
30
44
  Data-generated page families (glossary, integrations, locations): [programmatic-seo.md](programmatic-seo.md).
31
45
 
@@ -78,6 +92,135 @@ Return a route tree, route matrix, content and model matrix, folder plan, compon
78
92
  sitemap and redirect policy, open ownership decisions, and a phased implementation plan. On a
79
93
  live project, implement only the scope the user asked for and verify the routes you touched.
80
94
 
95
+ ## Production shapes every blueprint uses
96
+
97
+ Read this once; every catalog below assumes it. A site is often two types (a SaaS site with a
98
+ blog and docs): open each matching catalog, merge them, and create shared collections (Authors,
99
+ Topics, FAQs, Testimonials) once. Drop any collection the source does not have.
100
+
101
+ ### Foundations
102
+
103
+ | Piece | Where it lives |
104
+ |---|---|
105
+ | Header, footer, announcement bar | Global Layout via `update_layout`. Never a page section. Footer link columns are a repeater of groups; the newsletter box points at a form. A bar or promo that runs for a period takes a `window` on its Section, and a seasonal band one on its page block ([publishing.md](publishing.md) §6) |
106
+ | Company name, address, phone, social profiles | One place in the Global Layout, read by the footer and the contact page. Site JSON-LD repeats it once in `siteSchema` (`update_seo`) |
107
+ | Cookie consent | A third-party consent tool stays code. Report it; do not rebuild it as content |
108
+ | 404, legal, thank-you pages | Static pages in a `System` Content sidebar folder. Thank-you pages are noindex |
109
+ | Meta and structured data | Native per page and entry, site defaults with `update_seo` ([seo.md](seo.md)). Never modelled |
110
+
111
+ ### Field types
112
+
113
+ Field keys below are entry keys in one flat namespace per model. `title` and the slug are on
114
+ every entry and are not repeated in the tables. SEO fields are never listed.
115
+
116
+ | The source shows | Production field |
117
+ |---|---|
118
+ | Short text, long plain text | `text`, `longtext` |
119
+ | The one long body | `document` (one per model, top level). Shorter formatted copy is `richtext` |
120
+ | Image, download | `image`, `file` |
121
+ | Postal address | `group` of `text` children: `"street"`, `"city"`, `"region"`, `"postal_code"`, `"country"` |
122
+ | Map pin | `location` (lat, lng, optional label). Never two text fields |
123
+ | Tags, amenities, bullet lists | `array` with `itemType: "text"`. One choice from a fixed list is `select` |
124
+ | Rows of the same shape (hours, facts, metrics, tickets) | `repeater` of `text`, `number`, `select`, `boolean` children |
125
+ | Mixed ordered blocks in a case study | `modular` |
126
+ | One or many other entries | `reference`, `multi-reference`. A missing target is refused on write |
127
+ | Call to action | `button` (label and link in one value). A bare link is `link`; also `email`, `phone` |
128
+ | Date, date and time | `date`, `datetime`. Keep the zone as a `text` IANA name beside it when the site shows times |
129
+ | A spec or comparison table | `repeater` with one field per column (any field type) and `ui.layout: "table"`. There is no separate table field type |
130
+
131
+ An external system's id (`"store_product_id"`, `"ats_job_id"`, `"feed_id"`, `"lms_course_id"`)
132
+ is a `text` field kept as migration metadata and locked: `readOnly: "developer"` with a
133
+ `lockReason` naming the system it comes from. Place it last. A write that changes it without
134
+ schema access is refused (403 FIELD_LOCKED). The lock is enforced on entries only.
135
+
136
+ ### Lists
137
+
138
+ A collection's list is its own dynamic page holding one `collection` block. The block takes
139
+ `limit` (1 to 200, default 20), `order` (`newest` or `oldest`, by creation time), `emptyText`,
140
+ and optional card and detail components. Entry URLs are that page's path plus the entry slug.
141
+
142
+ With `source: { modelId }` the block lists that collection on any hosted page, and takes
143
+ `filters` (max 5; `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `contains`, `in`), `sort`
144
+ (`-publishedAt,title`), `pinnedIds` (max 10, shown first) and `excludeCurrent`. In an article
145
+ template a filter value may be `{{field}}` of the entry shown. Filter values are stored on the
146
+ block: a visitor cannot change them, and none means "today".
147
+
148
+ Not available: pagination, grouping, visitor-driven filters. Only the hosted renderer draws the
149
+ block; a route built from the repo's code queries the Delivery API with the same filter grammar.
150
+ So for each hub the source has, choose and report one of:
151
+
152
+ | Source list | Production shape |
153
+ |---|---|
154
+ | All entries, newest first | The `collection` block |
155
+ | Filtered by category, location or status | On a hosted page a `collection` block with `source` and `filters`. On a code-built site the site's own code filters the delivered entries |
156
+ | Sorted by a field (A to Z, a date, an order number) | `sort` on a sourced block |
157
+ | Upcoming or past, by today's date | The site's code, or a `"status"` field the block filters on |
158
+ | "Related" row | On a hosted template, a sourced block filtered by a `{{field}}` of the entry shown, with `excludeCurrent` |
159
+ | "Featured" row | `pinnedIds`, a filter on a featured flag, or a short `multi-reference` chosen by hand |
160
+ | Numbered pages, groups under headings, filter chips | The site's code |
161
+ | Counts ("1,240 venues", "42 lessons") | Rendered by code from the entries. Never typed into copy |
162
+
163
+ ### URLs and redirects
164
+
165
+ - Every collection gets a `urlPattern` holding `:slug` exactly once (`/blog/:slug`). A fixed
166
+ multi-segment prefix is valid (`/docs/v2/:slug`).
167
+ - Nested (`/:category/:listing`) and dated (`/2024/05/my-post/`) source paths cannot be
168
+ expressed. Use the flat pattern and add one `create_redirect` (301) per old URL.
169
+ - A slug change on an entry with a pattern 301s by itself.
170
+ - Redirect types are 301, 302, 307, 308 and 410. A retired entry either stays published and
171
+ marked ended, redirects to a successor, or is unpublished and answers 410 (`create_redirect`
172
+ with `redirectType: "410"`, no destination).
173
+
174
+ ### Structured data
175
+
176
+ Order: Automatic, then `schemaType`, then pasted `schema`. `schemaType` accepts `WebPage`,
177
+ `AboutPage`, `ContactPage`, `CollectionPage`, `Blog`, `BlogPosting`, `Article`, `Product`,
178
+ `FAQPage`, `Event`, `Organization`, `LocalBusiness`, `JobPosting`, `Course`, `Recipe`, `HowTo`,
179
+ `Service`, `Person`, `VideoObject`. What the common ones emit from the content:
180
+
181
+ | `schemaType` | Emits |
182
+ |---|---|
183
+ | `BlogPosting`, `Article` | Headline, description, image, published and modified dates, author |
184
+ | `Product` | Name, description, image, SKU, offer |
185
+ | `Event` | Name, description, image, start, end, a location name |
186
+ | `LocalBusiness`, `Organization` | Name, description, URL, image or logo, telephone, email, address as one line |
187
+ | `FAQPage` | The page's question and answer pairs. Never automatic |
188
+
189
+ Anything richer is pasted `schema` built only from facts visible on the page: opening hours, geo
190
+ coordinates, event status and offers, salary, `RealEstateListing`, `Restaurant`,
191
+ `SoftwareApplication`, `NewsArticle`. A hosted page gets its `BreadcrumbList` automatically. Set a
192
+ collection-wide default in `collectionSchemaTypes`. Never a self-serving `AggregateRating`.
193
+
194
+ ### Forms
195
+
196
+ Thirteen field types: `text`, `textarea`, `email`, `phone`, `number`, `url`, `date`, `select`,
197
+ `radio`, `checkbox`, `checkboxes`, `consent`, `hidden`, plus the `step` divider for multi-step forms. No file upload, so CVs, floor plans and
198
+ attachments go to the external system. Payment details never go in a form. Submissions land in
199
+ the inbox; an editor turns one into an entry. Full pipeline:
200
+ [forms-and-interactions.md](forms-and-interactions.md).
201
+
202
+ ### Checklist
203
+
204
+ - Every collection exists in the source, or was dropped on purpose.
205
+ - Shared collections exist once across merged catalogs.
206
+ - Store, ATS, LMS and feed facts are ids, never copies.
207
+ - Every list has `emptyText` and a stated shape for each filter the source had.
208
+ - Every collection has a `urlPattern`; every changed URL has a 301.
209
+ - Structured data is built from visible fields, with the type its catalog names.
210
+
211
+ ### Built-in sections
212
+
213
+ The section library ships 23 platform blueprints in 17 families, so a standard band needs no new
214
+ component: Hero, Feature grid (three, two, bento), Feature rows, Steps, Timeline, Stats, Logo cloud,
215
+ Testimonial (quote, three cards), Team, Case study, Comparison, Pricing, FAQ, Call to action,
216
+ Capture (newsletter band) and Listing (latest, related). Read one with `list_components`; inline its
217
+ `blockJson` as a `section` block for a one-off band, or copy it with `create_component` `from:
218
+ "builtin:<key>"` for a recurring one (see [components.md](components.md)). Two ship unbound on purpose: the Capture form has no form until you
219
+ set its `form` prop, and both Listing layouts list the page's own entries until you give them a
220
+ `source`; each shows its empty text meanwhile. Not available as a section, so build it in the repo
221
+ or leave it out: mega menu, docs sidebar, cookie consent, exit intent, store locator, map, billing
222
+ toggle, countdown and anything commerce.
223
+
81
224
  ## Portfolio website
82
225
 
83
226
  Use this when the primary job is to show selected work and make an individual or small studio credible. First determine whether the work is private, client-approved, dated, or still under embargo.
@@ -108,6 +251,32 @@ Use repeatable gallery items rather than a long list of `image1` fields. Give ea
108
251
 
109
252
  Check case-study consent, working gallery at different aspect ratios, captions, full-size media performance, downloadable file links, contact submission, related-work references, archived projects, and redirect behavior for renamed slugs. A portfolio with only a few projects does not need auto-generated service × industry pages.
110
253
 
254
+ ### Production detail
255
+
256
+ Projects collection, `urlPattern` `/work/:slug` (or the established prefix):
257
+
258
+ | Field key | Type | Notes |
259
+ |---|---|---|
260
+ | `"client"` | `reference` to Clients, or `text` | A reference only when clients have a logo or page of their own |
261
+ | `"year"` | `number` | |
262
+ | `"services"` | `multi-reference` to Services, or `array` of text | References when service pages exist |
263
+ | `"cover_image"`, `"excerpt"` | `image`, `longtext` | Words baked into a hero image move into fields |
264
+ | `"modules"` or `"body"` | `modular`, or `document` | `modular` (text, image, gallery, quote, video, stats blocks) for image-led studies; `document` when mostly prose |
265
+ | `"results"` | `repeater`: `"value"`, `"label"` | Only verified outcomes |
266
+ | `"credits"` | `repeater`: `"role"`, `"name"` | |
267
+ | `"testimonial"` | `reference` to Testimonials | |
268
+ | `"live_link"` | `link` | |
269
+ | `"is_featured"` | `boolean` | The home grid is a hand-picked `multi-reference` or code filtering on this |
270
+
271
+ - **Chrome:** a minimal header and a footer with contact details, both in the Global Layout.
272
+ - **Forms:** one enquiry form (name, email, message, consent).
273
+ - **Structured data:** `Article` on projects, `AboutPage` and `ContactPage` on those pages,
274
+ `Organization` or a `Person` node in `siteSchema`.
275
+ - **Pitfalls:** videos without a poster or captions; scroll-driven storytelling flattened into
276
+ one blob (keep each step a `modular` item); downscaled uploads (upload the original once, the
277
+ media pipeline serves sizes); client work under NDA published without confirmed rights (flag
278
+ it in the report and ask).
279
+
111
280
  ## Agency website
112
281
 
113
282
  Use when the site must explain services, prove results, and convert qualified leads. The key distinction from a portfolio is a durable offer taxonomy and repeatable proof linked to each offer.
@@ -138,6 +307,27 @@ Content sidebar folders should match editor work: `Core pages`, `Services`, `Ind
138
307
 
139
308
  Review lead form destination, attribution and consent, internal links from every offer to relevant proof, case-study rights, schema claims, duplicate service routes, old URL redirects, and mobile CTA visibility. Keep draft or client-confidential work out of sitemaps and public data feeds.
140
309
 
310
+ ### Production detail
311
+
312
+ | Collection | `urlPattern` | Fields |
313
+ |---|---|---|
314
+ | Case studies | `/work/:slug` | The Projects fields in [Portfolio](#portfolio-website), plus `"industry"` `reference` |
315
+ | Services | `/services/:slug` | `"icon"` `image`, `"excerpt"` `longtext`, `"body"` `document`, `"related_work"` `multi-reference` |
316
+ | Clients | none (no detail page) | `"logo"` `image` with the company name as alt, `"website"` `link` |
317
+ | Team | `/team/:slug`, only when profiles have pages | `"role"` `text`, `"photo"` `image`, `"bio"` `longtext`, `"social_links"` `repeater`: `"network"` `select`, `"url"` `link` |
318
+ | Testimonials | none | `"quote"` `longtext`, `"name"`, `"role"`, `"company"` `text`, `"photo"`, `"logo"` `image` |
319
+
320
+ - **Lists:** "work by service" on a service page is `"related_work"` picked by hand, code
321
+ filtering the delivered case studies, or on a hosted page a `collection` block with `source`
322
+ and a filter on the service.
323
+ - **Forms:** a new-business enquiry with `select` fields for budget and timeline, plus `hidden`
324
+ fields for campaign attribution.
325
+ - **Structured data:** `Organization` in `siteSchema`, `Article` on case studies, `WebPage` on
326
+ services, `FAQPage` only where a service page shows real questions. A `Person` node per team
327
+ page is pasted `schema`.
328
+ - **Pitfalls:** a logo wall with no alt text; client names retyped per page instead of
329
+ referenced; a testimonial with no record of consent (keep a `"consented_on"` `date`).
330
+
141
331
  ## Corporate website
142
332
 
143
333
  Use for multiple audiences, brands, regions, business units, approval chains, or regulated statements. The main modeling problem is ownership: who can update a statement, which sites reuse it, and when it expires.
@@ -212,6 +402,33 @@ Build topic hubs and link related solution, industry, integration, case, and glo
212
402
 
213
403
  Verify product claims against shipped capability, integration statuses, pricing consistency, demo form routing, docs links, internal topic paths, glossary duplicates, comparison accuracy, rendering of code/rich text, and desktop/mobile navigation. Noindex preview, gated assets, and incomplete pages.
214
404
 
405
+ ### Production detail
406
+
407
+ | Collection | `urlPattern` | Fields |
408
+ |---|---|---|
409
+ | Case studies | `/customers/:slug` | `"customer"` `text`, `"logo"`, `"cover_image"` `image`, `"industry"` `reference`, `"excerpt"` `longtext`, `"metrics"` `repeater` (`"value"`, `"label"`, at most 3 rows), `"quote"` `reference` to Testimonials, `"body"` `document` with challenge, solution, results as H2s, `"facts"` `repeater` (`"label"`, `"value"`) |
410
+ | Testimonials | none | `"quote"`, `"name"`, `"role"`, `"company"`, `"photo"`, `"logo"`, `"source"` `link`, `"consented_on"` `date`. `"rating"` `number` only when the source shows one |
411
+ | Integrations | `/integrations/:slug` | `"logo"`, `"category"` `reference`, `"excerpt"`, `"body"` `document`, `"app_link"` `button`, `"docs_link"` `link`, `"is_native"` `boolean`, `"status"` `select` (live, beta, planned) |
412
+ | Releases | `/changelog/:slug`, or none when releases are anchors on one page | `"version"` `text`, `"published_on"` `datetime`, `"changes"` `repeater` (`"kind"` `select`: new, improved, fixed; `"text"`), `"body"` `richtext` |
413
+ | Comparisons | `/compare/:slug` | `"competitor"` `text`, `"competitor_logo"`, `"intro"` `longtext`, `"matrix"` `repeater` (`"feature"`, `"us"`, `"them"`), `"verified_on"` `date`, `"body"` `document` |
414
+
415
+ - **Static until repeated:** feature and product pages stay static pages until three or more
416
+ share one layout. Pricing tiers stay in the pricing page's fields unless the same plans show
417
+ on two or more pages; then a Plans collection.
418
+ - **Pricing:** store the monthly and the yearly price as two fields read from the source. Never
419
+ derive one from a discount. The full feature grid is a `repeater` whose row holds one value
420
+ per plan; inside a component it can be the `table` prop ([components.md](components.md)).
421
+ - **Lists:** customer and integration hubs are one `collection` block each. Category filter
422
+ chips are code on a code-built site; otherwise leave them out and say so. A home logo wall is
423
+ a `repeater` of images, or a `multi-reference` when logos link to stories.
424
+ - **Forms:** demo request and contact sales with `hidden` attribution fields, and a newsletter
425
+ form with `consent`. A gated asset is a form whose success state links the file.
426
+ - **Structured data:** `Organization` and a `WebSite` node in `siteSchema`, `FAQPage` on the
427
+ pricing page (one per URL), `BlogPosting` on posts. `JobPosting` as the careers collection's
428
+ `schemaType`; `SoftwareApplication` is pasted `schema` from visible facts.
429
+ - **Pitfalls:** "Trusted by 10,000 teams" style counters with no source (report as unverified);
430
+ a feature matrix pasted as an image; a screenshot holding the only copy of a claim.
431
+
215
432
  ## Large ecommerce website
216
433
 
217
434
  Use for a catalog-heavy site regardless of commerce engine. The core choice is the boundary between **commerce facts** and **editorial merchandising**.
@@ -233,7 +450,7 @@ Products, variants, SKUs, price, availability, tax, orders, checkout, customer a
233
450
  | Search, cart, account, checkout | Runtime routes | Usually excluded from sitemap; private data protected |
234
451
  | Filters and sort | Parameters on category routes | Explicit crawl/index policy; avoid unbounded URL combinations |
235
452
 
236
- Keep existing paths if the store already uses another taxonomy. Decide whether unavailable products stay indexable, redirect to a successor, or return 404/410 based on permanence and user value. Do not redirect every missing product to the homepage.
453
+ Keep existing paths if the store already uses another taxonomy. Decide whether unavailable products stay indexable, redirect to a successor, or are unpublished and return 404 or a 410 rule, based on permanence and user value. Do not redirect every missing product to the homepage.
237
454
 
238
455
  ### Editor model and reuse
239
456
 
@@ -248,3 +465,431 @@ Provide crawlable category → subcategory → product links and paginated resul
248
465
  ### Validation
249
466
 
250
467
  Test a product with several variants, a sold-out product, missing image, empty category, deep pagination, filter combinations, mobile search, currency/market change, cart handoff, and changed price after cache expiry. Verify that sitemap counts match eligible catalog records and that deleted routes/redirects do not strand internal links.
468
+
469
+ ### Production detail
470
+
471
+ Applies when BetterCMS renders product or category pages. A Shopify-backed store follows
472
+ [shopify-headless.md](shopify-headless.md) first.
473
+
474
+ | Collection | `urlPattern` | Fields |
475
+ |---|---|---|
476
+ | Products | `/products/:slug`, slug equal to the store handle | `"store_product_id"` `text` (migration metadata), `"excerpt"` `longtext`, `"story"` `document`, `"gallery"` `repeater` (`"image"`, `"caption"`; editorial images only), `"highlights"` `array` of text, `"specs"` `repeater` (`"label"`, `"value"`), `"care"` `richtext`, `"categories"` `multi-reference`, `"badge"` `select`, `"related"` `multi-reference` (at most 4), `"faqs"` `multi-reference` |
477
+ | Categories | `/collections/:slug` | `"intro"` `longtext`, `"image"`, `"parent"` `reference` to Categories (one level), `"store_collection_id"` `text` |
478
+ | FAQs (when shared) | none | `"question"` `text`, `"answer"` `richtext` |
479
+
480
+ A catalogue with no commerce engine adds `"price"` and `"compare_at_price"` `number`, a
481
+ `"currency"` `select` of ISO codes, `"sku"` `text`, `"is_in_stock"` `boolean`.
482
+
483
+ - **Lists:** a category's product grid is code filtering by the category, or the store's own
484
+ listing. A "Staff picks" row is a `multi-reference`.
485
+ - **Chrome:** header (search, account and cart are app state in code, never content), a promo
486
+ bar, a footer whose payment badges are a fixed set in code.
487
+ - **Forms:** newsletter, contact, wholesale enquiry. Back-in-stock and checkout stay with the
488
+ store.
489
+ - **Structured data:** `Product` only where price and availability are entry fields. When the
490
+ store owns them, the site's code emits `Product` with the live `Offer`; do not paste a frozen
491
+ price. `CollectionPage` on categories. `AggregateRating` only from a connected review source.
492
+ - **Redirects:** facet and sort parameters canonical to the category. Cart, checkout, account
493
+ and search are noindex.
494
+ - **Pitfalls:** price, stock, options or variants copied into content (they drift within a
495
+ day); one static page per product, or one entry per variant; a size guide as an image instead
496
+ of a `repeater`; typed review counts or star scores; editorial images hotlinked from the
497
+ store CDN instead of uploaded.
498
+
499
+ ## Directory or listings website
500
+
501
+ Use when the source has hundreds of same-shaped detail pages (businesses, tools, venues,
502
+ vendors), category and location hubs, filters, a "Submit a listing" form, often a map.
503
+
504
+ ### Routes
505
+
506
+ | Family | Path | Model |
507
+ |---|---|---|
508
+ | Home, submit, featured-listing pricing, about, contact | `/`, `/submit`, ... | Static pages |
509
+ | Listings list and detail | `/listings`, `/listings/:slug` | Listings collection, dynamic page, `urlPattern` `/listings/:slug` |
510
+ | Category hub | `/categories`, `/categories/:slug` | Categories collection |
511
+ | Location hub | `/locations`, `/locations/:slug` | Locations collection (cities or regions) |
512
+
513
+ ### Collections
514
+
515
+ | Collection | Fields |
516
+ |---|---|
517
+ | Listings | `"logo"`, `"cover_image"` `image`; `"excerpt"` `longtext`; `"body"` `document`; `"category"` `reference` (required); `"subcategories"` `multi-reference`; `"place"` `reference` to Locations; `"address"` `group`; `"map_pin"` `location`; `"website"` `link`; `"email"` `email`; `"phone"` `phone`; `"opening_hours"` `repeater` (`"day"` `select`, `"opens"`, `"closes"` `text`, `"is_closed"` `boolean`); `"amenities"` `array` of text, or `multi-reference` to Features when each has a page; `"facts"` `repeater` (`"label"`, `"value"`); `"gallery"` `repeater` (`"image"`, `"caption"`); `"social_links"` `repeater`; `"plan"` `select` (free, featured, premium); `"is_verified"` `boolean`; `"external_id"` `text` |
518
+ | Categories | `"intro"` `longtext`, `"icon"` `image`, `"parent"` `reference` to Categories |
519
+ | Locations | `"region"` `text`, `"country"` `select`, `"map_pin"` `location`, `"intro"` `longtext`, `"cover_image"` `image` |
520
+
521
+ `"plan"`, `"is_verified"` and `"external_id"` are locked: `readOnly: "developer"` with a
522
+ `lockReason`. A write without schema access that changes one is refused.
523
+
524
+ - **Chrome:** header with search, footer, an announcement for featured-listing offers.
525
+ - **Lists:** the all-listings page is a `collection` block. On a hosted page a category or
526
+ location hub is a block with `source` and a filter, A to Z is `sort`, featured first is
527
+ `pinnedIds`, and "related listings" is a filter on a `{{field}}` of the listing shown with
528
+ `excludeCurrent`. Numbered pagination needs the site's code: report the gap. Site search
529
+ uses the hosted index: run `refresh_search_index` after the import.
530
+ - **Forms:** submit a listing, claim a listing, report a problem, contact. A submission never
531
+ publishes itself; an editor creates or updates the entry from the inbox.
532
+ - **Structured data:** `schemaType: "LocalBusiness"` on listings that are places. Paste `schema`
533
+ when the page shows hours or coordinates, or needs a subtype. `CollectionPage` on hubs.
534
+ - **Redirects:** nested source paths (`/:category/:listing`) become `/listings/:slug` plus one
535
+ 301 each.
536
+ - **Pitfalls:** filterable facts stored as free tags (use `select`, `reference`, `number`);
537
+ duplicate listings from several sources (match on website domain, phone or address before
538
+ creating); a one-line address when structured data needs the parts; a paid outbound link with
539
+ no `rel="sponsored"` (the `link` value carries no rel, so the rendering code adds it from
540
+ `"plan"`); hub pages with fewer than three listings left indexable (set noindex until they
541
+ grow); thousands of near-empty pages (run `generate_pages_from_dataset` with `dryRun: true`
542
+ first, and only for rows with real content).
543
+
544
+ ## Blog or publication
545
+
546
+ Use when the source has dated articles with bylines, categories, an index and a feed.
547
+
548
+ ### Routes
549
+
550
+ | Family | Path | Model |
551
+ |---|---|---|
552
+ | Home (or the index itself), about, newsletter, contact | `/`, `/about`, ... | Static pages |
553
+ | Post list and detail | `/blog`, `/blog/:slug` (or the site's own word, `/journal/:slug`) | Posts collection, `urlPattern` `/blog/:slug` |
554
+ | Author | `/authors/:slug` | Authors collection |
555
+ | Category, topic | `/category/:slug`, `/topics/:slug` | One collection per taxonomy the source really has |
556
+
557
+ ### Collections
558
+
559
+ | Collection | Fields |
560
+ |---|---|
561
+ | Posts | `"dek"`, `"excerpt"` `longtext`; `"cover_image"` `image`; `"body"` `document`; `"authors"` `multi-reference` (at least one); `"category"` `reference`; `"topics"` `multi-reference`; `"published_on"`, `"updated_on"` `datetime`; `"series"` `reference`; `"is_featured"` `boolean`; `"related"` `multi-reference` (at most 3) only when the source curates them |
562
+ | Authors | Title is the name. `"role"` `text`, `"photo"` `image`, `"bio"` `longtext`, `"social_links"` `repeater` |
563
+ | Categories, Topics | `"intro"` `longtext` |
564
+
565
+ - **Chrome:** header, footer with the newsletter form, announcement.
566
+ - **Lists:** the index is a `collection` block, `order: "newest"`. It orders by creation time,
567
+ so import posts oldest first, or the list will not follow `"published_on"`. A block with
568
+ `source` takes `sort: "-published_on"` instead. Category, topic and author lists on a hosted
569
+ page are sourced blocks with a filter; numbered pagination needs the site's code. A pinned
570
+ feature slot is `pinnedIds` on a sourced block, or a `reference` on the index page.
571
+ - **Forms:** newsletter with `consent`, contact, tips.
572
+ - **Structured data:** `BlogPosting` or `Article` as the collection default in
573
+ `collectionSchemaTypes`; `Blog` on the index. `NewsArticle` is pasted `schema`; an author page takes
574
+ `schemaType: "Person"`. Turn the feed on in `rssConfig`; item links come from
575
+ the `urlPattern`. A syndicated post keeps its cross-site canonical in the entry's `meta`.
576
+ - **Redirects:** dated WordPress paths (`/2024/05/my-post/`) become `/blog/:slug` with a 301
577
+ per post.
578
+ - **Pitfalls:** share bars, author boxes and related cards inside the body (they are template
579
+ sections); an H1 inside the body, or the cover repeated as the first body image; authors as
580
+ strings; a stored reading time (compute it); WordPress shortcodes (`[gallery]`, `[caption]`)
581
+ left as text (map them to a Portable Text block or a component, report unknown ones); tags
582
+ and categories that duplicate each other.
583
+
584
+ ## Docs or knowledge base
585
+
586
+ Use when the source has a left navigation tree, "On this page" anchors, code blocks,
587
+ admonitions, a version switcher, search, "Was this helpful?".
588
+
589
+ ### Routes
590
+
591
+ | Family | Path | Model |
592
+ |---|---|---|
593
+ | Docs home, support home, contact support | `/docs`, `/support`, ... | Static pages |
594
+ | Doc page | `/docs/:slug` | Docs collection |
595
+ | Versioned docs | `/docs/v2/:slug` | One collection per supported major version, each with its own fixed-prefix `urlPattern` |
596
+ | Knowledge-base article | `/help/:slug` | KB articles collection |
597
+ | Glossary | `/glossary`, `/glossary/:slug` | Terms collection ([programmatic-seo.md](programmatic-seo.md)) |
598
+
599
+ ### Collections
600
+
601
+ | Collection | Fields |
602
+ |---|---|
603
+ | Docs | `"excerpt"` `longtext`; `"body"` `document`; `"section"` `reference` to Doc sections (required); `"parent"` `reference` to Docs (at most 3 levels); `"order"` `number`; `"status"` `select` (new, beta, deprecated); `"last_reviewed_on"` `date` |
604
+ | Doc sections | `"icon"` `image`, `"order"` `number` |
605
+ | KB articles | `"body"` `document`, `"category"` `reference`, `"related"` `multi-reference` |
606
+ | Terms | `"definition"` `richtext`, `"related"` `multi-reference` |
607
+
608
+ - **Chrome:** header with search and footer in the Global Layout. The sidebar tree is rendered
609
+ by code from `"section"`, `"parent"` and `"order"`; previous and next links and "On this page"
610
+ are derived the same way. Curate the tree in the Layout only when the source hand-orders it.
611
+ - **Lists:** a section landing page ordered by `"order"` is a `collection` block with `source`,
612
+ a filter on `"section"` and `sort: "order"` on a hosted page, or the site's code. Run
613
+ `refresh_search_index` after the import.
614
+ - **Forms:** a one-question feedback form (`radio` plus a `hidden` page path), contact support.
615
+ - **Structured data:** `Article` or `WebPage`; `FAQPage` only on real question pages. A deprecated version is noindex or canonicals to the current one.
616
+ - **Redirects:** relative `.md` links in source files resolve to the target entry's URL. A
617
+ nested source path (`/docs/guides/setup/install`) becomes `/docs/:slug` plus a 301.
618
+ - **Pitfalls:** the navigation tree copied by hand (it drifts); admonitions flattened to
619
+ blockquotes with the tone lost (map each to an embedded block that keeps its tone,
620
+ [portable-text.md](portable-text.md)); code as images or highlighted markup (a `bcmsCode`
621
+ block keeps the plain text and a language); MDX components with no
622
+ equivalent (map known ones to components, report the rest as logic left in code); an OpenAPI
623
+ reference converted by hand (it is generated from the spec: link or embed it and report it).
624
+
625
+ ## Local business or restaurant
626
+
627
+ Use when the source shows an address, hours, a phone number, a menu, "Book a table", a map, and
628
+ one or a few locations.
629
+
630
+ ### Routes
631
+
632
+ | Family | Path | Model |
633
+ |---|---|---|
634
+ | One location | `/visit` | Static page; address and hours live once in the Global Layout |
635
+ | Two or more locations | `/locations`, `/locations/:slug` | Locations collection |
636
+ | Menu | `/menu`, or `/menus/:slug` | A page section; a Menus collection when menus differ per location or season |
637
+ | Events, specials | `/events/:slug` | Collections only when dated |
638
+
639
+ ### Collections
640
+
641
+ | Collection | Fields |
642
+ |---|---|
643
+ | Locations | `"address"` `group`; `"map_pin"` `location`; `"phone"` `phone`; `"email"` `email`; `"opening_hours"` `repeater` (`"day"` `select`, `"opens"`, `"closes"` `text`, `"is_closed"` `boolean`); `"special_hours"` `repeater` (`"date"` `date`, `"opens"`, `"closes"`, `"is_closed"`, `"note"`); `"time_zone"` `text`; `"cover_image"` `image`; `"menus"` `multi-reference`; `"booking_provider"` `select`; `"booking_id"` `text` |
644
+ | Menus | Title is "Dinner", "Brunch". `"sections"` `repeater` (`"title"`, `"items"` `repeater`: `"name"`, `"description"`, `"price"` `number`, `"dietary"` `array` of text, `"is_new"` `boolean`); `"notes"` `longtext` (allergen statement); `"pdf"` `file` (a download, never the only copy) |
645
+ | Events, Specials | `"starts_at"`, `"ends_at"` `datetime`; `"place"` `reference` to Locations; `"body"` `richtext`; `"image"` |
646
+
647
+ Give `"opens"` and `"closes"` help text with the format (24-hour `HH:MM`). The currency is a
648
+ site setting in code, not a symbol typed into each price.
649
+
650
+ - **Chrome:** header with a booking button that links to the provider; footer listing
651
+ locations. Hours appear in one source: with a Locations collection the footer renders from
652
+ the entries in code, otherwise from the Global Layout.
653
+ - **Forms:** private-events enquiry, contact, newsletter. Reservations are the provider's
654
+ embed, stored as provider and id only.
655
+ - **Structured data:** `schemaType: "LocalBusiness"` gives name, phone, email and a one-line
656
+ address. Hours, geo, a structured postal address and the `Restaurant` subtype need pasted
657
+ `schema` built from the fields. Same name, address and phone everywhere. `Event` on events.
658
+ - **Redirects:** a single-location site that grows to several moves `/visit` to
659
+ `/locations/:slug` with a 301.
660
+ - **Pitfalls:** a menu as a PDF or image only; hours typed in the footer, the contact page and
661
+ the location page; prices as "$14" strings; "Closed" typed as a time; holiday hours
662
+ forgotten; Google reviews copied as testimonials (connect the source or get consent).
663
+
664
+ ## Events or conference
665
+
666
+ Use when the source shows dates with times, an agenda, speakers, tickets, a venue, sponsors, a
667
+ countdown.
668
+
669
+ ### Routes
670
+
671
+ | Family | Path | Model |
672
+ |---|---|---|
673
+ | Current edition home, tickets, travel, code of conduct, call for papers | `/`, `/tickets`, ... | Static pages |
674
+ | Event | `/events`, `/events/:slug` | Events collection |
675
+ | Session | `/sessions/:slug` | Sessions collection |
676
+ | Speaker | `/speakers`, `/speakers/:slug` | Speakers collection |
677
+ | Venue, sponsor | none unless the source has pages | Venues, Sponsors collections |
678
+
679
+ ### Collections
680
+
681
+ | Collection | Fields |
682
+ |---|---|
683
+ | Events | `"starts_at"`, `"ends_at"` `datetime`; `"time_zone"` `text`; `"status"` `select` (scheduled, postponed, rescheduled, cancelled, moved online); `"attendance"` `select` (in person, online, mixed); `"venue"` `reference`; `"excerpt"`; `"cover_image"`; `"body"` `document`; `"tickets_button"` `button`; `"tickets"` `repeater` (`"name"`, `"price"` `number`, `"currency"` `select`, `"availability"` `select`) only when no ticketing provider owns them; `"speakers"`, `"sponsors"` `multi-reference` |
684
+ | Sessions | `"event"` `reference`; `"starts_at"`, `"ends_at"`; `"room"` `text`; `"track"` `reference` or `select`; `"format"` `select` (talk, workshop, panel, break); `"level"` `select`; `"speakers"` `multi-reference`; `"description"` `richtext`; `"recording_url"` `link` |
685
+ | Speakers | Title is the name. `"role"`, `"company"`, `"photo"`, `"bio"`, `"social_links"` |
686
+ | Venues | `"address"` `group`, `"map_pin"` `location`, `"accessibility"`, `"directions"` `richtext` |
687
+ | Sponsors | `"logo"`, `"tier"` `select`, `"website"` `link` |
688
+
689
+ - **Chrome:** header with a tickets button; an announcement bar whose countdown end is one
690
+ `datetime` with its zone, and whose Section `window` ends with it. Never migrate a countdown
691
+ that resets per visitor.
692
+ - **Lists:** a `collection` block with `source` sorts by `"starts_at"`, but its filter values
693
+ are fixed: none means "today". Upcoming and past lists, and an agenda grouped by day and
694
+ track, are the site's code filtering on `"starts_at"`. State that in the report; do not fake
695
+ it with two hand-kept lists.
696
+ - **Forms:** registration (or the ticketing provider's embed), call for papers (links only, no
697
+ upload), sponsor enquiry, newsletter.
698
+ - **Structured data:** `schemaType: "Event"` gives name, dates and a location name. Event
699
+ status, attendance mode, a `Place` with address or a `VirtualLocation`, offers from visible
700
+ ticket prices and performers need pasted `schema`.
701
+ - **Redirects:** past events keep their URLs and stay published. A yearly path
702
+ (`/2025/schedule/talk-name`) becomes `/sessions/:slug` plus a 301.
703
+ - **Pitfalls:** times with no zone (ask when the site shows none); the agenda as an image or
704
+ PDF only; speakers retyped per session; past events deleted; a sponsor link with no
705
+ `rel="sponsored"` in the rendering code.
706
+
707
+ ## Course or education
708
+
709
+ Use when the source has courses with a curriculum, instructors, levels, durations, enrol
710
+ buttons, cohorts.
711
+
712
+ ### Routes
713
+
714
+ | Family | Path | Model |
715
+ |---|---|---|
716
+ | Home, catalogue hub, pricing, for teams, FAQ | `/`, `/courses`, ... | Static pages; the hub holds the `collection` block |
717
+ | Course | `/courses/:slug` | Courses collection |
718
+ | Instructor | `/instructors/:slug` | Instructors collection |
719
+ | Cohort | none | Cohorts collection, only when dated |
720
+
721
+ Lesson content behind a login belongs to the LMS, not BetterCMS.
722
+
723
+ ### Collections
724
+
725
+ | Collection | Fields |
726
+ |---|---|
727
+ | Courses | `"excerpt"`; `"cover_image"`; `"level"` `select`; `"duration"` `text` (ISO 8601, `PT6H30M`); `"category"` `reference`; `"instructors"` `multi-reference`; `"outcomes"`, `"prerequisites"` `array` of text; `"curriculum"` `repeater` (`"title"`, `"lessons"` `repeater`: `"title"`, `"duration"`, `"is_preview"` `boolean`); `"enrol_button"` `button`; `"lms_course_id"` `text`; `"price"` `number` only when no checkout owns it |
728
+ | Cohorts | `"course"` `reference`; `"starts_at"`, `"ends_at"` `datetime`; `"time_zone"` `text`; `"seats"` `text` only when shown; `"status"` `select` |
729
+ | Instructors | Title is the name. `"role"`, `"photo"`, `"bio"` |
730
+
731
+ - **Chrome:** header with a sign-in link to the LMS, footer.
732
+ - **Lists:** the catalogue is a `collection` block; category and level filter chips need the
733
+ site's code. A fixed category or level list on a hosted page is a block with `source` and a
734
+ filter. An instructor's courses are a `multi-reference` or code.
735
+ - **Forms:** waitlist, team enquiry, newsletter.
736
+ - **Structured data:** `Course` as the collection's `schemaType` (name, description, provider);
737
+ offers and course instances from cohorts need pasted `schema`. `FAQPage` on a course with a real FAQ.
738
+ - **Redirects:** LMS marketing paths (`/p/course-name`) become `/courses/:slug` plus 301s.
739
+ - **Pitfalls:** stored totals ("6 modules, 42 lessons, 8 hours"); lesson videos or paid content
740
+ copied in; cohort dates with no zone; prices copied from the checkout.
741
+
742
+ ## Job board or careers
743
+
744
+ Use when the source lists open roles by department, location tags, Apply buttons, Greenhouse,
745
+ Lever or Ashby links, salary ranges, `JobPosting` JSON-LD.
746
+
747
+ ### Routes
748
+
749
+ | Family | Path | Model |
750
+ |---|---|---|
751
+ | Careers home, benefits, culture, hiring process | `/careers`, ... | Static pages |
752
+ | Job | `/careers/:slug` or `/jobs/:slug` | Jobs collection |
753
+ | Department | `/departments/:slug`, only if the source has them | Departments collection |
754
+ | Employer (multi-company boards) | `/companies/:slug` | Employers collection |
755
+
756
+ ### Collections
757
+
758
+ | Collection | Fields |
759
+ |---|---|
760
+ | Jobs | `"department"` `reference`; `"offices"` `multi-reference` to Locations; `"workplace"` `select` (on site, hybrid, remote); `"employment_type"` `select` (full time, part time, contract, internship); `"salary"` `group` (`"min"`, `"max"` `number`, `"currency"`, `"period"` `select`) only when shown; `"excerpt"`; `"body"` `document`; `"apply_button"` `button` (the ATS URL); `"posted_on"` `date`; `"closes_at"` `datetime`; `"status"` `select` (open, closed); `"ats_job_id"` `text` |
761
+ | Departments | `"intro"` `longtext` |
762
+ | Employers | `"logo"`, `"about"` `richtext`, `"website"` `link` |
763
+
764
+ - **Chrome:** the header carries a "Careers" link; no separate chrome.
765
+ - **Lists:** open roles grouped by department with location filter chips need the site's code.
766
+ On a hosted page a `collection` block with `source` and a filter on `"status"` lists the open
767
+ jobs, one block per department when the source groups them; set `emptyText` for "no open
768
+ roles".
769
+ - **Forms:** applications go to the ATS. Forms have no file upload, so never collect a CV in a
770
+ BetterCMS form. A "Stay in touch" form is fine.
771
+ - **Structured data:** `JobPosting` as the collection's `schemaType`: it fills title,
772
+ description, posted date, valid through, employment type, hiring organisation and job location
773
+ from the entry's fields, and is not emitted without a company and a posting date. A remote type
774
+ with applicant location requirements, or a base salary, needs pasted `schema`, only when
775
+ visible. Unpublish the entry when the job closes.
776
+ - **Redirects:** closed jobs keep their URLs. ATS-hosted paths that move onto the site get a
777
+ 301 where the old host allows one.
778
+ - **Pitfalls:** hand-copying an ATS (sync by `"ats_job_id"` in code or link out, and report
779
+ which); deleting closed jobs; one job duplicated per city; invented salaries.
780
+
781
+ ## Real estate or property listings
782
+
783
+ Use when the source has properties with price, beds, baths, area, status, galleries, floor
784
+ plans, map search, agents, a viewing request form.
785
+
786
+ ### Routes
787
+
788
+ | Family | Path | Model |
789
+ |---|---|---|
790
+ | Home, valuation, sell, let, about, contact | `/`, `/valuation`, ... | Static pages |
791
+ | Property | `/properties`, `/properties/:slug` | Properties collection |
792
+ | Area guide | `/areas/:slug` | Areas collection |
793
+ | Agent | `/agents/:slug` | Agents collection |
794
+ | Office | `/offices/:slug` | Offices collection, fields as Locations in [Local business](#local-business-or-restaurant) |
795
+
796
+ ### Collections
797
+
798
+ | Collection | Fields |
799
+ |---|---|
800
+ | Properties | Title is the display address or name. `"status"` `select` (for sale, for rent, under offer, sold, let); `"listing_type"` `select` (sale, rent); `"price"` `number`; `"currency"` `select`; `"price_period"` `select` (month, week); `"price_qualifier"` `select` (guide price, offers over, price on application); `"address"` `group`; `"map_pin"` `location`; `"property_type"` `select`; `"bedrooms"`, `"bathrooms"`, `"receptions"`, `"area"` `number`; `"area_unit"` `select`; `"features"` `array` of text; `"body"` `document`; `"gallery"` `repeater` (`"image"`, `"caption"`; at least one); `"floor_plans"` `repeater` of images with alt; `"virtual_tour"` `link`; `"energy_rating"` `select`; `"facts"` `repeater` (`"label"`, `"value"`); `"agent"`, `"office"`, `"area_guide"` `reference`; `"available_from"` `date`; `"feed_id"` `text` |
801
+ | Agents | Title is the name. `"role"`, `"photo"`, `"phone"` `phone`, `"email"` `email`, `"office"` `reference` |
802
+ | Areas | `"intro"`, `"cover_image"`, `"body"` `document`, `"map_pin"` `location` |
803
+
804
+ - **Chrome:** header with a valuation button, footer listing offices.
805
+ - **Lists:** search by area, type, price band and bedrooms, the map view and "similar
806
+ properties" need the site's code or the CRM's search. On a hosted page a `collection` block
807
+ with `source` gives a fixed list (one area, one type), never a visitor's search.
808
+ - **Forms:** viewing request with the property passed in a `hidden` field, valuation request,
809
+ contact.
810
+ - **Structured data:** `RealEstateListing` with an `Offer` is pasted `schema` from visible
811
+ facts. `schemaType: "LocalBusiness"` on offices. A portal copy keeps the canonical on this
812
+ site.
813
+ - **Redirects:** sold and let properties keep their URLs and stay published. Portal-style
814
+ paths (`/for-sale/london/3-bed-flat-12345`) become `/properties/:slug` plus 301s.
815
+ - **Pitfalls:** prices as strings ("£450,000"); status typed into the title ("SOLD - 3
816
+ bed..."); listings hand-imported when a CRM feed owns them (sync by `"feed_id"` in code and
817
+ report it); images hotlinked from the feed host; floor plans with no alt text; sold
818
+ properties deleted.
819
+
820
+ ## Nonprofit
821
+
822
+ Use when the source has a Donate button, programs, impact numbers, stories, annual reports,
823
+ volunteers, a charity number.
824
+
825
+ ### Routes
826
+
827
+ | Family | Path | Model |
828
+ |---|---|---|
829
+ | Home, mission, impact, donate, get involved, contact | `/`, `/donate`, ... | Static pages |
830
+ | Program or campaign | `/programs/:slug` | Programs collection |
831
+ | Story, news | `/stories/:slug` | Stories collection, post fields from [Blog](#blog-or-publication) |
832
+ | Report | `/reports/:slug` | Reports collection |
833
+ | Events | `/events/:slug` | As [Events](#events-or-conference) |
834
+ | Team and board, partners | none unless the source has pages | People, Partners collections |
835
+
836
+ ### Collections
837
+
838
+ | Collection | Fields |
839
+ |---|---|
840
+ | Programs | `"excerpt"`; `"cover_image"`; `"body"` `document`; `"impact"` `repeater` (`"value"`, `"label"`, `"as_of"` `date`, `"source"` `link`); `"place"` `reference`; `"is_active"` `boolean`; `"donate_button"` `button` |
841
+ | Stories | Post fields, plus `"consented_on"` `date` for any person shown |
842
+ | Reports | `"year"` `number`; `"file"` `file`; `"cover_image"`; `"summary"` `richtext` holding the key facts as text |
843
+ | Partners | `"logo"`, `"tier"` `select`, `"website"` `link` |
844
+ | People | `"role"`, `"photo"`, `"bio"`, `"team"` `select` (staff, board, advisors) |
845
+
846
+ - **Chrome:** header with a Donate button; footer with the registered charity number, kept
847
+ once in the Global Layout.
848
+ - **Donations:** the payment provider (Stripe, Donorbox, Classy, GiveWP) owns amounts,
849
+ recurring options and payment fields. Store the provider and its id, plus the copy around
850
+ the widget. The widget script stays code.
851
+ - **Forms:** volunteer sign-up, newsletter, contact, event registration. Never payment details.
852
+ - **Structured data:** `Organization` in `siteSchema` (`NGO` when it fits) with the registered
853
+ number, `Article` on stories, `Event` on events.
854
+ - **Redirects:** campaign microsite URLs that fold into the main site get 301s to the program.
855
+ - **Pitfalls:** impact numbers with no date or source (fill `"as_of"` and `"source"`, report
856
+ the rest as unverified); beneficiary photos and stories with no recorded consent (report as
857
+ rights unconfirmed); annual reports only as PDFs.
858
+
859
+ ## Multi-brand and multi-language
860
+
861
+ Use when the source has several brands on one design system, `hreflang` alternates, `/de/` and
862
+ `/fr/` paths or country domains, or a language switcher.
863
+
864
+ ### Multi-brand
865
+
866
+ | Decision | Rule |
867
+ |---|---|
868
+ | Project boundary | One project per brand and domain. Each has its own brand kit, tokens, Global Layout and SEO settings |
869
+ | Starting a new brand | Build one template project, then `clone_project` per brand. The clone copies pages, models, entries, components, media, forms and SEO settings; it leaves out submissions, analytics, domains and secrets |
870
+ | Shared sections | The cloned components. After the clone they are separate copies: a later change is applied per project |
871
+ | Shared content | Projects cannot reference each other's entries. Copy it and note where it came from, or serve it from one project that every brand's code reads. Report the choice |
872
+ | Routes | Each brand keeps its own routes and `urlPattern` values; identical patterns across brands are fine |
873
+ | Structured data | One `Organization` per brand in its own `siteSchema`. A parent company is a `parentOrganization` property, not a shared node |
874
+
875
+ ### Multi-language
876
+
877
+ Full method: [localization.md](localization.md). The decisions that shape a blueprint:
878
+
879
+ - A translation is its own page or entry carrying `locale` and `translationOf` (the source id).
880
+ Per-field localization does not exist. Never model `"title_en"` beside `"title_fr"`.
881
+ - When crawls disagree on the default locale, take the one the root path serves and report
882
+ it; do not ask. The default fills the source rows; every other locale links back to them.
883
+ - Match sections across locales by position and structure, never by text.
884
+ - Never translate during a conversion. Copy what each locale publishes; report missing
885
+ translations. `translate_entry` creates drafts, costs AI credits and runs only on request.
886
+ - Slugs are per locale; keep each locale's URLs. Alt text is copy and is translated; the image
887
+ is shared unless a locale shows a different one.
888
+ - `hreflang` is never a field. The sitemap emits it from `locale` and `translationOf`; the
889
+ hosted head does not, so a site that needs it in the head renders the links in its own code.
890
+ - A market page with a different structure (a US-only pricing page) is a separate page, not a
891
+ translation. A per-market header is a Layout locale variant, not a translation of copy.
892
+ - **Structured data:** one canonical per locale. Never canonical every locale to the default.
893
+ - **Redirects:** no IP-based redirects. A retired locale 301s to the default locale's page.
894
+ - **Pitfalls:** machine translation during import; one model with a field per language; a
895
+ switcher linking to a page that exists in one locale only.