@bettercms-ai/kit 1.0.0 → 2.0.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.
- package/README.md +33 -0
- package/dist/adapters/dock-values.d.ts +28 -0
- package/dist/adapters/dock-values.js +41 -0
- package/dist/adapters/dock-values.js.map +1 -0
- package/{adapters → dist/adapters}/icons.js +2 -2
- package/dist/adapters/icons.js.map +1 -0
- package/{adapters → dist/adapters}/lossy.d.ts +90 -31
- package/dist/adapters/lossy.js +414 -0
- package/dist/adapters/lossy.js.map +1 -0
- package/{adapters → dist/adapters}/prod-types.d.ts +82 -4
- package/{adapters → dist/adapters}/prod-types.js +71 -3
- package/dist/adapters/prod-types.js.map +1 -0
- package/{adapters → dist/adapters}/schema.d.ts +40 -1
- package/{adapters → dist/adapters}/schema.js +996 -179
- package/dist/adapters/schema.js.map +1 -0
- package/{adapters → dist/adapters}/values.d.ts +26 -5
- package/{adapters → dist/adapters}/values.js +87 -17
- package/dist/adapters/values.js.map +1 -0
- package/dist/brand/logo.js +96 -0
- package/dist/brand/logo.js.map +1 -0
- package/{context.d.ts → dist/context.d.ts} +92 -2
- package/{context.js → dist/context.js} +5 -2
- package/dist/context.js.map +1 -0
- package/{controls → dist/controls}/button.d.ts +19 -5
- package/{controls → dist/controls}/button.js +125 -54
- package/dist/controls/button.js.map +1 -0
- package/{controls → dist/controls}/c1.js +1 -1
- package/dist/controls/c1.js.map +1 -0
- package/{controls → dist/controls}/c2.js +1 -1
- package/dist/controls/c2.js.map +1 -0
- package/{controls → dist/controls}/choice.d.ts +7 -2
- package/{controls → dist/controls}/choice.js +71 -18
- package/dist/controls/choice.js.map +1 -0
- package/{controls → dist/controls}/code-register.js +1 -1
- package/dist/controls/code-register.js.map +1 -0
- package/{controls → dist/controls}/code.js +10 -9
- package/dist/controls/code.js.map +1 -0
- package/{controls → dist/controls}/color.js +8 -8
- package/dist/controls/color.js.map +1 -0
- package/{controls → dist/controls}/component-services.d.ts +42 -0
- package/dist/controls/component-services.js +7 -0
- package/dist/controls/component-services.js.map +1 -0
- package/{controls → dist/controls}/date-register.js +1 -1
- package/dist/controls/date-register.js.map +1 -0
- package/{controls → dist/controls}/date-zone.js +8 -2
- package/dist/controls/date-zone.js.map +1 -0
- package/{controls → dist/controls}/date.js +32 -24
- package/dist/controls/date.js.map +1 -0
- package/{controls → dist/controls}/form-picker-register.js +1 -1
- package/dist/controls/form-picker-register.js.map +1 -0
- package/{controls → dist/controls}/form-picker.js +11 -9
- package/dist/controls/form-picker.js.map +1 -0
- package/{controls → dist/controls}/icon-register.js +1 -1
- package/dist/controls/icon-register.js.map +1 -0
- package/{controls → dist/controls}/icon.d.ts +4 -2
- package/{controls → dist/controls}/icon.js +32 -27
- package/dist/controls/icon.js.map +1 -0
- package/{controls → dist/controls}/index.d.ts +1 -1
- package/{controls → dist/controls}/index.js +4 -3
- package/dist/controls/index.js.map +1 -0
- package/{controls → dist/controls}/json-register.js +1 -1
- package/dist/controls/json-register.js.map +1 -0
- package/{controls → dist/controls}/json.js +13 -3
- package/dist/controls/json.js.map +1 -0
- package/{controls → dist/controls}/link.js +6 -6
- package/dist/controls/link.js.map +1 -0
- package/{controls → dist/controls}/location-register.js +1 -1
- package/dist/controls/location-register.js.map +1 -0
- package/{controls → dist/controls}/location.js +64 -40
- package/dist/controls/location.js.map +1 -0
- package/{controls → dist/controls}/media-register.js +1 -1
- package/dist/controls/media-register.js.map +1 -0
- package/{controls → dist/controls}/media-services.d.ts +6 -0
- package/dist/controls/media-services.js +0 -0
- package/{controls → dist/controls}/media.d.ts +35 -4
- package/{controls → dist/controls}/media.js +491 -171
- package/dist/controls/media.js.map +1 -0
- package/{controls → dist/controls}/number.js +10 -9
- package/dist/controls/number.js.map +1 -0
- package/{controls → dist/controls}/reference-register.js +1 -1
- package/dist/controls/reference-register.js.map +1 -0
- package/{controls → dist/controls}/reference-services.d.ts +4 -0
- package/dist/controls/reference-services.js +0 -0
- package/{controls → dist/controls}/reference.d.ts +1 -1
- package/{controls → dist/controls}/reference.js +93 -77
- package/dist/controls/reference.js.map +1 -0
- package/dist/controls/rich-text/blocks.d.ts +49 -0
- package/{controls → dist/controls}/rich-text/blocks.js +471 -144
- package/dist/controls/rich-text/blocks.js.map +1 -0
- package/{controls → dist/controls}/rich-text/catalog.d.ts +4 -1
- package/{controls → dist/controls}/rich-text/catalog.js +75 -65
- package/dist/controls/rich-text/catalog.js.map +1 -0
- package/{controls → dist/controls}/rich-text/codec.d.ts +9 -1
- package/{controls → dist/controls}/rich-text/codec.js +3 -1
- package/dist/controls/rich-text/codec.js.map +1 -0
- package/{controls → dist/controls}/rich-text/component-card.d.ts +1 -1
- package/{controls → dist/controls}/rich-text/component-card.js +417 -99
- package/dist/controls/rich-text/component-card.js.map +1 -0
- package/dist/controls/rich-text/component-panel.d.ts +47 -0
- package/dist/controls/rich-text/component-panel.js +141 -0
- package/dist/controls/rich-text/component-panel.js.map +1 -0
- package/{controls → dist/controls}/rich-text/control.d.ts +1 -2
- package/{controls → dist/controls}/rich-text/control.js +84 -42
- package/dist/controls/rich-text/control.js.map +1 -0
- package/{controls → dist/controls}/rich-text/dom.d.ts +52 -9
- package/{controls → dist/controls}/rich-text/dom.js +379 -50
- package/dist/controls/rich-text/dom.js.map +1 -0
- package/{controls → dist/controls}/rich-text/editor.d.ts +7 -0
- package/{controls → dist/controls}/rich-text/editor.js +148 -48
- package/dist/controls/rich-text/editor.js.map +1 -0
- package/{controls → dist/controls}/rich-text/floating.d.ts +6 -1
- package/{controls → dist/controls}/rich-text/floating.js +35 -18
- package/dist/controls/rich-text/floating.js.map +1 -0
- package/{controls → dist/controls}/rich-text/index.js +1 -1
- package/dist/controls/rich-text/index.js.map +1 -0
- package/dist/controls/rich-text/link-form.js +147 -0
- package/dist/controls/rich-text/link-form.js.map +1 -0
- package/{controls → dist/controls}/rich-text/markdown.js +4 -2
- package/dist/controls/rich-text/markdown.js.map +1 -0
- package/{controls → dist/controls}/rich-text/ops.js +17 -15
- package/dist/controls/rich-text/ops.js.map +1 -0
- package/dist/controls/rich-text/plain.d.ts +17 -0
- package/dist/controls/rich-text/plain.js +44 -0
- package/dist/controls/rich-text/plain.js.map +1 -0
- package/{controls → dist/controls}/rich-text/toolbar.d.ts +6 -2
- package/{controls → dist/controls}/rich-text/toolbar.js +12 -7
- package/dist/controls/rich-text/toolbar.js.map +1 -0
- package/{controls → dist/controls}/scale.js +5 -5
- package/dist/controls/scale.js.map +1 -0
- package/{controls → dist/controls}/slot-register.js +1 -1
- package/dist/controls/slot-register.js.map +1 -0
- package/{controls → dist/controls}/slot-services.d.ts +25 -0
- package/dist/controls/slot-services.js +155 -0
- package/dist/controls/slot-services.js.map +1 -0
- package/{controls → dist/controls}/slot.js +42 -24
- package/dist/controls/slot.js.map +1 -0
- package/{controls → dist/controls}/table/index.js +1 -1
- package/dist/controls/table/index.js.map +1 -0
- package/{controls → dist/controls}/table/prod.js +3 -3
- package/dist/controls/table/prod.js.map +1 -0
- package/{controls → dist/controls}/table/refs.js +13 -2
- package/dist/controls/table/refs.js.map +1 -0
- package/{controls → dist/controls}/table/stored.js +1 -1
- package/dist/controls/table/stored.js.map +1 -0
- package/{controls → dist/controls}/table/table-editor.js +51 -48
- package/dist/controls/table/table-editor.js.map +1 -0
- package/{controls → dist/controls}/table/table-link.js +3 -3
- package/dist/controls/table/table-link.js.map +1 -0
- package/{controls → dist/controls}/table/table.js +3 -3
- package/dist/controls/table/table.js.map +1 -0
- package/{controls → dist/controls}/text.d.ts +2 -2
- package/{controls → dist/controls}/text.js +78 -43
- package/dist/controls/text.js.map +1 -0
- package/dist/controls/unknown-value.d.ts +17 -0
- package/dist/controls/unknown-value.js +66 -0
- package/dist/controls/unknown-value.js.map +1 -0
- package/dist/core/core.d.ts +63 -0
- package/dist/core/core.js +171 -0
- package/dist/core/core.js.map +1 -0
- package/dist/core/facets.d.ts +347 -0
- package/dist/core/facets.js +334 -0
- package/dist/core/facets.js.map +1 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +3 -0
- package/{engine → dist/engine}/array.js +34 -21
- package/dist/engine/array.js.map +1 -0
- package/{engine → dist/engine}/bits.js +1 -1
- package/dist/engine/bits.js.map +1 -0
- package/{engine → dist/engine}/breadcrumb.d.ts +3 -3
- package/{engine → dist/engine}/breadcrumb.js +12 -36
- package/dist/engine/breadcrumb.js.map +1 -0
- package/{engine → dist/engine}/expand.d.ts +2 -4
- package/{engine → dist/engine}/expand.js +5 -22
- package/dist/engine/expand.js.map +1 -0
- package/{engine → dist/engine}/field-list.js +1 -1
- package/dist/engine/field-list.js.map +1 -0
- package/{engine → dist/engine}/field-shell.js +249 -121
- package/dist/engine/field-shell.js.map +1 -0
- package/{engine → dist/engine}/frame.d.ts +3 -1
- package/{engine → dist/engine}/frame.js +16 -14
- package/dist/engine/frame.js.map +1 -0
- package/{engine → dist/engine}/group-summary.d.ts +2 -1
- package/{engine → dist/engine}/group-summary.js +7 -3
- package/dist/engine/group-summary.js.map +1 -0
- package/{engine → dist/engine}/grouping.d.ts +19 -5
- package/{engine → dist/engine}/grouping.js +193 -21
- package/dist/engine/grouping.js.map +1 -0
- package/{engine → dist/engine}/item-list.d.ts +11 -1
- package/{engine → dist/engine}/item-list.js +35 -13
- package/dist/engine/item-list.js.map +1 -0
- package/{engine → dist/engine}/kit-field.js +45 -10
- package/dist/engine/kit-field.js.map +1 -0
- package/{engine → dist/engine}/schema-form.d.ts +27 -2
- package/{engine → dist/engine}/schema-form.js +182 -40
- package/dist/engine/schema-form.js.map +1 -0
- package/{engine → dist/engine}/slot.js +5 -15
- package/dist/engine/slot.js.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/{kit.css → dist/kit.css} +19 -0
- package/{lib → dist/lib}/cn.js +7 -4
- package/dist/lib/cn.js.map +1 -0
- package/{lib → dist/lib}/columns.js +4 -1
- package/dist/lib/columns.js.map +1 -0
- package/{lib → dist/lib}/frames.js +2 -2
- package/dist/lib/frames.js.map +1 -0
- package/dist/lib/icon-image.d.ts +10 -0
- package/dist/lib/icon-image.js +8 -0
- package/dist/lib/icon-image.js.map +1 -0
- package/{lib → dist/lib}/icons.js +4 -3
- package/dist/lib/icons.js.map +1 -0
- package/dist/lib/input-modality.d.ts +11 -0
- package/dist/lib/input-modality.js +47 -0
- package/dist/lib/input-modality.js.map +1 -0
- package/dist/lib/key-event.d.ts +11 -0
- package/dist/lib/key-event.js +16 -0
- package/dist/lib/key-event.js.map +1 -0
- package/{lib → dist/lib}/path.js +4 -2
- package/dist/lib/path.js.map +1 -0
- package/{lib → dist/lib}/schema.d.ts +34 -3
- package/{lib → dist/lib}/schema.js +110 -13
- package/dist/lib/schema.js.map +1 -0
- package/{lib → dist/lib}/utils.js +1 -1
- package/dist/lib/utils.js.map +1 -0
- package/{lib → dist/lib}/validate.d.ts +11 -1
- package/{lib → dist/lib}/validate.js +85 -11
- package/dist/lib/validate.js.map +1 -0
- package/dist/library/analysis.d.ts +23 -0
- package/dist/library/analysis.js +160 -0
- package/dist/library/analysis.js.map +1 -0
- package/dist/library/catalog.d.ts +57 -0
- package/dist/library/catalog.js +307 -0
- package/dist/library/catalog.js.map +1 -0
- package/dist/library/coreNotes.d.ts +8 -0
- package/dist/library/coreNotes.js +111 -0
- package/dist/library/coreNotes.js.map +1 -0
- package/dist/library/coverage.d.ts +40 -0
- package/dist/library/coverage.js +59 -0
- package/dist/library/coverage.js.map +1 -0
- package/dist/library/entries/advanced.entries.d.ts +2 -0
- package/dist/library/entries/advanced.entries.js +6579 -0
- package/dist/library/entries/advanced.entries.js.map +1 -0
- package/dist/library/entries/commerce.entries.d.ts +2 -0
- package/dist/library/entries/commerce.entries.js +4140 -0
- package/dist/library/entries/commerce.entries.js.map +1 -0
- package/dist/library/entries/company.entries.d.ts +2 -0
- package/dist/library/entries/company.entries.js +2731 -0
- package/dist/library/entries/company.entries.js.map +1 -0
- package/dist/library/entries/conversion.entries.d.ts +2 -0
- package/dist/library/entries/conversion.entries.js +3707 -0
- package/dist/library/entries/conversion.entries.js.map +1 -0
- package/dist/library/entries/data.entries.d.ts +7 -0
- package/dist/library/entries/data.entries.js +4613 -0
- package/dist/library/entries/data.entries.js.map +1 -0
- package/dist/library/entries/events.entries.d.ts +2 -0
- package/dist/library/entries/events.entries.js +3428 -0
- package/dist/library/entries/events.entries.js.map +1 -0
- package/dist/library/entries/features.entries.d.ts +2 -0
- package/dist/library/entries/features.entries.js +3157 -0
- package/dist/library/entries/features.entries.js.map +1 -0
- package/dist/library/entries/heroes.entries.d.ts +2 -0
- package/dist/library/entries/heroes.entries.js +3401 -0
- package/dist/library/entries/heroes.entries.js.map +1 -0
- package/dist/library/entries/index.d.ts +3 -0
- package/dist/library/entries/index.js +35 -0
- package/dist/library/entries/index.js.map +1 -0
- package/dist/library/entries/listings.entries.d.ts +2 -0
- package/dist/library/entries/listings.entries.js +2976 -0
- package/dist/library/entries/listings.entries.js.map +1 -0
- package/dist/library/entries/longform.entries.d.ts +2 -0
- package/dist/library/entries/longform.entries.js +4227 -0
- package/dist/library/entries/longform.entries.js.map +1 -0
- package/dist/library/entries/media.entries.d.ts +2 -0
- package/dist/library/entries/media.entries.js +2781 -0
- package/dist/library/entries/media.entries.js.map +1 -0
- package/dist/library/entries/navigation.entries.d.ts +2 -0
- package/dist/library/entries/navigation.entries.js +4011 -0
- package/dist/library/entries/navigation.entries.js.map +1 -0
- package/dist/library/entries/pricing.entries.d.ts +2 -0
- package/dist/library/entries/pricing.entries.js +2383 -0
- package/dist/library/entries/pricing.entries.js.map +1 -0
- package/dist/library/entries/social-proof.entries.d.ts +2 -0
- package/dist/library/entries/social-proof.entries.js +2297 -0
- package/dist/library/entries/social-proof.entries.js.map +1 -0
- package/dist/library/examples/bento.d.ts +92 -0
- package/dist/library/examples/bento.js +201 -0
- package/dist/library/examples/bento.js.map +1 -0
- package/dist/library/examples/blocks.d.ts +14 -0
- package/dist/library/examples/blocks.js +313 -0
- package/dist/library/examples/blocks.js.map +1 -0
- package/dist/library/examples/faq.d.ts +28 -0
- package/dist/library/examples/faq.js +230 -0
- package/dist/library/examples/faq.js.map +1 -0
- package/dist/library/examples/hero.d.ts +43 -0
- package/dist/library/examples/hero.js +222 -0
- package/dist/library/examples/hero.js.map +1 -0
- package/dist/library/examples/index.d.ts +36 -0
- package/dist/library/examples/index.js +109 -0
- package/dist/library/examples/index.js.map +1 -0
- package/dist/library/examples/journalStory.d.ts +93 -0
- package/dist/library/examples/journalStory.js +338 -0
- package/dist/library/examples/journalStory.js.map +1 -0
- package/dist/library/examples/megaMenu.d.ts +134 -0
- package/dist/library/examples/megaMenu.js +442 -0
- package/dist/library/examples/megaMenu.js.map +1 -0
- package/dist/library/examples/pricing.d.ts +103 -0
- package/dist/library/examples/pricing.js +479 -0
- package/dist/library/examples/pricing.js.map +1 -0
- package/dist/library/examples/tabs.d.ts +87 -0
- package/dist/library/examples/tabs.js +180 -0
- package/dist/library/examples/tabs.js.map +1 -0
- package/dist/library/export.d.ts +42 -0
- package/dist/library/export.js +80 -0
- package/dist/library/export.js.map +1 -0
- package/dist/library/index.d.ts +13 -0
- package/dist/library/index.js +6 -0
- package/dist/library/kit-catalog.json +1510 -0
- package/dist/library/types.d.ts +42 -0
- package/dist/library/types.js +0 -0
- package/{schema → dist/schema}/adapters.js +1 -2
- package/dist/schema/adapters.js.map +1 -0
- package/{schema → dist/schema}/help.d.ts +2 -0
- package/{schema → dist/schema}/help.js +6 -4
- package/dist/schema/help.js.map +1 -0
- package/{schema → dist/schema}/scale.js +1 -1
- package/dist/schema/scale.js.map +1 -0
- package/{schema → dist/schema}/sources.js +1 -1
- package/dist/schema/sources.js.map +1 -0
- package/{schema → dist/schema}/table.js +11 -10
- package/dist/schema/table.js.map +1 -0
- package/{schema → dist/schema}/types.d.ts +29 -1
- package/{schema → dist/schema}/types.js +1 -1
- package/dist/schema/types.js.map +1 -0
- package/dist/styles.css +2 -0
- package/dist/table/table-view.d.ts +14 -0
- package/dist/table/table-view.js +140 -0
- package/dist/table/table-view.js.map +1 -0
- package/dist/table/table.css +73 -0
- package/dist/testing/corpus.d.ts +63 -0
- package/dist/testing/corpus.js +129 -0
- package/dist/testing/corpus.js.map +1 -0
- package/dist/testing/test-form.d.ts +11 -0
- package/dist/testing/test-form.js +38 -0
- package/dist/testing/test-form.js.map +1 -0
- package/dist/theme.css +345 -0
- package/dist/theme.d.ts +22 -0
- package/dist/theme.js +43 -0
- package/dist/theme.js.map +1 -0
- package/dist/tokens.css +542 -0
- package/dist/types.js +0 -0
- package/{ui → dist/ui}/action-tray.d.ts +1 -1
- package/{ui → dist/ui}/action-tray.js +20 -19
- package/dist/ui/action-tray.js.map +1 -0
- package/dist/ui/autocomplete.d.ts +30 -0
- package/dist/ui/autocomplete.js +139 -0
- package/dist/ui/autocomplete.js.map +1 -0
- package/{ui → dist/ui}/button.d.ts +3 -3
- package/{ui → dist/ui}/button.js +6 -7
- package/dist/ui/button.js.map +1 -0
- package/{ui → dist/ui}/choice-switch.js +1 -1
- package/dist/ui/choice-switch.js.map +1 -0
- package/{ui → dist/ui}/controls.js +51 -39
- package/dist/ui/controls.js.map +1 -0
- package/{ui → dist/ui}/display.d.ts +10 -3
- package/{ui → dist/ui}/display.js +57 -46
- package/dist/ui/display.js.map +1 -0
- package/{ui → dist/ui}/expanded-editor-modal.d.ts +2 -2
- package/{ui → dist/ui}/expanded-editor-modal.js +6 -6
- package/dist/ui/expanded-editor-modal.js.map +1 -0
- package/{ui → dist/ui}/field-context.js +1 -1
- package/dist/ui/field-context.js.map +1 -0
- package/{ui → dist/ui}/floating-panel.d.ts +1 -1
- package/{ui → dist/ui}/floating-panel.js +28 -12
- package/dist/ui/floating-panel.js.map +1 -0
- package/{ui → dist/ui}/index.d.ts +4 -0
- package/dist/ui/index.js +18 -0
- package/{ui → dist/ui}/input.d.ts +1 -1
- package/{ui → dist/ui}/input.js +9 -9
- package/dist/ui/input.js.map +1 -0
- package/{ui → dist/ui}/mode-bar.js +3 -3
- package/dist/ui/mode-bar.js.map +1 -0
- package/{ui → dist/ui}/overlay.d.ts +77 -5
- package/dist/ui/overlay.js +697 -0
- package/dist/ui/overlay.js.map +1 -0
- package/{ui → dist/ui}/path-crumbs.js +37 -3
- package/dist/ui/path-crumbs.js.map +1 -0
- package/{ui → dist/ui}/presence.js +2 -2
- package/dist/ui/presence.js.map +1 -0
- package/{ui → dist/ui}/select.js +89 -87
- package/dist/ui/select.js.map +1 -0
- package/{ui → dist/ui}/shortcut-hint.js +1 -1
- package/dist/ui/shortcut-hint.js.map +1 -0
- package/dist/ui/spinner.d.ts +24 -0
- package/dist/ui/spinner.js +42 -0
- package/dist/ui/spinner.js.map +1 -0
- package/dist/ui/split-button.d.ts +32 -0
- package/dist/ui/split-button.js +60 -0
- package/dist/ui/split-button.js.map +1 -0
- package/{ui → dist/ui}/surfaces.js +1 -1
- package/dist/ui/surfaces.js.map +1 -0
- package/dist/ui/tab.d.ts +38 -0
- package/dist/ui/tab.js +87 -0
- package/dist/ui/tab.js.map +1 -0
- package/dist/ui/toast.d.ts +20 -0
- package/dist/ui/toast.js +115 -0
- package/dist/ui/toast.js.map +1 -0
- package/{ui → dist/ui}/tooltip-root.d.ts +4 -3
- package/{ui → dist/ui}/tooltip-root.js +9 -8
- package/dist/ui/tooltip-root.js.map +1 -0
- package/{ui → dist/ui}/variants.js +1 -1
- package/dist/ui/variants.js.map +1 -0
- package/package.json +77 -23
- package/adapters/icons.js.map +0 -1
- package/adapters/lossy.js +0 -233
- package/adapters/lossy.js.map +0 -1
- package/adapters/prod-types.js.map +0 -1
- package/adapters/schema.js.map +0 -1
- package/adapters/values.js.map +0 -1
- package/brand/logo.js +0 -9
- package/brand/logo.js.map +0 -1
- package/context.js.map +0 -1
- package/controls/button.js.map +0 -1
- package/controls/c1.js.map +0 -1
- package/controls/c2.js.map +0 -1
- package/controls/choice.js.map +0 -1
- package/controls/code-register.js.map +0 -1
- package/controls/code.js.map +0 -1
- package/controls/color.js.map +0 -1
- package/controls/date-register.js.map +0 -1
- package/controls/date-zone.js.map +0 -1
- package/controls/date.js.map +0 -1
- package/controls/form-picker-register.js.map +0 -1
- package/controls/form-picker.js.map +0 -1
- package/controls/icon-register.js.map +0 -1
- package/controls/icon.js.map +0 -1
- package/controls/index.js.map +0 -1
- package/controls/json-register.js.map +0 -1
- package/controls/json.js.map +0 -1
- package/controls/link.js.map +0 -1
- package/controls/location-register.js.map +0 -1
- package/controls/location.js.map +0 -1
- package/controls/media-register.js.map +0 -1
- package/controls/media.js.map +0 -1
- package/controls/number.js.map +0 -1
- package/controls/reference-register.js.map +0 -1
- package/controls/reference.js.map +0 -1
- package/controls/rich-text/blocks.d.ts +0 -28
- package/controls/rich-text/blocks.js.map +0 -1
- package/controls/rich-text/catalog.js.map +0 -1
- package/controls/rich-text/codec.js.map +0 -1
- package/controls/rich-text/component-card.js.map +0 -1
- package/controls/rich-text/control.js.map +0 -1
- package/controls/rich-text/dom.js.map +0 -1
- package/controls/rich-text/editor.js.map +0 -1
- package/controls/rich-text/floating.js.map +0 -1
- package/controls/rich-text/index.js.map +0 -1
- package/controls/rich-text/link-form.js +0 -111
- package/controls/rich-text/link-form.js.map +0 -1
- package/controls/rich-text/markdown.js.map +0 -1
- package/controls/rich-text/ops.js.map +0 -1
- package/controls/rich-text/toolbar.js.map +0 -1
- package/controls/scale.js.map +0 -1
- package/controls/slot-register.js.map +0 -1
- package/controls/slot-services.js +0 -38
- package/controls/slot-services.js.map +0 -1
- package/controls/slot.js.map +0 -1
- package/controls/table/index.js.map +0 -1
- package/controls/table/prod.js.map +0 -1
- package/controls/table/refs.js.map +0 -1
- package/controls/table/stored.js.map +0 -1
- package/controls/table/table-editor.js.map +0 -1
- package/controls/table/table-link.js.map +0 -1
- package/controls/table/table.js.map +0 -1
- package/controls/text.js.map +0 -1
- package/engine/array.js.map +0 -1
- package/engine/bits.js.map +0 -1
- package/engine/breadcrumb.js.map +0 -1
- package/engine/expand.js.map +0 -1
- package/engine/field-list.js.map +0 -1
- package/engine/field-shell.js.map +0 -1
- package/engine/frame.js.map +0 -1
- package/engine/group-summary.js.map +0 -1
- package/engine/grouping.js.map +0 -1
- package/engine/item-list.js.map +0 -1
- package/engine/kit-field.js.map +0 -1
- package/engine/schema-form.js.map +0 -1
- package/engine/slot.js.map +0 -1
- package/index.js +0 -55
- package/index.js.map +0 -1
- package/lib/cn.js.map +0 -1
- package/lib/columns.js.map +0 -1
- package/lib/frames.js.map +0 -1
- package/lib/icons.js.map +0 -1
- package/lib/path.js.map +0 -1
- package/lib/schema.js.map +0 -1
- package/lib/utils.js.map +0 -1
- package/lib/validate.js.map +0 -1
- package/schema/adapters.js.map +0 -1
- package/schema/help.js.map +0 -1
- package/schema/scale.js.map +0 -1
- package/schema/sources.js.map +0 -1
- package/schema/table.js.map +0 -1
- package/schema/types.js.map +0 -1
- package/ui/action-tray.js.map +0 -1
- package/ui/button.js.map +0 -1
- package/ui/choice-switch.js.map +0 -1
- package/ui/controls.js.map +0 -1
- package/ui/display.js.map +0 -1
- package/ui/expanded-editor-modal.js.map +0 -1
- package/ui/field-context.js.map +0 -1
- package/ui/floating-panel.js.map +0 -1
- package/ui/index.js +0 -13
- package/ui/input.js.map +0 -1
- package/ui/mode-bar.js.map +0 -1
- package/ui/overlay.js +0 -378
- package/ui/overlay.js.map +0 -1
- package/ui/path-crumbs.js.map +0 -1
- package/ui/presence.js.map +0 -1
- package/ui/select.js.map +0 -1
- package/ui/shortcut-hint.js.map +0 -1
- package/ui/surfaces.js.map +0 -1
- package/ui/tooltip-root.js.map +0 -1
- package/ui/variants.js.map +0 -1
- /package/{_virtual → dist/_virtual}/_rolldown/runtime.js +0 -0
- /package/{adapters → dist/adapters}/icons.d.ts +0 -0
- /package/{brand → dist/brand}/logo.d.ts +0 -0
- /package/{controls → dist/controls}/c1.d.ts +0 -0
- /package/{controls → dist/controls}/c2.d.ts +0 -0
- /package/{controls → dist/controls}/code-register.d.ts +0 -0
- /package/{controls → dist/controls}/code.d.ts +0 -0
- /package/{controls → dist/controls}/color.d.ts +0 -0
- /package/{controls → dist/controls}/date-register.d.ts +0 -0
- /package/{controls → dist/controls}/date-zone.d.ts +0 -0
- /package/{controls → dist/controls}/date.d.ts +0 -0
- /package/{controls → dist/controls}/form-picker-register.d.ts +0 -0
- /package/{controls → dist/controls}/form-picker.d.ts +0 -0
- /package/{controls → dist/controls}/icon-register.d.ts +0 -0
- /package/{controls → dist/controls}/json-register.d.ts +0 -0
- /package/{controls → dist/controls}/json.d.ts +0 -0
- /package/{controls → dist/controls}/link.d.ts +0 -0
- /package/{controls → dist/controls}/location-register.d.ts +0 -0
- /package/{controls → dist/controls}/location.d.ts +0 -0
- /package/{controls → dist/controls}/media-register.d.ts +0 -0
- /package/{controls → dist/controls}/number.d.ts +0 -0
- /package/{controls → dist/controls}/reference-register.d.ts +0 -0
- /package/{controls → dist/controls}/rich-text/index.d.ts +0 -0
- /package/{controls → dist/controls}/rich-text/link-form.d.ts +0 -0
- /package/{controls → dist/controls}/rich-text/markdown.d.ts +0 -0
- /package/{controls → dist/controls}/rich-text/ops.d.ts +0 -0
- /package/{controls → dist/controls}/scale.d.ts +0 -0
- /package/{controls → dist/controls}/slot-register.d.ts +0 -0
- /package/{controls → dist/controls}/slot.d.ts +0 -0
- /package/{controls → dist/controls}/table/index.d.ts +0 -0
- /package/{controls → dist/controls}/table/prod.d.ts +0 -0
- /package/{controls → dist/controls}/table/refs.d.ts +0 -0
- /package/{controls → dist/controls}/table/stored.d.ts +0 -0
- /package/{controls → dist/controls}/table/table-editor.d.ts +0 -0
- /package/{controls → dist/controls}/table/table-link.d.ts +0 -0
- /package/{controls → dist/controls}/table/table.d.ts +0 -0
- /package/{engine → dist/engine}/array.d.ts +0 -0
- /package/{engine → dist/engine}/bits.d.ts +0 -0
- /package/{engine → dist/engine}/field-list.d.ts +0 -0
- /package/{engine → dist/engine}/field-shell.d.ts +0 -0
- /package/{engine → dist/engine}/kit-field.d.ts +0 -0
- /package/{engine → dist/engine}/slot.d.ts +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{lib → dist/lib}/cn.d.ts +0 -0
- /package/{lib → dist/lib}/columns.d.ts +0 -0
- /package/{lib → dist/lib}/frames.d.ts +0 -0
- /package/{lib → dist/lib}/icons.d.ts +0 -0
- /package/{lib → dist/lib}/path.d.ts +0 -0
- /package/{lib → dist/lib}/utils.d.ts +0 -0
- /package/{schema → dist/schema}/adapters.d.ts +0 -0
- /package/{schema → dist/schema}/scale.d.ts +0 -0
- /package/{schema → dist/schema}/sources.d.ts +0 -0
- /package/{schema → dist/schema}/table.d.ts +0 -0
- /package/{types.d.ts → dist/types.d.ts} +0 -0
- /package/{ui → dist/ui}/choice-switch.d.ts +0 -0
- /package/{ui → dist/ui}/controls.d.ts +0 -0
- /package/{ui → dist/ui}/field-context.d.ts +0 -0
- /package/{ui → dist/ui}/mode-bar.d.ts +0 -0
- /package/{ui → dist/ui}/path-crumbs.d.ts +0 -0
- /package/{ui → dist/ui}/presence.d.ts +0 -0
- /package/{ui → dist/ui}/select.d.ts +0 -0
- /package/{ui → dist/ui}/shortcut-hint.d.ts +0 -0
- /package/{ui → dist/ui}/surfaces.d.ts +0 -0
- /package/{ui → dist/ui}/variants.d.ts +0 -0
|
@@ -0,0 +1,1510 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "GENERATED from the kit's Library (packages/kit) (KIT_LIBRARY + EXAMPLES) by scripts/kit-export.ts. Do not edit. Regenerate (bun run --cwd packages/kit kit:export).",
|
|
3
|
+
"fieldConfig": {
|
|
4
|
+
"read": {
|
|
5
|
+
"*": [
|
|
6
|
+
"placeholder",
|
|
7
|
+
"zones",
|
|
8
|
+
"options",
|
|
9
|
+
"fields",
|
|
10
|
+
"linkHref"
|
|
11
|
+
],
|
|
12
|
+
"text": [
|
|
13
|
+
"minLength",
|
|
14
|
+
"maxLength",
|
|
15
|
+
"pattern",
|
|
16
|
+
"patternMessage",
|
|
17
|
+
"inputType"
|
|
18
|
+
],
|
|
19
|
+
"longtext": [
|
|
20
|
+
"minLength",
|
|
21
|
+
"maxLength",
|
|
22
|
+
"pattern",
|
|
23
|
+
"patternMessage",
|
|
24
|
+
"inputType"
|
|
25
|
+
],
|
|
26
|
+
"url": [
|
|
27
|
+
"minLength",
|
|
28
|
+
"maxLength",
|
|
29
|
+
"pattern",
|
|
30
|
+
"patternMessage",
|
|
31
|
+
"inputType"
|
|
32
|
+
],
|
|
33
|
+
"richtext": [
|
|
34
|
+
"minLength",
|
|
35
|
+
"maxLength",
|
|
36
|
+
"pattern",
|
|
37
|
+
"patternMessage",
|
|
38
|
+
"inputType"
|
|
39
|
+
],
|
|
40
|
+
"document": [
|
|
41
|
+
"minLength",
|
|
42
|
+
"maxLength",
|
|
43
|
+
"pattern",
|
|
44
|
+
"patternMessage",
|
|
45
|
+
"inputType"
|
|
46
|
+
],
|
|
47
|
+
"number": [
|
|
48
|
+
"min",
|
|
49
|
+
"max",
|
|
50
|
+
"step",
|
|
51
|
+
"scale",
|
|
52
|
+
"unit",
|
|
53
|
+
"validation"
|
|
54
|
+
],
|
|
55
|
+
"rating": [
|
|
56
|
+
"min",
|
|
57
|
+
"max",
|
|
58
|
+
"step",
|
|
59
|
+
"scale",
|
|
60
|
+
"unit",
|
|
61
|
+
"validation"
|
|
62
|
+
],
|
|
63
|
+
"select": [
|
|
64
|
+
"multiSelect"
|
|
65
|
+
],
|
|
66
|
+
"multiselect": [
|
|
67
|
+
"min",
|
|
68
|
+
"max",
|
|
69
|
+
"allowCustom"
|
|
70
|
+
],
|
|
71
|
+
"tags": [
|
|
72
|
+
"min",
|
|
73
|
+
"max",
|
|
74
|
+
"allowCustom"
|
|
75
|
+
],
|
|
76
|
+
"code": [
|
|
77
|
+
"language",
|
|
78
|
+
"languages"
|
|
79
|
+
],
|
|
80
|
+
"date": [
|
|
81
|
+
"timezone",
|
|
82
|
+
"includeTime"
|
|
83
|
+
],
|
|
84
|
+
"datetime": [
|
|
85
|
+
"timezone",
|
|
86
|
+
"includeTime"
|
|
87
|
+
],
|
|
88
|
+
"reference": [
|
|
89
|
+
"contentModelId",
|
|
90
|
+
"polymorphic",
|
|
91
|
+
"allowedModelSlugs",
|
|
92
|
+
"targets",
|
|
93
|
+
"filter"
|
|
94
|
+
],
|
|
95
|
+
"multi-reference": [
|
|
96
|
+
"contentModelId",
|
|
97
|
+
"polymorphic",
|
|
98
|
+
"allowedModelSlugs",
|
|
99
|
+
"targets",
|
|
100
|
+
"filter",
|
|
101
|
+
"min",
|
|
102
|
+
"max"
|
|
103
|
+
],
|
|
104
|
+
"array": [
|
|
105
|
+
"itemType",
|
|
106
|
+
"minItems",
|
|
107
|
+
"maxItems",
|
|
108
|
+
"item"
|
|
109
|
+
],
|
|
110
|
+
"image": [
|
|
111
|
+
"media"
|
|
112
|
+
],
|
|
113
|
+
"modular": [
|
|
114
|
+
"blockSlugs",
|
|
115
|
+
"minItems",
|
|
116
|
+
"maxItems"
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
"host": {
|
|
120
|
+
"any": [
|
|
121
|
+
"sectionLabel",
|
|
122
|
+
"sectionShape",
|
|
123
|
+
"sectionTitle",
|
|
124
|
+
"format"
|
|
125
|
+
],
|
|
126
|
+
"richtext": [
|
|
127
|
+
"accept",
|
|
128
|
+
"allowMultipleParagraphs",
|
|
129
|
+
"inline",
|
|
130
|
+
"allowTargetBlank",
|
|
131
|
+
"components",
|
|
132
|
+
"hostTag",
|
|
133
|
+
"features"
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
"server": [
|
|
137
|
+
"seeded",
|
|
138
|
+
"shared",
|
|
139
|
+
"sharedValue",
|
|
140
|
+
"attr",
|
|
141
|
+
"numeric",
|
|
142
|
+
"iconFont",
|
|
143
|
+
"htmlClasses",
|
|
144
|
+
"units",
|
|
145
|
+
"lead",
|
|
146
|
+
"band",
|
|
147
|
+
"family",
|
|
148
|
+
"shape"
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
"categories": [
|
|
152
|
+
{
|
|
153
|
+
"name": "Navigation & chrome",
|
|
154
|
+
"slug": "navigation-chrome",
|
|
155
|
+
"entries": [
|
|
156
|
+
"nav-simple-cta",
|
|
157
|
+
"nav-announcement-utility",
|
|
158
|
+
"nav-mega-featured-promo",
|
|
159
|
+
"nav-docs-sidebar",
|
|
160
|
+
"nav-ecommerce-header",
|
|
161
|
+
"nav-locale-header",
|
|
162
|
+
"sticky-mobile-cta-bar",
|
|
163
|
+
"announcement-countdown",
|
|
164
|
+
"cookie-consent",
|
|
165
|
+
"breadcrumb-config",
|
|
166
|
+
"footer-multi-column",
|
|
167
|
+
"footer-minimal",
|
|
168
|
+
"footer-mega-locations"
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"name": "Heroes",
|
|
173
|
+
"slug": "heroes",
|
|
174
|
+
"entries": [
|
|
175
|
+
"hero-split-media",
|
|
176
|
+
"hero-centered-gradient",
|
|
177
|
+
"hero-video-fullbleed",
|
|
178
|
+
"hero-slider",
|
|
179
|
+
"hero-product",
|
|
180
|
+
"hero-search",
|
|
181
|
+
"hero-lead-form",
|
|
182
|
+
"hero-app-download",
|
|
183
|
+
"hero-stats",
|
|
184
|
+
"hero-logo-wall",
|
|
185
|
+
"hero-split-testimonial",
|
|
186
|
+
"hero-event",
|
|
187
|
+
"hero-personalized"
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "Features & storytelling",
|
|
192
|
+
"slug": "features-storytelling",
|
|
193
|
+
"entries": [
|
|
194
|
+
"feature-icon-grid",
|
|
195
|
+
"feature-alternating-rows",
|
|
196
|
+
"bento-feature-showcase",
|
|
197
|
+
"steps-how-it-works",
|
|
198
|
+
"timeline-company-history",
|
|
199
|
+
"tabs-with-media",
|
|
200
|
+
"accordion-feature-image-swap",
|
|
201
|
+
"comparison-vs-competitors",
|
|
202
|
+
"stats-band",
|
|
203
|
+
"manifesto-pull-quote",
|
|
204
|
+
"problem-solution",
|
|
205
|
+
"integrations-grid"
|
|
206
|
+
]
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"name": "Social proof",
|
|
210
|
+
"slug": "social-proof",
|
|
211
|
+
"entries": [
|
|
212
|
+
"testimonial-grid",
|
|
213
|
+
"testimonial-video-slider",
|
|
214
|
+
"wall-of-love",
|
|
215
|
+
"case-study-cards",
|
|
216
|
+
"logo-cloud",
|
|
217
|
+
"press-mentions",
|
|
218
|
+
"awards-certifications",
|
|
219
|
+
"review-summary"
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"name": "Media & galleries",
|
|
224
|
+
"slug": "media-galleries",
|
|
225
|
+
"entries": [
|
|
226
|
+
"masonry-gallery",
|
|
227
|
+
"lightbox-gallery-filters",
|
|
228
|
+
"video-section",
|
|
229
|
+
"before-after-slider",
|
|
230
|
+
"image-hotspots",
|
|
231
|
+
"interactive-3d-lottie-embed",
|
|
232
|
+
"mixed-media-carousel",
|
|
233
|
+
"parallax-image-overlay",
|
|
234
|
+
"ugc-social-feed"
|
|
235
|
+
]
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"name": "Commerce",
|
|
239
|
+
"slug": "commerce",
|
|
240
|
+
"entries": [
|
|
241
|
+
"commerce-product-grid-query",
|
|
242
|
+
"commerce-product-detail",
|
|
243
|
+
"commerce-featured-product",
|
|
244
|
+
"commerce-collection-header-facets",
|
|
245
|
+
"commerce-bundle-builder",
|
|
246
|
+
"commerce-cart-cross-sell",
|
|
247
|
+
"commerce-promo-countdown",
|
|
248
|
+
"commerce-shop-the-look",
|
|
249
|
+
"commerce-size-guide",
|
|
250
|
+
"commerce-store-locator",
|
|
251
|
+
"commerce-product-comparison"
|
|
252
|
+
]
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"name": "Pricing & plans",
|
|
256
|
+
"slug": "pricing-plans",
|
|
257
|
+
"entries": [
|
|
258
|
+
"pricing-tiers-billing-toggle",
|
|
259
|
+
"pricing-usage-calculator",
|
|
260
|
+
"pricing-feature-matrix",
|
|
261
|
+
"pricing-single-plan-addons",
|
|
262
|
+
"pricing-enterprise-card",
|
|
263
|
+
"pricing-faq",
|
|
264
|
+
"pricing-annual-savings-banner"
|
|
265
|
+
]
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"name": "Conversion & forms",
|
|
269
|
+
"slug": "conversion-forms",
|
|
270
|
+
"entries": [
|
|
271
|
+
"conversion-cta-band",
|
|
272
|
+
"conversion-newsletter-consent",
|
|
273
|
+
"conversion-lead-capture",
|
|
274
|
+
"conversion-multi-step-form",
|
|
275
|
+
"conversion-booking-embed",
|
|
276
|
+
"conversion-waitlist-referral",
|
|
277
|
+
"conversion-contact-section",
|
|
278
|
+
"conversion-quiz-product-finder",
|
|
279
|
+
"conversion-exit-intent-popup",
|
|
280
|
+
"conversion-trust-badges"
|
|
281
|
+
]
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"name": "Listings & feeds",
|
|
285
|
+
"slug": "listings-feeds",
|
|
286
|
+
"entries": [
|
|
287
|
+
"blog-index",
|
|
288
|
+
"category-filter-bar",
|
|
289
|
+
"related-posts",
|
|
290
|
+
"resource-library",
|
|
291
|
+
"podcast-episode-list",
|
|
292
|
+
"changelog",
|
|
293
|
+
"public-roadmap",
|
|
294
|
+
"search-results-page"
|
|
295
|
+
]
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"name": "Long-form & docs",
|
|
299
|
+
"slug": "long-form-docs",
|
|
300
|
+
"entries": [
|
|
301
|
+
"rich-blog-post",
|
|
302
|
+
"author-bio",
|
|
303
|
+
"docs-page",
|
|
304
|
+
"code-tabs",
|
|
305
|
+
"kb-article",
|
|
306
|
+
"glossary",
|
|
307
|
+
"recipe",
|
|
308
|
+
"course-curriculum",
|
|
309
|
+
"legal-policy",
|
|
310
|
+
"case-study",
|
|
311
|
+
"podcast-episode"
|
|
312
|
+
]
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"name": "Company & people",
|
|
316
|
+
"slug": "company-people",
|
|
317
|
+
"entries": [
|
|
318
|
+
"team-grid",
|
|
319
|
+
"leadership-bios",
|
|
320
|
+
"values-culture",
|
|
321
|
+
"careers-list",
|
|
322
|
+
"job-posting",
|
|
323
|
+
"about-timeline",
|
|
324
|
+
"press-kit"
|
|
325
|
+
]
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"name": "Events & places",
|
|
329
|
+
"slug": "events-places",
|
|
330
|
+
"entries": [
|
|
331
|
+
"events-list",
|
|
332
|
+
"event-detail",
|
|
333
|
+
"webinar-registration",
|
|
334
|
+
"locations-directory",
|
|
335
|
+
"restaurant-menu",
|
|
336
|
+
"location-page"
|
|
337
|
+
]
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
"name": "Data & interactive",
|
|
341
|
+
"slug": "data-interactive",
|
|
342
|
+
"entries": [
|
|
343
|
+
"data-table-specs",
|
|
344
|
+
"table-comparison",
|
|
345
|
+
"chart-attrition",
|
|
346
|
+
"roi-calculator",
|
|
347
|
+
"product-configurator",
|
|
348
|
+
"stockist-map",
|
|
349
|
+
"product-compare-slider",
|
|
350
|
+
"countdown-live-counter",
|
|
351
|
+
"reader-poll",
|
|
352
|
+
"safe-embed",
|
|
353
|
+
"code-snippet-tabs"
|
|
354
|
+
]
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"name": "Advanced patterns",
|
|
358
|
+
"slug": "advanced-patterns",
|
|
359
|
+
"entries": [
|
|
360
|
+
"page-builder-six-deep",
|
|
361
|
+
"polymorphic-modules",
|
|
362
|
+
"conditional-source-matrix",
|
|
363
|
+
"global-announcement-overrides",
|
|
364
|
+
"localized-section",
|
|
365
|
+
"personalized-hero",
|
|
366
|
+
"responsive-layout-settings",
|
|
367
|
+
"grid-cell-layout",
|
|
368
|
+
"references-web",
|
|
369
|
+
"changelog-nested-validation",
|
|
370
|
+
"rich-text-embedded-components",
|
|
371
|
+
"dynamic-query-listing",
|
|
372
|
+
"overlays-with-triggers",
|
|
373
|
+
"scrollytelling",
|
|
374
|
+
"a11y-critical-media-form",
|
|
375
|
+
"section-theme-tokens"
|
|
376
|
+
]
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
"name": "Core examples",
|
|
380
|
+
"slug": "core-examples",
|
|
381
|
+
"entries": [
|
|
382
|
+
"core-faq",
|
|
383
|
+
"core-megaMenu",
|
|
384
|
+
"core-tabs",
|
|
385
|
+
"core-pricing",
|
|
386
|
+
"core-bento",
|
|
387
|
+
"core-journalStory",
|
|
388
|
+
"core-hero"
|
|
389
|
+
]
|
|
390
|
+
}
|
|
391
|
+
],
|
|
392
|
+
"tags": [
|
|
393
|
+
{
|
|
394
|
+
"tag": "deep-nesting",
|
|
395
|
+
"label": "Deep nesting",
|
|
396
|
+
"hint": "4+ levels of drill-in",
|
|
397
|
+
"entries": [
|
|
398
|
+
"nav-mega-featured-promo",
|
|
399
|
+
"nav-docs-sidebar",
|
|
400
|
+
"footer-mega-locations",
|
|
401
|
+
"hero-personalized",
|
|
402
|
+
"comparison-vs-competitors",
|
|
403
|
+
"commerce-product-detail",
|
|
404
|
+
"commerce-cart-cross-sell",
|
|
405
|
+
"commerce-shop-the-look",
|
|
406
|
+
"commerce-store-locator",
|
|
407
|
+
"commerce-product-comparison",
|
|
408
|
+
"pricing-usage-calculator",
|
|
409
|
+
"pricing-feature-matrix",
|
|
410
|
+
"conversion-multi-step-form",
|
|
411
|
+
"conversion-quiz-product-finder",
|
|
412
|
+
"changelog",
|
|
413
|
+
"public-roadmap",
|
|
414
|
+
"recipe",
|
|
415
|
+
"course-curriculum",
|
|
416
|
+
"legal-policy",
|
|
417
|
+
"press-kit",
|
|
418
|
+
"event-detail",
|
|
419
|
+
"locations-directory",
|
|
420
|
+
"restaurant-menu",
|
|
421
|
+
"location-page",
|
|
422
|
+
"product-configurator",
|
|
423
|
+
"page-builder-six-deep",
|
|
424
|
+
"personalized-hero",
|
|
425
|
+
"core-megaMenu"
|
|
426
|
+
]
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
"tag": "repeater",
|
|
430
|
+
"label": "Repeater",
|
|
431
|
+
"hint": "Arrays of objects",
|
|
432
|
+
"entries": [
|
|
433
|
+
"nav-simple-cta",
|
|
434
|
+
"nav-announcement-utility",
|
|
435
|
+
"nav-docs-sidebar",
|
|
436
|
+
"nav-ecommerce-header",
|
|
437
|
+
"nav-locale-header",
|
|
438
|
+
"announcement-countdown",
|
|
439
|
+
"cookie-consent",
|
|
440
|
+
"breadcrumb-config",
|
|
441
|
+
"footer-multi-column",
|
|
442
|
+
"footer-minimal",
|
|
443
|
+
"footer-mega-locations",
|
|
444
|
+
"hero-split-media",
|
|
445
|
+
"hero-video-fullbleed",
|
|
446
|
+
"hero-slider",
|
|
447
|
+
"hero-product",
|
|
448
|
+
"hero-lead-form",
|
|
449
|
+
"hero-app-download",
|
|
450
|
+
"hero-stats",
|
|
451
|
+
"hero-logo-wall",
|
|
452
|
+
"hero-personalized",
|
|
453
|
+
"feature-icon-grid",
|
|
454
|
+
"feature-alternating-rows",
|
|
455
|
+
"steps-how-it-works",
|
|
456
|
+
"timeline-company-history",
|
|
457
|
+
"tabs-with-media",
|
|
458
|
+
"accordion-feature-image-swap",
|
|
459
|
+
"comparison-vs-competitors",
|
|
460
|
+
"stats-band",
|
|
461
|
+
"problem-solution",
|
|
462
|
+
"testimonial-video-slider",
|
|
463
|
+
"wall-of-love",
|
|
464
|
+
"logo-cloud",
|
|
465
|
+
"press-mentions",
|
|
466
|
+
"awards-certifications",
|
|
467
|
+
"masonry-gallery",
|
|
468
|
+
"lightbox-gallery-filters",
|
|
469
|
+
"before-after-slider",
|
|
470
|
+
"commerce-product-grid-query",
|
|
471
|
+
"commerce-product-detail",
|
|
472
|
+
"commerce-featured-product",
|
|
473
|
+
"commerce-collection-header-facets",
|
|
474
|
+
"commerce-bundle-builder",
|
|
475
|
+
"commerce-cart-cross-sell",
|
|
476
|
+
"commerce-shop-the-look",
|
|
477
|
+
"commerce-size-guide",
|
|
478
|
+
"commerce-store-locator",
|
|
479
|
+
"commerce-product-comparison",
|
|
480
|
+
"pricing-tiers-billing-toggle",
|
|
481
|
+
"pricing-usage-calculator",
|
|
482
|
+
"pricing-feature-matrix",
|
|
483
|
+
"pricing-single-plan-addons",
|
|
484
|
+
"pricing-enterprise-card",
|
|
485
|
+
"pricing-faq",
|
|
486
|
+
"conversion-newsletter-consent",
|
|
487
|
+
"conversion-lead-capture",
|
|
488
|
+
"conversion-booking-embed",
|
|
489
|
+
"conversion-waitlist-referral",
|
|
490
|
+
"conversion-contact-section",
|
|
491
|
+
"conversion-quiz-product-finder",
|
|
492
|
+
"conversion-trust-badges",
|
|
493
|
+
"resource-library",
|
|
494
|
+
"podcast-episode-list",
|
|
495
|
+
"changelog",
|
|
496
|
+
"public-roadmap",
|
|
497
|
+
"search-results-page",
|
|
498
|
+
"code-tabs",
|
|
499
|
+
"glossary",
|
|
500
|
+
"recipe",
|
|
501
|
+
"course-curriculum",
|
|
502
|
+
"legal-policy",
|
|
503
|
+
"case-study",
|
|
504
|
+
"podcast-episode",
|
|
505
|
+
"leadership-bios",
|
|
506
|
+
"values-culture",
|
|
507
|
+
"job-posting",
|
|
508
|
+
"about-timeline",
|
|
509
|
+
"press-kit",
|
|
510
|
+
"event-detail",
|
|
511
|
+
"locations-directory",
|
|
512
|
+
"restaurant-menu",
|
|
513
|
+
"location-page",
|
|
514
|
+
"data-table-specs",
|
|
515
|
+
"chart-attrition",
|
|
516
|
+
"roi-calculator",
|
|
517
|
+
"product-configurator",
|
|
518
|
+
"stockist-map",
|
|
519
|
+
"product-compare-slider",
|
|
520
|
+
"reader-poll",
|
|
521
|
+
"code-snippet-tabs",
|
|
522
|
+
"page-builder-six-deep",
|
|
523
|
+
"polymorphic-modules",
|
|
524
|
+
"localized-section",
|
|
525
|
+
"personalized-hero",
|
|
526
|
+
"responsive-layout-settings",
|
|
527
|
+
"references-web",
|
|
528
|
+
"changelog-nested-validation",
|
|
529
|
+
"dynamic-query-listing",
|
|
530
|
+
"overlays-with-triggers",
|
|
531
|
+
"scrollytelling",
|
|
532
|
+
"core-faq",
|
|
533
|
+
"core-megaMenu",
|
|
534
|
+
"core-tabs",
|
|
535
|
+
"core-pricing"
|
|
536
|
+
]
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
"tag": "polymorphic-array",
|
|
540
|
+
"label": "Polymorphic array",
|
|
541
|
+
"hint": "Array of mixed block types",
|
|
542
|
+
"entries": [
|
|
543
|
+
"wall-of-love",
|
|
544
|
+
"mixed-media-carousel",
|
|
545
|
+
"commerce-product-detail",
|
|
546
|
+
"conversion-multi-step-form",
|
|
547
|
+
"polymorphic-modules",
|
|
548
|
+
"core-tabs",
|
|
549
|
+
"core-bento"
|
|
550
|
+
]
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
"tag": "slots",
|
|
554
|
+
"label": "Slots",
|
|
555
|
+
"hint": "Blocks placed inside a block",
|
|
556
|
+
"entries": [
|
|
557
|
+
"nav-mega-featured-promo",
|
|
558
|
+
"bento-feature-showcase",
|
|
559
|
+
"page-builder-six-deep",
|
|
560
|
+
"grid-cell-layout",
|
|
561
|
+
"overlays-with-triggers",
|
|
562
|
+
"core-tabs",
|
|
563
|
+
"core-bento"
|
|
564
|
+
]
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
"tag": "variants",
|
|
568
|
+
"label": "Variants",
|
|
569
|
+
"hint": "Layout variants of one block",
|
|
570
|
+
"entries": [
|
|
571
|
+
"nav-simple-cta",
|
|
572
|
+
"nav-mega-featured-promo",
|
|
573
|
+
"cookie-consent",
|
|
574
|
+
"hero-split-media",
|
|
575
|
+
"hero-centered-gradient",
|
|
576
|
+
"hero-stats",
|
|
577
|
+
"hero-logo-wall",
|
|
578
|
+
"hero-split-testimonial",
|
|
579
|
+
"feature-icon-grid",
|
|
580
|
+
"feature-alternating-rows",
|
|
581
|
+
"bento-feature-showcase",
|
|
582
|
+
"steps-how-it-works",
|
|
583
|
+
"tabs-with-media",
|
|
584
|
+
"accordion-feature-image-swap",
|
|
585
|
+
"comparison-vs-competitors",
|
|
586
|
+
"stats-band",
|
|
587
|
+
"manifesto-pull-quote",
|
|
588
|
+
"problem-solution",
|
|
589
|
+
"integrations-grid",
|
|
590
|
+
"testimonial-grid",
|
|
591
|
+
"testimonial-video-slider",
|
|
592
|
+
"case-study-cards",
|
|
593
|
+
"logo-cloud",
|
|
594
|
+
"press-mentions",
|
|
595
|
+
"video-section",
|
|
596
|
+
"before-after-slider",
|
|
597
|
+
"image-hotspots",
|
|
598
|
+
"interactive-3d-lottie-embed",
|
|
599
|
+
"parallax-image-overlay",
|
|
600
|
+
"commerce-product-grid-query",
|
|
601
|
+
"commerce-featured-product",
|
|
602
|
+
"commerce-promo-countdown",
|
|
603
|
+
"pricing-tiers-billing-toggle",
|
|
604
|
+
"conversion-cta-band",
|
|
605
|
+
"conversion-contact-section",
|
|
606
|
+
"conversion-trust-badges",
|
|
607
|
+
"blog-index",
|
|
608
|
+
"category-filter-bar",
|
|
609
|
+
"related-posts",
|
|
610
|
+
"changelog",
|
|
611
|
+
"author-bio",
|
|
612
|
+
"code-tabs",
|
|
613
|
+
"team-grid",
|
|
614
|
+
"leadership-bios",
|
|
615
|
+
"values-culture",
|
|
616
|
+
"careers-list",
|
|
617
|
+
"about-timeline",
|
|
618
|
+
"events-list",
|
|
619
|
+
"webinar-registration",
|
|
620
|
+
"restaurant-menu",
|
|
621
|
+
"page-builder-six-deep",
|
|
622
|
+
"global-announcement-overrides",
|
|
623
|
+
"responsive-layout-settings",
|
|
624
|
+
"grid-cell-layout",
|
|
625
|
+
"scrollytelling",
|
|
626
|
+
"section-theme-tokens",
|
|
627
|
+
"core-tabs",
|
|
628
|
+
"core-pricing",
|
|
629
|
+
"core-bento",
|
|
630
|
+
"core-hero"
|
|
631
|
+
]
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
"tag": "conditional",
|
|
635
|
+
"label": "Conditional",
|
|
636
|
+
"hint": "Fields shown or hidden by other values",
|
|
637
|
+
"entries": [
|
|
638
|
+
"nav-announcement-utility",
|
|
639
|
+
"nav-mega-featured-promo",
|
|
640
|
+
"nav-docs-sidebar",
|
|
641
|
+
"nav-ecommerce-header",
|
|
642
|
+
"nav-locale-header",
|
|
643
|
+
"sticky-mobile-cta-bar",
|
|
644
|
+
"announcement-countdown",
|
|
645
|
+
"cookie-consent",
|
|
646
|
+
"breadcrumb-config",
|
|
647
|
+
"hero-split-media",
|
|
648
|
+
"hero-centered-gradient",
|
|
649
|
+
"hero-video-fullbleed",
|
|
650
|
+
"hero-slider",
|
|
651
|
+
"hero-product",
|
|
652
|
+
"hero-search",
|
|
653
|
+
"hero-lead-form",
|
|
654
|
+
"hero-app-download",
|
|
655
|
+
"hero-logo-wall",
|
|
656
|
+
"hero-split-testimonial",
|
|
657
|
+
"hero-event",
|
|
658
|
+
"hero-personalized",
|
|
659
|
+
"feature-alternating-rows",
|
|
660
|
+
"bento-feature-showcase",
|
|
661
|
+
"timeline-company-history",
|
|
662
|
+
"tabs-with-media",
|
|
663
|
+
"comparison-vs-competitors",
|
|
664
|
+
"manifesto-pull-quote",
|
|
665
|
+
"integrations-grid",
|
|
666
|
+
"testimonial-grid",
|
|
667
|
+
"testimonial-video-slider",
|
|
668
|
+
"case-study-cards",
|
|
669
|
+
"logo-cloud",
|
|
670
|
+
"awards-certifications",
|
|
671
|
+
"review-summary",
|
|
672
|
+
"video-section",
|
|
673
|
+
"image-hotspots",
|
|
674
|
+
"interactive-3d-lottie-embed",
|
|
675
|
+
"mixed-media-carousel",
|
|
676
|
+
"parallax-image-overlay",
|
|
677
|
+
"ugc-social-feed",
|
|
678
|
+
"commerce-product-grid-query",
|
|
679
|
+
"commerce-product-detail",
|
|
680
|
+
"commerce-featured-product",
|
|
681
|
+
"commerce-collection-header-facets",
|
|
682
|
+
"commerce-bundle-builder",
|
|
683
|
+
"commerce-cart-cross-sell",
|
|
684
|
+
"commerce-promo-countdown",
|
|
685
|
+
"commerce-store-locator",
|
|
686
|
+
"commerce-product-comparison",
|
|
687
|
+
"pricing-tiers-billing-toggle",
|
|
688
|
+
"pricing-usage-calculator",
|
|
689
|
+
"pricing-feature-matrix",
|
|
690
|
+
"pricing-single-plan-addons",
|
|
691
|
+
"pricing-enterprise-card",
|
|
692
|
+
"pricing-faq",
|
|
693
|
+
"pricing-annual-savings-banner",
|
|
694
|
+
"conversion-cta-band",
|
|
695
|
+
"conversion-newsletter-consent",
|
|
696
|
+
"conversion-lead-capture",
|
|
697
|
+
"conversion-multi-step-form",
|
|
698
|
+
"conversion-booking-embed",
|
|
699
|
+
"conversion-waitlist-referral",
|
|
700
|
+
"conversion-contact-section",
|
|
701
|
+
"conversion-quiz-product-finder",
|
|
702
|
+
"conversion-exit-intent-popup",
|
|
703
|
+
"conversion-trust-badges",
|
|
704
|
+
"blog-index",
|
|
705
|
+
"category-filter-bar",
|
|
706
|
+
"related-posts",
|
|
707
|
+
"resource-library",
|
|
708
|
+
"podcast-episode-list",
|
|
709
|
+
"changelog",
|
|
710
|
+
"public-roadmap",
|
|
711
|
+
"search-results-page",
|
|
712
|
+
"rich-blog-post",
|
|
713
|
+
"author-bio",
|
|
714
|
+
"docs-page",
|
|
715
|
+
"kb-article",
|
|
716
|
+
"glossary",
|
|
717
|
+
"recipe",
|
|
718
|
+
"course-curriculum",
|
|
719
|
+
"legal-policy",
|
|
720
|
+
"case-study",
|
|
721
|
+
"podcast-episode",
|
|
722
|
+
"team-grid",
|
|
723
|
+
"leadership-bios",
|
|
724
|
+
"careers-list",
|
|
725
|
+
"job-posting",
|
|
726
|
+
"press-kit",
|
|
727
|
+
"events-list",
|
|
728
|
+
"event-detail",
|
|
729
|
+
"webinar-registration",
|
|
730
|
+
"locations-directory",
|
|
731
|
+
"restaurant-menu",
|
|
732
|
+
"location-page",
|
|
733
|
+
"data-table-specs",
|
|
734
|
+
"chart-attrition",
|
|
735
|
+
"roi-calculator",
|
|
736
|
+
"product-configurator",
|
|
737
|
+
"stockist-map",
|
|
738
|
+
"product-compare-slider",
|
|
739
|
+
"countdown-live-counter",
|
|
740
|
+
"reader-poll",
|
|
741
|
+
"safe-embed",
|
|
742
|
+
"code-snippet-tabs",
|
|
743
|
+
"conditional-source-matrix",
|
|
744
|
+
"global-announcement-overrides",
|
|
745
|
+
"localized-section",
|
|
746
|
+
"personalized-hero",
|
|
747
|
+
"responsive-layout-settings",
|
|
748
|
+
"references-web",
|
|
749
|
+
"changelog-nested-validation",
|
|
750
|
+
"dynamic-query-listing",
|
|
751
|
+
"overlays-with-triggers",
|
|
752
|
+
"scrollytelling",
|
|
753
|
+
"a11y-critical-media-form",
|
|
754
|
+
"section-theme-tokens",
|
|
755
|
+
"core-faq",
|
|
756
|
+
"core-pricing"
|
|
757
|
+
]
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
"tag": "references",
|
|
761
|
+
"label": "References",
|
|
762
|
+
"hint": "Links to pages or CMS items",
|
|
763
|
+
"entries": [
|
|
764
|
+
"nav-mega-featured-promo",
|
|
765
|
+
"nav-docs-sidebar",
|
|
766
|
+
"nav-ecommerce-header",
|
|
767
|
+
"breadcrumb-config",
|
|
768
|
+
"footer-multi-column",
|
|
769
|
+
"hero-product",
|
|
770
|
+
"hero-search",
|
|
771
|
+
"hero-lead-form",
|
|
772
|
+
"hero-split-testimonial",
|
|
773
|
+
"integrations-grid",
|
|
774
|
+
"testimonial-grid",
|
|
775
|
+
"testimonial-video-slider",
|
|
776
|
+
"case-study-cards",
|
|
777
|
+
"review-summary",
|
|
778
|
+
"image-hotspots",
|
|
779
|
+
"mixed-media-carousel",
|
|
780
|
+
"ugc-social-feed",
|
|
781
|
+
"commerce-product-grid-query",
|
|
782
|
+
"commerce-product-detail",
|
|
783
|
+
"commerce-featured-product",
|
|
784
|
+
"commerce-collection-header-facets",
|
|
785
|
+
"commerce-bundle-builder",
|
|
786
|
+
"commerce-cart-cross-sell",
|
|
787
|
+
"commerce-shop-the-look",
|
|
788
|
+
"commerce-size-guide",
|
|
789
|
+
"commerce-store-locator",
|
|
790
|
+
"commerce-product-comparison",
|
|
791
|
+
"pricing-enterprise-card",
|
|
792
|
+
"pricing-faq",
|
|
793
|
+
"conversion-newsletter-consent",
|
|
794
|
+
"conversion-lead-capture",
|
|
795
|
+
"conversion-multi-step-form",
|
|
796
|
+
"conversion-waitlist-referral",
|
|
797
|
+
"conversion-contact-section",
|
|
798
|
+
"conversion-quiz-product-finder",
|
|
799
|
+
"conversion-exit-intent-popup",
|
|
800
|
+
"blog-index",
|
|
801
|
+
"category-filter-bar",
|
|
802
|
+
"related-posts",
|
|
803
|
+
"resource-library",
|
|
804
|
+
"podcast-episode-list",
|
|
805
|
+
"public-roadmap",
|
|
806
|
+
"search-results-page",
|
|
807
|
+
"rich-blog-post",
|
|
808
|
+
"author-bio",
|
|
809
|
+
"docs-page",
|
|
810
|
+
"kb-article",
|
|
811
|
+
"glossary",
|
|
812
|
+
"recipe",
|
|
813
|
+
"course-curriculum",
|
|
814
|
+
"case-study",
|
|
815
|
+
"podcast-episode",
|
|
816
|
+
"team-grid",
|
|
817
|
+
"leadership-bios",
|
|
818
|
+
"careers-list",
|
|
819
|
+
"job-posting",
|
|
820
|
+
"about-timeline",
|
|
821
|
+
"events-list",
|
|
822
|
+
"event-detail",
|
|
823
|
+
"webinar-registration",
|
|
824
|
+
"location-page",
|
|
825
|
+
"table-comparison",
|
|
826
|
+
"product-configurator",
|
|
827
|
+
"stockist-map",
|
|
828
|
+
"product-compare-slider",
|
|
829
|
+
"reader-poll",
|
|
830
|
+
"polymorphic-modules",
|
|
831
|
+
"conditional-source-matrix",
|
|
832
|
+
"references-web",
|
|
833
|
+
"rich-text-embedded-components",
|
|
834
|
+
"dynamic-query-listing",
|
|
835
|
+
"overlays-with-triggers",
|
|
836
|
+
"core-journalStory"
|
|
837
|
+
]
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
"tag": "dynamic-query",
|
|
841
|
+
"label": "Dynamic query",
|
|
842
|
+
"hint": "Pulls N items from a collection by filter",
|
|
843
|
+
"entries": [
|
|
844
|
+
"nav-docs-sidebar",
|
|
845
|
+
"hero-search",
|
|
846
|
+
"integrations-grid",
|
|
847
|
+
"testimonial-grid",
|
|
848
|
+
"case-study-cards",
|
|
849
|
+
"ugc-social-feed",
|
|
850
|
+
"commerce-product-grid-query",
|
|
851
|
+
"commerce-collection-header-facets",
|
|
852
|
+
"commerce-store-locator",
|
|
853
|
+
"blog-index",
|
|
854
|
+
"category-filter-bar",
|
|
855
|
+
"related-posts",
|
|
856
|
+
"resource-library",
|
|
857
|
+
"podcast-episode-list",
|
|
858
|
+
"changelog",
|
|
859
|
+
"search-results-page",
|
|
860
|
+
"author-bio",
|
|
861
|
+
"glossary",
|
|
862
|
+
"team-grid",
|
|
863
|
+
"careers-list",
|
|
864
|
+
"events-list",
|
|
865
|
+
"locations-directory",
|
|
866
|
+
"conditional-source-matrix",
|
|
867
|
+
"references-web",
|
|
868
|
+
"dynamic-query-listing",
|
|
869
|
+
"core-faq"
|
|
870
|
+
]
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
"tag": "global-synced",
|
|
874
|
+
"label": "Global / synced",
|
|
875
|
+
"hint": "Shared block with per-instance overrides",
|
|
876
|
+
"entries": [
|
|
877
|
+
"nav-simple-cta",
|
|
878
|
+
"nav-announcement-utility",
|
|
879
|
+
"nav-mega-featured-promo",
|
|
880
|
+
"nav-docs-sidebar",
|
|
881
|
+
"nav-ecommerce-header",
|
|
882
|
+
"nav-locale-header",
|
|
883
|
+
"announcement-countdown",
|
|
884
|
+
"cookie-consent",
|
|
885
|
+
"footer-multi-column",
|
|
886
|
+
"footer-minimal",
|
|
887
|
+
"footer-mega-locations",
|
|
888
|
+
"hero-app-download",
|
|
889
|
+
"docs-page",
|
|
890
|
+
"global-announcement-overrides",
|
|
891
|
+
"core-megaMenu"
|
|
892
|
+
]
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
"tag": "localized",
|
|
896
|
+
"label": "Localized",
|
|
897
|
+
"hint": "Per-field translations",
|
|
898
|
+
"entries": [
|
|
899
|
+
"nav-announcement-utility",
|
|
900
|
+
"nav-locale-header",
|
|
901
|
+
"announcement-countdown",
|
|
902
|
+
"cookie-consent",
|
|
903
|
+
"feature-icon-grid",
|
|
904
|
+
"tabs-with-media",
|
|
905
|
+
"stats-band",
|
|
906
|
+
"manifesto-pull-quote",
|
|
907
|
+
"press-mentions",
|
|
908
|
+
"masonry-gallery",
|
|
909
|
+
"lightbox-gallery-filters",
|
|
910
|
+
"video-section",
|
|
911
|
+
"before-after-slider",
|
|
912
|
+
"rich-blog-post",
|
|
913
|
+
"recipe",
|
|
914
|
+
"legal-policy",
|
|
915
|
+
"restaurant-menu",
|
|
916
|
+
"countdown-live-counter",
|
|
917
|
+
"localized-section",
|
|
918
|
+
"core-journalStory",
|
|
919
|
+
"core-hero"
|
|
920
|
+
]
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
"tag": "rich-text",
|
|
924
|
+
"label": "Rich text",
|
|
925
|
+
"hint": "Portable text body with embeds",
|
|
926
|
+
"entries": [
|
|
927
|
+
"feature-alternating-rows",
|
|
928
|
+
"timeline-company-history",
|
|
929
|
+
"accordion-feature-image-swap",
|
|
930
|
+
"manifesto-pull-quote",
|
|
931
|
+
"commerce-product-detail",
|
|
932
|
+
"pricing-single-plan-addons",
|
|
933
|
+
"pricing-faq",
|
|
934
|
+
"conversion-lead-capture",
|
|
935
|
+
"conversion-booking-embed",
|
|
936
|
+
"changelog",
|
|
937
|
+
"rich-blog-post",
|
|
938
|
+
"docs-page",
|
|
939
|
+
"kb-article",
|
|
940
|
+
"legal-policy",
|
|
941
|
+
"case-study",
|
|
942
|
+
"leadership-bios",
|
|
943
|
+
"about-timeline",
|
|
944
|
+
"polymorphic-modules",
|
|
945
|
+
"changelog-nested-validation",
|
|
946
|
+
"rich-text-embedded-components",
|
|
947
|
+
"core-journalStory"
|
|
948
|
+
]
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
"tag": "media-heavy",
|
|
952
|
+
"label": "Media-heavy",
|
|
953
|
+
"hint": "Images, video, galleries, focal points",
|
|
954
|
+
"entries": [
|
|
955
|
+
"nav-ecommerce-header",
|
|
956
|
+
"footer-mega-locations",
|
|
957
|
+
"hero-split-media",
|
|
958
|
+
"hero-centered-gradient",
|
|
959
|
+
"hero-video-fullbleed",
|
|
960
|
+
"hero-slider",
|
|
961
|
+
"hero-product",
|
|
962
|
+
"hero-search",
|
|
963
|
+
"hero-app-download",
|
|
964
|
+
"hero-stats",
|
|
965
|
+
"hero-logo-wall",
|
|
966
|
+
"hero-split-testimonial",
|
|
967
|
+
"hero-event",
|
|
968
|
+
"feature-alternating-rows",
|
|
969
|
+
"bento-feature-showcase",
|
|
970
|
+
"timeline-company-history",
|
|
971
|
+
"tabs-with-media",
|
|
972
|
+
"accordion-feature-image-swap",
|
|
973
|
+
"integrations-grid",
|
|
974
|
+
"testimonial-video-slider",
|
|
975
|
+
"wall-of-love",
|
|
976
|
+
"logo-cloud",
|
|
977
|
+
"press-mentions",
|
|
978
|
+
"masonry-gallery",
|
|
979
|
+
"lightbox-gallery-filters",
|
|
980
|
+
"video-section",
|
|
981
|
+
"before-after-slider",
|
|
982
|
+
"image-hotspots",
|
|
983
|
+
"interactive-3d-lottie-embed",
|
|
984
|
+
"mixed-media-carousel",
|
|
985
|
+
"parallax-image-overlay",
|
|
986
|
+
"ugc-social-feed",
|
|
987
|
+
"commerce-product-detail",
|
|
988
|
+
"commerce-featured-product",
|
|
989
|
+
"commerce-collection-header-facets",
|
|
990
|
+
"commerce-shop-the-look",
|
|
991
|
+
"commerce-size-guide",
|
|
992
|
+
"pricing-enterprise-card",
|
|
993
|
+
"conversion-cta-band",
|
|
994
|
+
"conversion-booking-embed",
|
|
995
|
+
"conversion-waitlist-referral",
|
|
996
|
+
"conversion-quiz-product-finder",
|
|
997
|
+
"conversion-exit-intent-popup",
|
|
998
|
+
"conversion-trust-badges",
|
|
999
|
+
"podcast-episode-list",
|
|
1000
|
+
"rich-blog-post",
|
|
1001
|
+
"recipe",
|
|
1002
|
+
"course-curriculum",
|
|
1003
|
+
"case-study",
|
|
1004
|
+
"podcast-episode",
|
|
1005
|
+
"values-culture",
|
|
1006
|
+
"about-timeline",
|
|
1007
|
+
"press-kit",
|
|
1008
|
+
"location-page",
|
|
1009
|
+
"page-builder-six-deep",
|
|
1010
|
+
"polymorphic-modules",
|
|
1011
|
+
"grid-cell-layout",
|
|
1012
|
+
"rich-text-embedded-components",
|
|
1013
|
+
"scrollytelling",
|
|
1014
|
+
"a11y-critical-media-form",
|
|
1015
|
+
"section-theme-tokens",
|
|
1016
|
+
"core-bento",
|
|
1017
|
+
"core-journalStory",
|
|
1018
|
+
"core-hero"
|
|
1019
|
+
]
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
"tag": "embed",
|
|
1023
|
+
"label": "Embed",
|
|
1024
|
+
"hint": "iframe, HTML, code or third-party",
|
|
1025
|
+
"entries": [
|
|
1026
|
+
"video-section",
|
|
1027
|
+
"interactive-3d-lottie-embed",
|
|
1028
|
+
"ugc-social-feed",
|
|
1029
|
+
"commerce-store-locator",
|
|
1030
|
+
"conversion-booking-embed",
|
|
1031
|
+
"conversion-contact-section",
|
|
1032
|
+
"podcast-episode-list",
|
|
1033
|
+
"rich-blog-post",
|
|
1034
|
+
"docs-page",
|
|
1035
|
+
"code-tabs",
|
|
1036
|
+
"podcast-episode",
|
|
1037
|
+
"careers-list",
|
|
1038
|
+
"webinar-registration",
|
|
1039
|
+
"stockist-map",
|
|
1040
|
+
"safe-embed",
|
|
1041
|
+
"code-snippet-tabs",
|
|
1042
|
+
"rich-text-embedded-components",
|
|
1043
|
+
"core-journalStory"
|
|
1044
|
+
]
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
"tag": "form",
|
|
1048
|
+
"label": "Form",
|
|
1049
|
+
"hint": "Collects visitor input",
|
|
1050
|
+
"entries": [
|
|
1051
|
+
"footer-multi-column",
|
|
1052
|
+
"hero-lead-form",
|
|
1053
|
+
"pricing-enterprise-card",
|
|
1054
|
+
"conversion-newsletter-consent",
|
|
1055
|
+
"conversion-lead-capture",
|
|
1056
|
+
"conversion-multi-step-form",
|
|
1057
|
+
"conversion-waitlist-referral",
|
|
1058
|
+
"conversion-contact-section",
|
|
1059
|
+
"conversion-quiz-product-finder",
|
|
1060
|
+
"conversion-exit-intent-popup",
|
|
1061
|
+
"resource-library",
|
|
1062
|
+
"public-roadmap",
|
|
1063
|
+
"kb-article",
|
|
1064
|
+
"job-posting",
|
|
1065
|
+
"webinar-registration",
|
|
1066
|
+
"roi-calculator",
|
|
1067
|
+
"reader-poll",
|
|
1068
|
+
"overlays-with-triggers",
|
|
1069
|
+
"a11y-critical-media-form"
|
|
1070
|
+
]
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
"tag": "commerce",
|
|
1074
|
+
"label": "Commerce",
|
|
1075
|
+
"hint": "Price, inventory, variants",
|
|
1076
|
+
"entries": [
|
|
1077
|
+
"nav-mega-featured-promo",
|
|
1078
|
+
"nav-ecommerce-header",
|
|
1079
|
+
"sticky-mobile-cta-bar",
|
|
1080
|
+
"footer-multi-column",
|
|
1081
|
+
"hero-product",
|
|
1082
|
+
"review-summary",
|
|
1083
|
+
"image-hotspots",
|
|
1084
|
+
"mixed-media-carousel",
|
|
1085
|
+
"ugc-social-feed",
|
|
1086
|
+
"commerce-product-grid-query",
|
|
1087
|
+
"commerce-product-detail",
|
|
1088
|
+
"commerce-featured-product",
|
|
1089
|
+
"commerce-collection-header-facets",
|
|
1090
|
+
"commerce-bundle-builder",
|
|
1091
|
+
"commerce-cart-cross-sell",
|
|
1092
|
+
"commerce-promo-countdown",
|
|
1093
|
+
"commerce-shop-the-look",
|
|
1094
|
+
"commerce-product-comparison",
|
|
1095
|
+
"pricing-tiers-billing-toggle",
|
|
1096
|
+
"pricing-usage-calculator",
|
|
1097
|
+
"pricing-single-plan-addons",
|
|
1098
|
+
"pricing-annual-savings-banner",
|
|
1099
|
+
"conversion-quiz-product-finder",
|
|
1100
|
+
"event-detail",
|
|
1101
|
+
"restaurant-menu",
|
|
1102
|
+
"table-comparison",
|
|
1103
|
+
"product-configurator",
|
|
1104
|
+
"product-compare-slider",
|
|
1105
|
+
"conditional-source-matrix",
|
|
1106
|
+
"grid-cell-layout",
|
|
1107
|
+
"core-pricing"
|
|
1108
|
+
]
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
"tag": "personalization",
|
|
1112
|
+
"label": "Personalization",
|
|
1113
|
+
"hint": "Audience or A/B variants",
|
|
1114
|
+
"entries": [
|
|
1115
|
+
"hero-personalized",
|
|
1116
|
+
"commerce-cart-cross-sell",
|
|
1117
|
+
"commerce-promo-countdown",
|
|
1118
|
+
"pricing-annual-savings-banner",
|
|
1119
|
+
"conversion-quiz-product-finder",
|
|
1120
|
+
"conversion-exit-intent-popup",
|
|
1121
|
+
"personalized-hero"
|
|
1122
|
+
]
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
"tag": "validation",
|
|
1126
|
+
"label": "Validation",
|
|
1127
|
+
"hint": "Limits, required, patterns",
|
|
1128
|
+
"entries": [
|
|
1129
|
+
"nav-simple-cta",
|
|
1130
|
+
"sticky-mobile-cta-bar",
|
|
1131
|
+
"announcement-countdown",
|
|
1132
|
+
"cookie-consent",
|
|
1133
|
+
"footer-multi-column",
|
|
1134
|
+
"footer-minimal",
|
|
1135
|
+
"footer-mega-locations",
|
|
1136
|
+
"hero-centered-gradient",
|
|
1137
|
+
"hero-video-fullbleed",
|
|
1138
|
+
"hero-slider",
|
|
1139
|
+
"hero-lead-form",
|
|
1140
|
+
"hero-app-download",
|
|
1141
|
+
"hero-stats",
|
|
1142
|
+
"hero-event",
|
|
1143
|
+
"hero-personalized",
|
|
1144
|
+
"feature-icon-grid",
|
|
1145
|
+
"steps-how-it-works",
|
|
1146
|
+
"timeline-company-history",
|
|
1147
|
+
"comparison-vs-competitors",
|
|
1148
|
+
"stats-band",
|
|
1149
|
+
"problem-solution",
|
|
1150
|
+
"testimonial-grid",
|
|
1151
|
+
"testimonial-video-slider",
|
|
1152
|
+
"wall-of-love",
|
|
1153
|
+
"logo-cloud",
|
|
1154
|
+
"press-mentions",
|
|
1155
|
+
"awards-certifications",
|
|
1156
|
+
"review-summary",
|
|
1157
|
+
"masonry-gallery",
|
|
1158
|
+
"lightbox-gallery-filters",
|
|
1159
|
+
"video-section",
|
|
1160
|
+
"before-after-slider",
|
|
1161
|
+
"image-hotspots",
|
|
1162
|
+
"interactive-3d-lottie-embed",
|
|
1163
|
+
"ugc-social-feed",
|
|
1164
|
+
"commerce-product-grid-query",
|
|
1165
|
+
"commerce-product-detail",
|
|
1166
|
+
"commerce-collection-header-facets",
|
|
1167
|
+
"commerce-bundle-builder",
|
|
1168
|
+
"commerce-cart-cross-sell",
|
|
1169
|
+
"commerce-promo-countdown",
|
|
1170
|
+
"commerce-shop-the-look",
|
|
1171
|
+
"commerce-size-guide",
|
|
1172
|
+
"commerce-store-locator",
|
|
1173
|
+
"commerce-product-comparison",
|
|
1174
|
+
"pricing-tiers-billing-toggle",
|
|
1175
|
+
"pricing-usage-calculator",
|
|
1176
|
+
"pricing-feature-matrix",
|
|
1177
|
+
"pricing-single-plan-addons",
|
|
1178
|
+
"pricing-faq",
|
|
1179
|
+
"pricing-annual-savings-banner",
|
|
1180
|
+
"conversion-newsletter-consent",
|
|
1181
|
+
"conversion-lead-capture",
|
|
1182
|
+
"conversion-multi-step-form",
|
|
1183
|
+
"conversion-booking-embed",
|
|
1184
|
+
"conversion-waitlist-referral",
|
|
1185
|
+
"conversion-quiz-product-finder",
|
|
1186
|
+
"conversion-exit-intent-popup",
|
|
1187
|
+
"conversion-trust-badges",
|
|
1188
|
+
"blog-index",
|
|
1189
|
+
"related-posts",
|
|
1190
|
+
"resource-library",
|
|
1191
|
+
"changelog",
|
|
1192
|
+
"public-roadmap",
|
|
1193
|
+
"search-results-page",
|
|
1194
|
+
"rich-blog-post",
|
|
1195
|
+
"docs-page",
|
|
1196
|
+
"code-tabs",
|
|
1197
|
+
"kb-article",
|
|
1198
|
+
"glossary",
|
|
1199
|
+
"recipe",
|
|
1200
|
+
"course-curriculum",
|
|
1201
|
+
"legal-policy",
|
|
1202
|
+
"case-study",
|
|
1203
|
+
"podcast-episode",
|
|
1204
|
+
"team-grid",
|
|
1205
|
+
"values-culture",
|
|
1206
|
+
"job-posting",
|
|
1207
|
+
"about-timeline",
|
|
1208
|
+
"press-kit",
|
|
1209
|
+
"event-detail",
|
|
1210
|
+
"webinar-registration",
|
|
1211
|
+
"locations-directory",
|
|
1212
|
+
"restaurant-menu",
|
|
1213
|
+
"location-page",
|
|
1214
|
+
"data-table-specs",
|
|
1215
|
+
"table-comparison",
|
|
1216
|
+
"chart-attrition",
|
|
1217
|
+
"roi-calculator",
|
|
1218
|
+
"product-configurator",
|
|
1219
|
+
"product-compare-slider",
|
|
1220
|
+
"countdown-live-counter",
|
|
1221
|
+
"reader-poll",
|
|
1222
|
+
"safe-embed",
|
|
1223
|
+
"code-snippet-tabs",
|
|
1224
|
+
"conditional-source-matrix",
|
|
1225
|
+
"global-announcement-overrides",
|
|
1226
|
+
"localized-section",
|
|
1227
|
+
"personalized-hero",
|
|
1228
|
+
"references-web",
|
|
1229
|
+
"changelog-nested-validation",
|
|
1230
|
+
"dynamic-query-listing",
|
|
1231
|
+
"a11y-critical-media-form",
|
|
1232
|
+
"section-theme-tokens",
|
|
1233
|
+
"core-faq",
|
|
1234
|
+
"core-megaMenu",
|
|
1235
|
+
"core-pricing",
|
|
1236
|
+
"core-journalStory"
|
|
1237
|
+
]
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
"tag": "a11y",
|
|
1241
|
+
"label": "Accessibility",
|
|
1242
|
+
"hint": "Alt text, captions, labels",
|
|
1243
|
+
"entries": [
|
|
1244
|
+
"announcement-countdown",
|
|
1245
|
+
"cookie-consent",
|
|
1246
|
+
"breadcrumb-config",
|
|
1247
|
+
"hero-split-media",
|
|
1248
|
+
"hero-video-fullbleed",
|
|
1249
|
+
"hero-slider",
|
|
1250
|
+
"hero-logo-wall",
|
|
1251
|
+
"tabs-with-media",
|
|
1252
|
+
"accordion-feature-image-swap",
|
|
1253
|
+
"comparison-vs-competitors",
|
|
1254
|
+
"stats-band",
|
|
1255
|
+
"testimonial-video-slider",
|
|
1256
|
+
"wall-of-love",
|
|
1257
|
+
"logo-cloud",
|
|
1258
|
+
"awards-certifications",
|
|
1259
|
+
"review-summary",
|
|
1260
|
+
"masonry-gallery",
|
|
1261
|
+
"lightbox-gallery-filters",
|
|
1262
|
+
"video-section",
|
|
1263
|
+
"before-after-slider",
|
|
1264
|
+
"image-hotspots",
|
|
1265
|
+
"interactive-3d-lottie-embed",
|
|
1266
|
+
"mixed-media-carousel",
|
|
1267
|
+
"parallax-image-overlay",
|
|
1268
|
+
"commerce-product-detail",
|
|
1269
|
+
"commerce-shop-the-look",
|
|
1270
|
+
"commerce-size-guide",
|
|
1271
|
+
"commerce-product-comparison",
|
|
1272
|
+
"pricing-feature-matrix",
|
|
1273
|
+
"pricing-enterprise-card",
|
|
1274
|
+
"conversion-newsletter-consent",
|
|
1275
|
+
"conversion-multi-step-form",
|
|
1276
|
+
"conversion-contact-section",
|
|
1277
|
+
"conversion-exit-intent-popup",
|
|
1278
|
+
"conversion-trust-badges",
|
|
1279
|
+
"category-filter-bar",
|
|
1280
|
+
"podcast-episode-list",
|
|
1281
|
+
"search-results-page",
|
|
1282
|
+
"author-bio",
|
|
1283
|
+
"glossary",
|
|
1284
|
+
"legal-policy",
|
|
1285
|
+
"podcast-episode",
|
|
1286
|
+
"team-grid",
|
|
1287
|
+
"leadership-bios",
|
|
1288
|
+
"values-culture",
|
|
1289
|
+
"press-kit",
|
|
1290
|
+
"events-list",
|
|
1291
|
+
"locations-directory",
|
|
1292
|
+
"location-page",
|
|
1293
|
+
"data-table-specs",
|
|
1294
|
+
"table-comparison",
|
|
1295
|
+
"chart-attrition",
|
|
1296
|
+
"stockist-map",
|
|
1297
|
+
"countdown-live-counter",
|
|
1298
|
+
"safe-embed",
|
|
1299
|
+
"localized-section",
|
|
1300
|
+
"rich-text-embedded-components",
|
|
1301
|
+
"overlays-with-triggers",
|
|
1302
|
+
"scrollytelling",
|
|
1303
|
+
"a11y-critical-media-form",
|
|
1304
|
+
"section-theme-tokens",
|
|
1305
|
+
"core-hero"
|
|
1306
|
+
]
|
|
1307
|
+
},
|
|
1308
|
+
{
|
|
1309
|
+
"tag": "seo-structured",
|
|
1310
|
+
"label": "SEO structured",
|
|
1311
|
+
"hint": "Emits JSON-LD",
|
|
1312
|
+
"entries": [
|
|
1313
|
+
"breadcrumb-config",
|
|
1314
|
+
"footer-mega-locations",
|
|
1315
|
+
"hero-event",
|
|
1316
|
+
"awards-certifications",
|
|
1317
|
+
"review-summary",
|
|
1318
|
+
"video-section",
|
|
1319
|
+
"commerce-product-detail",
|
|
1320
|
+
"commerce-store-locator",
|
|
1321
|
+
"pricing-faq",
|
|
1322
|
+
"rich-blog-post",
|
|
1323
|
+
"glossary",
|
|
1324
|
+
"recipe",
|
|
1325
|
+
"course-curriculum",
|
|
1326
|
+
"podcast-episode",
|
|
1327
|
+
"job-posting",
|
|
1328
|
+
"event-detail",
|
|
1329
|
+
"restaurant-menu",
|
|
1330
|
+
"location-page",
|
|
1331
|
+
"table-comparison",
|
|
1332
|
+
"stockist-map",
|
|
1333
|
+
"core-journalStory"
|
|
1334
|
+
]
|
|
1335
|
+
},
|
|
1336
|
+
{
|
|
1337
|
+
"tag": "responsive-layout",
|
|
1338
|
+
"label": "Responsive layout",
|
|
1339
|
+
"hint": "Per-breakpoint layout settings",
|
|
1340
|
+
"entries": [
|
|
1341
|
+
"sticky-mobile-cta-bar",
|
|
1342
|
+
"bento-feature-showcase",
|
|
1343
|
+
"masonry-gallery",
|
|
1344
|
+
"mixed-media-carousel",
|
|
1345
|
+
"parallax-image-overlay",
|
|
1346
|
+
"commerce-product-grid-query",
|
|
1347
|
+
"commerce-product-comparison",
|
|
1348
|
+
"pricing-feature-matrix",
|
|
1349
|
+
"category-filter-bar",
|
|
1350
|
+
"resource-library",
|
|
1351
|
+
"locations-directory",
|
|
1352
|
+
"product-compare-slider",
|
|
1353
|
+
"responsive-layout-settings",
|
|
1354
|
+
"grid-cell-layout",
|
|
1355
|
+
"core-bento"
|
|
1356
|
+
]
|
|
1357
|
+
}
|
|
1358
|
+
],
|
|
1359
|
+
"entries": [
|
|
1360
|
+
{"id":"nav-simple-cta","name":"Simple navbar with CTA","category":"Navigation & chrome","kind":"global","tags":["global-synced","repeater","validation","variants"],"proves":"Smallest global header · inline link rows · one button unit · tap-to-call link · design kept out of content","why":"A header is site chrome, so it is ONE global (kind: global) that every page renders — never a block copied onto each page, or a menu change becomes 40 edits. Links are an array of tiny objects (label + link) that open inline because two fields don't deserve a drill-in. The CTA is a `button` (label + link + style as one unit) so the label can never drift from where it goes. Phone is a `link` of type phone, not a string, so the renderer gets a valid tel: href. Theme, sticky, transparency and the mobile menu style live in Design: they change how it looks, not what it says.","htmlSignals":["<header> (or role=banner) as the first child of <body>, containing <nav> with a short <ul> of <a>","A logo <a href=\"/\"> wrapping an <img>/<svg> at the start of the row","One visually distinct <a>/<button> at the end of the row (filled background) — the CTA","An <a href=\"tel:…\"> or phone icon; a hamburger <button aria-expanded> hidden on desktop","position: sticky/fixed on the header; identical markup on every crawled page"],"mcp":"Detect on 2+ pages with identical markup → create ONE global `header` and reference it from layouts; never per page. Map logo <img> → logo (alt → logoText if no visible text), each top-level <nav> <a> → links[] { label: textContent, link: internal page if same origin else url }. The last styled anchor/button → button (style from its class: filled → primary, bordered → outline). tel: anchors → phone. position sticky/fixed → isSticky: true; a transparent background over a hero → isTransparent: true. Ignore the hamburger markup — it's the renderer's job.","pitfalls":["A 'Book a table' widget that opens a third-party modal is still a button with an external link — don't model the widget's script.","Don't create a link for the logo; it always goes home."],"notCarried":["category","item openMode","kind:global","variants"],"component":{"name":"Header","slug":"header","props":[{"key":"logo","label":"Logo","type":"image","helpText":"SVG, or a PNG at least 64px tall.","ui":{"tab":{"id":"content","label":"Content"},"group":{"id":"brand","label":"Brand","kind":"group"}}},{"key":"logoText","label":"Logo text","type":"text","helpText":"Read by screen readers. Shown if the logo can't load.","required":true,"config":{"maxLength":40},"ui":{"tab":{"id":"content"},"group":{"id":"brand"}}},{"key":"links","label":"Links","type":"table","config":{"minItems":1,"maxItems":6,"fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":24}},{"key":"link","label":"Links to","type":"link","required":true}]},"ui":{"itemLabel":"Link","icon":"link","preview":{"title":"label","subtitle":"link.href","fallback":"Untitled link"},"tab":{"id":"content"}}},{"key":"phone","label":"Phone","type":"link","helpText":"Leave empty to hide it.","ui":{"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"isSticky","label":"Stick to the top","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design"}}},{"key":"isTransparent","label":"Transparent over heroes","type":"boolean","ui":{"tab":{"id":"design"}}},{"key":"mobileMenu","label":"Mobile menu","type":"select","defaultValue":"drawer","config":{"options":[{"label":"Drawer","value":"drawer","description":"Slides in from the right"},{"label":"Full screen","value":"fullscreen","description":"Covers the page, big type"}]},"ui":{"control":"radio","tab":{"id":"design"}}}],"description":"Logo, a few links and one main button. Synced across every page.","category":"navbar","icon":"layout-navbar","preview":{"title":"logoText","media":"logo"}}},
|
|
1361
|
+
{"id":"nav-announcement-utility","name":"Header with announcement + utility row","category":"Navigation & chrome","kind":"global","tags":["global-synced","conditional","repeater","localized"],"proves":"Three stacked rows in one global · conditional row content · tab groups per row · design values split from content","why":"Three visual rows, ONE global: the rows always ship together and share a sticky behavior, so splitting them into three globals would make 'what stays on scroll' impossible to express. Tab groups mirror the rows (Main row / Top rows / Design) so a marketer changing the shipping message never sees the menu. The announcement is an object with an `showAnnouncement` switch and its fields conditional on it — turning the strip off keeps the copy for next time. Its color is a design choice and lives in Design, not inside the announcement object.","htmlSignals":["Two or three stacked rows inside <header>: a full-width colored strip with one sentence, then a row of small links, then the main nav","The strip text contains shipping/discount wording ('Free shipping', '% off', 'Use code')","Utility row uses smaller type and icons (truck, help, map pin) and is display:none under ~768px","Only the bottom row has position: sticky"],"mcp":"Treat the stacked rows as ONE global. Colored single-sentence strip → announcement { showAnnouncement: true, message, link: the strip's <a> if any }; its background color → announcementTone (red/orange → sale, brand color → brand, gray → neutral). Small links above the main nav → utilityLinks (keep the icon name if an icon library class is present). Main <nav> anchors → items. If the strip is a separate dismissible element with its own close button and a countdown, use `announcement-countdown` instead and leave this one's announcement disabled.","pitfalls":["A rotating announcement (several messages cycling) is an array, not one string — switch `message` to an array of {message, link} rather than joining with ·.","Don't put the account/cart icons in utility links — those are app state; see `nav-ecommerce-header`."],"notCarried":["ai","category","item openMode","kind:global"],"component":{"name":"Utility header","slug":"utility-header","props":[{"key":"logo","label":"Logo","type":"image","helpText":"SVG, or a PNG at least 64px tall.","ui":{"tab":{"id":"content","label":"Main row"},"group":{"id":"brand","label":"Brand","kind":"group"}}},{"key":"logoText","label":"Logo text","type":"text","helpText":"Read by screen readers. Shown if the logo can't load.","required":true,"config":{"maxLength":40},"ui":{"tab":{"id":"content"},"group":{"id":"brand"}}},{"key":"items","label":"Menu items","type":"table","config":{"minItems":1,"maxItems":7,"fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":20}},{"key":"link","label":"Links to","type":"link","required":true}]},"ui":{"itemLabel":"Menu item","itemName":"link","itemTitle":"Link","icon":"link","preview":{"title":"label","subtitle":"link.href","fallback":"Untitled link"},"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"announcement","label":"Announcement row","type":"object","openMode":"drill","config":{"fields":[{"key":"showAnnouncement","label":"Show the announcement","type":"boolean","defaultValue":true},{"key":"message","label":"Message","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"showAnnouncement","op":"eq","value":"true"}]},"recommendedMax":70,"localized":true,"config":{"maxLength":90}},{"key":"link","label":"Links to","type":"link","helpText":"Leave empty for plain text.","showIf":{"match":"all","rules":[{"field":"showAnnouncement","op":"eq","value":"true"}]}}]},"ui":{"icon":"speakerphone","tab":{"id":"rows","label":"Top rows"}}},{"key":"utilityLinks","label":"Utility links","type":"table","helpText":"Hidden on phones.","config":{"maxItems":4,"fields":[{"key":"icon","label":"Icon","type":"icon"},{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":20}},{"key":"link","label":"Links to","type":"link","required":true}]},"ui":{"itemLabel":"Utility link","icon":"link","preview":{"title":"label","subtitle":"link.href","fallback":"Untitled link"},"tab":{"id":"rows"}}},{"key":"announcementTone","label":"Announcement color","type":"select","defaultValue":"brand","config":{"options":[{"label":"Neutral","value":"neutral"},{"label":"Brand","value":"brand"},{"label":"Sale","value":"sale"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"stick","label":"What stays on scroll","type":"select","defaultValue":"main","config":{"options":[{"label":"Nothing","value":"none","description":"The whole header scrolls away"},{"label":"Main row only","value":"main","description":"Top rows scroll away, the menu stays"},{"label":"All rows","value":"all"}]},"ui":{"control":"radio","tab":{"id":"design"}}}],"description":"An announcement row, a utility row and the main menu. Synced across every page.","category":"navbar","icon":"layout-navbar","preview":{"title":"logoText","subtitle":"announcement.message","media":"logo"}}},
|
|
1362
|
+
{"id":"nav-mega-featured-promo","name":"Mega menu with featured cards + promo slot","category":"Navigation & chrome","kind":"global","tags":["deep-nesting","slots","global-synced","conditional","references","variants","commerce"],"proves":"Six levels deep · product references inside a menu · a marketer-owned promo SLOT next to structured columns","why":"Same global header shape as the core Mega menu, but the panel separates what is STRUCTURED from what is CAMPAIGN. Link columns and product cards are typed fields (a converter and a validator can reason about them); the promo is a `slot` limited to one block from a short allow-list, so marketing can drop in a card, image or video for a launch without a developer adding fields. Product cards are REFERENCES to Shopify products — photo, name, price and stock flow in, so the menu never shows a stale price. `opens` switches between a plain link and a panel; the panel object only shows when needed. The badge is a popover object because it's 3 small fields edited rarely, and it carries its own expiry.","htmlSignals":["Top-level nav <li> containing a <div> panel (often role=menu or [data-mega], display:none until hover/aria-expanded)","Panel holds 2–4 <ul> lists each preceded by a small heading — the columns","Image cards inside the panel with a price or 'Shop' link — product cards (look for /products/ hrefs and price text)","A visually different tile (full-bleed image or colored card) at the panel edge — the promo","Small pills (<span class=\"badge\">New</span>) next to some links"],"mcp":"Per top-level <li>: if it has a panel → opens: 'panel', else opens: 'link'. Panel headings + their <ul> → columns[] (a trailing 'View all/Shop all' anchor → viewAll, not a link). Card with a price and a /products/<handle> href → featuredCards[] { source: 'product', product: resolve handle to the Shopify product id }; card without price → { source: 'custom', image, title, button }. The remaining odd-one-out tile → promo slot as ctaCard (text+button), imageBlock (image+caption) or videoBlock. Pills → badge.label; red/sale colors → tone: 'sale'. Never copy the price text into a field.","pitfalls":["Product cards that hard-code price text go stale on the next price change — always resolve to a reference.","If two menu items share the same panel content, that is still two panels in content; don't invent a shared-panel global for it.","Hover-only panels fail keyboard users — the converter should default `openOn` to click when the source has no aria-expanded handling."],"notCarried":["ai","category","kind:global","shopify reference","variants"],"component":{"name":"Mega menu header","slug":"mega-menu-header","props":[{"key":"logo","label":"Logo","type":"image","helpText":"SVG, or a PNG at least 64px tall.","ui":{"tab":{"id":"content","label":"Content"},"group":{"id":"brand","label":"Brand","kind":"group"}}},{"key":"logoText","label":"Logo text","type":"text","helpText":"Read by screen readers. Shown if the logo can't load.","required":true,"config":{"maxLength":40},"ui":{"tab":{"id":"content"},"group":{"id":"brand"}}},{"key":"items","label":"Menu items","type":"table","config":{"minItems":1,"maxItems":6,"fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":20}},{"key":"opens","label":"Opens","type":"select","defaultValue":"link","options":[{"label":"A page","value":"link"},{"label":"A panel","value":"panel"}],"ui":{"control":"segmented"}},{"key":"link","label":"Links to","type":"link","required":true,"showIf":{"match":"all","rules":[{"field":"opens","op":"eq","value":"link"}]}},{"key":"panel","label":"Panel","type":"object","showIf":{"match":"all","rules":[{"field":"opens","op":"eq","value":"panel"}]},"openMode":"drill","fields":[{"key":"columns","label":"Link columns","type":"table","fields":[{"key":"title","label":"Column title","type":"text","required":true,"config":{"maxLength":24}},{"key":"viewAll","label":"View all link","type":"link"},{"key":"links","label":"Links","type":"table","fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":32}},{"key":"link","label":"Links to","type":"link","required":true},{"key":"description","label":"Description","type":"text","recommendedMax":60},{"key":"badge","label":"Badge","type":"object","openMode":"popover","fields":[{"key":"label","label":"Label","type":"text","config":{"maxLength":12}},{"key":"tone","label":"Color","type":"select","options":[{"label":"Brand","value":"brand"},{"label":"Sale","value":"sale"},{"label":"Neutral","value":"neutral"}],"ui":{"control":"segmented"}},{"key":"hideAfter","label":"Hide after","type":"datetime"}],"ui":{"icon":"badge"}}],"config":{"maxItems":8},"ui":{"itemLabel":"Link","itemName":"panelLink","icon":"link","preview":{"title":"label","subtitle":"description","fallback":"Untitled link"}}}],"config":{"minItems":1,"maxItems":4},"ui":{"itemLabel":"Column","icon":"columns-3","preview":{"title":"title","fallback":"Untitled column"}}},{"key":"featuredCards","label":"Featured cards","type":"table","fields":[{"key":"source","label":"Shows","type":"select","defaultValue":"product","options":[{"label":"A product","value":"product"},{"label":"Custom card","value":"custom"}],"ui":{"control":"segmented"}},{"key":"product","label":"Product","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"product"}]},"readOnly":true,"lockReason":"A Shopify product id. No Shopify integration backs a reference in BetterCMS yet, so the id is kept as it is."},{"key":"image","label":"Image","type":"image","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"custom"}]}},{"key":"title","label":"Title","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"custom"}]},"recommendedMax":40},{"key":"button","label":"Button","type":"button","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"custom"}]}}],"config":{"maxItems":2},"ui":{"itemLabel":"Card","icon":"layout-cards","preview":{"title":"title","subtitle":"source","media":"image","fallback":"Untitled product card"}}},{"key":"promo","label":"Promo","type":"slot","config":{"multiple":true,"maxItems":1,"allow":["ctaCard","imageBlock","productCard","videoBlock"]}}],"ui":{"icon":"layout-board"}}]},"ui":{"itemLabel":"Menu item","itemName":"menuItem","icon":"menu","preview":{"title":"label","subtitle":"opens","fallback":"Untitled item"},"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"openOn","label":"Panels open on","type":"select","defaultValue":"hover","config":{"options":[{"label":"Hover","value":"hover"},{"label":"Click","value":"click"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"useBackdrop","label":"Dim the page behind","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design"}}}],"description":"Menu items that open wide panels with link columns, product cards and a promo.","category":"navbar","icon":"layout-navbar","preview":{"title":"logoText","media":"logo"}}},
|
|
1363
|
+
{"id":"nav-docs-sidebar","name":"Docs sidebar navigation","category":"Navigation & chrome","kind":"global","tags":["deep-nesting","repeater","global-synced","conditional","dynamic-query","references"],"proves":"Four-level tree at a fixed depth · curated vs auto from the page tree · version switcher · status badges","why":"A docs sidebar is shared by every docs page, so it is a global. Most teams want BOTH modes: a curated order (what to read first) and an automatic tree (no one forgets to add the new page), so `source` decides which fields show. The tree is modelled to a FIXED depth — Section › Page › Sub-page — because our schema language has no recursive types; three levels is also the usability ceiling for a sidebar. Badges are a select (a fixed vocabulary the renderer can color and the search index can filter), not free text. Versions are a small inline array because each one is just a label, a link and a status.","htmlSignals":["<aside>/<nav aria-label=\"Docs\"> fixed to the left with a long nested <ul> and collapsible groups (<details>/<button aria-expanded>)","Group headings in small caps above lists of links whose hrefs share a /docs/ prefix","A version <select> or dropdown ('v3', 'latest') at the top; a search input with a ⌘K / Ctrl K hint","Pills like 'New', 'Beta', 'Deprecated' next to links","The same sidebar on every /docs/* page with the current link marked aria-current=page"],"mcp":"Crawl 2+ /docs pages; identical sidebars → ONE global `docsSidebar`. If the tree order matches the URL/folder order exactly and there's no hand-written grouping → source: 'auto' with root = the docs index page; otherwise source: 'curated'. Group headings → sections[].title; links under them → pages[]; nested lists → children[] (flatten anything deeper into the sub-page level and report it). Pills → badge (map unknown text to '' and report). Version dropdown options → versions[] (status 'legacy' for anything not marked latest).","pitfalls":["Sidebars 4+ levels deep can't be represented — flatten to sub-pages and flag it rather than inventing nested objects.","Don't import the 'On this page' table of contents — it's generated from each page's headings."],"notCarried":["category","item openMode","kind:global"],"component":{"name":"Docs sidebar","slug":"docs-sidebar","props":[{"key":"versions","label":"Versions","type":"table","helpText":"Leave empty to hide the switcher.","config":{"maxItems":6,"fields":[{"key":"label","label":"Label","type":"text","required":true,"placeholder":"v3","config":{"maxLength":16}},{"key":"link","label":"Docs home","type":"link","required":true},{"key":"status","label":"Status","type":"select","options":[{"label":"Current","value":"current"},{"label":"Beta","value":"beta"},{"label":"Legacy","value":"legacy","description":"Shows an out-of-date warning on its pages"}]}]},"ui":{"itemLabel":"Version","icon":"versions","preview":{"title":"label","subtitle":"status","fallback":"Untitled version"},"tab":{"id":"content","label":"Content"}}},{"key":"searchPlaceholder","label":"Search placeholder","type":"text","placeholder":"Search docs","config":{"maxLength":40},"ui":{"tab":{"id":"content"}}},{"key":"source","label":"Tree comes from","type":"select","defaultValue":"curated","config":{"options":[{"label":"Curated","value":"curated","icon":"list-tree","description":"You order sections and pages here"},{"label":"Page tree","value":"auto","icon":"folders","description":"Follows the docs folder; new pages appear on their own"}]},"ui":{"control":"cards","tab":{"id":"content"}}},{"key":"root","label":"Docs root page","type":"reference","helpText":"Every page under this one is listed, in folder order.","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"auto"}]},"config":{"targets":["page"]},"ui":{"tab":{"id":"content"}}},{"key":"sections","label":"Sections","type":"table","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"curated"}]},"config":{"minItems":1,"maxItems":12,"fields":[{"key":"title","label":"Section title","type":"text","required":true,"config":{"maxLength":32}},{"key":"icon","label":"Icon","type":"icon"},{"key":"isCollapsed","label":"Start collapsed","type":"boolean"},{"key":"pages","label":"Pages","type":"table","fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":40}},{"key":"link","label":"Links to","type":"link","required":true},{"key":"badge","label":"Badge","type":"select","options":[{"label":"None","value":""},{"label":"New","value":"new"},{"label":"Beta","value":"beta"},{"label":"Deprecated","value":"deprecated"}],"ui":{"control":"segmented"}},{"key":"children","label":"Sub-pages","type":"table","helpText":"One level only.","fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":40}},{"key":"link","label":"Links to","type":"link","required":true}],"config":{"maxItems":15},"ui":{"itemLabel":"Sub-page","itemName":"docSubLink","icon":"file","preview":{"title":"label","fallback":"Untitled sub-page"}}}],"config":{"minItems":1,"maxItems":30},"ui":{"itemLabel":"Page","itemName":"docLink","icon":"file-text","preview":{"title":"label","subtitle":"badge","fallback":"Untitled page"}}}]},"ui":{"itemLabel":"Section","icon":"folder","preview":{"title":"title","fallback":"Untitled section"},"tab":{"id":"content"}}},{"key":"footerLinks","label":"Footer links","type":"table","config":{"maxItems":4,"fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":24}},{"key":"link","label":"Links to","type":"link","required":true}]},"ui":{"itemLabel":"Link","itemName":"link","icon":"link","preview":{"title":"label","subtitle":"link.href","fallback":"Untitled link"},"tab":{"id":"content"}}},{"key":"isCurrentOpen","label":"Open the current section","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"behavior","label":"Behavior"}}},{"key":"showPrevNext","label":"Previous and next links","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"behavior"}}}],"description":"The left-hand tree on every docs page, with search and a version switcher.","category":"navbar","icon":"layout-sidebar","preview":{"title":"searchPlaceholder"}}},
|
|
1364
|
+
{"id":"nav-ecommerce-header","name":"E-commerce header","category":"Navigation & chrome","kind":"global","tags":["global-synced","commerce","references","repeater","conditional","media-heavy"],"proves":"Search, account and cart config in one global · product references for search · app state (cart count) NOT modelled","why":"Everything a store header SAYS is content; everything it COUNTS is app state. The cart count, the signed-in name and search results come from the commerce/session layer at runtime and are deliberately not fields. What remains — categories, search hints, empty states, which way the cart opens — is one global. Popular products are references (photos and prices stay live). Suggested searches are `tags` because they're short strings with no other properties. Groups follow the visitor's mental model (Content / Search / Account & cart) so the merchandiser doesn't wade through cart copy to change a category.","htmlSignals":["Header with a search <input type=\"search\"> or <form role=\"search\"> plus account and bag/cart icons on the right","A cart icon with a numeric badge (e.g. <span class=\"cart-count\">2</span>) — runtime state","A row of collection links (/collections/…, /shop/…) under or beside the logo","Search dropdown markup with 'Popular searches' or 'Trending' lists and product tiles","A hidden cart drawer (<aside aria-label=\"Cart\"> / role=dialog) elsewhere in the DOM"],"mcp":"Create ONE global `storeHeader`. Collection links → categories[] (a link colored red / labeled Sale → isHighlighted: true). Search placeholder → searchPlaceholder; 'Popular searches' text items → suggestions; product tiles inside the search dropdown → popularProducts resolved by handle to Shopify ids. A cart drawer element present → cartOpens: 'drawer' and copy its empty-state text/button into emptyCart/drawerButton. NEVER create fields for the cart count, prices, the customer name or currency — report them as runtime.","pitfalls":["Copying '(2)' from the cart badge into content is the most common converter mistake — it's session state.","Free-shipping thresholds belong to commerce settings, not the header; reference them from the renderer."],"notCarried":["category","kind:global","shopify reference","variants"],"component":{"name":"Store header","slug":"store-header","props":[{"key":"logo","label":"Logo","type":"image","helpText":"SVG, or a PNG at least 64px tall.","ui":{"tab":{"id":"content","label":"Content"},"group":{"id":"brand","label":"Brand","kind":"group"}}},{"key":"logoText","label":"Logo text","type":"text","helpText":"Read by screen readers. Shown if the logo can't load.","required":true,"config":{"maxLength":40},"ui":{"tab":{"id":"content"},"group":{"id":"brand"}}},{"key":"categories","label":"Categories","type":"table","config":{"minItems":1,"maxItems":8,"fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":20}},{"key":"link","label":"Links to","type":"link","required":true},{"key":"thumbnail","label":"Thumbnail","type":"image"},{"key":"isHighlighted","label":"Show in the sale color","type":"boolean"}]},"ui":{"itemLabel":"Category","icon":"category","preview":{"title":"label","subtitle":"link.href","media":"thumbnail","fallback":"Untitled category"},"tab":{"id":"content"}}},{"key":"searchPlaceholder","label":"Placeholder","type":"text","config":{"maxLength":48},"ui":{"tab":{"id":"search","label":"Search"}}},{"key":"suggestions","label":"Suggested searches","type":"array","config":{"itemType":"text","maxItems":6},"ui":{"itemLabel":"Suggestion","itemTitle":"Suggestion","tab":{"id":"search"}}},{"key":"popularProducts","label":"Popular products","type":"array","readOnly":true,"lockReason":"A Shopify product id. No Shopify integration backs a reference in BetterCMS yet, so the id is kept as it is.","config":{"maxItems":4,"itemType":"text"},"ui":{"itemLabel":"Product","itemName":"product","itemTitle":"Product","tab":{"id":"search"}}},{"key":"noResults","label":"No results message","type":"text","placeholder":"Nothing matched. Try \"skillet\" or \"kettle\".","config":{"maxLength":80},"ui":{"tab":{"id":"search"}}},{"key":"accountLabel","label":"Account label","type":"text","helpText":"Shown to signed-out visitors only.","defaultValue":"Sign in","config":{"maxLength":16},"ui":{"tab":{"id":"cart","label":"Account & cart"}}},{"key":"accountLink","label":"Account page","type":"link","ui":{"tab":{"id":"cart"}}},{"key":"cartOpens","label":"Cart opens","type":"select","defaultValue":"drawer","config":{"options":[{"label":"A drawer","value":"drawer","description":"Slides in; people keep their place"},{"label":"The cart page","value":"page"}]},"ui":{"control":"radio","tab":{"id":"cart"}}},{"key":"emptyCart","label":"Empty cart message","type":"text","showIf":{"match":"all","rules":[{"field":"cartOpens","op":"eq","value":"drawer"}]},"config":{"maxLength":80},"ui":{"tab":{"id":"cart"}}},{"key":"drawerButton","label":"Drawer button","type":"button","showIf":{"match":"all","rules":[{"field":"cartOpens","op":"eq","value":"drawer"}]},"ui":{"tab":{"id":"cart"}}},{"key":"isSticky","label":"Stick to the top","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"categoryBar","label":"Category bar","type":"select","defaultValue":"below","config":{"options":[{"label":"Below the logo","value":"below"},{"label":"Next to the logo","value":"inline"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Logo, category bar, search, account and cart. Synced across every page.","category":"navbar","icon":"layout-navbar","preview":{"title":"logoText","media":"logo"}}},
|
|
1365
|
+
{"id":"nav-locale-header","name":"Multi-locale header with switchers","category":"Navigation & chrome","kind":"global","tags":["localized","global-synced","conditional","repeater"],"proves":"Localized labels inside a global · switcher config with conditional lists · what belongs to site settings vs content","why":"Labels are `localized: true` (one field, a value per locale) instead of one header per language: the menu STRUCTURE is shared, only the words change, so adding a link once adds it everywhere. Links point at pages, and the router resolves each locale's translation — no per-locale URLs typed in. The switcher's lists are content today because the site owner curates order and native names, but they mirror what should be site settings (published locales, enabled currencies) — see the gap note. Display (codes vs names) and placement are Design.","htmlSignals":["<html lang> differs across crawled URLs (/de/, /fr/ prefixes or subdomains) while the header markup is identical","A dropdown listing language names in their own script (Deutsch, Français, 日本語) and/or currency codes/symbols","<link rel=\"alternate\" hreflang=\"…\"> tags in <head> (native SEO — don't model them)","A 'Shop in your language?' geo banner or modal"],"mcp":"Crawl the same page in each locale. Header with identical structure → ONE global; for each nav label, write the per-locale strings into the localized `label` (match items by href position, not by text). Language dropdown options → languages[] { locale from hreflang/path prefix, nativeName: the option text }. Currency options → currencies (ISO codes). Never model hreflang tags — the platform emits them. If headers differ structurally by locale (different items), report it: that's a per-market header, not a translation.","pitfalls":["Using flags for languages (🇬🇧 for English) — map to codes and set display: 'code'.","Translating the link target by typing /de/... URLs — link to the page and let locale routing resolve it."],"notCarried":["category","item openMode","kind:global"],"component":{"name":"Multilingual header","slug":"multilingual-header","props":[{"key":"logo","label":"Logo","type":"image","helpText":"SVG, or a PNG at least 64px tall.","ui":{"tab":{"id":"content","label":"Content"},"group":{"id":"brand","label":"Brand","kind":"group"}}},{"key":"logoText","label":"Logo text","type":"text","helpText":"Read by screen readers. Shown if the logo can't load.","required":true,"config":{"maxLength":40},"ui":{"tab":{"id":"content"},"group":{"id":"brand"}}},{"key":"items","label":"Menu items","type":"table","config":{"minItems":1,"maxItems":6,"fields":[{"key":"label","label":"Label","type":"text","required":true,"localized":true,"config":{"maxLength":20}},{"key":"link","label":"Links to","type":"link","helpText":"Each language opens its own translation.","required":true}]},"ui":{"itemLabel":"Menu item","itemName":"localizedLink","itemTitle":"Link","icon":"link","preview":{"title":"label","subtitle":"link.href","fallback":"Untitled link"},"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","localized":true,"ui":{"tab":{"id":"content"}}},{"key":"showLanguage","label":"Language switcher","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"switchers","label":"Switchers"}}},{"key":"languages","label":"Languages","type":"table","helpText":"Only languages the site is published in.","showIf":{"match":"all","rules":[{"field":"showLanguage","op":"eq","value":"true"}]},"config":{"minItems":2,"maxItems":12,"fields":[{"key":"locale","label":"Locale","type":"select","required":true,"unique":true,"options":[{"label":"English (US)","value":"en-US"},{"label":"English (UK)","value":"en-GB"},{"label":"German","value":"de-DE"},{"label":"French","value":"fr-FR"},{"label":"Japanese","value":"ja-JP"}]},{"key":"nativeName","label":"Name in the menu","type":"text","helpText":"In its own language, e.g. Deutsch.","required":true,"config":{"maxLength":20}}]},"ui":{"itemLabel":"Language","icon":"language","preview":{"title":"nativeName","subtitle":"locale","fallback":"Untitled language"},"tab":{"id":"switchers"}}},{"key":"showCurrency","label":"Currency switcher","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"switchers"}}},{"key":"currencies","label":"Currencies","type":"multiselect","showIf":{"match":"all","rules":[{"field":"showCurrency","op":"eq","value":"true"}]},"config":{"options":[{"label":"US dollar · $","value":"USD"},{"label":"Euro · €","value":"EUR"},{"label":"Pound sterling · £","value":"GBP"},{"label":"Canadian dollar · CA$","value":"CAD"},{"label":"Japanese yen · ¥","value":"JPY"}]},"ui":{"tab":{"id":"switchers"}}},{"key":"firstVisit","label":"On the first visit","type":"select","defaultValue":"suggest","config":{"options":[{"label":"Suggest a locale","value":"suggest","description":"A banner offers the visitor's language; nothing changes until they pick"},{"label":"Stay on the address they opened","value":"stay"}]},"ui":{"control":"radio","tab":{"id":"switchers"}}},{"key":"suggestMessage","label":"Suggestion message","type":"text","showIf":{"match":"all","rules":[{"field":"firstVisit","op":"eq","value":"suggest"}]},"localized":true,"placeholder":"Looks like you're in Germany. Shop in Deutsch · EUR?","config":{"maxLength":80},"ui":{"tab":{"id":"switchers"}}},{"key":"display","label":"Switcher shows","type":"select","defaultValue":"code","config":{"options":[{"label":"EN · USD","value":"code"},{"label":"English · $","value":"name"},{"label":"Globe icon","value":"globe"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"placement","label":"Placement","type":"select","defaultValue":"header","config":{"options":[{"label":"Header","value":"header"},{"label":"Footer","value":"footer"},{"label":"Both","value":"both"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Main menu plus language and currency switchers. Labels are translated per locale.","category":"navbar","icon":"layout-navbar","preview":{"title":"logoText","media":"logo"}}},
|
|
1366
|
+
{"id":"sticky-mobile-cta-bar","name":"Sticky bottom CTA bar (mobile)","category":"Navigation & chrome","kind":"block","tags":["responsive-layout","conditional","validation","commerce"],"proves":"A per-template block with breakpoint visibility, scroll triggers and a secondary contact action","why":"Unlike the header, this bar is placed per template (product pages, a landing page), so it's an ordinary block, not a global. Breakpoint visibility is a multiselect because our schema has no per-breakpoint value type — this is the honest minimum (see gaps). The price is a switch, not a number: it's read live from the product the page is about. The second action is a select with a conditional phone link, so a chat action doesn't ask for a number it won't use.","htmlSignals":["A fixed element at bottom: 0 with a single prominent button, often display:none ≥ 768px","IntersectionObserver / scroll listener toggling a class like is-visible on the bar","Duplicates the main CTA text from the hero or buy box","env(safe-area-inset-bottom) padding"],"mcp":"position: fixed + bottom: 0 + one button → `stickyBar`, placed on the template it appears on (not global unless it's on every page). Read media queries for showOn. A scroll listener tied to the hero → trigger: 'pastHero'. Copy the price only as showPrice: true; never the number. If the button submits a cart form, keep href '#buy' and report that the renderer must wire it to the product's add-to-cart.","pitfalls":["'Add to cart' is an ACTION, not a link — our button type only carries links (gap: action buttons).","A bar covering the cookie banner or chat launcher: the renderer must stack them; nothing to model."],"notCarried":["ai","category"],"component":{"name":"Sticky action bar","slug":"sticky-bar","props":[{"key":"message","label":"Message","type":"text","recommendedMax":40,"config":{"maxLength":60},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"showPrice","label":"Show the product price","type":"boolean","helpText":"Product pages only.","ui":{"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","required":true,"ui":{"tab":{"id":"content"}}},{"key":"secondaryAction","label":"Second action","type":"select","defaultValue":"none","config":{"options":[{"label":"None","value":"none"},{"label":"Call","value":"call"},{"label":"Chat","value":"chat"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"phone","label":"Phone","type":"link","required":true,"showIf":{"match":"all","rules":[{"field":"secondaryAction","op":"eq","value":"call"}]},"ui":{"tab":{"id":"content"}}},{"key":"showOn","label":"Show on","type":"multiselect","defaultValue":["mobile"],"config":{"min":1,"options":[{"label":"Phones","value":"mobile"},{"label":"Tablets","value":"tablet"},{"label":"Desktop","value":"desktop"}]},"ui":{"tab":{"id":"behavior","label":"Behavior"}}},{"key":"trigger","label":"Appears","type":"select","defaultValue":"pastHero","config":{"options":[{"label":"Right away","value":"always"},{"label":"After the first section scrolls away","value":"pastHero","description":"Avoids two identical buttons on screen"},{"label":"After part of the page","value":"percent"}]},"ui":{"control":"radio","tab":{"id":"behavior"}}},{"key":"percent","label":"Scrolled","type":"number","defaultValue":30,"showIf":{"match":"all","rules":[{"field":"trigger","op":"eq","value":"percent"}]},"config":{"min":10,"max":90,"step":5,"unit":"%"},"ui":{"control":"slider","tab":{"id":"behavior"}}},{"key":"hideNearFooter","label":"Hide near the footer","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"behavior"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}}],"description":"A bar pinned to the bottom of the screen with one main action.","category":"navbar","icon":"layout-bottombar-expand","preview":{"title":"message","subtitle":"button.label"}}},
|
|
1367
|
+
{"id":"announcement-countdown","name":"Announcement bar with countdown","category":"Navigation & chrome","kind":"global","tags":["global-synced","conditional","validation","repeater","localized","a11y"],"proves":"Scheduled global with time zone · countdown with an after-end state · dismiss memory · rotating messages","why":"A standalone global (not part of the header) because it has its own lifecycle: scheduled on and off, often swapped weekly while the header stays put. Messages are a small array so rotation is data, not a hack of joined strings. The countdown is an object with an `showCountdown` switch whose fields appear only when it's on, including what happens at zero — the moment most hand-built bars break. Dismissal is behavior that belongs to the content (how long a closed bar stays closed). The pause control is locked on for marketers because WCAG 2.2.2 requires moving content to be pausable.","htmlSignals":["A full-width strip above <header> with one sentence and a close (×) <button aria-label=\"Close\">","Digits ticking every second (setInterval) formatted as 02d 14h 05m, plus an end timestamp in a data attribute","localStorage/cookie key like announcement_dismissed","Several messages in a track with translateX/opacity animation — rotation"],"mcp":"Separate global `countdownBar`, independent from the header. Each rotating text → messages[] { text, link }. A countdown's target timestamp (data-end, a Date string in the script) → countdown.endsAt WITH its offset; never store the rendered '02d 14h'. A close button → isDismissible: true; a cookie/localStorage max-age → rememberDays. If the script hides the bar at zero → afterEnd: 'hide'. Map red/sale colors to tone: 'sale'.","pitfalls":["Storing the countdown as text ('2 days left') — it's wrong the next day. Always a datetime with offset.","Hide-after earlier than the countdown end can't be caught today (gap: cross-field validation)."],"notCarried":["ai","category","item openMode","kind:global"],"component":{"name":"Countdown bar","slug":"countdown-bar","props":[{"key":"messages","label":"Messages","type":"table","helpText":"Messages rotate every 5 seconds.","config":{"minItems":1,"maxItems":3,"fields":[{"key":"text","label":"Text","type":"text","required":true,"recommendedMax":60,"localized":true,"config":{"maxLength":80}},{"key":"link","label":"Links to","type":"link"}]},"ui":{"itemLabel":"Message","icon":"message","preview":{"title":"text","subtitle":"link.href","fallback":"Untitled message"},"tab":{"id":"content","label":"Content"}}},{"key":"countdown","label":"Countdown","type":"object","openMode":"drill","config":{"fields":[{"key":"showCountdown","label":"Show a countdown","type":"boolean"},{"key":"endsAt","label":"Counts down to","type":"datetime","helpText":"In the sale's time zone. Visitors see their local time.","required":true,"showIf":{"match":"all","rules":[{"field":"showCountdown","op":"eq","value":"true"}]}},{"key":"label","label":"Label","type":"text","showIf":{"match":"all","rules":[{"field":"showCountdown","op":"eq","value":"true"}]},"placeholder":"Ends in","config":{"maxLength":20}},{"key":"afterEnd","label":"When it reaches zero","type":"select","defaultValue":"hide","showIf":{"match":"all","rules":[{"field":"showCountdown","op":"eq","value":"true"}]},"options":[{"label":"Hide the bar","value":"hide"},{"label":"Show a different message","value":"message"}],"ui":{"control":"radio"}},{"key":"endedText","label":"Message after the end","type":"text","showIf":{"match":"all","rules":[{"field":"afterEnd","op":"eq","value":"message"}]},"localized":true,"placeholder":"The sale has ended. Thanks for shopping.","config":{"maxLength":80}}]},"ui":{"icon":"clock","tab":{"id":"content"}}},{"key":"isDismissible","label":"Visitors can close it","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"content"}}},{"key":"rememberDays","label":"Stay closed for","type":"number","helpText":"Changing the message shows it again to people who closed it.","defaultValue":7,"showIf":{"match":"all","rules":[{"field":"isDismissible","op":"eq","value":"true"}]},"config":{"min":1,"max":90,"step":1,"unit":"days"},"ui":{"tab":{"id":"content"}}},{"key":"startsAt","label":"Show from","type":"datetime","helpText":"Leave empty to show it as soon as it's published.","ui":{"tab":{"id":"schedule","label":"Schedule"}}},{"key":"endsAt","label":"Hide after","type":"datetime","helpText":"Must be on or after the countdown's end.","ui":{"tab":{"id":"schedule"}}},{"key":"tone","label":"Color","type":"select","defaultValue":"brand","config":{"options":[{"label":"Neutral","value":"neutral"},{"label":"Brand","value":"brand"},{"label":"Sale","value":"sale"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"usePauseOnHover","label":"Pause on hover and focus","type":"boolean","defaultValue":true,"readOnly":"developer","lockReason":"Required for accessibility: moving content must be pausable.","ui":{"access":{"edit":"schema.manage"},"tab":{"id":"design"}}}],"description":"A strip above the header for a sale or launch, with an optional countdown.","category":"navbar","icon":"alarm","preview":{"title":"messages.0.text","subtitle":"countdown.endsAt"}}},
|
|
1368
|
+
{"id":"cookie-consent","name":"Cookie consent banner","category":"Navigation & chrome","kind":"global","tags":["global-synced","repeater","validation","a11y","localized","conditional","variants"],"proves":"Consent categories as data · action labels that are NOT links · developer-locked keys · localized legal copy","why":"Consent is legal copy plus a contract with the site's scripts, so the two halves are modelled differently: categories are an array of objects whose `key` is developer-locked (scripts gate on it) while labels and descriptions are localized and editable. 'Accept all' and 'Reject all' are STRINGS, not buttons — our `button` type is label + link and these don't navigate. Layout (bar/box/modal) is a block variant, not a field, because it changes the component not the copy. It's a global: one consent UI for the whole site.","htmlSignals":["A fixed bottom/corner element or role=dialog with the words cookie/consent/privacy and 2–3 buttons (Accept / Reject / Preferences)","A second hidden panel with toggles (<input type=\"checkbox\" role=\"switch\">) per category","Scripts with type=\"text/plain\" and data-category=\"analytics\" (a consent manager pattern)","A cookie like cookie_consent / cc_cookie / OptanonConsent"],"mcp":"Create ONE global `cookieBanner`. Banner heading/body → title/body; the policy <a> → policyLink. Button TEXTS → labels.* (never buttons). Each toggle in the preferences panel → categories[] { key from data-category/name attribute, label, description, required: disabled+checked toggle }. If the site uses a third-party CMP (OneTrust, Cookiebot) via a script tag, report it instead of modelling — that content lives in the vendor's dashboard.","pitfalls":["Pre-ticked optional categories are unlawful in the EU — the converter must set isOnByDefault: false and report any source that pre-ticks.","A 'Reject all' hidden behind 'Preferences' is a dark pattern; flag it, don't replicate it."],"notCarried":["category","kind:global","variants"],"component":{"name":"Cookie banner","slug":"cookie-banner","props":[{"key":"title","label":"Title","type":"text","localized":true,"placeholder":"We use cookies","config":{"maxLength":48},"ui":{"tab":{"id":"content","label":"Banner"}}},{"key":"body","label":"Body","type":"longtext","required":true,"recommendedMax":220,"localized":true,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"policyLink","label":"Cookie policy","type":"link","helpText":"Most privacy laws require it.","required":true,"ui":{"tab":{"id":"content"}}},{"key":"labels","label":"Button labels","type":"object","openMode":"drill","config":{"fields":[{"key":"acceptAll","label":"Accept all","type":"text","required":true,"defaultValue":"Accept all","localized":true,"config":{"maxLength":20}},{"key":"rejectAll","label":"Reject all","type":"text","required":true,"defaultValue":"Reject all","localized":true,"config":{"maxLength":20}},{"key":"customize","label":"Open preferences","type":"text","required":true,"defaultValue":"Choose cookies","localized":true,"config":{"maxLength":24}},{"key":"save","label":"Save preferences","type":"text","required":true,"defaultValue":"Save my choices","localized":true,"config":{"maxLength":24}}]},"ui":{"icon":"click","tab":{"id":"content"}}},{"key":"categories","label":"Categories","type":"table","config":{"minItems":1,"maxItems":6,"fields":[{"key":"key","label":"Key","type":"text","required":true,"unique":true,"readOnly":"developer","lockReason":"Scripts check this key before they load.","config":{"pattern":"^[a-z][a-z_]*$","patternMessage":"Use lowercase letters and underscores, like analytics_storage."},"ui":{"access":{"edit":"schema.manage"}}},{"key":"label","label":"Label","type":"text","required":true,"localized":true,"config":{"maxLength":32}},{"key":"description","label":"What it covers","type":"longtext","required":true,"recommendedMax":200,"localized":true,"ui":{"rows":2}},{"key":"isRequired","label":"Always on","type":"boolean","helpText":"Visitors can't switch it off."},{"key":"isOnByDefault","label":"On by default","type":"boolean","helpText":"Must stay off in the EU and UK for anything but essential cookies.","showIf":{"match":"all","rules":[{"field":"isRequired","op":"eq","value":"false"}]}}]},"ui":{"itemLabel":"Category","icon":"toggle-right","preview":{"title":"label","subtitle":"key","fallback":"Untitled category"},"tab":{"id":"categories","label":"Categories"}}},{"key":"showTo","label":"Show the banner to","type":"select","defaultValue":"everyone","config":{"options":[{"label":"Everyone","value":"everyone"},{"label":"Visitors in regions that require consent","value":"regulated","description":"EU, UK, Switzerland, Brazil, California"}]},"ui":{"control":"radio","tab":{"id":"settings","label":"Settings"}}},{"key":"useGpcSignal","label":"Honor Global Privacy Control","type":"boolean","helpText":"Browsers that send GPC start with optional cookies off.","defaultValue":true,"ui":{"tab":{"id":"settings"}}},{"key":"renewMonths","label":"Ask again after","type":"number","helpText":"EU regulators expect 13 months at most.","defaultValue":12,"config":{"min":1,"max":13,"step":1,"unit":"months"},"ui":{"tab":{"id":"settings"}}}],"description":"The consent banner and its preferences panel. Shown until a visitor chooses.","category":"navbar","icon":"cookie","preview":{"title":"title","subtitle":"body"}}},
|
|
1369
|
+
{"id":"breadcrumb-config","name":"Breadcrumb trail","category":"Navigation & chrome","kind":"block","tags":["conditional","seo-structured","a11y","references","repeater"],"proves":"A mostly-automatic block: trail from the page tree, manual override only when needed · native JSON-LD","why":"Breadcrumbs are derived data — the right default is 'from the page tree' with zero typing, so the block is mostly settings. A manual `crumbs` array appears only when `source` is manual (campaign pages whose URL doesn't match their place in the IA). `parent` is a reference so a product can sit under its collection even when the URL is flat. The BreadcrumbList JSON-LD is emitted natively from the SAME resolved trail, so there is no schema/SEO field here — modelling one would create a second truth.","htmlSignals":["<nav aria-label=\"Breadcrumb\"> with an <ol> of <a> separated by › or /","The last item has aria-current=\"page\" and isn't a link","JSON-LD script with \"@type\": \"BreadcrumbList\" (native — read it to verify, don't model it)","Appears directly above the H1 on inner pages, absent on home"],"mcp":"If every crumb's href is a prefix of the current URL path → source: 'auto' and model nothing else. Otherwise → source: 'manual', crumbs = the middle crumbs (drop Home and the current page). Read separator glyph → separator. Never create JSON-LD fields; verify the platform output matches the source's BreadcrumbList instead.","pitfalls":["Importing the full trail per page as manual data — it rots the first time a page moves. Prefer auto.","A breadcrumb that's really a 'Back to results' link is navigation state, not a trail."],"notCarried":["category","item openMode"],"component":{"name":"Breadcrumbs","slug":"breadcrumbs","props":[{"key":"source","label":"Trail comes from","type":"select","defaultValue":"auto","config":{"options":[{"label":"Where the page sits","value":"auto","icon":"sitemap","description":"Folders and parent pages, updated on its own"},{"label":"Written here","value":"manual","icon":"pencil","description":"For pages whose address doesn't match the trail"}]},"ui":{"control":"cards","tab":{"id":"content","label":"Content"}}},{"key":"parent","label":"Treat as a child of","type":"reference","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"auto"}]},"config":{"targets":["page"]},"ui":{"tab":{"id":"content"}}},{"key":"crumbs","label":"Crumbs","type":"table","helpText":"Home and the current page are added for you.","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"manual"}]},"config":{"minItems":1,"maxItems":5,"fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":32}},{"key":"link","label":"Links to","type":"link","required":true}]},"ui":{"itemLabel":"Crumb","itemName":"link","itemTitle":"Link","icon":"link","preview":{"title":"label","subtitle":"link.href","fallback":"Untitled link"},"tab":{"id":"content"}}},{"key":"homeLabel","label":"Home label","type":"text","helpText":"Leave empty to show a house icon.","defaultValue":"Home","config":{"maxLength":16},"ui":{"tab":{"id":"content"}}},{"key":"showCurrent","label":"Show the current page","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"content"}}},{"key":"separator","label":"Separator","type":"select","defaultValue":"chevron","config":{"options":[{"label":"›","value":"chevron"},{"label":"/","value":"slash"},{"label":"·","value":"dot"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"collapseAfter","label":"Shorten trails longer than","type":"number","helpText":"Phones only.","defaultValue":4,"config":{"min":3,"max":6,"step":1,"unit":"crumbs"},"ui":{"tab":{"id":"design"}}}],"description":"The trail of links back up to home. Built from where the page sits.","category":"navbar","icon":"chevrons-right","preview":{"title":"source","subtitle":"homeLabel"}}},
|
|
1370
|
+
{"id":"footer-multi-column","name":"Multi-column footer","category":"Navigation & chrome","kind":"global","tags":["global-synced","repeater","references","form","validation","commerce"],"proves":"Link groups · newsletter as a form reference · social profiles · legal row with a year token · payment badges as a fixed set","why":"One global, grouped by what people change together: Links, Newsletter, Legal. Link groups are an array of objects each holding an array of links — two levels, so the converter never flattens columns into one list. The newsletter is a REFERENCE to a form in Forms: fields, consent text, double opt-in and the destination (Mailchimp, Klaviyo) live in Forms, so the footer can't silently drop the consent checkbox. Payment badges are a multiselect from a fixed set because the renderer owns the official logos — authors pick, never upload. `{year}` keeps the copyright from going stale every January.","htmlSignals":["<footer> (role=contentinfo) with 3–5 lists of links, each under a small heading","A newsletter <form> with a single email input and a submit button","A row of social icons linking to instagram.com / youtube.com etc.","A bottom row with © year, Privacy/Terms links and a strip of card logos (svg titled Visa, Mastercard…)"],"mcp":"ONE global `footer`. Each heading + <ul> → groups[] { title, links }. Social anchors → socialLinks[] { platform from the hostname, url }. A form with one email input → create/reuse a newsletter form and set newsletterForm to its id; its heading/paragraph → newsletterHeading/Body. © text → copyright with the 4-digit year replaced by {year}. Card logo <svg>/<img> titles → payments (unknown brands → report). Never model the form's inputs or hidden list ids in the footer.","pitfalls":["Flattening columns into one long links array loses the headings — keep groups.","Uploading payment logos as images: they go stale and break brand rules. Use the fixed set."],"notCarried":["ai","category","item openMode","kind:global","variants"],"component":{"name":"Footer","slug":"footer","props":[{"key":"logo","label":"Logo","type":"image","ui":{"tab":{"id":"content","label":"Links"}}},{"key":"tagline","label":"Tagline","type":"longtext","recommendedMax":120,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"groups","label":"Link groups","type":"table","config":{"minItems":1,"maxItems":5,"fields":[{"key":"title","label":"Group title","type":"text","required":true,"config":{"maxLength":24}},{"key":"links","label":"Links","type":"table","fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":32}},{"key":"link","label":"Links to","type":"link","required":true}],"config":{"minItems":1,"maxItems":8},"ui":{"itemLabel":"Link","icon":"link","preview":{"title":"label","subtitle":"link.href","fallback":"Untitled link"}}}]},"ui":{"itemLabel":"Group","itemName":"linkGroup","itemTitle":"Link group","icon":"list","preview":{"title":"title","fallback":"Untitled group"},"tab":{"id":"content"}}},{"key":"socialLinks","label":"Social profiles","type":"table","config":{"maxItems":6,"fields":[{"key":"platform","label":"Platform","type":"select","required":true,"options":[{"label":"Instagram","value":"instagram","icon":"brand-instagram"},{"label":"YouTube","value":"youtube","icon":"brand-youtube"},{"label":"TikTok","value":"tiktok","icon":"brand-tiktok"},{"label":"Facebook","value":"facebook","icon":"brand-facebook"},{"label":"X","value":"x","icon":"brand-x"},{"label":"LinkedIn","value":"linkedin","icon":"brand-linkedin"},{"label":"Pinterest","value":"pinterest","icon":"brand-pinterest"},{"label":"Dribbble","value":"dribbble","icon":"brand-dribbble"}]},{"key":"url","label":"Profile address","type":"url","required":true,"config":{"pattern":"^https://","patternMessage":"Use the full address, like https://your-site.com."}}]},"ui":{"itemLabel":"Profile","itemTitle":"Social profile","icon":"share","preview":{"title":"platform","subtitle":"url","fallback":"Untitled profile"},"tab":{"id":"content"}}},{"key":"newsletterHeading","label":"Heading","type":"text","config":{"maxLength":48},"ui":{"tab":{"id":"newsletter","label":"Newsletter"}}},{"key":"newsletterBody","label":"Body","type":"longtext","recommendedMax":140,"ui":{"rows":2,"tab":{"id":"newsletter"}}},{"key":"newsletterForm","label":"Form","type":"form","ui":{"tab":{"id":"newsletter"}}},{"key":"legalLinks","label":"Legal links","type":"table","config":{"maxItems":6,"fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":28}},{"key":"link","label":"Links to","type":"link","required":true}]},"ui":{"itemLabel":"Link","itemName":"link","icon":"link","preview":{"title":"label","subtitle":"link.href","fallback":"Untitled link"},"tab":{"id":"legal","label":"Legal"}}},{"key":"copyright","label":"Copyright line","type":"text","helpText":"{year} becomes the current year.","placeholder":"© {year} Caldera Cookware Co.","config":{"maxLength":80},"ui":{"tab":{"id":"legal"}}},{"key":"payments","label":"Payment badges","type":"multiselect","config":{"options":[{"label":"Visa","value":"visa"},{"label":"Mastercard","value":"mastercard"},{"label":"American Express","value":"amex"},{"label":"Apple Pay","value":"apple-pay"},{"label":"Google Pay","value":"google-pay"},{"label":"Shop Pay","value":"shop-pay"},{"label":"PayPal","value":"paypal"},{"label":"Klarna","value":"klarna"}]},"ui":{"tab":{"id":"legal"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}}],"description":"Link columns, newsletter sign-up, social links and the legal row. Synced across every page.","category":"navbar","icon":"layout-bottombar","preview":{"title":"tagline","media":"logo"}}},
|
|
1371
|
+
{"id":"footer-minimal","name":"Minimal footer","category":"Navigation & chrome","kind":"global","tags":["global-synced","repeater","validation"],"proves":"The smallest useful global: one line of links, an email, socials, the year token — nothing invented","why":"Restraint is the point: a minimal footer still has structure (links, socials, email are three different things) but no groups and no form. Email is a `link` of type email so the renderer gets mailto: plus a subject line; storing a bare string would lose that. Same `{year}` token as the big footer. It's a global so a studio with 30 case-study pages edits it once.","htmlSignals":["A single-row <footer> under 120px tall with a few inline links and © text","A visible mailto: link with the address as its text","2–4 icon-only social links (aria-label or <title> naming the network)","Optionally a large display-type sentence above the row"],"mcp":"Footer with no headings and fewer than ~8 links → `compactFooter` (else use `footer-multi-column`). mailto: anchor → email { type: 'email', email, subject from ?subject= }. Inline anchors → links; icon anchors → socialLinks by hostname. © line → copyright with the year replaced by {year}.","pitfalls":["Don't promote a minimal footer to the multi-column model just to have one footer type — the editor gets empty groups to ignore."],"notCarried":["ai","category","item openMode","kind:global"],"component":{"name":"Compact footer","slug":"compact-footer","props":[{"key":"statement","label":"Sign-off","type":"text","config":{"maxLength":60},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"email","label":"Contact email","type":"link","ui":{"tab":{"id":"content"}}},{"key":"links","label":"Links","type":"table","config":{"maxItems":5,"fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":20}},{"key":"link","label":"Links to","type":"link","required":true}]},"ui":{"itemLabel":"Link","icon":"link","preview":{"title":"label","subtitle":"link.href","fallback":"Untitled link"},"tab":{"id":"content"}}},{"key":"socialLinks","label":"Social profiles","type":"table","config":{"maxItems":4,"fields":[{"key":"platform","label":"Platform","type":"select","required":true,"options":[{"label":"Instagram","value":"instagram","icon":"brand-instagram"},{"label":"YouTube","value":"youtube","icon":"brand-youtube"},{"label":"TikTok","value":"tiktok","icon":"brand-tiktok"},{"label":"Facebook","value":"facebook","icon":"brand-facebook"},{"label":"X","value":"x","icon":"brand-x"},{"label":"LinkedIn","value":"linkedin","icon":"brand-linkedin"},{"label":"Pinterest","value":"pinterest","icon":"brand-pinterest"},{"label":"Dribbble","value":"dribbble","icon":"brand-dribbble"}]},{"key":"url","label":"Profile address","type":"url","required":true,"config":{"pattern":"^https://","patternMessage":"Use the full address, like https://your-site.com."}}]},"ui":{"itemLabel":"Profile","itemTitle":"Social profile","icon":"share","preview":{"title":"platform","subtitle":"url","fallback":"Untitled profile"},"tab":{"id":"content"}}},{"key":"copyright","label":"Copyright line","type":"text","helpText":"{year} becomes the current year.","config":{"maxLength":60},"ui":{"tab":{"id":"content"}}},{"key":"alignment","label":"Alignment","type":"select","defaultValue":"between","config":{"options":[{"label":"Spread","value":"between"},{"label":"Centered","value":"center"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"One row: a line of links, a contact email, social icons and the copyright.","category":"navbar","icon":"layout-bottombar","preview":{"title":"copyright"}}},
|
|
1372
|
+
{"id":"footer-mega-locations","name":"Mega footer with locations + app badges","category":"Navigation & chrome","kind":"global","tags":["global-synced","deep-nesting","repeater","seo-structured","validation","media-heavy"],"proves":"Locations → opening hours at four levels · geopoints · store badges with URL patterns · LocalBusiness data from content","why":"Locations are the deep part: Footer › Locations › Location › Hours › Row is four drill levels, which is exactly what real multi-site restaurants and retailers need (split hours, brunch-only Sundays, closed Mondays). Hours are structured rows (days multiselect + 24-hour times), never a free-text 'Tue–Thu 5–10', because the same data feeds LocalBusiness/Restaurant JSON-LD and 'Open now' — natively, without SEO fields. `geo` is a geopoint so directions and maps never geocode a typo. App badges are a select + URL with a pattern, rendered with official artwork. If the site also has a Locations collection, this array should become `array<reference>` to it (see pitfalls).","htmlSignals":["Footer columns each with a place name, a street address, a phone link and a small hours table","Google Maps links (maps.google.com/?q=… or /maps/place/) or embedded map iframes per location","App Store / Google Play badge images linking to apps.apple.com and play.google.com","JSON-LD with @type Restaurant/LocalBusiness and openingHoursSpecification"],"mcp":"Per location block: name, address (keep line breaks), tel: → phone, maps link coordinates (@lat,lng or ?ll=) → geo, the hours table → hours[] (group days with identical times; 'Closed' → isClosed: true; convert 5pm → 17:00). If a /locations/* page exists per place, create a Locations collection and store references here instead. Badge <a> by hostname → appBadges { store, url }; never import the badge images. Verify the platform's JSON-LD matches the source's openingHoursSpecification.","pitfalls":["Free-text hours ('Tue–Thu 5–10ish') can't power Open now or structured data — always normalize to rows.","Duplicating locations in the footer AND a Locations collection — two truths; use references once a collection exists.","Times without a zone are read in the site's time zone; a chain spanning zones needs a zone per location (not modelled)."],"notCarried":["category","item openMode","kind:global"],"component":{"name":"Locations footer","slug":"locations-footer","props":[{"key":"logo","label":"Logo","type":"image","ui":{"tab":{"id":"links","label":"Links & app","order":1}}},{"key":"tagline","label":"Tagline","type":"text","config":{"maxLength":80},"ui":{"tab":{"id":"links"}}},{"key":"locations","label":"Locations","type":"table","helpText":"Also feeds the details search engines show.","config":{"minItems":1,"maxItems":8,"fields":[{"key":"name","label":"Name","type":"text","required":true,"placeholder":"Pearl District","config":{"maxLength":32}},{"key":"photo","label":"Photo","type":"image"},{"key":"address","label":"Address","type":"longtext","required":true,"ui":{"rows":2}},{"key":"geo","label":"Map pin","type":"location","helpText":"Drop it on the entrance. Directions use it."},{"key":"phone","label":"Phone","type":"link"},{"key":"hours","label":"Opening hours","type":"table","helpText":"One row per set of days with the same hours.","fields":[{"key":"days","label":"Days","type":"multiselect","required":true,"options":[{"label":"Mon","value":"mon"},{"label":"Tue","value":"tue"},{"label":"Wed","value":"wed"},{"label":"Thu","value":"thu"},{"label":"Fri","value":"fri"},{"label":"Sat","value":"sat"},{"label":"Sun","value":"sun"}]},{"key":"isClosed","label":"Closed","type":"boolean"},{"key":"opens","label":"Opens","type":"text","showIf":{"match":"all","rules":[{"field":"isClosed","op":"eq","value":"false"}]},"placeholder":"17:00","config":{"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","patternMessage":"Use 24-hour time, like 17:00."}},{"key":"closes","label":"Closes","type":"text","showIf":{"match":"all","rules":[{"field":"isClosed","op":"eq","value":"false"}]},"placeholder":"22:30","config":{"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","patternMessage":"Use 24-hour time, like 22:30."}}],"config":{"minItems":1,"maxItems":7},"ui":{"itemLabel":"Row","itemName":"hoursRow","itemTitle":"Hours","icon":"clock","preview":{"title":"days","subtitle":"opens","fallback":"Untitled row"}}},{"key":"reserveButton","label":"Reserve button","type":"button"}]},"ui":{"itemLabel":"Location","icon":"map-pin","preview":{"title":"name","subtitle":"address","media":"photo","fallback":"Untitled location"},"tab":{"id":"content","label":"Locations","order":0}}},{"key":"appBadges","label":"App badges","type":"table","config":{"maxItems":2,"fields":[{"key":"store","label":"Store","type":"select","required":true,"unique":true,"options":[{"label":"App Store","value":"app-store"},{"label":"Google Play","value":"google-play"}]},{"key":"url","label":"Store address","type":"url","required":true,"config":{"pattern":"^https://(apps\\.apple\\.com|play\\.google\\.com)/","patternMessage":"Use an apps.apple.com or play.google.com address, like https://apps.apple.com/app/id123."}}]},"ui":{"itemLabel":"Badge","icon":"device-mobile","preview":{"title":"store","subtitle":"url"},"tab":{"id":"links"}}},{"key":"groups","label":"Link groups","type":"table","config":{"maxItems":3,"fields":[{"key":"title","label":"Group title","type":"text","required":true,"config":{"maxLength":24}},{"key":"links","label":"Links","type":"table","fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":32}},{"key":"link","label":"Links to","type":"link","required":true}],"config":{"minItems":1,"maxItems":6},"ui":{"itemLabel":"Link","icon":"link","preview":{"title":"label","subtitle":"link.href","fallback":"Untitled link"}}}]},"ui":{"itemLabel":"Group","itemName":"linkGroup","itemTitle":"Link group","icon":"list","preview":{"title":"title","fallback":"Untitled group"},"tab":{"id":"links"}}},{"key":"socialLinks","label":"Social profiles","type":"table","config":{"maxItems":5,"fields":[{"key":"platform","label":"Platform","type":"select","required":true,"options":[{"label":"Instagram","value":"instagram","icon":"brand-instagram"},{"label":"YouTube","value":"youtube","icon":"brand-youtube"},{"label":"TikTok","value":"tiktok","icon":"brand-tiktok"},{"label":"Facebook","value":"facebook","icon":"brand-facebook"},{"label":"X","value":"x","icon":"brand-x"},{"label":"LinkedIn","value":"linkedin","icon":"brand-linkedin"},{"label":"Pinterest","value":"pinterest","icon":"brand-pinterest"},{"label":"Dribbble","value":"dribbble","icon":"brand-dribbble"}]},{"key":"url","label":"Profile address","type":"url","required":true,"config":{"pattern":"^https://","patternMessage":"Use the full address, like https://your-site.com."}}]},"ui":{"itemLabel":"Profile","itemTitle":"Social profile","icon":"share","preview":{"title":"platform","subtitle":"url","fallback":"Untitled profile"},"tab":{"id":"links"}}},{"key":"copyright","label":"Copyright line","type":"text","helpText":"{year} becomes the current year.","config":{"maxLength":80},"ui":{"tab":{"id":"links"}}},{"key":"locationLayout","label":"Locations shown as","type":"select","defaultValue":"cards","config":{"options":[{"label":"Photo cards","value":"cards"},{"label":"Compact list","value":"list"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design","order":2}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Every location with its hours, app download badges, link columns and the legal row.","category":"navbar","icon":"layout-bottombar","preview":{"title":"tagline","media":"logo"}}},
|
|
1373
|
+
{"id":"hero-split-media","name":"Split hero with media switch","category":"Heroes","kind":"block","tags":["variants","conditional","media-heavy","a11y","repeater"],"proves":"One media slot that can be an image, a video or a small gallery · a phone crop · benefit list as plain strings","why":"The core Hero has one image; real split heroes swap between an image, a muted video and a 2–4 image gallery. One `mediaType` select drives which media fields show, so the content model never holds an image AND a video that disagree. The poster is its own required field because the video type can't require one. The phone crop is a separate optional image, not a crop setting, because art direction on phones is often a different photo. Key points are an array of strings — no icon, no link — so the converter doesn't invent features. Side (left/right/overlap) is a variant; fit, theme and spacing are Design.","htmlSignals":["First <section> after the header with a two-column grid: text column (h1, p, 1–2 links) + media column","Media column contains <img>, <picture> with a different mobile <source>, or <video autoplay muted loop playsinline poster>","A short <ul> of 2–3 items with check icons under the paragraph","Column order flipped via flex-direction: row-reverse or order — the media-left variant"],"mcp":"One H1 + paragraph + buttons + a single media column → `mediaHero`. <video> → mediaType 'video', video = src, poster = poster attribute (create an asset from it). Several <img> in the media column → mediaType 'gallery'. <picture> with a (max-width) <source> of a different image → mobileImage. Check-mark list items → points (text only). Media column first in the DOM or row-reverse → variant 'media-left'. object-fit: contain → fit 'contain'.","pitfalls":["A hero with BOTH a background video and a side image is a different section (full-bleed video) — don't merge.","Alt text on a decorative hero photo should still describe it when it shows the product."],"notCarried":["ai","category","variants"],"component":{"name":"Media hero","slug":"media-hero","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"config":{"maxLength":100},"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","recommendedMax":180,"localized":true,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"points","label":"Key points","type":"array","config":{"maxItems":3,"itemType":"text","item":{"maxLength":48}},"ui":{"itemLabel":"Point","itemTitle":"Point","tab":{"id":"content"}}},{"key":"primaryButton","label":"Primary button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"secondaryButton","label":"Secondary button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"mediaType","label":"Media","type":"select","defaultValue":"image","config":{"options":[{"label":"Image","value":"image"},{"label":"Video","value":"video"},{"label":"Gallery","value":"gallery"}]},"ui":{"control":"segmented","tab":{"id":"media","label":"Media"}}},{"key":"image","label":"Image","type":"image","helpText":"At least 1600px wide.","required":true,"showIf":{"match":"all","rules":[{"field":"mediaType","op":"eq","value":"image"}]},"ui":{"tab":{"id":"media"}}},{"key":"video","label":"Video","type":"video","helpText":"Plays muted and loops.","required":true,"showIf":{"match":"all","rules":[{"field":"mediaType","op":"eq","value":"video"}]},"ui":{"tab":{"id":"media"}}},{"key":"poster","label":"Poster","type":"image","required":true,"showIf":{"match":"all","rules":[{"field":"mediaType","op":"eq","value":"video"}]},"ui":{"tab":{"id":"media"}}},{"key":"gallery","label":"Gallery","type":"array","showIf":{"match":"all","rules":[{"field":"mediaType","op":"eq","value":"gallery"}]},"config":{"minItems":2,"maxItems":4,"itemType":"image"},"ui":{"itemLabel":"Image","layout":"gallery","itemName":"image","itemTitle":"Image","tab":{"id":"media"}}},{"key":"mobileImage","label":"Phone image","type":"image","helpText":"Leave empty to reuse the main image.","ui":{"tab":{"id":"media"}}},{"key":"fit","label":"Image fit","type":"select","defaultValue":"cover","config":{"options":[{"label":"Fill","value":"cover"},{"label":"Fit","value":"contain","description":"For product cut-outs on a plain background"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"spacing","label":"Spacing","type":"select","defaultValue":"default","config":{"options":[{"label":"Compact","value":"compact"},{"label":"Default","value":"default"},{"label":"Spacious","value":"spacious"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Copy on one side, an image, video or gallery on the other.","category":"hero","icon":"layout-columns","preview":{"title":"heading","subtitle":"eyebrow","media":"image"}}},
|
|
1374
|
+
{"id":"hero-centered-gradient","name":"Centered hero with gradient + screenshot","category":"Heroes","kind":"block","tags":["variants","conditional","validation","media-heavy"],"proves":"Announcement chip as a link unit · highlighted phrase in a plain string · gradient as design data, not CSS in content","why":"The gradient is DESIGN data: a style select plus colors in the Design group, so a rebrand changes three values instead of rewriting CSS strings pasted into content. Which color fields show depends on the style (mesh needs three, plain needs none). The highlighted phrase is a separate string that must match part of the headline — a pragmatic stand-in for inline marks in a single-line field (richtext would let authors add bold, links and line breaks we don't want in an H1). The announcement chip is an inline object of label + link because it's a link with its own copy, not a button.","htmlSignals":["A centered <section> (text-align:center, max-width ~720px) with a pill-shaped <a> above the H1","A <span> inside the H1 with a gradient/color class — the highlighted phrase","background-image: linear-gradient/radial-gradient on the section, or absolutely positioned blurred blobs (mesh)","A large <img> of app UI below the buttons, often with a perspective transform or bottom fade","A small muted line under the buttons ('No credit card required')"],"mcp":"Centered H1 + optional pill link → `launchHero`. Pill → chip { label: text, link: href }. A <span>/<em> inside the H1 → highlightedWords = its text; heading = the H1's full plain text. Parse the section's gradient: linear-gradient(angle, a, b) → { style: 'linear', angle, from: a, to: b }; radial-gradient → 'radial'; blurred absolutely-positioned blobs → 'mesh' with up to three colors. Snap colors to the nearest brand token when one is within ΔE 5 (report the rest). Screenshot <img> → screenshot; variant 'screenshot' when present else 'text'.","pitfalls":["Editing the headline can orphan the highlight — the renderer must ignore a highlight that no longer matches (no cross-field validation yet).","Don't store raw CSS gradient strings — they can't be re-themed or validated."],"notCarried":["ai","category","variants"],"component":{"name":"Launch hero","slug":"launch-hero","props":[{"key":"chip","label":"Announcement chip","type":"object","openMode":"inline","config":{"fields":[{"key":"label","label":"Label","type":"text","placeholder":"New: Traces for Kubernetes","config":{"maxLength":48}},{"key":"link","label":"Links to","type":"link"}]},"ui":{"icon":"sparkles","tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":56,"localized":true,"config":{"maxLength":100},"ui":{"tab":{"id":"content"}}},{"key":"highlightedWords","label":"Highlighted words","type":"text","helpText":"Must match words in the headline exactly. Leave empty for none.","config":{"maxLength":32},"ui":{"tab":{"id":"content"}}},{"key":"subheading","label":"Subheading","type":"longtext","recommendedMax":160,"localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"primaryButton","label":"Primary button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"secondaryButton","label":"Secondary button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"note","label":"Small print","type":"text","placeholder":"No credit card required","config":{"maxLength":64},"ui":{"tab":{"id":"content"}}},{"key":"screenshot","label":"Screenshot","type":"image","helpText":"At least 2400px wide.","ui":{"tab":{"id":"media","label":"Media"}}},{"key":"background","label":"Background","type":"object","openMode":"drill","config":{"fields":[{"key":"style","label":"Style","type":"select","defaultValue":"radial","options":[{"label":"Plain","value":"none","icon":"square"},{"label":"Linear","value":"linear","icon":"arrows-diagonal"},{"label":"Glow","value":"radial","icon":"circle-dot","description":"A soft light from the top"},{"label":"Mesh","value":"mesh","icon":"grid-dots","description":"Blended color blobs"}],"ui":{"control":"cards"}},{"key":"from","label":"Color 1","type":"color","showIf":{"match":"all","rules":[{"field":"style","op":"in","value":["linear","radial","mesh"]}]}},{"key":"to","label":"Color 2","type":"color","showIf":{"match":"all","rules":[{"field":"style","op":"in","value":["linear","radial","mesh"]}]}},{"key":"via","label":"Color 3","type":"color","showIf":{"match":"all","rules":[{"field":"style","op":"eq","value":"mesh"}]}},{"key":"angle","label":"Angle","type":"number","defaultValue":180,"showIf":{"match":"all","rules":[{"field":"style","op":"eq","value":"linear"}]},"config":{"min":0,"max":360,"step":15,"unit":"°"},"ui":{"control":"slider"}}]},"ui":{"icon":"palette","tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"dark","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"width","label":"Text width","type":"select","defaultValue":"narrow","config":{"options":[{"label":"Narrow","value":"narrow"},{"label":"Wide","value":"wide"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A centered headline over a gradient, with an optional product screenshot below.","category":"hero","icon":"layout-align-center","preview":{"title":"heading","subtitle":"chip.label","media":"screenshot"}}},
|
|
1375
|
+
{"id":"hero-video-fullbleed","name":"Full-bleed video hero with captions","category":"Heroes","kind":"block","tags":["media-heavy","a11y","conditional","repeater","validation"],"proves":"An Accessibility tab: caption tracks, transcript, text alternative · poster required · pause control locked on","why":"Video heroes fail accessibility more than any other section, so accessibility gets its own tab instead of being buried. Caption tracks are an array (one per language: language + menu label + .vtt file) because the `video` type can't carry tracks itself. A text alternative is required — a muted background film still communicates. The pause control is locked on for marketers with the WCAG reason shown. The poster is required: it's what people with reduced motion and slow connections actually see. Autoplay/loop/overlay/height are Design; loop only appears when autoplay is on.","htmlSignals":["<video autoplay muted loop playsinline poster=…> absolutely positioned behind the H1 (object-fit: cover)","<track kind=\"captions\" srclang=… src=….vtt> children (often missing — report it)","A pause/play <button aria-label=\"Pause video\"> overlaid in a corner","@media (prefers-reduced-motion) rule hiding the video or pausing it","A gradient/black overlay div between video and text"],"mcp":"Background <video> behind text → `videoHero`. src (or the highest-quality <source>) → video.url; poster attribute → poster (upload as an asset; if missing, grab the first frame and report). Each <track> → captions[] { language: srclang, label, file }. If there is no <track> and the film has speech, report an a11y gap — don't invent captions. aria-label/description near the video → textAlternative; else draft one with AI and mark it for review. Overlay opacity → overlay %.","pitfalls":["A YouTube iframe can't autoplay muted reliably as a background — prefer MP4/Vimeo background mode; flag YouTube sources.","Captions and transcript are different things: captions sync to the film, a transcript is a page. Model both."],"notCarried":["ai","category","item openMode"],"component":{"name":"Video hero","slug":"video-hero","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":56,"localized":true,"config":{"maxLength":100},"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","recommendedMax":140,"localized":true,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"primaryButton","label":"Primary button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"secondaryButton","label":"Secondary button","type":"button","helpText":"Often links to the full film with sound, e.g. Watch the 4-minute film.","ui":{"tab":{"id":"content"}}},{"key":"video","label":"Background video","type":"video","helpText":"MP4 or a Vimeo link, under 10 MB.","required":true,"ui":{"tab":{"id":"media","label":"Media"}}},{"key":"mobileVideo","label":"Phone video","type":"video","helpText":"Leave empty to show the poster on phones.","ui":{"tab":{"id":"media"}}},{"key":"poster","label":"Poster","type":"image","required":true,"ui":{"tab":{"id":"media"}}},{"key":"captions","label":"Caption tracks","type":"table","helpText":"Required when the video has speech.","config":{"maxItems":6,"fields":[{"key":"language","label":"Language","type":"select","required":true,"unique":true,"options":[{"label":"English","value":"en"},{"label":"Spanish","value":"es"},{"label":"French","value":"fr"}]},{"key":"label","label":"Menu label","type":"text","required":true,"placeholder":"English (CC)","config":{"maxLength":24}},{"key":"file","label":"Caption file","type":"file","helpText":"A .vtt file.","required":true}]},"ui":{"itemLabel":"Track","itemName":"track","icon":"subtask","preview":{"title":"label","subtitle":"language","fallback":"Untitled track"},"tab":{"id":"a11y","label":"Accessibility"}}},{"key":"textAlternative","label":"What the video shows","type":"longtext","helpText":"Not shown. Read by screen readers instead of the video.","required":true,"recommendedMax":200,"ui":{"rows":2,"tab":{"id":"a11y"}}},{"key":"transcript","label":"Transcript","type":"link","ui":{"tab":{"id":"a11y"}}},{"key":"showPauseButton","label":"Show pause button","type":"boolean","defaultValue":true,"readOnly":"developer","lockReason":"Moving video over 5 seconds must be pausable (WCAG 2.2.2).","ui":{"access":{"edit":"schema.manage"},"tab":{"id":"a11y"}}},{"key":"autoplay","label":"Play automatically","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"loop","label":"Loop the video","type":"boolean","defaultValue":true,"showIf":{"match":"all","rules":[{"field":"autoplay","op":"eq","value":"true"}]},"ui":{"tab":{"id":"design"}}},{"key":"overlay","label":"Image overlay","type":"number","defaultValue":35,"config":{"min":0,"max":80,"step":5,"unit":"%"},"ui":{"control":"slider","tab":{"id":"design"}}},{"key":"height","label":"Height","type":"select","defaultValue":"screen","config":{"options":[{"label":"Full screen","value":"screen"},{"label":"Large","value":"large"},{"label":"Medium","value":"medium"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"dark","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A muted background film behind a headline, with captions and a transcript.","category":"hero","icon":"movie","preview":{"title":"heading","subtitle":"eyebrow","media":"poster"}}},
|
|
1376
|
+
{"id":"hero-slider","name":"Slider hero","category":"Heroes","kind":"block","tags":["repeater","media-heavy","conditional","validation","a11y"],"proves":"Slides array with per-slide schedule and art direction · autoplay settings only when autoplay is on · carousel a11y name","why":"Slides are an array of objects with the same shape (each with ONE button — a slide with two CTAs is a sign it should be its own hero). Per-slide text position and text color live on the slide because they depend on that photo; section-level behavior lives in Behavior/Design. Each slide carries an optional schedule so a sale slide can appear and vanish without a republish. `interval` only shows when autoplay is on. The carousel name is required content because screen readers announce it ('Featured collections, carousel').","htmlSignals":["Swiper/Splide/Slick/Flickity class names (swiper-slide, splide__slide, slick-slide) or [aria-roledescription=\"carousel\"]","2+ full-width slides, each with its own H2/H1, paragraph, button and background image","Dots (<button aria-label=\"Go to slide 2\">) and prev/next arrows","data-autoplay / autoplay: { delay: 6000 } in the init script"],"mcp":"Carousel library markup at the top of the page → `heroCarousel`. Each non-cloned slide → slides[] (skip duplicates the library clones for looping: .swiper-slide-duplicate, .slick-cloned). Only the first slide's heading should be an H1 in the renderer. Read autoplay delay (ms → s) into interval; fade effect → transition 'fade'. Background images per slide → image; mobile <source> → mobileImage. Missing aria-label on the carousel → draft label from the slides' theme and report.","pitfalls":["Cloned slides doubling the count — dedupe by image URL + heading.","Slide schedules that leave fewer than 2 live slides: the renderer should fall back to a static hero (min can't be enforced over time)."],"notCarried":["ai","category","fieldset (derived)"],"component":{"name":"Hero carousel","slug":"hero-carousel","props":[{"key":"slides","label":"Slides","type":"table","config":{"minItems":2,"maxItems":5,"fields":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":50,"localized":true,"config":{"maxLength":80}},{"key":"body","label":"Body","type":"longtext","recommendedMax":120,"localized":true,"ui":{"rows":2}},{"key":"button","label":"Button","type":"button","required":true},{"key":"image","label":"Image","type":"image","helpText":"At least 2400 × 1200.","required":true},{"key":"mobileImage","label":"Phone image","type":"image","helpText":"Leave empty to reuse the main image."},{"key":"textPosition","label":"Text position","type":"select","defaultValue":"left","options":[{"label":"Left","value":"left"},{"label":"Center","value":"center"},{"label":"Right","value":"right"}],"ui":{"control":"segmented"}},{"key":"tone","label":"Text color","type":"select","defaultValue":"light","options":[{"label":"Light text","value":"light"},{"label":"Dark text","value":"dark"}],"ui":{"control":"segmented"}},{"key":"showFrom","label":"Show from","type":"datetime","ui":{"group":{"id":"Schedule","label":"Schedule","kind":"group"}}},{"key":"hideAfter","label":"Hide after","type":"datetime","ui":{"group":{"id":"Schedule"}}}]},"ui":{"itemLabel":"Slide","icon":"photo","preview":{"title":"heading","subtitle":"eyebrow","media":"image","fallback":"Untitled slide"},"tab":{"id":"content","label":"Slides"}}},{"key":"label","label":"Carousel name","type":"text","helpText":"Not shown. Read by screen readers.","required":true,"config":{"maxLength":40},"ui":{"tab":{"id":"content"}}},{"key":"autoplay","label":"Play automatically","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"behavior","label":"Behavior"}}},{"key":"interval","label":"Each slide shows for","type":"number","defaultValue":6,"showIf":{"match":"all","rules":[{"field":"autoplay","op":"eq","value":"true"}]},"config":{"min":4,"max":12,"step":1,"unit":"s"},"ui":{"tab":{"id":"behavior"}}},{"key":"transition","label":"Transition","type":"select","defaultValue":"fade","config":{"options":[{"label":"Fade","value":"fade"},{"label":"Slide","value":"slide"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"controls","label":"Controls","type":"multiselect","defaultValue":["dots","arrows"],"config":{"options":[{"label":"Dots","value":"dots"},{"label":"Arrows","value":"arrows"},{"label":"Progress bar","value":"progress"}]},"ui":{"tab":{"id":"design"}}},{"key":"height","label":"Height","type":"select","defaultValue":"large","config":{"options":[{"label":"Full screen","value":"screen"},{"label":"Large","value":"large"},{"label":"Medium","value":"medium"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Two to five full-width slides, each with its own message and button.","category":"hero","icon":"slideshow","preview":{"title":"slides.0.heading","subtitle":"label","media":"slides.0.image"}}},
|
|
1377
|
+
{"id":"hero-product","name":"Product hero with variant picker","category":"Heroes","kind":"block","tags":["references","commerce","media-heavy","conditional","repeater"],"proves":"Live commerce data through a reference · overrides that fall back to the product · Add to cart is an action label","why":"Everything that changes with stock and price is a REFERENCE to the commerce product — the hero stores only editorial choices: an optional headline override, tagline, badge, highlights, and HOW to present the buy box (which option pickers, price style). Override fields say 'leave empty to use the product …' because the schema can't yet declare fallback semantics. Add to cart is a string label, not a `button`: it performs an action, it doesn't navigate. The gallery is optional and only overrides the store photos when set.","htmlSignals":["A product form: <form action=\"/cart/add\"> with <select name=\"id\"> or radio swatches and a submit button","Price markup with a <s>/<del> compare-at price and currency symbol; Klarna/Afterpay/Shop Pay installment widgets","Product JSON (<script type=\"application/json\" data-product-json> or ShopifyAnalytics.meta.product)","A thumbnail strip + main image (gallery) beside the buy box","JSON-LD @type Product with offers (native — derived from the reference)"],"mcp":"Product form at the top of a landing/product page → `productHero`. Resolve the product by its handle/id from the product JSON → product reference. NEVER copy price, compare-at, SKU, stock or option values into fields. If the visible H1 differs from the product title → heading override; else leave empty. Option groups shown as pickers → pickers (by option name: Color/Color → color). Submit button text → addLabel. Gallery images that equal the store's product media → leave gallery empty.","pitfalls":["Hard-coding the price in the tagline ('Just $165!') — it goes stale; flag any currency in copy.","Picker options are a fixed list here; a product with a 'Handle wrap' option can't be picked until options come from the reference (gap)."],"notCarried":["ai","category","shopify reference","variants"],"component":{"name":"Product hero","slug":"product-hero","props":[{"key":"product","label":"Product","type":"text","required":true,"readOnly":true,"lockReason":"A Shopify product id. No Shopify integration backs a reference in BetterCMS yet, so the id is kept as it is.","ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","helpText":"Leave empty to use the product name.","config":{"maxLength":80},"ui":{"tab":{"id":"content"}}},{"key":"tagline","label":"Tagline","type":"longtext","recommendedMax":140,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"badge","label":"Badge","type":"text","placeholder":"New color","config":{"maxLength":16},"ui":{"tab":{"id":"content"}}},{"key":"highlights","label":"Highlights","type":"table","openMode":"inline","config":{"maxItems":4,"fields":[{"key":"icon","label":"Icon","type":"icon"},{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":40}}]},"ui":{"itemLabel":"Highlight","icon":"circle-check","preview":{"title":"label","fallback":"Untitled highlight"},"tab":{"id":"content"}}},{"key":"pickers","label":"Option pickers","type":"multiselect","helpText":"Options left out use their default.","config":{"options":[{"label":"Color","value":"color"},{"label":"Size","value":"size"},{"label":"Finish","value":"finish"}]},"ui":{"tab":{"id":"buy","label":"Buy box"}}},{"key":"swatchStyle","label":"Color picker","type":"select","defaultValue":"swatch","config":{"options":[{"label":"Swatches","value":"swatch"},{"label":"Dropdown","value":"dropdown"}]},"ui":{"control":"segmented","tab":{"id":"buy"}}},{"key":"price","label":"Price","type":"select","defaultValue":"full","config":{"options":[{"label":"Show price","value":"full"},{"label":"From $…","value":"from"},{"label":"Hide","value":"hidden"}]},"ui":{"control":"segmented","tab":{"id":"buy"}}},{"key":"showCompareAtPrice","label":"Show compare-at price","type":"boolean","showIf":{"match":"all","rules":[{"field":"price","op":"eq","value":"full"}]},"ui":{"tab":{"id":"buy"}}},{"key":"showInstallments","label":"Show pay-later line","type":"boolean","showIf":{"match":"all","rules":[{"field":"price","op":"eq","value":"full"}]},"ui":{"tab":{"id":"buy"}}},{"key":"addLabel","label":"Add to cart label","type":"text","required":true,"defaultValue":"Add to cart","config":{"maxLength":24},"ui":{"tab":{"id":"buy"}}},{"key":"soldOutLabel","label":"Sold out label","type":"text","defaultValue":"Notify me","config":{"maxLength":24},"ui":{"tab":{"id":"buy"}}},{"key":"detailsButton","label":"Details button","type":"button","ui":{"tab":{"id":"buy"}}},{"key":"showRating","label":"Show rating","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"buy"}}},{"key":"gallery","label":"Gallery","type":"array","helpText":"Leave empty to use the product's photos.","config":{"maxItems":8,"itemType":"image"},"ui":{"itemLabel":"Image","layout":"gallery","itemName":"image","itemTitle":"Image","tab":{"id":"media","label":"Media"}}},{"key":"video","label":"Video","type":"video","ui":{"tab":{"id":"media"}}},{"key":"isBuyBoxSticky","label":"Keep the buy box in view","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"One product with its gallery, options, live price and an add-to-cart button.","category":"hero","icon":"shopping-bag","preview":{"title":"heading","subtitle":"badge","media":"gallery.0"}}},
|
|
1378
|
+
{"id":"hero-search","name":"Search hero","category":"Heroes","kind":"block","tags":["references","conditional","dynamic-query","media-heavy"],"proves":"Search box config · manual vs automatic suggestions · topic chips as references · scope and results page","why":"The search box itself is behavior, so the block stores its CONFIG: what it searches (scope), what it says (placeholder, an accessible button label) and where suggestions come from. `suggestionSource` switches between hand-written tags and a dynamic 'most searched' query (with its window), so a converter can mark a site's trending list as dynamic instead of freezing it. Topic chips are references to the Topics collection, so a renamed topic updates here. Results render on the site's search page — no results UI is modelled.","htmlSignals":["A hero whose main control is <form role=\"search\"> / <input type=\"search\"> with a large placeholder","Links below the input like /search?q=… ('Popular: …') — suggestions","Pill links to /topics/*, /tags/* or /category/* — topic chips","Form action pointing to /search, /recipes or an Algolia/Typesense endpoint (scope)"],"mcp":"Hero containing a search form → `searchHero`. placeholder attribute → placeholder; the form action path → scope (/recipes → 'recipes', /search → 'all'). Links with ?q= → suggestions (the q values, decoded) and suggestionSource 'manual'; if they change between two crawls a day apart → 'popular'. Pills linking to taxonomy pages → topics, resolved to Topics entries by slug (create missing ones and report).","pitfalls":["Don't model the autocomplete dropdown's results — they come from the search index."],"notCarried":["ai","category"],"component":{"name":"Search hero","slug":"search-hero","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"localized":true,"config":{"maxLength":100},"ui":{"tab":{"id":"content"}}},{"key":"subheading","label":"Subheading","type":"longtext","recommendedMax":120,"localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"placeholder","label":"Placeholder","type":"text","required":true,"localized":true,"placeholder":"Try dutch oven bread","config":{"maxLength":60},"ui":{"tab":{"id":"search","label":"Search"}}},{"key":"buttonLabel","label":"Search button label","type":"text","helpText":"Read by screen readers when the button shows only an icon.","defaultValue":"Search","config":{"maxLength":16},"ui":{"tab":{"id":"search"}}},{"key":"scope","label":"Searches","type":"select","defaultValue":"recipes","config":{"options":[{"label":"Recipes","value":"recipes"},{"label":"Journal stories","value":"journal"},{"label":"Products","value":"products"},{"label":"The whole site","value":"all"}]},"ui":{"control":"radio","tab":{"id":"search"}}},{"key":"suggestionSource","label":"Suggested searches","type":"select","defaultValue":"manual","config":{"options":[{"label":"Written here","value":"manual"},{"label":"Most searched","value":"popular"},{"label":"None","value":"none"}]},"ui":{"control":"segmented","tab":{"id":"search"}}},{"key":"suggestions","label":"Suggestions","type":"array","showIf":{"match":"all","rules":[{"field":"suggestionSource","op":"eq","value":"manual"}]},"config":{"itemType":"text","maxItems":6},"ui":{"itemLabel":"Suggestion","tab":{"id":"search"}}},{"key":"popularDays","label":"Count searches from the last","type":"number","defaultValue":30,"showIf":{"match":"all","rules":[{"field":"suggestionSource","op":"eq","value":"popular"}]},"config":{"min":7,"max":90,"step":1,"unit":"days"},"ui":{"tab":{"id":"search"}}},{"key":"topics","label":"Topic chips","type":"multi-reference","config":{"max":6,"contentModelId":"col_topics"},"ui":{"itemLabel":"Topic","itemTitle":"Topic","tab":{"id":"content"}}},{"key":"backgroundImage","label":"Background image","type":"image","ui":{"tab":{"id":"media","label":"Media"}}},{"key":"overlay","label":"Image overlay","type":"number","defaultValue":35,"config":{"min":0,"max":80,"step":5,"unit":"%"},"ui":{"control":"slider","tab":{"id":"design","label":"Design"}}},{"key":"alignment","label":"Alignment","type":"select","defaultValue":"center","config":{"options":[{"label":"Left","value":"left"},{"label":"Centered","value":"center"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A headline with a big search box, suggested searches and topic chips.","category":"hero","icon":"input-search","preview":{"title":"heading","subtitle":"placeholder","media":"backgroundImage"}}},
|
|
1379
|
+
{"id":"hero-lead-form","name":"Lead-form hero","category":"Heroes","kind":"block","tags":["form","references","conditional","validation","repeater"],"proves":"A form REFERENCE plus per-placement settings: hidden source fields, label override, message vs redirect","why":"The form is a separate form in Forms referenced by the hero — its inputs, validation, spam protection and where submissions go belong to Forms and are reused across the site. What's specific to THIS placement stays here: a heading, a submit-label override, what happens after sending (message vs redirect, with the right field shown for each) and hidden values that tag where leads came from. Modelling the inputs as hero fields would fork the form every time it's placed.","htmlSignals":["A hero section that contains a <form> with 3–6 inputs (name, email, company, message) beside or below the pitch copy","<input type=\"hidden\" name=\"source\" value=\"…\"> or utm_* hidden inputs","HubSpot/Typeform/Formspark embeds (hbspt.forms.create, data-tf-widget) — a third-party form","A small privacy sentence under the submit button"],"mcp":"Hero + <form> → `leadFormHero`. Create (or match by its input set) a BetterCMS form from the inputs — labels, types, required — and set `form` to its id. Hidden inputs with static values → hiddenFields (skip ones filled by script, e.g. utm_* read from the URL — those are automatic). Submit text → submitLabel only if it differs from the form's default. A JS redirect after submit or form action to /thanks → afterSubmit 'redirect'. For HubSpot/Typeform embeds, report the provider and form id instead of recreating it.","pitfalls":["Copying inputs into the hero's schema — the form then can't be reused or receive submissions properly.","Hidden values that contain personal data (an email from the URL) must not be stored in content."],"notCarried":["ai","category","item openMode","variants"],"component":{"name":"Lead form hero","slug":"lead-form-hero","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Pitch"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":64,"localized":true,"config":{"maxLength":100},"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","recommendedMax":200,"localized":true,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"points","label":"What they get","type":"array","config":{"maxItems":4,"itemType":"text","item":{"maxLength":60}},"ui":{"itemLabel":"Point","itemTitle":"Point","tab":{"id":"content"}}},{"key":"trustLine","label":"Trust line","type":"text","config":{"maxLength":90},"ui":{"tab":{"id":"content"}}},{"key":"image","label":"Image","type":"image","ui":{"tab":{"id":"content"}}},{"key":"form","label":"Form","type":"form","required":true,"ui":{"tab":{"id":"form","label":"Form"}}},{"key":"formHeading","label":"Form heading","type":"text","config":{"maxLength":48},"ui":{"tab":{"id":"form"}}},{"key":"submitLabel","label":"Submit label","type":"text","helpText":"Leave empty to use the label set on the form.","config":{"maxLength":24},"ui":{"tab":{"id":"form"}}},{"key":"afterSubmit","label":"After sending","type":"select","defaultValue":"message","config":{"options":[{"label":"Show a message","value":"message","description":"The form is replaced by your thank-you note"},{"label":"Go to a page","value":"redirect","description":"Best when you track conversions on a thank-you page"}]},"ui":{"control":"radio","tab":{"id":"form"}}},{"key":"successMessage","label":"Thank-you message","type":"longtext","required":true,"showIf":{"match":"all","rules":[{"field":"afterSubmit","op":"eq","value":"message"}]},"config":{"maxLength":240},"ui":{"rows":2,"tab":{"id":"form"}}},{"key":"redirect","label":"Thank-you page","type":"link","required":true,"showIf":{"match":"all","rules":[{"field":"afterSubmit","op":"eq","value":"redirect"}]},"ui":{"tab":{"id":"form"}}},{"key":"hiddenFields","label":"Hidden values","type":"table","helpText":"Not shown. Sent with every submission from this section.","config":{"maxItems":6,"fields":[{"key":"key","label":"Key","type":"text","required":true,"unique":true,"placeholder":"source","config":{"pattern":"^[a-z][a-z0-9_]*$","patternMessage":"Use lowercase letters, numbers and underscores, like page_view."}},{"key":"value","label":"Value","type":"text","required":true,"placeholder":"homepage-hero","config":{"maxLength":80}}]},"ui":{"itemLabel":"Value","itemTitle":"Hidden value","icon":"eye-off","preview":{"title":"key","subtitle":"value","fallback":"Untitled value"},"tab":{"id":"form"}}},{"key":"privacyNote","label":"Privacy note","type":"text","config":{"maxLength":120},"ui":{"tab":{"id":"form"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}}],"description":"A pitch on one side and a short form on the other.","category":"hero","icon":"clipboard-text","preview":{"title":"heading","subtitle":"formHeading","media":"image"}}},
|
|
1380
|
+
{"id":"hero-app-download","name":"App download hero","category":"Heroes","kind":"block","tags":["media-heavy","conditional","validation","global-synced","repeater"],"proves":"A field synced from a global (store links) inside a normal block · QR code derived from a link · device screens","why":"Store links appear in the hero, the footer and emails, so the badges field is SYNCED from an 'App links' global (field-level `synced`) — one App Store URL change updates every badge, while the rest of the hero stays local. The QR code is DERIVED from a URL (we store the target, never an uploaded QR image that can't be updated); its fields only show when it's on. Badges are a select + URL with a pattern because the renderer draws the official artwork. Screenshots are a small image array; the device frame and background are Design.","htmlSignals":["Official App Store / Google Play badge images linking to apps.apple.com and play.google.com","A QR code <img>/<svg>/<canvas> near the badges, often desktop-only","Phone mockup frames (rounded rect with notch) around screenshots, sometimes overlapping","Smart-banner or apple-itunes-app meta tag in <head> (an app exists)"],"mcp":"Hero with store badges → `appDownloadHero`. Badge hrefs → badges[] { store by hostname, url }; also create/update the 'App links' global with them. A QR image: decode it if possible and set qr.target to the decoded URL (never store the QR image); else ask. Screenshots inside device frames → screens (crop to the screen area if the frame is baked into the image, and report). Star rating text → rating.","pitfalls":["Uploading badge or QR images — they can't be updated and the QR silently breaks when the link changes.","Ratings copied from the store go stale; the help text says so because there's no live source."],"notCarried":["ai","category","item openMode"],"component":{"name":"App download hero","slug":"app-download-hero","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":56,"localized":true,"config":{"maxLength":100},"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","recommendedMax":160,"localized":true,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"badges","label":"Store links","type":"table","helpText":"Shared: changes apply to every badge on the site.","config":{"minItems":1,"maxItems":2,"fields":[{"key":"store","label":"Store","type":"select","required":true,"unique":true,"options":[{"label":"App Store","value":"app-store"},{"label":"Google Play","value":"google-play"}]},{"key":"url","label":"Store address","type":"url","required":true,"config":{"pattern":"^https://(apps\\.apple\\.com|play\\.google\\.com)/","patternMessage":"Use an apps.apple.com or play.google.com address, like https://apps.apple.com/app/id123."}}]},"ui":{"itemLabel":"Store","synced":{"from":"App links"},"itemName":"storeLink","icon":"device-mobile","preview":{"title":"store","subtitle":"url"},"tab":{"id":"content"}}},{"key":"qr","label":"QR code","type":"object","openMode":"drill","config":{"fields":[{"key":"showQrCode","label":"Show a QR code","type":"boolean","helpText":"Desktop only. Phones see the badges.","defaultValue":true},{"key":"target","label":"QR opens","type":"url","helpText":"One link that sends iPhones and Androids to the right store.","required":true,"showIf":{"match":"all","rules":[{"field":"showQrCode","op":"eq","value":"true"}]}},{"key":"caption","label":"Caption","type":"text","showIf":{"match":"all","rules":[{"field":"showQrCode","op":"eq","value":"true"}]},"placeholder":"Scan to download","config":{"maxLength":32}}]},"ui":{"icon":"qrcode","tab":{"id":"content"}}},{"key":"rating","label":"Store rating","type":"object","config":{"fields":[{"key":"value","label":"Rating","type":"number","config":{"min":0,"max":5,"step":0.1}},{"key":"label","label":"Label","type":"text","placeholder":"12,400 ratings","config":{"maxLength":32}}]},"ui":{"icon":"star","tab":{"id":"content"}}},{"key":"screens","label":"Screenshots","type":"array","helpText":"Phone screens at 1170 × 2532.","config":{"minItems":1,"maxItems":3,"itemType":"image"},"ui":{"itemLabel":"Screenshot","layout":"gallery","tab":{"id":"media","label":"Media"}}},{"key":"device","label":"Device frame","type":"select","defaultValue":"iphone","config":{"options":[{"label":"iPhone","value":"iphone"},{"label":"Android","value":"android"},{"label":"No frame","value":"none"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"background","label":"Background color","type":"color","ui":{"tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Pitch the app with store badges, a QR code for desktop visitors and phone screenshots.","category":"hero","icon":"device-mobile","preview":{"title":"heading","subtitle":"eyebrow","media":"screens.0"}}},
|
|
1381
|
+
{"id":"hero-stats","name":"Stats hero","category":"Heroes","kind":"block","tags":["repeater","validation","variants","media-heavy"],"proves":"Numbers stored as numbers with prefix/suffix (count-up safe) · footnoted sources · an 'as of' date","why":"Each stat stores the NUMBER as a number with separate prefix/suffix/decimals, not '$4.2M' as text — the renderer can count up, format per locale (4,2 M in German) and a validator can check it. Labels are short and required. `source` per stat plus an `asOf` date make claims auditable, which nonprofits and regulated industries need. Two layouts are variants; count-up is Design. The shared Stat block in the core library uses a string value — this entry exists to show when the number must stay a number.","htmlSignals":["Large numerals (font-size ≥ 40px) in a row of 2–4 items, each with a short caption under it","data-count / data-target attributes or a countUp.js/odometer script","Footnote markers (¹ ² or *) next to numbers with sources at the bottom","Text like 'as of March 2026' or 'FY2025' near the row"],"mcp":"Hero + 2–4 big numerals → `statsHero`. Parse each numeral: leading non-digits → prefix, trailing letters/% /+ → suffix, the number → value (use data-target when present — the visible text may be mid-animation, e.g. '0'), decimals from the fraction part. Caption → label. Footnotes → source on the matching stat. A date near the row → asOf (ISO).","pitfalls":["Reading '0' from a count-up that hasn't run — always prefer data-target/data-count.","'1.2k' style abbreviations: store 1.2 with suffix 'k', not 1200, so the copy stays as written."],"notCarried":["ai","category","fieldset (derived)","variants"],"component":{"name":"Stats hero","slug":"stats-hero","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":64,"localized":true,"config":{"maxLength":100},"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","recommendedMax":180,"localized":true,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"primaryButton","label":"Primary button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"secondaryButton","label":"Secondary button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"stats","label":"Stats","type":"table","config":{"minItems":2,"maxItems":4,"fields":[{"key":"prefix","label":"Before","type":"text","placeholder":"$","config":{"maxLength":3},"ui":{"group":{"id":"Number","label":"Number","kind":"group"}}},{"key":"value","label":"Number","type":"number","required":true,"config":{"min":0,"step":0.1},"ui":{"group":{"id":"Number"}}},{"key":"suffix","label":"After","type":"text","placeholder":"M, %, +","config":{"maxLength":6},"ui":{"group":{"id":"Number"}}},{"key":"decimals","label":"Decimals","type":"number","defaultValue":0,"config":{"min":0,"max":2,"step":1},"ui":{"group":{"id":"Number"}}},{"key":"label","label":"Label","type":"text","required":true,"placeholder":"river miles reopened","config":{"maxLength":40}},{"key":"source","label":"Source","type":"text","config":{"maxLength":120}}]},"ui":{"itemLabel":"Stat","icon":"chart-bar","preview":{"title":"label","subtitle":"value","fallback":"Untitled stat"},"tab":{"id":"content"}}},{"key":"asOf","label":"Figures as of","type":"date","ui":{"tab":{"id":"content"}}},{"key":"image","label":"Image","type":"image","ui":{"tab":{"id":"media","label":"Media"}}},{"key":"useCountUp","label":"Count up on scroll","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"spacing","label":"Spacing","type":"select","defaultValue":"default","config":{"options":[{"label":"Compact","value":"compact"},{"label":"Default","value":"default"},{"label":"Spacious","value":"spacious"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A headline backed by a row of two to four numbers.","category":"hero","icon":"chart-arrows","preview":{"title":"heading","subtitle":"eyebrow","media":"image"}}},
|
|
1382
|
+
{"id":"hero-logo-wall","name":"Hero with logo wall","category":"Heroes","kind":"block","tags":["repeater","media-heavy","a11y","conditional","variants"],"proves":"Logos as a grid array with the company name as the accessible text · monochrome treatment and marquee as design","why":"Each logo is an object whose REQUIRED `name` is the accessible text; the image's alt help line is turned off (description: '') and the name is copied into alt, so authors aren't asked twice. That's the a11y rule converters most often break: logos with alt='logo'. Logos are an array in a grid layout so they read as tiles. One-color treatment and the marquee are Design — the same content can be still or scrolling. Speed only appears for the marquee. At scale (the same logos on pricing, about and case studies) this array should become references to a Customers collection.","htmlSignals":["A row/grid of 4–18 small <img>/<svg> elements with similar heights and grayscale filters (filter: grayscale(1) / opacity)","A small label above them ('Trusted by…', 'Used by teams at…')","A duplicated track with @keyframes translateX — the marquee (logos appear twice in the DOM)","Alt texts like 'logo', 'client-1' (weak) or company names (good)"],"mcp":"Hero + a strip of logos → `logoWallHero`. Dedupe logos duplicated for the marquee (same src) and set motion 'marquee'. For each logo: name = alt if it's a real name, else the file name or <title> cleaned up, else ask — never 'logo'. grayscale filter or single-fill SVGs → treatment 'mono'. If the same set appears on 3+ pages, create a Customers collection and switch to references (report it).","pitfalls":["Alt text 'logo' x12 — screen readers hear nothing useful. Name is required for that reason.","Marquee without a pause control is a WCAG 2.2.2 problem; the renderer pauses on hover/focus and reduced motion."],"notCarried":["ai","category","variants"],"component":{"name":"Logo wall hero","slug":"logo-wall-hero","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":56,"localized":true,"config":{"maxLength":100},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"subheading","label":"Subheading","type":"longtext","recommendedMax":160,"localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"primaryButton","label":"Primary button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"secondaryButton","label":"Secondary button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"logosLabel","label":"Logo row label","type":"text","placeholder":"Trusted by 4,000 engineering teams","config":{"maxLength":60},"ui":{"tab":{"id":"content"}}},{"key":"logos","label":"Logos","type":"table","config":{"minItems":4,"maxItems":18,"fields":[{"key":"name","label":"Company","type":"text","helpText":"Not shown. Read by screen readers in place of the logo.","required":true,"config":{"maxLength":40}},{"key":"logo","label":"Logo","type":"image","helpText":"","required":true},{"key":"link","label":"Case study","type":"link"}]},"ui":{"itemLabel":"Logo","layout":"grid","itemName":"customerLogo","icon":"building","preview":{"title":"name","media":"logo","fallback":"Untitled logo"},"tab":{"id":"media","label":"Media"}}},{"key":"treatment","label":"Logo color","type":"select","defaultValue":"mono","config":{"options":[{"label":"One color","value":"mono"},{"label":"Original","value":"original"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"motion","label":"Motion","type":"select","defaultValue":"static","config":{"options":[{"label":"Still","value":"static"},{"label":"Scrolling","value":"marquee"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"speed","label":"Scroll speed","type":"number","defaultValue":30,"showIf":{"match":"all","rules":[{"field":"motion","op":"eq","value":"marquee"}]},"config":{"min":10,"max":60,"step":5,"unit":"s per loop"},"ui":{"control":"slider","tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"dark","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A headline with a strip of customer logos under it.","category":"hero","icon":"badges","preview":{"title":"heading","subtitle":"logosLabel"}}},
|
|
1383
|
+
{"id":"hero-split-testimonial","name":"Split hero with testimonial card","category":"Heroes","kind":"block","tags":["references","conditional","variants","media-heavy"],"proves":"Pick from the Testimonials collection OR write one here — a conditional reference/inline pair inside an object","why":"Social proof should usually come from ONE place (the Testimonials collection) so a quote that's withdrawn disappears everywhere. But landing pages often need a one-off quote, so `source` toggles between a reference and inline fields — never both visible, never both stored as the truth. The card is an object so the whole quote travels together when the variant changes from 'over the photo' to 'under the copy'.","htmlSignals":["A <figure>/<blockquote> card with a photo, name and role positioned absolutely over the hero image","Star icons or '★★★★★' next to the quote","The same quote text appearing on a /reviews or /testimonials page — collection candidate"],"mcp":"Hero with a quote card → `testimonialHero`. Search the Testimonials collection for the quote text (normalized, ≥ 90% match): found → source 'collection', entry = its id; not found → create the testimonial entry if the site has a testimonials page, else source 'custom' and fill quote/name/role/photo/rating. Card positioned over the image → variant 'overlap'.","pitfalls":["Quotes typed inline on every landing page drift from the approved wording — prefer the collection."],"notCarried":["ai","category","variants"],"component":{"name":"Testimonial hero","slug":"testimonial-hero","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"config":{"maxLength":100},"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","recommendedMax":180,"localized":true,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"primaryButton","label":"Primary button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"secondaryButton","label":"Secondary button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"testimonial","label":"Testimonial","type":"object","openMode":"drill","config":{"fields":[{"key":"source","label":"Quote from","type":"select","defaultValue":"collection","options":[{"label":"Testimonials","value":"collection"},{"label":"Write one here","value":"custom"}],"ui":{"control":"segmented"}},{"key":"entry","label":"Testimonial","type":"reference","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"collection"}]},"config":{"contentModelId":"col_testimonials"}},{"key":"quote","label":"Quote","type":"longtext","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"custom"}]},"recommendedMax":160,"ui":{"rows":3}},{"key":"name","label":"Name","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"custom"}]},"config":{"maxLength":40}},{"key":"role","label":"Role","type":"text","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"custom"}]},"placeholder":"Backcountry guide, Montana","config":{"maxLength":60}},{"key":"photo","label":"Photo","type":"image","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"custom"}]}},{"key":"rating","label":"Rating","type":"rating","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"custom"}]},"config":{"step":0.1}}]},"ui":{"icon":"quote","tab":{"id":"content"}}},{"key":"image","label":"Image","type":"image","required":true,"ui":{"tab":{"id":"media","label":"Media"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}}],"description":"Copy and buttons beside a photo, with a customer quote card overlapping it.","category":"hero","icon":"message-circle-2","preview":{"title":"heading","subtitle":"eyebrow","media":"image"}}},
|
|
1384
|
+
{"id":"hero-event","name":"Event hero","category":"Heroes","kind":"block","tags":["conditional","seo-structured","validation","media-heavy"],"proves":"Date/time with zone · in-person/online/hybrid venue with conditional map or stream · sold-out swap · Event data from content","why":"The event's FACTS (start/end with time zone, format, venue, map pin, stream) are structured because the same values drive the countdown, the add-to-calendar file and native Event JSON-LD — no separate SEO fields. Format decides which place fields show (venue/address/pin for in-person, stream for online, both for hybrid). Ticket status is one select that swaps the button: on sale → registerButton, sold out → waitlistButton, cancelled → a required note. If the site lists many events, these fields belong on an Events collection entry and the hero becomes a single reference (see pitfalls).","htmlSignals":["A hero with a date/time line (<time datetime=…>), a place line and a 'Register'/'Get tickets' button","A ticking countdown (days/hours/minutes) bound to a data-date attribute","Ticketing links (eventbrite.com, tito.io, lu.ma) or a 'Sold out' badge","JSON-LD @type Event with eventAttendanceMode (Offline/Online/Mixed) and location/VirtualLocation","An 'Add to calendar' link or .ics download"],"mcp":"Hero with a date + place + ticket button → `eventHero`. <time datetime> or the countdown's data-date → startsAt with its offset (ask for the zone if the source has none). JSON-LD eventAttendanceMode → format (Offline → in-person, Online → online, Mixed → hybrid); location.name/address/geo → venue/address/location. Ticket link → registerButton; 'Sold out' text → status 'sold-out'. Never model the add-to-calendar link — it's generated. If the page is one of many event pages, create an Events collection and report it.","pitfalls":["Dates stored as text ('Nov 14, 6pm') can't drive the countdown or JSON-LD — always a datetime with offset.","End before start isn't caught today (gap: cross-field validation)."],"notCarried":["ai","category"],"component":{"name":"Event hero","slug":"event-hero","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Event name","type":"text","required":true,"recommendedMax":48,"localized":true,"config":{"maxLength":100},"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","recommendedMax":160,"localized":true,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"image","label":"Image","type":"image","required":true,"ui":{"tab":{"id":"content"}}},{"key":"startsAt","label":"Starts","type":"datetime","helpText":"In the event's own time zone. Visitors see it converted.","required":true,"ui":{"tab":{"id":"when","label":"When & where"}}},{"key":"endsAt","label":"Ends","type":"datetime","ui":{"tab":{"id":"when"}}},{"key":"format","label":"Format","type":"select","defaultValue":"in-person","config":{"options":[{"label":"In person","value":"in-person"},{"label":"Online","value":"online"},{"label":"Hybrid","value":"hybrid"}]},"ui":{"control":"segmented","tab":{"id":"when"}}},{"key":"venue","label":"Venue","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"format","op":"in","value":["in-person","hybrid"]}]},"config":{"maxLength":60},"ui":{"tab":{"id":"when"}}},{"key":"address","label":"Address","type":"longtext","showIf":{"match":"all","rules":[{"field":"format","op":"in","value":["in-person","hybrid"]}]},"ui":{"rows":2,"tab":{"id":"when"}}},{"key":"location","label":"Map pin","type":"location","showIf":{"match":"all","rules":[{"field":"format","op":"in","value":["in-person","hybrid"]}]},"ui":{"tab":{"id":"when"}}},{"key":"stream","label":"Stream link","type":"link","helpText":"Shown on the page only once the event starts.","showIf":{"match":"all","rules":[{"field":"format","op":"in","value":["online","hybrid"]}]},"ui":{"tab":{"id":"when"}}},{"key":"showCountdown","label":"Show a countdown","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"when"}}},{"key":"status","label":"Ticket status","type":"select","defaultValue":"on-sale","config":{"options":[{"label":"On sale","value":"on-sale"},{"label":"Few left","value":"few-left","description":"Adds a ‘Few tickets left’ note"},{"label":"Sold out","value":"sold-out","description":"Swaps the button for a waitlist"},{"label":"Cancelled","value":"cancelled"}]},"ui":{"control":"radio","tab":{"id":"tickets","label":"Tickets"}}},{"key":"registerButton","label":"Register button","type":"button","required":true,"showIf":{"match":"all","rules":[{"field":"status","op":"in","value":["on-sale","few-left"]}]},"ui":{"tab":{"id":"tickets"}}},{"key":"price","label":"Price line","type":"text","showIf":{"match":"all","rules":[{"field":"status","op":"in","value":["on-sale","few-left"]}]},"placeholder":"From $150 · Tables of 8 available","config":{"maxLength":48},"ui":{"tab":{"id":"tickets"}}},{"key":"waitlistButton","label":"Waitlist button","type":"button","required":true,"showIf":{"match":"all","rules":[{"field":"status","op":"eq","value":"sold-out"}]},"ui":{"tab":{"id":"tickets"}}},{"key":"cancelNote","label":"Cancellation note","type":"longtext","required":true,"showIf":{"match":"all","rules":[{"field":"status","op":"eq","value":"cancelled"}]},"config":{"maxLength":200},"ui":{"rows":2,"tab":{"id":"tickets"}}},{"key":"overlay","label":"Image overlay","type":"number","defaultValue":35,"config":{"min":0,"max":80,"step":5,"unit":"%"},"ui":{"control":"slider","tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"dark","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Event name, date, place, a countdown and the register button.","category":"hero","icon":"calendar-event","preview":{"title":"heading","subtitle":"startsAt","media":"image"}}},
|
|
1385
|
+
{"id":"hero-personalized","name":"Personalized hero (audiences + A/B)","category":"Heroes","kind":"block","tags":["personalization","conditional","repeater","validation","deep-nesting"],"proves":"Default content + audience versions that override only what they set · rule types with conditional inputs · an experiment split","why":"Personalization is modelled as ONE hero with a default ('Everyone') and an ordered array of audience versions that override only the fields they fill — so a returning-visitor version can change just the button. Each audience holds a rule object whose inputs depend on the rule type (a string for UTM/referrer, countries for geo, a segment select for CRM), making Hero › Audiences › Audience › Rule a four-level drill. The A/B test is a separate object in its own tab; its settings only appear when it's on. Overrides are plain optional fields with 'leave empty to keep' help, because the schema can't yet express inherit-from-default.","htmlSignals":["Server-rendered HTML differs between crawls with different ?utm_campaign= or Accept-Language/geo — compare responses","Multiple hidden hero variants in the DOM with data-variant / data-audience attributes","Experiment SDKs: Optimizely, VWO, Google Optimize remnants, LaunchDarkly, Ninetailed, Statsig (window.optimizely, _vwo_code)","Flicker-prevention snippets (async-hide, opacity:0 on body until the SDK decides)"],"mcp":"Only mark a hero personalized with evidence: hidden variants in the DOM or different responses for different UTM/geo requests. The variant shown with no parameters → the Everyone fields. Each other variant → audiences[] with ONLY the fields that differ (leave the rest empty), rule from the parameter that triggered it. Experiment SDK config (traffic allocation, goal event) → test. Never import third-party experiment ids as content; report the SDK so the team can decide whether BetterCMS replaces it.","pitfalls":["Copying the whole hero into every audience — later edits to Everyone no longer reach them. Store only the differences.","Two audiences that both match: order decides; the converter must preserve the source's precedence.","Personalized copy must not be indexed as the canonical content — crawlers see Everyone."],"notCarried":["ai","category"],"component":{"name":"Audience hero","slug":"audience-hero","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Everyone"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"config":{"maxLength":100},"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","recommendedMax":180,"localized":true,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"primaryButton","label":"Primary button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"image","label":"Image","type":"image","ui":{"tab":{"id":"content"}}},{"key":"audiences","label":"Audience versions","type":"table","helpText":"Checked top to bottom; the first match wins.","config":{"maxItems":5,"fields":[{"key":"name","label":"Name","type":"text","helpText":"Only editors see this.","required":true,"config":{"maxLength":40}},{"key":"rule","label":"Who sees it","type":"object","openMode":"drill","fields":[{"key":"type","label":"Match by","type":"select","required":true,"options":[{"label":"Ad campaign (utm_campaign)","value":"utm_campaign"},{"label":"Came from a site","value":"referrer"},{"label":"Country","value":"country"},{"label":"Returning visitor","value":"returning","description":"Visited in the last 30 days"},{"label":"CRM segment","value":"segment"}]},{"key":"match","label":"Contains","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"type","op":"in","value":["utm_campaign","referrer"]}]},"placeholder":"k8s-launch","config":{"maxLength":80}},{"key":"countries","label":"Countries","type":"multiselect","showIf":{"match":"all","rules":[{"field":"type","op":"eq","value":"country"}]},"options":[{"label":"United States","value":"US"},{"label":"United Kingdom","value":"GB"},{"label":"Germany","value":"DE"},{"label":"India","value":"IN"},{"label":"Japan","value":"JP"}],"config":{"min":1}},{"key":"segment","label":"Segment","type":"select","required":true,"showIf":{"match":"all","rules":[{"field":"type","op":"eq","value":"segment"}]},"options":[{"label":"Enterprise accounts (1,000+ staff)","value":"enterprise"},{"label":"Active trials","value":"trial"},{"label":"Cancelled in the last 90 days","value":"churned"}]}],"ui":{"icon":"filter"}},{"key":"eyebrow","label":"Eyebrow","type":"text","helpText":"Leave empty to keep the Everyone version.","config":{"maxLength":40}},{"key":"heading","label":"Heading","type":"text","helpText":"Leave empty to keep the Everyone version.","recommendedMax":60,"config":{"maxLength":100}},{"key":"body","label":"Body","type":"longtext","helpText":"Leave empty to keep the Everyone version.","recommendedMax":180,"ui":{"rows":3}},{"key":"primaryButton","label":"Primary button","type":"button","helpText":"Leave empty to keep the Everyone version."},{"key":"image","label":"Image","type":"image","helpText":"Leave empty to keep the Everyone version."}]},"ui":{"itemLabel":"Audience","icon":"user-search","preview":{"title":"name","subtitle":"rule.type","fallback":"Untitled audience"},"tab":{"id":"audiences","label":"Audiences"}}},{"key":"test","label":"A/B test","type":"object","openMode":"drill","config":{"fields":[{"key":"isRunning","label":"Run a test","type":"boolean"},{"key":"share","label":"Share seeing the new version","type":"number","defaultValue":50,"showIf":{"match":"all","rules":[{"field":"isRunning","op":"eq","value":"true"}]},"config":{"min":10,"max":90,"step":5,"unit":"%"},"ui":{"control":"slider"}},{"key":"goal","label":"Success means","type":"select","required":true,"showIf":{"match":"all","rules":[{"field":"isRunning","op":"eq","value":"true"}]},"options":[{"label":"Clicked the primary button","value":"primary-click"},{"label":"Started a trial","value":"signup"},{"label":"Booked a demo","value":"demo"}]},{"key":"endsOn","label":"Stop on","type":"date","showIf":{"match":"all","rules":[{"field":"isRunning","op":"eq","value":"true"}]}}]},"ui":{"icon":"flask","tab":{"id":"experiment","label":"Experiment"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}}],"description":"One hero with versions for different audiences, and an optional A/B test.","category":"hero","icon":"target","preview":{"title":"heading","subtitle":"eyebrow","media":"image"}}},
|
|
1386
|
+
{"id":"feature-icon-grid","name":"Icon feature grid","category":"Features & storytelling","kind":"block","tags":["repeater","variants","validation","localized"],"proves":"Bounded repeater · per-item optional link · column count as a variant, not a number field","why":"Features are an ARRAY OF OBJECTS because they are owned by this section and never reused elsewhere — if the same benefits appear on five product pages, promote them to a `col_features` collection and switch the array to references. Column count is a VARIANT (3/4/list), not a number, because the layout changes (icon beside vs above text), and variants swap without losing content. The per-feature link is a `link` (not a button) because it is a quiet text link; the section CTA is a `button`. Icon style and dividers are design and live in the Design group.","htmlSignals":["A heading followed by a grid (`display:grid` / `grid-cols-3|4`) of 3–12 sibling cards with identical structure","Each card starts with an `<svg>` or icon font `<i>` (24–48px) before an `<h3>`/`<h4>` and one `<p>`","No images larger than 64px inside the cards (that would be a card grid, not a feature grid)","Optional trailing `<a>` per card with text like “Learn more” / “Read more”"],"mcp":"Map the section `<h2>` → heading, the small uppercase text above it → eyebrow, the first `<p>` after it → intro. For each repeated card: SVG/icon → `icon` (match to the nearest Tabler name by the icon's `aria-label`, class name like `ti-flame`/`lucide-flame`, or path similarity; fall back to `sparkles`), `<h3>` → title, `<p>` → body, trailing anchor → link (`type: \"url\"` for absolute hrefs, `page` when the href resolves to a known page). Pick the variant from the computed column count at desktop width (3 → three, 4 → four, 2 with icon beside text → list). Do not create fields for card background, padding or icon color — they are theme tokens.","pitfalls":["Do not invent a `columns` number field — column count is the variant; a free number lets editors break the layout.","Feature cards with large photos are an image card grid, not this block.","Icon fonts with ligatures (Material Symbols) store the icon as TEXT content — read the text, not the class."],"notCarried":["ai","category","variants"],"component":{"name":"Feature grid","slug":"feature-grid","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":200,"localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"features","label":"Features","type":"table","config":{"minItems":2,"maxItems":12,"fields":[{"key":"icon","label":"Icon","type":"icon","helpText":"Leave empty to show a number instead."},{"key":"title","label":"Title","type":"text","required":true,"localized":true,"config":{"maxLength":48}},{"key":"body","label":"Body","type":"longtext","recommendedMax":140,"localized":true,"ui":{"rows":2}},{"key":"link","label":"Learn more link","type":"link"}]},"ui":{"itemLabel":"Feature","icon":"sparkles","preview":{"title":"title","subtitle":"body","fallback":"Untitled feature"},"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"iconStyle","label":"Icon style","type":"select","defaultValue":"tile","config":{"options":[{"label":"Plain","value":"plain","icon":"point","description":"Icon only"},{"label":"Tile","value":"tile","icon":"square","description":"Icon on a soft square"},{"label":"Circle","value":"circle","icon":"circle","description":"Icon on a soft circle"}]},"ui":{"control":"cards","tab":{"id":"design","label":"Design"}}},{"key":"alignment","label":"Alignment","type":"select","defaultValue":"left","config":{"options":[{"label":"Left","value":"left"},{"label":"Center","value":"center"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"showDividers","label":"Show dividers","type":"boolean","ui":{"tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"anchor","label":"Anchor","type":"text","advanced":true,"config":{"pattern":"^[a-z0-9-]*$","patternMessage":"Use lowercase letters, numbers and hyphens, like our-story."},"ui":{"tab":{"id":"design"}}}],"description":"Icon, title and a short line for each benefit, in 2–4 columns","category":"content","icon":"layout-grid","preview":{"title":"heading","subtitle":"eyebrow","icon":"layout-grid"}}},
|
|
1387
|
+
{"id":"feature-alternating-rows","name":"Alternating feature rows","category":"Features & storytelling","kind":"block","tags":["repeater","conditional","media-heavy","variants","rich-text"],"proves":"Per-row media type (image or video) via conditional fields · auto-alternating side with per-row override","why":"Rows are an ARRAY OF OBJECTS — each row is a mini-section with its own media, so a slot would be overkill and would let editors drop unrelated blocks in. The image side is DERIVED (section `startSide` + row index) and only stored when an editor overrides it (`side: auto|left|right`), so reordering rows keeps the zigzag intact. Image vs video is a `mediaType` select with conditional fields rather than two optional fields, so exactly one is required and validation is honest. Bullet points are a primitive string array, not rich text, so the checkmark list renders consistently.","htmlSignals":["Two or more consecutive blocks each with a 2-column layout (text + media) where the DOM order or `flex-direction: row-reverse` / `order-last` flips on alternating rows","Each block has its own `<h2>`/`<h3>`, a `<p>` and often a `<ul>` with check icons","Media is an `<img>`/`<picture>` or a muted `<video autoplay loop playsinline>`","Class names like `feature-row`, `zigzag`, `media-text`, `lg:flex-row-reverse` on every second row"],"mcp":"Group consecutive text+media rows into ONE featureRows block (not one block per row). Per row: `<h2|h3>` → title, small text above → eyebrow, paragraphs → body (keep `<strong>`, `<em>`, `<a>`; drop wrappers), `<ul>` items → points (strip icons), anchor/button → button. `<video>` → mediaType video + video.url from `<source src>` and poster from `poster=`; else image with `alt` from the img. Detect `startSide` from where the FIRST row's media sits; only set a row's `side` when it breaks the alternation. Media aspect ratio → mediaRatio (round to the nearest option).","pitfalls":["Storing `side` on every row breaks the zigzag the moment rows are reordered — store `auto` unless the source deliberately breaks the pattern.","Autoplay videos without a poster flash black; always extract or generate a poster."],"notCarried":["ai","category","variants"],"component":{"name":"Feature rows","slug":"feature-rows","props":[{"key":"heading","label":"Heading","type":"text","recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content","label":"Content","order":0}}},{"key":"rows","label":"Rows","type":"table","config":{"minItems":1,"maxItems":6,"fields":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32}},{"key":"title","label":"Title","type":"text","required":true,"recommendedMax":60,"localized":true},{"key":"body","label":"Body","type":"richtext","localized":true},{"key":"points","label":"Points","type":"array","config":{"maxItems":5,"itemType":"text","item":{"maxLength":80,"localized":true}},"ui":{"itemLabel":"Point"}},{"key":"button","label":"Button","type":"button"},{"key":"mediaType","label":"Media","type":"select","defaultValue":"image","options":[{"label":"Image","value":"image"},{"label":"Video","value":"video"}],"ui":{"control":"segmented","tab":{"id":"media"}}},{"key":"image","label":"Image","type":"image","required":true,"showIf":{"match":"all","rules":[{"field":"mediaType","op":"eq","value":"image"}]},"ui":{"tab":{"id":"media"}}},{"key":"video","label":"Video","type":"video","helpText":"Plays muted and loops.","required":true,"showIf":{"match":"all","rules":[{"field":"mediaType","op":"eq","value":"video"}]},"ui":{"tab":{"id":"media"}}},{"key":"side","label":"Image side","type":"select","helpText":"Auto alternates from the section's first side.","defaultValue":"auto","options":[{"label":"Auto","value":"auto"},{"label":"Left","value":"left"},{"label":"Right","value":"right"}],"ui":{"control":"segmented","tab":{"id":"design"}}}]},"ui":{"itemLabel":"Row","icon":"layout-rows","preview":{"title":"title","subtitle":"eyebrow","media":"image","fallback":"Untitled row"},"tab":{"id":"content"}}},{"key":"startSide","label":"First image on","type":"select","helpText":"Later rows alternate from here.","defaultValue":"right","config":{"options":[{"label":"Left","value":"left"},{"label":"Right","value":"right"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design","order":2}}},{"key":"mediaRatio","label":"Media shape","type":"select","defaultValue":"4:3","config":{"options":[{"label":"Square","value":"1:1"},{"label":"4:3","value":"4:3"},{"label":"Portrait","value":"3:4"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"anchor","label":"Anchor","type":"text","advanced":true,"config":{"pattern":"^[a-z0-9-]*$","patternMessage":"Use lowercase letters, numbers and hyphens, like our-story."},"ui":{"tab":{"id":"design"}}}],"description":"Image and text side by side, swapping sides row after row","category":"content","icon":"layout-rows","preview":{"title":"heading","icon":"layout-rows"},"tabs":[{"id":"media","label":"Media","order":1}]}},
|
|
1388
|
+
{"id":"bento-feature-showcase","name":"Feature bento (responsive spans)","category":"Features & storytelling","kind":"block","tags":["slots","responsive-layout","conditional","media-heavy","variants"],"proves":"Slot of blocks with per-breakpoint span, phone order and conditional background in slotSettings","why":"The cells are a SLOT (blocks inside a block) because bento cells are genuinely heterogeneous — a screenshot, a stat, a quote, a clip — and each needs its own schema. Unlike the core bento (one `span` preset), sizing here is RESPONSIVE: desktop column/row spans, a tablet width and a phone order are stored in `_settings` on each child, so the cell's CONTENT stays clean and the layout data travels with the cell when it is reordered. The background image is a slot setting (layout/surface), not cell content, and only appears when surface = image.","htmlSignals":["A CSS grid container with children using different `grid-column: span N` / `col-span-2` / `row-span-2` classes","Children of different internal shapes (one has a big image, one a giant number, one a blockquote)","Breakpoint-prefixed span classes (`md:col-span-2 lg:col-span-3`) or `order-*` classes for mobile","Rounded tiles with differing backgrounds (dark, brand color, photo)"],"mcp":"Treat each grid child as a slot child; classify by content: big number + short label → statBlock, img/video dominant → imageBlock/videoBlock, blockquote/quote marks → quoteBlock, icon+h3+p → featureBlock, heading+button → ctaCard, else textBlock. Read spans from computed `grid-column-end`/`grid-row-end` at 1280px → desktopWidth/rowsDesktop, at 768px → tabletWidth (full when it spans every column). DOM order vs visual order on a 375px viewport → mobileOrder only when they differ. Background color → surface (match to brand tokens; photo backgrounds → surface image + background).","pitfalls":["Do not put span values on the child block's own fields — reusing that block elsewhere would carry a meaningless span.","Phones stack cells; if the source reorders them with CSS `order`, capture it or the story reads out of order.","6-column variants need spans up to 6; validate spans against the variant's column count."],"notCarried":["ai","category","variants"],"component":{"name":"Feature bento","slug":"feature-bento","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"subheading","label":"Subheading","type":"longtext","recommendedMax":180,"localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"cells","label":"Cells","type":"slot","config":{"multiple":true,"minItems":3,"maxItems":10,"allow":["featureBlock","statBlock","imageBlock","videoBlock","quoteBlock","ctaCard","textBlock"],"slotSettings":[{"key":"desktopWidth","label":"Width on desktop","type":"number","helpText":"In grid columns.","defaultValue":2,"config":{"min":1,"max":6,"step":1,"unit":"cols"},"ui":{"control":"slider"}},{"key":"rowsDesktop","label":"Height on desktop","type":"number","defaultValue":1,"config":{"min":1,"max":3,"step":1,"unit":"rows"},"ui":{"control":"slider"}},{"key":"tabletWidth","label":"Width on tablet","type":"select","defaultValue":"half","options":[{"label":"Half","value":"half"},{"label":"Full","value":"full"}],"ui":{"control":"segmented"}},{"key":"mobileOrder","label":"Order on phones","type":"number","helpText":"Lower numbers go first. Leave empty to keep the desktop order.","config":{"min":1,"max":10,"step":1}},{"key":"surface","label":"Surface","type":"select","defaultValue":"muted","options":[{"label":"Muted","value":"muted"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"},{"label":"Image","value":"image"}],"ui":{"control":"segmented"}},{"key":"background","label":"Background image","type":"image","showIf":{"match":"all","rules":[{"field":"surface","op":"eq","value":"image"}]}}]},"ui":{"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"gap","label":"Gap","type":"select","defaultValue":"md","config":{"options":[{"label":"S","value":"sm"},{"label":"M","value":"md"},{"label":"L","value":"lg"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"radius","label":"Corner radius","type":"select","defaultValue":"lg","config":{"options":[{"label":"None","value":"none"},{"label":"M","value":"md"},{"label":"L","value":"lg"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Product capabilities as a mosaic of cells, each its own block, sized per screen","category":"content","icon":"layout-dashboard","preview":{"title":"heading","subtitle":"eyebrow","icon":"layout-dashboard"}}},
|
|
1389
|
+
{"id":"steps-how-it-works","name":"Steps / how it works","category":"Features & storytelling","kind":"block","tags":["repeater","variants","validation"],"proves":"Order is the number (never stored) · optional per-step image used by one variant only","why":"Steps are an ORDERED ARRAY; the step number is computed from the index so drag-to-reorder can never desync it. `duration` is a short string (\"10 days\", \"Day 3\") rather than a number+unit pair because it is display copy, not data anyone filters on. The per-step image is optional and only rendered by the Vertical variant — keeping it on the item (instead of a separate array) means switching variants never loses content.","htmlSignals":["An `<ol>` (or divs) of 2–6 items each starting with a large number (“1”, “01”, “Step 1”) or a numbered circle","Connector lines between items (`::before`/`::after` borders, SVG dashes)","Headings like “How it works”, “Getting started”, “What happens next”"],"mcp":"Strip the visible number from each step's title (“01 — Start a claim” → “Start a claim”) and set numberStyle from its format (1 → digits, 01 → padded, none visible → none). Per step: heading → title, paragraph → body, small pill/meta text (“2 min”) → duration, icon → icon. Pick the variant from the layout direction (horizontal → row, vertical with images → stack, bordered tiles → cards). A dashed line/border between numbers → showConnector true.","pitfalls":["Never store the number as content — the converter must strip it, otherwise reordering shows “3, 1, 2”.","HowTo rich results were retired by Google; do not emit HowTo JSON-LD for this block."],"notCarried":["ai","category","variants"],"component":{"name":"Steps","slug":"how-it-works","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"steps","label":"Steps","type":"table","helpText":"Numbers come from the order.","config":{"minItems":2,"maxItems":6,"fields":[{"key":"title","label":"Title","type":"text","required":true,"localized":true,"config":{"maxLength":48}},{"key":"body","label":"Body","type":"longtext","recommendedMax":160,"localized":true,"ui":{"rows":2}},{"key":"icon","label":"Icon","type":"icon","helpText":"Cards layout only. Replaces the number."},{"key":"duration","label":"Time it takes","type":"text","placeholder":"2 minutes","config":{"maxLength":20}},{"key":"image","label":"Image","type":"image","helpText":"Vertical layout only."},{"key":"link","label":"Link","type":"link"}]},"ui":{"itemLabel":"Step","icon":"circle-number-1","preview":{"title":"title","subtitle":"body","media":"image","fallback":"Untitled step"},"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"numberStyle","label":"Numbering","type":"select","defaultValue":"digits","config":{"options":[{"label":"1 2 3","value":"digits"},{"label":"01 02 03","value":"padded"},{"label":"None","value":"none"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"showConnector","label":"Connect the steps","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A numbered process: how it works, getting started, what happens next","category":"content","icon":"list-numbers","preview":{"title":"heading","icon":"list-numbers"}}},
|
|
1390
|
+
{"id":"timeline-company-history","name":"Timeline / company history","category":"Features & storytelling","kind":"block","tags":["repeater","conditional","media-heavy","validation","rich-text"],"proves":"Date precision select drives which date field shows · display order derived from the date, not the array","why":"Milestones are an array of objects owned by the About page — history is not reused, so no collection. Date PRECISION is modelled explicitly: most history only knows a year, so a single `date` field would force fake “Jan 1” dates. A `year` number shows for year precision and a real `date` for month/day; `label` is an optional display override (\"Spring 2016\") while sorting still uses the real value. The page sorts by date, so editors can append without hunting for the right slot.","htmlSignals":["A vertical line (`border-left`, `::before` with full height) with dots/markers and items on alternating sides","Each item leads with a year or date in a large or monospace style (“2014”, “Spring 2016”)","`<time datetime=…>` elements, or `<ol class=\"timeline\">`","Horizontal scroll containers with year markers (`overflow-x: auto` + `scroll-snap`)"],"mcp":"Parse each item's date text: a bare 4-digit year → precision year + `year`; “Month YYYY” → month + date YYYY-MM-01; full dates → day. Seasonal or fuzzy text (\"Spring 2016\", \"Early days\") → keep the parsed date AND put the original text into `label`. Prefer `<time datetime>` when present. Item heading → title, paragraphs → body (keep links). Larger or emphasized items → isHighlighted. Detect order by comparing the first and last dates.","pitfalls":["Storing “2014” in a date field becomes 2014-01-01 and later renders as “Jan 1, 2014”.","Two milestones with the same year keep their array order as the tie-breaker — keep the array roughly sorted."],"notCarried":["ai","category","variants"],"component":{"name":"Timeline","slug":"timeline","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content","order":0}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"intro","label":"Intro","type":"longtext","localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"milestones","label":"Milestones","type":"table","helpText":"Shown in date order, whatever the order here.","config":{"minItems":2,"maxItems":30,"fields":[{"key":"precision","label":"Date shown as","type":"select","defaultValue":"year","options":[{"label":"Year","value":"year"},{"label":"Month","value":"month"},{"label":"Exact day","value":"day"}],"ui":{"control":"segmented"}},{"key":"year","label":"Year","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"precision","op":"eq","value":"year"}]},"config":{"min":1800,"max":2100,"step":1}},{"key":"date","label":"Date","type":"date","helpText":"For Month, any day in the month works.","required":true,"showIf":{"match":"all","rules":[{"field":"precision","op":"in","value":["month","day"]}]}},{"key":"label","label":"Label override","type":"text","helpText":"Replaces the formatted date. Sorting still uses the date.","advanced":true,"placeholder":"Spring 2016","config":{"maxLength":24}},{"key":"title","label":"Title","type":"text","required":true,"localized":true,"config":{"maxLength":64}},{"key":"body","label":"Body","type":"richtext","localized":true},{"key":"image","label":"Image","type":"image","ui":{"tab":{"id":"media"}}},{"key":"isHighlighted","label":"Highlight this milestone","type":"boolean"}]},"ui":{"itemLabel":"Milestone","icon":"flag","preview":{"title":"title","subtitle":"year","media":"image","fallback":"Untitled milestone"},"tab":{"id":"content"}}},{"key":"order","label":"Order","type":"select","defaultValue":"asc","config":{"options":[{"label":"Oldest first","value":"asc"},{"label":"Newest first","value":"desc"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design","order":2}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"anchor","label":"Anchor","type":"text","advanced":true,"config":{"pattern":"^[a-z0-9-]*$","patternMessage":"Use lowercase letters, numbers and hyphens, like our-story."},"ui":{"tab":{"id":"design"}}}],"description":"Milestones in order: company history, product evolution, a trip log","category":"content","icon":"timeline","preview":{"title":"heading","icon":"timeline"},"tabs":[{"id":"media","label":"Media","order":1}]}},
|
|
1391
|
+
{"id":"tabs-with-media","name":"Tabs with media","category":"Features & storytelling","kind":"block","tags":["repeater","conditional","media-heavy","variants","a11y","localized"],"proves":"Fixed-shape tabs (no slot) · image/video conditional per tab · interval shown only when auto-advance is on","why":"This is the FIXED-SHAPE sibling of the core Tabs block: every tab has the same copy + media layout, so an array of objects gives marketers a predictable form and the converter a predictable target. Use the core slot-based Tabs only when tab panels hold arbitrary blocks. Media is a select + conditional image/video (exactly one required); the poster only shows for video. Auto-advance and its interval are design, and the interval is hidden until auto-advance is on.","htmlSignals":["`role=\"tablist\"` with `role=\"tab\"` buttons and `role=\"tabpanel\"` siblings, OR buttons with `aria-controls`/`data-tab` toggling panels","Each panel contains the SAME structure: heading + paragraph + one large screenshot or video","A progress bar inside the active tab button (auto-advance)","Screenshots inside a browser-chrome frame (three dots, fake URL bar)"],"mcp":"Tab button text → label (first line), secondary text inside the button → summary. Panel heading → title, paragraphs → body, panel button/link → button. Panel `<video>` → mediaType video (+ poster), `<img>` → screenshot. If tab buttons have an animated progress bar or the source JS rotates tabs with setInterval, set autoplay true and read the interval (ms → seconds). Browser/phone mockup chrome around media → frame. If panels differ structurally (one has a pricing table, one a form), use the core slot-based `tabs` instead.","pitfalls":["Auto-advancing tabs must stop on interaction and respect reduced motion — the renderer handles it; never model a “pause” field.","Tab labels longer than ~20 characters wrap on the top variant and break the underline."],"notCarried":["ai","category","variants"],"component":{"name":"Tabs with media","slug":"media-tabs","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content","order":0}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"tabs","label":"Tabs","type":"table","config":{"minItems":2,"maxItems":6,"fields":[{"key":"label","label":"Tab label","type":"text","required":true,"localized":true,"config":{"maxLength":20}},{"key":"icon","label":"Icon","type":"icon"},{"key":"summary","label":"Tab description","type":"longtext","helpText":"Side layout only.","recommendedMax":90,"localized":true,"ui":{"rows":2}},{"key":"title","label":"Panel title","type":"text","recommendedMax":60,"localized":true},{"key":"body","label":"Body","type":"richtext","localized":true},{"key":"button","label":"Button","type":"button"},{"key":"mediaType","label":"Media","type":"select","defaultValue":"image","options":[{"label":"Image","value":"image"},{"label":"Video","value":"video"}],"ui":{"control":"segmented","tab":{"id":"media"}}},{"key":"screenshot","label":"Screenshot","type":"image","helpText":"Export at 2× the shown size.","required":true,"showIf":{"match":"all","rules":[{"field":"mediaType","op":"eq","value":"image"}]},"ui":{"tab":{"id":"media"}}},{"key":"video","label":"Clip","type":"video","required":true,"showIf":{"match":"all","rules":[{"field":"mediaType","op":"eq","value":"video"}]},"ui":{"tab":{"id":"media"}}},{"key":"poster","label":"Poster","type":"image","showIf":{"match":"all","rules":[{"field":"mediaType","op":"eq","value":"video"}]},"ui":{"tab":{"id":"media"}}}]},"ui":{"itemLabel":"Tab","icon":"layout-navbar","preview":{"title":"label","subtitle":"title","media":"screenshot","fallback":"Untitled tab"},"tab":{"id":"content"}}},{"key":"autoplay","label":"Play automatically","type":"boolean","ui":{"tab":{"id":"design","label":"Design","order":2}}},{"key":"interval","label":"Seconds per tab","type":"number","defaultValue":6,"showIf":{"match":"all","rules":[{"field":"autoplay","op":"eq","value":"true"}]},"config":{"min":3,"max":15,"step":1,"unit":"s"},"ui":{"control":"slider","tab":{"id":"design"}}},{"key":"frame","label":"Media frame","type":"select","defaultValue":"browser","config":{"options":[{"label":"None","value":"none"},{"label":"Browser","value":"browser"},{"label":"Phone","value":"device"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A fixed-shape product tour: each tab has copy and its own screenshot or clip","category":"content","icon":"browser","preview":{"title":"heading","icon":"layout-navbar"},"tabs":[{"id":"media","label":"Media","order":1}]}},
|
|
1392
|
+
{"id":"accordion-feature-image-swap","name":"Accordion features with image swap","category":"Features & storytelling","kind":"block","tags":["repeater","media-heavy","a11y","variants","rich-text"],"proves":"One required image per item drives a shared media slot · open-state as design, not content","why":"Each accordion item OWNS the image shown while it is open, so the image lives on the item (required) rather than in a parallel array the editor would have to keep in sync. Which item starts open is a design toggle, not a per-item boolean, so there can never be two “default open” items. It is an array, not the FAQ block: these are product features with media, not questions, and must not emit FAQPage JSON-LD.","htmlSignals":["`<details>/<summary>` or buttons with `aria-expanded` in one column, and ONE large image in the other column","JS that swaps the image `src` (or toggles opacity on stacked images) when a row opens","Several `<img>` stacked absolutely with only one visible, one per accordion row"],"mcp":"Count accordion rows and images: if image count equals row count (stacked or swapped), map row i → image i. Summary/button text → title, revealed content → body (portable rich text, keep links), trailing link → link. If rows have NO images, this is an FAQ or plain accordion, not this entry. Image column side → variant. If the first row is expanded by default → isFirstOpen; a progress bar on the open row → autoplay.","pitfalls":["Images of different aspect ratios cause the layout to jump on every click — warn when the source images differ.","Do not tag these rows as FAQ structured data — they are not questions."],"notCarried":["ai","category","variants"],"component":{"name":"Accordion features","slug":"accordion-features","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content","order":0}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"items","label":"Features","type":"table","config":{"minItems":2,"maxItems":7,"fields":[{"key":"title","label":"Title","type":"text","required":true,"localized":true,"config":{"maxLength":56}},{"key":"body","label":"Body","type":"richtext","localized":true},{"key":"image","label":"Image","type":"image","helpText":"Replaces the main image while this row is open.","required":true,"ui":{"tab":{"id":"media"}}},{"key":"link","label":"Link","type":"link"}]},"ui":{"itemLabel":"Feature","icon":"chevron-down","preview":{"title":"title","media":"image","fallback":"Untitled feature"},"tab":{"id":"content"}}},{"key":"isFirstOpen","label":"Open the first feature","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design","order":2}}},{"key":"autoplay","label":"Play automatically","type":"boolean","ui":{"tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"An accordion of features; opening one swaps the image beside it","category":"content","icon":"list-details","preview":{"title":"heading","icon":"layout-list"},"tabs":[{"id":"media","label":"Media","order":1}]}},
|
|
1393
|
+
{"id":"comparison-vs-competitors","name":"Comparison vs competitors","category":"Features & storytelling","kind":"block","tags":["deep-nesting","conditional","repeater","validation","a11y","variants"],"proves":"Brand columns + one `table` keyed by brand key (reorder-safe) · row groups · Yes / no with words · partial answers as Text cells · source in the table's own note + a required last-checked date","why":"A comparison table is 2-D data: brand COLUMNS (name, logo, badge, price, button: the header) + one `table` (CPO D42) whose brand columns are keyed by the same `key`, so adding or reordering a competitor never shifts a value into the wrong column. Rows sit in row groups (tbody + th scope=rowgroup) instead of 5 drill levels. Brand columns are Yes / no, written as ✓ plus the words 'Included' / 'Not included'; a row of words is a Text row and a partial answer is one Text cell ('Partly: rules written by hand'); explainers are footnotes, so their words stay on the page. The source line is the table's own note (rendered under the table and linked with aria-describedby), not a second block field; the last-checked date is a required field because comparative advertising claims go stale. If the same competitors appear on several pages, columns should become references to a `col_competitors` collection.","htmlSignals":["A `<table>` (or CSS grid) whose header row contains brand names/logos, with the first data column of capability names","Cells containing check/cross SVGs, ✓ ✗ characters or `aria-label=\"Included\"`","One header column visually highlighted (tinted background, “Recommended” badge)","`<tbody>` sections or full-width rows acting as group headers (`colspan`)","Small print below: “Compared on …”, “Based on public pricing”, trademark notice"],"mcp":"Header cells → columns (slugify the name for `key`; img → logo; highlighted column → isHighlighted + badge text) AND the table's brand columns with `_key` = that key, type Yes / no. Body via the table detector (docs/TABLES.md): first cell → row header (tooltip → footnote); check icon/✓/“Yes”/aria-label included → true; cross/✗/“—”/“No” → false; “Limited”/“Partial” + words → a Text cell with those words; a row of words → row type Text. Full-width rows (colspan) start a row group. Small print under the table (source, trademark notice) → the table's `notes`; parse a date in it → verifiedOn, otherwise set verifiedOn to the import date and flag it for review. Never emit a separate `footnote` field on the block.","pitfalls":["RESOLVED (D42): cells are keyed by column; never emit positional cells.","Icons without text alternatives fail screen readers; the enum lets the renderer output “Included”/“Not included”.","Comparative claims without a date and source are a legal risk — `verifiedOn` is required on purpose."],"notCarried":["ai","category","variants"],"component":{"name":"Competitor comparison","slug":"competitor-comparison","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"intro","label":"Intro","type":"longtext","localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"columns","label":"Columns","type":"table","helpText":"The first column is you.","config":{"minItems":2,"maxItems":5,"fields":[{"key":"key","label":"Key","type":"slug","required":true,"unique":true,"readOnly":"developer","lockReason":"The table's column uses this key. Changing it would orphan its cells.","ui":{"access":{"edit":"schema.manage"}}},{"key":"name","label":"Name","type":"text","required":true,"config":{"maxLength":24}},{"key":"logo","label":"Logo","type":"image","helpText":"SVG or a transparent PNG."},{"key":"isHighlighted","label":"Highlight this column","type":"boolean"},{"key":"badge","label":"Badge","type":"text","showIf":{"match":"all","rules":[{"field":"isHighlighted","op":"eq","value":"true"}]},"placeholder":"Best value","config":{"maxLength":16}},{"key":"price","label":"Price line","type":"text","placeholder":"$29 / month","config":{"maxLength":24}},{"key":"button","label":"Button","type":"button","showIf":{"match":"all","rules":[{"field":"isHighlighted","op":"eq","value":"true"}]}}]},"ui":{"itemLabel":"Column","icon":"columns","preview":{"title":"name","subtitle":"key","media":"logo","fallback":"Untitled column"},"tab":{"id":"columns","label":"Brands"}}},{"key":"comparison","label":"Comparison","type":"table","required":true,"localized":true,"config":{"mode":"data","maxColumns":6,"maxRows":60,"columnTypes":["text","boolean","number","money","rating","link"],"jsonLd":["ItemList"],"settings":{"totals":false,"phones":[]}},"ui":{"tab":{"id":"content"}}},{"key":"verifiedOn","label":"Last checked","type":"date","helpText":"Shown under the table.","required":true,"ui":{"tab":{"id":"content"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}}],"description":"Your product against alternatives, row by row: included, not included, or a few words","category":"content","icon":"vs","preview":{"title":"heading","icon":"table"}}},
|
|
1394
|
+
{"id":"stats-band","name":"Stats band","category":"Features & storytelling","kind":"block","tags":["repeater","validation","variants","a11y","localized"],"proves":"Numbers stored as numbers (prefix/value/suffix) so they localise and count up · per-stat source + as-of date","why":"A stat is split into prefix · number · suffix so the number is real data: it can animate, be formatted per locale (1.2M vs 1,2 M) and be checked. The core `statBlock` stores a display string (\"120k+\") for quick bento cells — this section is for numbers that matter and need a SOURCE and AS-OF date (health, finance and B2B claims get audited). Count-up is design; the renderer skips it under reduced motion and always outputs the final number in the HTML.","htmlSignals":["A row of 2–5 items each with a very large number (font-size ≥ 2.5rem) and a small label beneath","`data-count`, `data-target`, `countup`, `odometer` attributes or classes","Numbers with units/magnitudes: “94%”, “$2.4B”, “11 min”, “10k+”","Footnote markers (*, †, superscript numbers) linking to a sources line"],"mcp":"Parse each big number with a regex: leading non-digits → prefix, the number (strip thousands separators; respect the page locale's decimal mark) → value, trailing text → suffix (keep the leading space if present), digits after the decimal → decimals. Prefer the `data-target`/`data-count` attribute over the visible text when both exist (the visible text may be mid-animation “0”). Label → label. Footnote text with a URL → source; any date in it → asOf. Presence of count-up attributes → useCountUp true.","pitfalls":["Scraping during a count-up animation captures “0” — read the data attribute or wait for the final value.","“1.2M” is value 1.2 + suffix M, not 1200000 with no suffix, unless the design formats compact numbers itself."],"notCarried":["ai","category","variants"],"component":{"name":"Stats band","slug":"stats-band","props":[{"key":"heading","label":"Heading","type":"text","helpText":"Heading + stats layout only.","recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"stats","label":"Stats","type":"table","config":{"minItems":2,"maxItems":5,"fields":[{"key":"prefix","label":"Before","type":"text","helpText":"Shown before the number, e.g. $.","placeholder":"$","config":{"maxLength":3}},{"key":"value","label":"Number","type":"number","helpText":"Digits only. Formatting is added per language.","required":true,"config":{"step":0.1}},{"key":"suffix","label":"After","type":"text","helpText":"Shown after the number, e.g. %, k+, yrs.","placeholder":"k+","config":{"maxLength":6}},{"key":"decimals","label":"Decimals","type":"number","defaultValue":0,"advanced":true,"config":{"min":0,"max":2,"step":1}},{"key":"label","label":"Label","type":"text","required":true,"localized":true,"config":{"maxLength":48}},{"key":"source","label":"Source","type":"link"},{"key":"asOf","label":"As of","type":"date"}]},"ui":{"itemLabel":"Stat","icon":"number","preview":{"title":"label","subtitle":"value","fallback":"Untitled stat"},"tab":{"id":"content"}}},{"key":"useCountUp","label":"Count up on scroll","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Three to five big numbers in a row, each with its source","category":"content","icon":"chart-bar","preview":{"title":"heading","icon":"chart-bar"}}},
|
|
1395
|
+
{"id":"manifesto-pull-quote","name":"Manifesto / pull quote","category":"Features & storytelling","kind":"block","tags":["conditional","rich-text","variants","localized"],"proves":"Mode select swaps an array of rich-text lines for a single quote · overlay shown only when a background exists (notEmpty)","why":"One block covers both a multi-line manifesto and a single pull quote because they share attribution and treatment; a `mode` select decides which content field shows so editors never see both. Each manifesto line is a tiny RICH-TEXT value (italic for emphasis only) rather than a plain string with markup, so emphasis survives translation. Attribution is a small inline object. The overlay slider only appears when a background image exists (`notEmpty` condition).","htmlSignals":["Very large type (≥ 2.5rem) over a full-width section with a few short lines or one `<blockquote>`","`<blockquote>` with `<cite>` or a `<figcaption>` holding name/role, often with a signature `<img>`","Lines revealed one at a time on scroll (`data-aos`, GSAP `SplitText`, staggered opacity)"],"mcp":"One `<blockquote>` (or one paragraph in display type) → mode quote, quote text WITHOUT surrounding quotation marks (the renderer adds typographic quotes). Several short display-size lines/paragraphs → mode lines, one line per element, keeping `<em>`/`<i>` as italic marks and dropping spans used only for color. `<cite>`/caption → attribution.name / role; signature image → attribution.signature. Section background-image → background + overlay from the gradient/scrim opacity.","pitfalls":["Quotation marks typed into content double up with the design's quotes — strip them.","Lines split by `<br>` inside one paragraph are still separate lines."],"notCarried":["category","variants"],"component":{"name":"Manifesto","slug":"manifesto","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"mode","label":"Content","type":"select","defaultValue":"lines","config":{"options":[{"label":"Lines","value":"lines"},{"label":"Quote","value":"quote"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"lines","label":"Lines","type":"array","showIf":{"match":"all","rules":[{"field":"mode","op":"eq","value":"lines"}]},"config":{"minItems":2,"maxItems":8,"itemType":"richtext","item":{"localized":true,"helpText":"Italic words are emphasised."}},"ui":{"itemLabel":"Line","tab":{"id":"content"}}},{"key":"quote","label":"Quote","type":"longtext","helpText":"Without quotation marks; the design adds them.","required":true,"showIf":{"match":"all","rules":[{"field":"mode","op":"eq","value":"quote"}]},"recommendedMax":220,"localized":true,"ui":{"rows":4,"tab":{"id":"content"}}},{"key":"attribution","label":"Attribution","type":"object","openMode":"inline","config":{"fields":[{"key":"name","label":"Name","type":"text","config":{"maxLength":48}},{"key":"role","label":"Role","type":"text","localized":true,"config":{"maxLength":64}},{"key":"signature","label":"Signature","type":"image","helpText":"A transparent PNG or SVG."}]},"ui":{"icon":"signature","tab":{"id":"content"}}},{"key":"background","label":"Background image","type":"image","ui":{"tab":{"id":"media","label":"Media"}}},{"key":"overlay","label":"Image overlay","type":"number","defaultValue":45,"showIf":{"match":"all","rules":[{"field":"background","op":"notEmpty"}]},"config":{"min":0,"max":80,"step":5,"unit":"%"},"ui":{"control":"slider","tab":{"id":"design","label":"Design"}}},{"key":"size","label":"Type size","type":"select","defaultValue":"xl","config":{"options":[{"label":"Large","value":"lg"},{"label":"Extra large","value":"xl"},{"label":"Display","value":"display"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A big-type statement of belief or a single pull quote, with attribution","category":"content","icon":"blockquote","preview":{"title":"eyebrow","subtitle":"attribution.name","icon":"quote"}}},
|
|
1396
|
+
{"id":"problem-solution","name":"Problem → solution","category":"Features & storytelling","kind":"block","tags":["repeater","variants","validation"],"proves":"Pairs keep both columns aligned · column labels as small objects with defaults","why":"Problems and solutions are stored as PAIRS, not two independent arrays, because the strongest version of this section lines each pain up with its fix — two arrays drift (four problems, three solutions) and the paired layout breaks. The two-column variant reads the same data column-wise. Column labels and summaries are small objects with sensible defaults so the common case needs no typing.","htmlSignals":["Two columns labeled “Before / After”, “Without X / With X”, “The old way / The new way”","One column with ✗ / red / strikethrough items, the other with ✓ / green items","Equal item counts in both columns, often vertically aligned"],"mcp":"Find the two lists; take their headers → problem.label / solution.label. If counts match, zip item i of each list into a pair; if they differ, create pairs for the shorter length and append the leftovers with an empty partner and a review flag. Small numeric callouts (“6 hours saved”) → metric. Strike-through/red styling → tone warning. Layout: rows where each problem sits next to its fix → pairs, two independent stacks → columns.","pitfalls":["Unequal lists in the source must not be silently truncated — flag them."],"notCarried":["ai","category","variants"],"component":{"name":"Problem and solution","slug":"problem-solution","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"problem","label":"Problem side","type":"object","config":{"fields":[{"key":"label","label":"Column label","type":"text","defaultValue":"Without Ledgerly","localized":true,"config":{"maxLength":32}},{"key":"summary","label":"Summary","type":"longtext","localized":true,"ui":{"rows":2}}]},"ui":{"icon":"alert-triangle","tab":{"id":"content"}}},{"key":"solution","label":"Solution side","type":"object","config":{"fields":[{"key":"label","label":"Column label","type":"text","defaultValue":"With Ledgerly","localized":true,"config":{"maxLength":32}},{"key":"summary","label":"Summary","type":"longtext","localized":true,"ui":{"rows":2}},{"key":"button","label":"Button","type":"button"}]},"ui":{"icon":"circle-check","tab":{"id":"content"}}},{"key":"pairs","label":"Pairs","type":"table","config":{"minItems":2,"maxItems":6,"fields":[{"key":"problem","label":"Problem","type":"text","required":true,"localized":true,"config":{"maxLength":90}},{"key":"solution","label":"Solution","type":"text","required":true,"localized":true,"config":{"maxLength":90}},{"key":"metric","label":"Proof","type":"text","localized":true,"placeholder":"4 hours saved a month","config":{"maxLength":40}}]},"ui":{"itemLabel":"Pair","icon":"arrows-exchange","preview":{"title":"problem","subtitle":"solution","fallback":"Untitled pair"},"tab":{"id":"content"}}},{"key":"tone","label":"Problem tone","type":"select","defaultValue":"muted","config":{"options":[{"label":"Muted","value":"muted"},{"label":"Warning","value":"warning"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"The pain your buyer has today, set against how you fix it","category":"content","icon":"arrows-exchange","preview":{"title":"heading","icon":"arrows-exchange"}}},
|
|
1397
|
+
{"id":"integrations-grid","name":"Integrations grid","category":"Features & storytelling","kind":"block","tags":["dynamic-query","references","conditional","variants","media-heavy"],"proves":"Dynamic query vs manual references to a collection · filter chips derived from referenced categories","why":"Integrations are REFERENCED, not inlined: each one has its own detail page (/integrations/stripe), logo, category and description, and appears on several pages — that is a collection (`col_integrations`) with a category reference (`col_integration_categories`). The section stores only HOW to pick: a dynamic query (categories, featured, limit, sort) so new integrations appear automatically, or a manual list of references for a curated row. Filter chips are derived from the categories of what is shown, so there is no second list to maintain. Inline the items only when the site has no integration pages and fewer than ~12 logos.","htmlSignals":["A grid of 12+ tiles each with a third-party logo, a product name and a short line, linking to `/integrations/<slug>`","Filter chips or a sidebar of categories (“All”, “Payments”, “CRM”) with `data-filter`/`aria-pressed`","A search input filtering the tiles; a “See all integrations” link"],"mcp":"If tiles link to per-integration pages (shared `/integrations/` prefix), create or match `col_integrations` entries (name, slug from href, logo, one-line description, category from the active filter or a `data-category` attribute) and set source to `query` with the categories present. If tiles do not link anywhere, use source `manual` and create entries anyway so logos are reusable. Filter chip labels → integration category entries (skip “All”). A visible search box → showSearch. Count tiles shown before a “See all” link → limit.","pitfalls":["Brand logos have usage rules — keep the original SVG, never recolor it in content; `isMonochrome` is a render-time filter.","Category options cannot come from a sibling array in our schema, which is exactly why categories are a referenced collection."],"notCarried":["ai","category","variants"],"component":{"name":"Integrations grid","slug":"integrations-grid","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"subheading","label":"Subheading","type":"longtext","localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"source","label":"Which integrations","type":"select","defaultValue":"query","config":{"options":[{"label":"From the Integrations collection","value":"query","description":"New integrations appear here on their own"},{"label":"Pick them","value":"manual","description":"A fixed list in your order"}]},"ui":{"control":"radio","tab":{"id":"content"}}},{"key":"categories","label":"Categories","type":"multi-reference","helpText":"Leave empty for all categories.","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"query"}]},"config":{"contentModelId":"col_integration_categories"},"ui":{"tab":{"id":"content"}}},{"key":"showFeaturedOnly","label":"Featured only","type":"boolean","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"query"}]},"ui":{"tab":{"id":"content"}}},{"key":"limit","label":"How many","type":"number","defaultValue":24,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"query"}]},"config":{"min":4,"max":96,"step":4},"ui":{"tab":{"id":"content"}}},{"key":"sort","label":"Sort by","type":"select","defaultValue":"popular","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"query"}]},"config":{"options":[{"label":"Most used","value":"popular"},{"label":"A to Z","value":"az"},{"label":"Newest","value":"new"}]},"ui":{"tab":{"id":"content"}}},{"key":"items","label":"Integrations","type":"multi-reference","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"manual"}]},"config":{"min":4,"max":48,"contentModelId":"col_integrations"},"ui":{"itemLabel":"Integration","layout":"gallery","itemName":"integration","tab":{"id":"content"}}},{"key":"showFilter","label":"Show category filter","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"content"}}},{"key":"showSearch","label":"Show search","type":"boolean","ui":{"tab":{"id":"content"}}},{"key":"viewAllButton","label":"View all button","type":"button","helpText":"Hidden when everything already fits.","ui":{"tab":{"id":"content"}}},{"key":"columns","label":"Columns on desktop","type":"number","defaultValue":4,"config":{"min":3,"max":6,"step":1},"ui":{"control":"slider","tab":{"id":"design","label":"Design"}}},{"key":"isMonochrome","label":"Monochrome logos","type":"boolean","ui":{"tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Logos of the tools you connect with, filterable by category, each linking to its page","category":"content","icon":"plug-connected","preview":{"title":"heading","icon":"plug-connected"}}},
|
|
1398
|
+
{"id":"testimonial-grid","name":"Testimonial grid","category":"Social proof","kind":"block","tags":["references","dynamic-query","conditional","variants","validation"],"proves":"References to a Testimonials collection · pick vs query (tags, limit, min rating) · uniqueness","why":"Testimonials are REFERENCES to `col_testimonials` because the same quote appears on the home page, product pages and the reviews page, and each one carries facts that must not drift between copies: the customer's consent date, their exact words, rating, photo and the product they bought. The section only stores which ones and how many. A query mode (by tag, limit, minimum rating) keeps product pages fresh without hand-curation. Inline the quotes only for a one-off campaign page whose quotes appear nowhere else.","htmlSignals":["A grid/masonry (`column-count`, `grid-auto-flow: dense`) of 3+ cards each with a quote, a person name, a role/company and often an avatar","Star ratings (5 SVG stars, `aria-label=\"5 out of 5 stars\"`, ★ characters)","`<blockquote>` + `<figcaption>`/`<cite>` per card","Headings like “What customers say”, “Reviews”, “Loved by …”"],"mcp":"For each card, create or match a `col_testimonials` entry: quote (strip surrounding quotation marks), name, role, company, photo, rating (count filled stars or parse aria-label), product if the card names one. Match existing entries by name + first 40 characters of the quote before creating. Store references in source `pick` in DOM order. Uniform card heights → grid; varying heights → masonry; one large first card → featured. Never import a testimonial without a name — flag it.","pitfalls":["Inlining quotes creates copies that drift when the customer asks for an edit or removal — reference them.","Grids look broken with 4 or 5 items in a 3-column layout; validation nudges multiples of 3.","Do not emit Review JSON-LD for testimonials about your own business — Google ignores self-serving reviews."],"notCarried":["ai","category","variants"],"component":{"name":"Testimonial grid","slug":"testimonial-grid","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"source","label":"Which testimonials","type":"select","defaultValue":"pick","config":{"options":[{"label":"Pick them","value":"pick","description":"A fixed set in your order"},{"label":"Latest matching","value":"query","description":"Filled from the collection by tag"}]},"ui":{"control":"radio","tab":{"id":"content"}}},{"key":"testimonials","label":"Testimonials","type":"multi-reference","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"pick"}]},"unique":true,"config":{"min":3,"max":12,"contentModelId":"col_testimonials"},"ui":{"itemLabel":"Testimonial","tab":{"id":"content"}}},{"key":"tags","label":"Tagged","type":"tags","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"query"}]},"ui":{"tab":{"id":"content"}}},{"key":"limit","label":"How many","type":"number","defaultValue":6,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"query"}]},"config":{"min":3,"max":12,"step":3},"ui":{"tab":{"id":"content"}}},{"key":"minRating","label":"Minimum rating","type":"rating","helpText":"Leave empty to include quotes without a rating.","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"query"}]},"config":{"step":0.1},"ui":{"tab":{"id":"content"}}},{"key":"showRating","label":"Show rating","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"showPhotos","label":"Show photos","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Customer quotes from the Testimonials collection, in a grid or masonry","category":"social-proof","icon":"message-star","preview":{"title":"heading","icon":"message-star"}}},
|
|
1399
|
+
{"id":"testimonial-video-slider","name":"Video testimonial slider","category":"Social proof","kind":"block","tags":["media-heavy","a11y","conditional","repeater","variants","references","validation"],"proves":"Video vs quote slides via conditional fields · captions file required for a11y · transcript as rich text","why":"Slides are an array of objects with a `kind` select: video slides REQUIRE a poster and a captions file (WCAG 1.2.2 — and most people watch muted), quote slides hide all video fields. The person is a small inline object because video stories are often recorded for one campaign; an optional reference to the written testimonial links consent and rating back to the collection when it exists. The transcript is rich text so it can carry speaker labels and is indexable.","htmlSignals":["A carousel (`swiper`, `splide`, `embla`, `scroll-snap-type: x`) of tall cards with a play button over a portrait poster","`<video>` with `<track kind=\"captions\" src=\"….vtt\">`, or YouTube/Vimeo iframes opened in a lightbox","A short quote and a name/role/company under each video, sometimes a duration badge (“1:24”)","“Read transcript” toggles or `<details>` blocks"],"mcp":"Per slide: `<video><source>` / iframe src / `data-video-url` → video.url (normalize YouTube/Vimeo to canonical watch URLs); `poster=` or the card background → poster; `<track kind=captions|subtitles>` → captions (download the .vtt; if missing on a hosted MP4, flag the slide — do NOT publish without captions). Overlay quote → pullQuote, name/role/company → person. Duration badge → duration (m:ss). Slides with no video → kind quote. Transcript blocks → transcript (keep speaker names bold).","pitfalls":["Hosted MP4s with no captions file fail accessibility — the converter must flag, not skip, them.","Autoplaying a slider while a video plays is hostile; the renderer pauses advance once any video starts."],"notCarried":["ai","category","variants"],"component":{"name":"Video testimonials","slug":"video-testimonials","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"slides","label":"Slides","type":"table","config":{"minItems":2,"maxItems":10,"fields":[{"key":"kind","label":"Type","type":"select","defaultValue":"video","options":[{"label":"Video","value":"video"},{"label":"Quote","value":"quote"}],"ui":{"control":"segmented"}},{"key":"video","label":"Video","type":"video","helpText":"Hosted MP4, YouTube or Vimeo.","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"video"}]}},{"key":"poster","label":"Poster","type":"image","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"video"}]}},{"key":"captions","label":"Captions","type":"file","helpText":"A .vtt file. Required for hosted MP4s.","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"video"}]}},{"key":"transcript","label":"Transcript","type":"richtext","showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"video"}]},"localized":true},{"key":"pullQuote","label":"Pull quote","type":"longtext","required":true,"recommendedMax":160,"localized":true,"ui":{"rows":3}},{"key":"duration","label":"Length","type":"text","showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"video"}]},"placeholder":"1:24","config":{"pattern":"^\\d{1,2}:\\d{2}$","patternMessage":"Use m:ss, like 1:24."}},{"key":"person","label":"Person","type":"object","fields":[{"key":"name","label":"Name","type":"text","required":true,"config":{"maxLength":48}},{"key":"role","label":"Role","type":"text","localized":true,"placeholder":"Owner","config":{"maxLength":64}},{"key":"company","label":"Company","type":"text","config":{"maxLength":48}},{"key":"photo","label":"Photo","type":"image","helpText":"Square, at least 192px."}],"ui":{"icon":"user"}},{"key":"testimonial","label":"Linked testimonial","type":"reference","advanced":true,"config":{"contentModelId":"col_testimonials"}},{"key":"caseStudy","label":"Case study link","type":"link"}]},"ui":{"itemLabel":"Slide","icon":"movie","preview":{"title":"person.name","subtitle":"pullQuote","media":"poster","fallback":"Untitled slide"},"tab":{"id":"content"}}},{"key":"autoplay","label":"Play automatically","type":"boolean","ui":{"tab":{"id":"design","label":"Design"}}},{"key":"allowInlinePlay","label":"Play on the card","type":"boolean","helpText":"Off opens videos in a lightbox.","defaultValue":false,"ui":{"tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A slider of customer stories: video with captions, or a written quote","category":"social-proof","icon":"video","preview":{"title":"heading","icon":"movie"}}},
|
|
1400
|
+
{"id":"wall-of-love","name":"Wall of love","category":"Social proof","kind":"block","tags":["polymorphic-array","repeater","validation","media-heavy","a11y"],"proves":"Polymorphic array (social post · store review · photo post) each with its own required source link and rights flag","why":"A wall of love mixes DIFFERENT kinds of proof, so `items` is a POLYMORPHIC ARRAY: each kind (`socialPost`, `review`, `photoPost`) has its own required fields instead of one bloated object where half the fields don't apply. Every item stores a link to the ORIGINAL so the wall stays verifiable (and FTC-safe); reposted photos carry an explicit `hasRights` flag. Posts are inlined rather than referenced because they are snapshots of third-party content that only this wall shows; if the brand runs a UGC programme, move them into a `col_ugc` collection.","htmlSignals":["A masonry of cards that look like embedded tweets/posts (avatar, @handle, network logo, date) mixed with review cards (stars + reviewer) and photo tiles","Embedded `blockquote.twitter-tweet`, `blockquote.instagram-media` or iframes from `platform.twitter.com`/`instagram.com`","Columns animating vertically in opposite directions (`@keyframes` translateY loops)","Headings like “Wall of love”, “#hashtag”, “Don't take our word for it”"],"mcp":"Classify each card: has @handle + network icon/embed → socialPost (network from the icon/embed host); has stars + reviewer → review (platform from the logo or link host; default site); image-dominant with handle → photoPost. Copy text EXACTLY. Extract the permalink from embed `cite=`/`href` → url; if a socialPost/photoPost has no permalink, flag it (url is required). Set hasRights false on every imported photoPost and flag for the editor to confirm — never assume permission. Opposite-scrolling columns → marquee.","pitfalls":["Third-party embed scripts (twitter widgets.js) slow the page and break when posts are deleted — store the content, link the original.","Reposting customer photos without permission is a copyright problem; the required flag forces the question."],"notCarried":["ai","category","variants"],"component":{"name":"Wall of love","slug":"wall-of-love","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"items","label":"Posts","type":"table","config":{"mode":"types","minItems":6,"maxItems":40,"itemTypes":[{"name":"socialPost","label":"Social post","fields":[{"key":"network","label":"Network","type":"select","defaultValue":"x","options":[{"label":"X","value":"x"},{"label":"Threads","value":"threads"},{"label":"Bluesky","value":"bluesky"},{"label":"LinkedIn","value":"linkedin"}],"ui":{"control":"segmented"}},{"key":"name","label":"Name","type":"text","required":true,"config":{"maxLength":48}},{"key":"handle","label":"Handle","type":"text","required":true,"placeholder":"@mayaoutside","config":{"pattern":"^@?[A-Za-z0-9_.]{1,30}$","patternMessage":"Use letters, numbers, dots and underscores, like custom.rank."}},{"key":"photo","label":"Photo","type":"image"},{"key":"text","label":"Post text","type":"longtext","helpText":"Copy it exactly, typos and emoji included.","required":true,"config":{"maxLength":500},"ui":{"rows":4}},{"key":"date","label":"Posted","type":"date"},{"key":"url","label":"Original post","type":"url","required":true}],"icon":"brand-x","preview":{"title":"handle","subtitle":"text","media":"photo","fallback":"Untitled social post"}},{"name":"review","label":"Store review","fields":[{"key":"platform","label":"Platform","type":"select","defaultValue":"site","options":[{"label":"Our store","value":"site"},{"label":"Google","value":"google"},{"label":"Trustpilot","value":"trustpilot"},{"label":"Amazon","value":"amazon"},{"label":"REI","value":"rei"}]},{"key":"rating","label":"Rating","type":"rating","required":true,"config":{"step":0.1}},{"key":"title","label":"Review title","type":"text","config":{"maxLength":80}},{"key":"text","label":"Review","type":"longtext","required":true,"config":{"maxLength":600},"ui":{"rows":4}},{"key":"reviewer","label":"Reviewer","type":"text","required":true,"placeholder":"Jordan K.","config":{"maxLength":48}},{"key":"isVerified","label":"Verified buyer","type":"boolean","helpText":"Only when the platform shows the badge."},{"key":"date","label":"Reviewed","type":"date"},{"key":"url","label":"Original post","type":"url","helpText":"Link to the review if the platform has one."}],"icon":"star","preview":{"title":"reviewer","subtitle":"title","fallback":"Untitled review"}},{"name":"photoPost","label":"Photo post","fields":[{"key":"image","label":"Image","type":"image","required":true},{"key":"handle","label":"Handle","type":"text","required":true,"placeholder":"@trailkitchen"},{"key":"caption","label":"Caption","type":"longtext","config":{"maxLength":300},"ui":{"rows":2}},{"key":"hasRights","label":"Creator gave permission","type":"boolean","helpText":"Reposting a photo needs the creator's written OK.","required":true},{"key":"url","label":"Original post","type":"url","required":true}],"icon":"brand-instagram","preview":{"title":"handle","subtitle":"caption","media":"image","fallback":"Untitled photo post"}}]},"ui":{"itemLabel":"Post","tab":{"id":"content"}}},{"key":"showSourceIcons","label":"Show platform icons","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"maxHeight","label":"Collapse after","type":"number","helpText":"Adds \"Show more\" below this height.","defaultValue":900,"config":{"min":400,"max":2000,"step":100,"unit":"px"},"ui":{"control":"slider","tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A masonry wall of real posts and reviews: social posts, store reviews, photos","category":"social-proof","icon":"heart","preview":{"title":"heading","icon":"heart"}}},
|
|
1401
|
+
{"id":"case-study-cards","name":"Case study cards","category":"Social proof","kind":"block","tags":["references","dynamic-query","conditional","variants"],"proves":"Array of objects wrapping a reference + per-card override · three ways to choose (pick, by industry, latest)","why":"Case studies are full CMS items (/customers/masa-bakery) with their own logo, industry, hero metric and body, so the cards REFERENCE `col_case_studies` — card title, logo, image and URL all come from the entry. Each pick is wrapped in a small object so a page can override the headline metric for its audience (the pricing page wants “paid 9 days sooner”, the home page wants “6 hours saved”) without editing the story. Query modes (industry, latest) suit industry landing pages that should update when a new story ships.","htmlSignals":["Cards with a customer logo, a bold result metric (“3× faster”, “−40% cost”), a short summary and a “Read story” link to `/customers/…` or `/case-studies/…`","An industry/segment label on each card","Large cover photos of the customer's team or premises"],"mcp":"For each card, follow its link: if the target is a case-study page, create/match a `col_case_studies` entry (title, slug from the URL, logo, industry, hero metric, cover image; the article body goes through the long-form converter). Reference it. If the card's visible metric differs from the story page's main metric, store it in metricOverride. When the cards are under an industry heading or filtered by `?industry=`, set source industry with that industry.","pitfalls":["Copying the card title/logo into the section duplicates the story — reference it, override only what differs.","`col_case_studies` must exist before import; create the collection first in the conversion plan."],"notCarried":["ai","category","variants"],"component":{"name":"Case study cards","slug":"case-study-cards","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"source","label":"Which stories","type":"select","defaultValue":"pick","config":{"options":[{"label":"Pick them","value":"pick"},{"label":"By industry","value":"industry","description":"Newest stories in the industries you choose"},{"label":"Latest","value":"latest"}]},"ui":{"control":"radio","tab":{"id":"content"}}},{"key":"stories","label":"Stories","type":"table","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"pick"}]},"unique":true,"config":{"minItems":1,"maxItems":6,"fields":[{"key":"caseStudy","label":"Case study","type":"reference","required":true,"config":{"contentModelId":"col_case_studies"}},{"key":"metricOverride","label":"Headline result","type":"text","helpText":"Replaces the story's headline metric on this card only.","placeholder":"Closed books 4× faster","config":{"maxLength":40}}]},"ui":{"itemLabel":"Story","itemName":"card","icon":"briefcase","preview":{"title":"caseStudy","subtitle":"metricOverride","fallback":"Untitled story"},"tab":{"id":"content"}}},{"key":"industries","label":"Industries","type":"multiselect","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"industry"}]},"config":{"options":[{"label":"Hospitality","value":"hospitality"},{"label":"Retail","value":"retail"},{"label":"Agencies","value":"agency"},{"label":"Construction","value":"construction"},{"label":"Nonprofits","value":"nonprofit"}]},"ui":{"tab":{"id":"content"}}},{"key":"limit","label":"How many","type":"number","defaultValue":3,"showIf":{"match":"all","rules":[{"field":"source","op":"in","value":["industry","latest"]}]},"config":{"min":1,"max":9,"step":1},"ui":{"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"showLogos","label":"Show customer logos","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Cards for customer stories: logo, headline result, link to the full story","category":"social-proof","icon":"briefcase","preview":{"title":"heading","icon":"briefcase"}}},
|
|
1402
|
+
{"id":"logo-cloud","name":"Logo cloud","category":"Social proof","kind":"block","tags":["repeater","variants","a11y","validation","conditional","media-heavy"],"proves":"Grid array of tiny popover objects · required company name for a11y · marquee-only field hidden by variant","why":"Logos are an array of tiny objects edited in a POPOVER (name + file + optional link + optical size) and shown as a grid of tiles, because a logo cloud is managed visually. The company NAME is required and separate from the image's alt because it is what screen readers announce and what the marquee duplicates with `aria-hidden`. Grayscale, height and speed are design; speed only appears for the marquee variant. If the same logos appear on several pages (customers, retailers), promote them to a global block (synced) or a `col_customers` collection.","htmlSignals":["A row/grid of 4–30 small `<img>`/`<svg>` logos with no other text except one label (“Trusted by…”, “As seen at”)","CSS filters `isGrayscale(1)` / `opacity: .6` that reset on hover","A duplicated track with `aria-hidden=\"true\"` and a `@keyframes` translateX animation (marquee)"],"mcp":"Each logo → name from `alt`, `title`, `aria-label` or the file name (title-case it: `williams-sonoma.svg` → Williams Sonoma); logo = original file (prefer SVG source). Wrapping `<a>` → link. If the track is duplicated for the marquee, import ONE copy only. Detect grayscale from the CSS filter, logoHeight from the rendered height. Variant: animated track → marquee, bordered cells → grid, else row.","pitfalls":["Marquees duplicate logos in the DOM — de-duplicate on import or you get every logo twice.","Alt text like “logo” or empty alt on linked logos fails a11y; the required name fixes it.","Conditions on `_variant` work today because the variant is stored on the block value — this is a schema-language gap worth making official."],"notCarried":["category","item openMode","variants"],"component":{"name":"Logo cloud","slug":"logo-cloud","props":[{"key":"heading","label":"Heading","type":"text","localized":true,"placeholder":"Sold at","config":{"maxLength":80},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"logos","label":"Logos","type":"table","config":{"minItems":3,"maxItems":30,"fields":[{"key":"name","label":"Company name","type":"text","helpText":"Not shown. Read by screen readers.","required":true,"config":{"maxLength":48}},{"key":"logo","label":"Logo","type":"image","helpText":"SVG or a transparent PNG.","required":true},{"key":"link","label":"Link","type":"link"},{"key":"scale","label":"Optical size","type":"number","helpText":"Nudges wide or tall marks so logos look the same size.","defaultValue":100,"config":{"min":60,"max":140,"step":5,"unit":"%"},"ui":{"control":"slider"}}]},"ui":{"itemLabel":"Logo","layout":"grid","icon":"photo","preview":{"title":"name","media":"logo","fallback":"Untitled logo"},"tab":{"id":"content"}}},{"key":"isGrayscale","label":"Grayscale logos","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"speed","label":"Scroll speed","type":"select","defaultValue":"slow","showIf":{"match":"all","rules":[{"field":"_variant","op":"eq","value":"marquee"}]},"config":{"options":[{"label":"Slow","value":"slow"},{"label":"Medium","value":"medium"},{"label":"Fast","value":"fast"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"logoHeight","label":"Logo height","type":"number","defaultValue":32,"config":{"min":20,"max":64,"step":4,"unit":"px"},"ui":{"control":"slider","tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Logos of customers, retailers or partners, as a static row or an endless marquee","category":"social-proof","icon":"building-skyscraper","preview":{"title":"heading","icon":"building-skyscraper"}}},
|
|
1403
|
+
{"id":"press-mentions","name":"Press mentions","category":"Social proof","kind":"block","tags":["repeater","validation","variants","media-heavy","localized"],"proves":"Quotes tied to a required source link · logo + quote + journalist per mention · rotator variant","why":"Each mention is an object with the quote, outlet logo and a REQUIRED article link, because a press quote without its source is unverifiable and editors are tempted to paraphrase. Mentions are inlined (not a collection) on most sites because the press page is the only other place they appear; if the site has a /press page listing every article, create `col_press` and reference it instead. `isFeatured` lets the rotator start on the strongest quote.","htmlSignals":["Publication logos (often grayscale) paired with short italic quotes, each linking to an external article","“As seen in”, “In the press”, “Featured in” labels","Rotators where clicking a logo swaps the quote above it (`aria-selected`, `data-quote`)"],"mcp":"Per mention: logo img → logo (keep the original file), `alt`/file name → outlet, quote text → quote (strip curly quotes), wrapping or nearby external link → article (type url, newTab true), byline → author, date text → date. Logos WITHOUT quotes are a logo cloud, not this block. Mark the first or visually largest quote as featured.","pitfalls":["Never shorten a quote by paraphrasing — use an ellipsis and keep the source link.","Outlet logos are trademarks; use the outlet's press-kit file and do not recolour it in content."],"notCarried":["category","variants"],"component":{"name":"Press mentions","slug":"press-mentions","props":[{"key":"heading","label":"Heading","type":"text","recommendedMax":48,"localized":true,"placeholder":"As seen in","ui":{"tab":{"id":"content","label":"Content"}}},{"key":"mentions","label":"Mentions","type":"table","config":{"minItems":2,"maxItems":8,"fields":[{"key":"outlet","label":"Publication","type":"text","required":true,"config":{"maxLength":48}},{"key":"logo","label":"Publication logo","type":"image","helpText":"SVG or a transparent PNG.","required":true},{"key":"quote","label":"Quote","type":"longtext","helpText":"Their exact words. Trim with \"…\" if needed.","required":true,"recommendedMax":140,"localized":true,"ui":{"rows":3}},{"key":"article","label":"Article","type":"link","required":true},{"key":"author","label":"Journalist","type":"text","config":{"maxLength":48}},{"key":"date","label":"Published","type":"date"},{"key":"isFeatured","label":"Featured","type":"boolean"}]},"ui":{"itemLabel":"Mention","icon":"news","preview":{"title":"outlet","subtitle":"quote","media":"logo","fallback":"Untitled mention"},"tab":{"id":"content"}}},{"key":"isGrayscale","label":"Grayscale logos","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"What publications said, with the outlet's logo and a link to the article","category":"social-proof","icon":"article","preview":{"title":"heading","icon":"news"}}},
|
|
1404
|
+
{"id":"awards-certifications","name":"Awards & certifications","category":"Social proof","kind":"block","tags":["conditional","validation","repeater","seo-structured","a11y"],"proves":"Kind select changes required fields (certificate id + expiry for certifications) · expiry hides stale badges","why":"Badges are an array of objects with a `kind` that changes what is REQUIRED: a certification needs an expiry (`validUntil`) and ideally a certificate number and verify link; an award needs a year. The renderer hides an expired certification automatically, which is the bug this entry exists to catch — sites routinely show lapsed badges for years. Badge images stay untouched (issuers have usage rules). If the brand has a trust center with the same badges, this should be a global block (synced) so a renewal updates every page.","htmlSignals":["A row of square/circular badge images (SOC 2, ISO, HIPAA, PCI, awards) with small captions","Links to trust centres (`trust.`, `security.` subdomains), Vanta/Drata/SafeBase pages or certification registries","Text like “Certified”, “Compliant”, “Award winner”, “Best of 2026”"],"mcp":"Per badge: image → image, alt/caption → name; classify: SOC, ISO, HITRUST, PCI, FedRAMP, GDPR-seal → certification; “award”, “best”, “winner”, year-bearing titles → award; association logos → membership. Linked registry/trust pages → verify. For certifications with no expiry in the source, set validUntil to one year from the import date and FLAG it for confirmation. If the source says “HIPAA certified”, flag the claim — there is no official HIPAA certification.","pitfalls":["Expired certifications shown as current are a compliance risk — `validUntil` is required on certifications for that reason.","“HIPAA certified” and “GDPR certified” badges are marketing inventions; the converter flags them.","Organization JSON-LD `award` should only list awards, never certifications."],"notCarried":["category","variants"],"component":{"name":"Awards and certifications","slug":"awards","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"badges","label":"Badges","type":"table","config":{"minItems":1,"maxItems":12,"fields":[{"key":"kind","label":"Kind","type":"select","defaultValue":"award","options":[{"label":"Award","value":"award"},{"label":"Certification","value":"certification","description":"Audited by a third party, e.g. SOC 2, ISO 27001"},{"label":"Membership","value":"membership"}],"ui":{"control":"segmented"}},{"key":"name","label":"Name","type":"text","helpText":"Exactly as the issuer writes it, e.g. SOC 2 Type II.","required":true,"config":{"maxLength":64}},{"key":"issuer","label":"Issued by","type":"text","required":true,"config":{"maxLength":64}},{"key":"image","label":"Image","type":"image","helpText":"The issuer's official badge file.","required":true},{"key":"year","label":"Year","type":"number","showIf":{"match":"all","rules":[{"field":"kind","op":"in","value":["award","membership"]}]},"config":{"min":1990,"max":2100,"step":1}},{"key":"certificateId","label":"Certificate number","type":"text","showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"certification"}]},"config":{"maxLength":48}},{"key":"validUntil","label":"Valid until","type":"date","helpText":"The badge hides itself after this date.","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"certification"}]}},{"key":"verify","label":"Verify link","type":"link","helpText":"Required for certifications."}]},"ui":{"itemLabel":"Badge","icon":"award","preview":{"title":"name","subtitle":"issuer","media":"image","fallback":"Untitled badge"},"tab":{"id":"content"}}},{"key":"note","label":"Note","type":"longtext","localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"isGrayscale","label":"Grayscale badges","type":"boolean","ui":{"tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Badges for awards, security certifications and compliance, each verifiable","category":"social-proof","icon":"award","preview":{"title":"heading","icon":"award"}}},
|
|
1405
|
+
{"id":"review-summary","name":"Review summary","category":"Social proof","kind":"block","tags":["seo-structured","references","conditional","validation","a11y","commerce"],"proves":"Synced vs typed ratings (developer-locked provider) · 5-bucket distribution validation · AggregateRating only with a product subject","why":"Ratings are DATA, not copy: when a review app is connected the average, count and distribution are synced (the provider is developer-locked) and editors only choose presentation; manual mode exists for sources that can't sync. The distribution is exactly five buckets (validation min=max=5) so bars always render 5→1. The block references its SUBJECT product because AggregateRating structured data is only valid for a specific product/service, and must match what is visible — so JSON-LD is emitted only when a subject is set (the toggle is hidden otherwise). Featured reviews reference the testimonials collection.","htmlSignals":["A big average number (“4.8”) next to stars and “Based on 2,418 reviews”","Five horizontal bars labeled 5★…1★ with counts or percentages (`<progress>`, `<meter>`, width-% divs)","Review-app markup: `jdgm-`, `okendo-`, `yotpo-`, `trustpilot-widget` classes or their script tags","`application/ld+json` with `AggregateRating` / `ratingValue` / `reviewCount`"],"mcp":"Detect the review app from class prefixes/scripts → dataSource connected + provider (and tell the developer to connect it); otherwise manual. Read average/count from JSON-LD first (most reliable), then visible text. Bars → ratingCounts: parse each bar's count; if only percentages are shown, compute counts from the total and round so they sum to it. Badges for other platforms → platforms with their profile links. The product the page is about → subject. Never emit AggregateRating for store-wide/self-serving ratings.","pitfalls":["AggregateRating on a home page about your own business is ignored or penalised — only with a product subject.","Typed-in ratings drift from the real ones; prefer the connected source and show “Synced …” in the UI.","Distribution counts that don't sum to the total look fake; validate the sum."],"notCarried":["category","item openMode","itemNoun (derived)","shopify reference","variants"],"component":{"name":"Review summary","slug":"review-summary","props":[{"key":"heading","label":"Heading","type":"text","recommendedMax":48,"localized":true,"placeholder":"What cooks are saying","ui":{"tab":{"id":"content","label":"Content"}}},{"key":"subject","label":"Reviews of","type":"reference","helpText":"Leave empty for store-wide reviews.","helpMore":"Star snippets in search only appear for a specific product.","config":{"contentModelId":"col_products"},"ui":{"tab":{"id":"content"}}},{"key":"dataSource","label":"Ratings come from","type":"select","defaultValue":"connected","config":{"options":[{"label":"Connected review app","value":"connected","description":"Synced nightly from the reviews integration"},{"label":"Typed in","value":"manual","description":"For ratings you can't sync, e.g. a trade show survey"}]},"ui":{"control":"radio","tab":{"id":"data","label":"Ratings"}}},{"key":"provider","label":"Review app","type":"select","showIf":{"match":"all","rules":[{"field":"dataSource","op":"eq","value":"connected"}]},"readOnly":"developer","lockReason":"Connected by the developer in Settings › Integrations.","config":{"options":[{"label":"Judge.me","value":"judgeme"},{"label":"Okendo","value":"okendo"},{"label":"Yotpo","value":"yotpo"},{"label":"Trustpilot","value":"trustpilot"}]},"ui":{"access":{"edit":"schema.manage"},"tab":{"id":"data"}}},{"key":"average","label":"Average rating","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"dataSource","op":"eq","value":"manual"}]},"config":{"min":1,"max":5,"step":0.1},"ui":{"tab":{"id":"data"}}},{"key":"count","label":"Number of reviews","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"dataSource","op":"eq","value":"manual"}]},"config":{"min":1,"step":1},"ui":{"tab":{"id":"data"}}},{"key":"ratingCounts","label":"Rating breakdown","type":"table","helpText":"One row per star level, 5 to 1. Counts must add up to the total.","showIf":{"match":"all","rules":[{"field":"dataSource","op":"eq","value":"manual"}]},"config":{"minItems":5,"maxItems":5,"fields":[{"key":"stars","label":"Stars","type":"number","readOnly":true,"config":{"min":1,"max":5,"step":1}},{"key":"count","label":"Reviews","type":"number","config":{"min":0,"step":1}}]},"ui":{"itemLabel":"Star level","itemName":"bucket","preview":{"title":"stars","subtitle":"count"},"tab":{"id":"data"}}},{"key":"platforms","label":"Other platforms","type":"table","config":{"maxItems":4,"fields":[{"key":"name","label":"Platform","type":"select","required":true,"options":[{"label":"Google","value":"google"},{"label":"Trustpilot","value":"trustpilot"},{"label":"Amazon","value":"amazon"},{"label":"REI","value":"rei"},{"label":"G2","value":"g2"}]},{"key":"rating","label":"Rating","type":"number","required":true,"config":{"min":1,"max":5,"step":0.1}},{"key":"count","label":"Reviews","type":"number","config":{"min":0,"step":1}},{"key":"profile","label":"Profile link","type":"link","required":true}]},"ui":{"itemLabel":"Platform","icon":"star","preview":{"title":"name","subtitle":"rating"},"tab":{"id":"data"}}},{"key":"featuredReviews","label":"Featured reviews","type":"multi-reference","config":{"max":3,"contentModelId":"col_testimonials"},"ui":{"itemLabel":"Review","itemName":"review","itemTitle":"Review","tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"useRichResults","label":"Send rating to search engines","type":"boolean","helpText":"Shows the stars in search results.","defaultValue":true,"showIf":{"match":"all","rules":[{"field":"subject","op":"notEmpty"}]},"ui":{"tab":{"id":"data"}}},{"key":"starColor","label":"Star color","type":"select","defaultValue":"brand","config":{"options":[{"label":"Brand","value":"brand"},{"label":"Gold","value":"gold"},{"label":"Ink","value":"ink"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Average rating, distribution bars, platform scores and a few top reviews","category":"social-proof","icon":"stars","preview":{"title":"heading","subtitle":"subject","icon":"stars"}}},
|
|
1406
|
+
{"id":"masonry-gallery","name":"Masonry gallery","category":"Media & galleries","kind":"block","tags":["media-heavy","a11y","responsive-layout","repeater","validation","localized"],"proves":"Bulk image array as tiles · caption + structured credit per photo · columns per breakpoint as design","why":"Photos are an array of small objects (image + caption + credit + optional link) shown as a GRID of tiles because galleries are managed visually and in bulk. Caption and credit are separate: the caption is localised content, the credit is attribution (name + link) that must survive translation and appear in the lightbox. Alt text lives on the image value itself. Column counts per breakpoint are DESIGN in the Design group — never per-photo sizes, because masonry keeps each photo's natural aspect ratio from its stored width/height.","htmlSignals":["Many `<img>` (6+) inside a container with `column-count`, `columns-3`, `grid-template-rows: masonry`, or Masonry.js / `data-masonry`","Images of differing aspect ratios with no fixed height","`<figure>` + `<figcaption>` per image, often with “Photo: Name” or © text","Links to full-size images (`href` ending .jpg) or useLightbox libraries (PhotoSwipe, GLightbox, fancybox) via `data-pswp-*`, `data-fancybox`"],"mcp":"Each figure → a photo: highest-resolution source (`srcset` largest, or the useLightbox `href`) → image.url, intrinsic width/height from `data-pswp-width`/natural size, `alt` → image.alt. Split `<figcaption>`: text after “Photo:”, “©”, “Credit:” or in a `<small>`/`<cite>` → credit.name (+ any link), the rest → caption. Count CSS columns at 1280/768/375px → desktopColumns/tabletColumns/phoneColumns. Lightbox library present → useLightbox true.","pitfalls":["Importing the thumbnail instead of the full-size file — always prefer the lightbox href or the largest srcset entry.","Credits pasted into captions get translated; keep them in the credit object.","Empty alt is right for decorative photos but wrong for a portfolio — flag empties for review."],"notCarried":["category","variants"],"component":{"name":"Masonry gallery","slug":"masonry-gallery","props":[{"key":"heading","label":"Heading","type":"text","recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"intro","label":"Intro","type":"longtext","localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"images","label":"Photos","type":"table","config":{"minItems":3,"maxItems":60,"fields":[{"key":"image","label":"Image","type":"image","helpText":"At least 1600px on the long side.","required":true},{"key":"caption","label":"Caption","type":"longtext","helpText":"Shown on hover and in the lightbox.","localized":true,"config":{"maxLength":200},"ui":{"rows":2}},{"key":"credit","label":"Credit","type":"object","openMode":"inline","fields":[{"key":"name","label":"Name","type":"text","config":{"maxLength":48}},{"key":"link","label":"Link","type":"link"}],"ui":{"icon":"camera"}},{"key":"link","label":"Link","type":"link","helpText":"Opens this instead of the lightbox."}]},"ui":{"itemLabel":"Photo","layout":"grid","itemName":"photo","icon":"photo","preview":{"title":"caption","subtitle":"credit.name","media":"image","fallback":"Untitled photo"},"tab":{"id":"media","label":"Media"}}},{"key":"useLightbox","label":"Open in a lightbox","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"captionDisplay","label":"Captions","type":"select","defaultValue":"hover","config":{"options":[{"label":"Below","value":"below"},{"label":"On hover","value":"hover"},{"label":"Lightbox only","value":"lightbox"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"desktopColumns","label":"Columns on desktop","type":"number","defaultValue":3,"config":{"min":2,"max":5,"step":1},"ui":{"control":"slider","tab":{"id":"design"}}},{"key":"tabletColumns","label":"Columns on tablet","type":"number","defaultValue":2,"config":{"min":1,"max":3,"step":1},"ui":{"control":"slider","tab":{"id":"design"}}},{"key":"phoneColumns","label":"Columns on phones","type":"number","defaultValue":1,"config":{"min":1,"max":2,"step":1},"ui":{"control":"slider","tab":{"id":"design"}}},{"key":"gap","label":"Gap","type":"select","defaultValue":"md","config":{"options":[{"label":"None","value":"none"},{"label":"S","value":"sm"},{"label":"M","value":"md"},{"label":"L","value":"lg"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Photos at their natural shapes, packed in columns, with captions and credits","category":"content","icon":"layout-board","preview":{"title":"heading","media":"images.0.image","icon":"layout-board"}}},
|
|
1407
|
+
{"id":"lightbox-gallery-filters","name":"Gallery with category filters","category":"Media & galleries","kind":"block","tags":["media-heavy","a11y","repeater","validation","localized"],"proves":"Filters defined once as key/label objects · photos tag themselves by key · URL-synced filter state","why":"Filter categories are defined ONCE on the section (label + stable key, localised labels) and each photo tags itself with keys, so renaming “Dining” to “Restaurant” never touches the photos and a photo can sit in two filters. The keys are a `tags` field today because our schema can't populate a select's options from a sibling array — a real gap (see notes). If the same photos are reused across pages, move them into the media library with media tags and switch the section to a query.","htmlSignals":["Filter buttons above a grid (`data-filter`, `aria-pressed`, Isotope/MixItUp/`data-category`) and tiles carrying category classes (`.dining`, `data-groups='[\"spa\"]'`)","A uniform tile grid (fixed aspect ratio, `object-fit: cover`)","A lightbox on click; sometimes a “Load more” button"],"mcp":"Filter button labels → categories (skip the “All”/“Everything” chip → allLabel; key from `data-filter` minus `.`/`*` or slugified label). For each tile, read its category classes/data attributes → categories (keys). Image from the lightbox href or largest srcset, alt → alt, lightbox caption → caption. Tile aspect → variant (1:1 square, else landscape). Visible count before “Load more” → initialCount. `?filter=`/hash usage in links → useUrlFilter.","pitfalls":["A photo tagged with a key that no filter defines is invisible under every filter except All — validate keys against the filters.","Free-typed keys are error-prone; a select fed by the section's filters is the right control (schema gap)."],"notCarried":["category","item openMode","variants"],"component":{"name":"Gallery with filters","slug":"filter-gallery","props":[{"key":"heading","label":"Heading","type":"text","recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"categories","label":"Filters","type":"table","config":{"minItems":2,"maxItems":8,"fields":[{"key":"label","label":"Label","type":"text","required":true,"localized":true,"config":{"maxLength":20}},{"key":"key","label":"Key","type":"slug","helpText":"Changing it unlinks the photos that use it.","required":true,"unique":true}]},"ui":{"itemLabel":"Filter","icon":"filter","preview":{"title":"label","subtitle":"key"},"tab":{"id":"content"}}},{"key":"allLabel","label":"\"All\" chip label","type":"text","helpText":"Leave empty to hide the All chip.","defaultValue":"All","localized":true,"config":{"maxLength":16},"ui":{"tab":{"id":"content"}}},{"key":"images","label":"Photos","type":"table","config":{"minItems":4,"maxItems":120,"fields":[{"key":"image","label":"Image","type":"image","required":true},{"key":"categories","label":"Filters","type":"tags","helpText":"Filter keys, e.g. rooms, dining.","config":{"min":1}},{"key":"caption","label":"Caption","type":"longtext","helpText":"Shown in the lightbox.","localized":true,"config":{"maxLength":160},"ui":{"rows":2}}]},"ui":{"itemLabel":"Photo","layout":"grid","itemName":"photo","icon":"photo","preview":{"title":"caption","subtitle":"categories","media":"image","fallback":"Untitled photo"},"tab":{"id":"media","label":"Media"}}},{"key":"useUrlFilter","label":"Remember filter in the URL","type":"boolean","advanced":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"columns","label":"Columns on desktop","type":"number","defaultValue":4,"config":{"min":2,"max":6,"step":1},"ui":{"control":"slider","tab":{"id":"design"}}},{"key":"initialCount","label":"Show before \"Load more\"","type":"number","defaultValue":12,"config":{"min":4,"max":48,"step":4},"ui":{"tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A uniform grid filtered by category chips; every photo opens in a lightbox","category":"content","icon":"photo-search","preview":{"title":"heading","media":"images.0.image","icon":"photo-search"}}},
|
|
1408
|
+
{"id":"video-section","name":"Video section (YouTube · Vimeo · hosted)","category":"Media & galleries","kind":"block","tags":["media-heavy","a11y","seo-structured","conditional","validation","embed","localized","variants"],"proves":"Source select switches link vs file · captions per language required for hosted · chapters with timestamp validation · VideoObject JSON-LD","why":"One block covers all three hosts with a `source` select: YouTube/Vimeo store a link (the renderer lazy-loads a facade until play), hosted stores a FILE so we can transcode and serve renditions. Captions are a per-language array required for hosted video (WCAG 1.2.2) — YouTube/Vimeo carry their own. Chapters are validated timestamps so the renderer can seek and emit `Clip` markup. Title, description, poster, duration and upload date are REQUIRED because they feed VideoObject JSON-LD — that is native SEO generated from content the section already needs, not modelled meta fields. Playback settings are design; loop only shows with autoplay.","htmlSignals":["`<iframe src=\"https://www.youtube(-nocookie).com/embed/…\">`, `player.vimeo.com/video/…`, `lite-youtube` / `lite-vimeo` facades","`<video>` with `<source type=\"video/mp4\">`, `poster=` and `<track kind=\"captions\" srclang=…>`","A list of timestamps (“1:12 Making the moulds”) next to or under the player","`application/ld+json` with `VideoObject`, `duration` (ISO 8601 like PT6M48S), `uploadDate`"],"mcp":"Host from the iframe/src domain → source; canonicalise YouTube/Vimeo to watch URLs (`youtube.com/watch?v=ID`, `vimeo.com/ID`) and keep `?start=`/`#t=` as startAt. `<video><source>` → source hosted + file (download the MP4); every `<track>` → captions (srclang → language, label → label, kind). Read title/description/duration/uploadDate from VideoObject JSON-LD first (convert PT6M48S → 6:48), else from visible text. Timestamp list → chapters (first must be 0:00 — prepend “Intro” if missing). `autoplay muted loop` attributes → design flags.","pitfalls":["Autoplay with sound is blocked by browsers; autoplay always means muted here.","Missing uploadDate/duration silently drops the video from rich results — they are required for that reason.","Embedding raw YouTube iframes costs ~1 MB of JS per video; the renderer uses a facade until play."],"notCarried":["category","item openMode","variants"],"component":{"name":"Video","slug":"video-section","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"title","label":"Title","type":"text","helpText":"Also the video's name for search engines.","required":true,"recommendedMax":70,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"description","label":"Description","type":"longtext","helpText":"Also sent to search engines.","recommendedMax":300,"localized":true,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"source","label":"Hosted on","type":"select","defaultValue":"youtube","config":{"options":[{"label":"YouTube","value":"youtube"},{"label":"Vimeo","value":"vimeo"},{"label":"Our media library","value":"hosted"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"video","label":"Video link","type":"video","helpText":"Paste the watch link.","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"in","value":["youtube","vimeo"]}]},"ui":{"tab":{"id":"content"}}},{"key":"file","label":"Video file","type":"file","helpText":"MP4 (H.264) up to 500 MB.","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"hosted"}]},"ui":{"tab":{"id":"content"}}},{"key":"poster","label":"Poster","type":"image","helpText":"At 16:9.","required":true,"ui":{"tab":{"id":"content"}}},{"key":"duration","label":"Length","type":"text","helpText":"Also sent to search engines.","required":true,"placeholder":"4:32","config":{"pattern":"^(\\d{1,2}:)?\\d{1,2}:\\d{2}$","patternMessage":"Use m:ss or h:mm:ss, like 4:05."},"ui":{"tab":{"id":"content"}}},{"key":"uploadDate","label":"Published","type":"date","helpText":"The date the video first went public.","required":true,"ui":{"tab":{"id":"content"}}},{"key":"captions","label":"Caption tracks","type":"table","helpText":"One WebVTT file per language.","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"hosted"}]},"config":{"minItems":1,"maxItems":12,"fields":[{"key":"language","label":"Language","type":"text","required":true,"placeholder":"en","config":{"pattern":"^[a-z]{2}(-[A-Z]{2})?$","patternMessage":"Use a language code, like en or es-MX."}},{"key":"label","label":"Label","type":"text","helpText":"Shown in the captions menu.","required":true,"placeholder":"English","config":{"maxLength":24}},{"key":"file","label":"WebVTT file","type":"file","required":true},{"key":"kind","label":"Kind","type":"select","defaultValue":"captions","options":[{"label":"Captions","value":"captions","description":"Speech plus sounds"},{"label":"Subtitles","value":"subtitles","description":"Translated speech only"}],"ui":{"control":"segmented"}},{"key":"isOnByDefault","label":"On by default","type":"boolean","helpText":"One track at most."}]},"ui":{"itemLabel":"Caption track","itemName":"track","icon":"badge-cc","preview":{"title":"label","subtitle":"language"},"tab":{"id":"access","label":"Captions & chapters"}}},{"key":"chapters","label":"Chapters","type":"table","helpText":"The first one must start at 0:00.","config":{"maxItems":30,"fields":[{"key":"start","label":"Starts at","type":"text","required":true,"placeholder":"1:05","config":{"pattern":"^(\\d{1,2}:)?\\d{1,2}:\\d{2}$","patternMessage":"Use m:ss or h:mm:ss, like 4:05."}},{"key":"title","label":"Title","type":"text","required":true,"localized":true,"config":{"maxLength":60}}]},"ui":{"itemLabel":"Chapter","icon":"list","preview":{"title":"title","subtitle":"start"},"tab":{"id":"access"}}},{"key":"transcript","label":"Transcript","type":"richtext","localized":true,"ui":{"tab":{"id":"access"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"autoplay","label":"Play automatically","type":"boolean","ui":{"tab":{"id":"design","label":"Playback"}}},{"key":"loop","label":"Loop the video","type":"boolean","showIf":{"match":"all","rules":[{"field":"autoplay","op":"eq","value":"true"}]},"ui":{"tab":{"id":"design"}}},{"key":"controls","label":"Show player controls","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design"}}},{"key":"startAt","label":"Start at","type":"text","advanced":true,"placeholder":"0:30","config":{"pattern":"^((\\d{1,2}:)?\\d{1,2}:\\d{2})?$","patternMessage":"Use m:ss, like 1:24."},"ui":{"tab":{"id":"design"}}},{"key":"usePrivacyMode","label":"Privacy-enhanced embed","type":"boolean","helpText":"No tracking cookies until play.","defaultValue":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"youtube"}]},"ui":{"tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A featured video from YouTube, Vimeo or your own host, with captions, chapters and a transcript","category":"content","icon":"player-play","preview":{"title":"title","media":"poster","icon":"player-play"}}},
|
|
1409
|
+
{"id":"before-after-slider","name":"Before / after slider","category":"Media & galleries","kind":"block","tags":["media-heavy","a11y","repeater","validation","variants","localized"],"proves":"Paired required images per comparison · localised labels with defaults · single vs tabs via one array","why":"A comparison is a PAIR of required images plus labels, so it's an object; an array of comparisons covers both the single and tabbed layouts without two blocks. Labels default to “Before/After” but are editable and localised (“1987 / 2025” is stronger). Divider start, orientation and the first-view nudge are design. Both images keep their own alt — screen readers get “Before: …, After: …” and a keyboard-operable slider from the renderer.","htmlSignals":["Two stacked images in one container where the top one is clipped (`clip-path: inset(0 50% 0 0)` or a width %) with a draggable handle","`<input type=\"range\">` overlaid on images, or libraries `img-comparison-slider`, `twentytwenty`, `juxtapose`, `beer-slider`","Labels “Before” / “After” pinned in the corners"],"mcp":"Find the two images inside the comparison container: the clipped/overlay one is usually AFTER (check the label nearest each image rather than assuming). Labels → beforeLabel/afterLabel. Initial clip/range value → start. Vertical clip (inset top/bottom) → orientation vertical. Multiple comparisons under tabs → one block with several comparisons and variant tabs. Compare the two images' natural sizes; if they differ by more than 2%, flag it.","pitfalls":["Before and after with different crops make the slider useless — dimensions should match (not enforceable in our schema yet).","A slider built from a div with mouse events only is not keyboard accessible; the renderer must use a real range input."],"notCarried":["category","variants"],"component":{"name":"Before and after","slug":"before-after","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"comparisons","label":"Comparisons","type":"table","helpText":"Add more than one to show tabs.","config":{"minItems":1,"maxItems":6,"fields":[{"key":"label","label":"Tab label","type":"text","helpText":"Only shown when there are several.","localized":true,"config":{"maxLength":24}},{"key":"before","label":"Before","type":"image","helpText":"Same position, size and crop as After.","required":true},{"key":"after","label":"After","type":"image","required":true},{"key":"beforeLabel","label":"Before label","type":"text","defaultValue":"Before","localized":true,"config":{"maxLength":16}},{"key":"afterLabel","label":"After label","type":"text","defaultValue":"After","localized":true,"config":{"maxLength":16}},{"key":"caption","label":"Caption","type":"longtext","localized":true,"config":{"maxLength":200},"ui":{"rows":2}},{"key":"credit","label":"Credit","type":"object","openMode":"inline","fields":[{"key":"name","label":"Name","type":"text","config":{"maxLength":48}},{"key":"link","label":"Link","type":"link"}],"ui":{"icon":"camera"}}]},"ui":{"itemLabel":"Comparison","icon":"arrows-horizontal","preview":{"title":"label","subtitle":"caption","media":"after","fallback":"Untitled comparison"},"tab":{"id":"media","label":"Media"}}},{"key":"start","label":"Divider starts at","type":"number","defaultValue":50,"config":{"min":10,"max":90,"step":5,"unit":"%"},"ui":{"control":"slider","tab":{"id":"design","label":"Design"}}},{"key":"orientation","label":"Direction","type":"select","defaultValue":"horizontal","config":{"options":[{"label":"Side by side","value":"horizontal"},{"label":"Top and bottom","value":"vertical"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"showHint","label":"Nudge on first view","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A draggable divider comparing two photos of the same view","category":"content","icon":"arrows-horizontal","preview":{"title":"heading","media":"comparisons.0.after","icon":"arrows-horizontal"}}},
|
|
1410
|
+
{"id":"image-hotspots","name":"Image with hotspots (shop the look)","category":"Media & galleries","kind":"block","tags":["media-heavy","conditional","references","commerce","a11y","validation","variants"],"proves":"Points stored as % coordinates · each pin opens a live product reference OR a note (conditional) · required pin labels","why":"Each hotspot stores its position as PERCENTAGES of the image (x, y 0–100), not pixels, so pins stay put at every screen size — which is why the image's crop must not change after pins are placed. A pin opens either a PRODUCT (a Shopify reference — name, price and stock stay live, never copied) or a NOTE (title/body/link); the `kind` select shows only the relevant fields. Every pin needs a text label because pins are tiny buttons with no visible text. The phone image is optional and shows the hotspots as a list, since pins on a small crop are unusable.","htmlSignals":["A relatively-positioned image wrapper containing absolutely-positioned buttons with `style=\"left: 24%; top: 58%\"`","Small circular buttons (“+”, numbers, pulsing dots) that open popovers with a product name, price and Add-to-cart","Shopify product handles/IDs in `data-product-id`, `data-handle` or links to `/products/…` inside the popovers"],"mcp":"For each positioned pin: parse `left`/`top` (or `inset`, `transform: translate`) as percentages of the wrapper → x/y (convert px using the wrapper's rendered size if needed, round to 0.5). Popover with a product link/handle → kind product + reference (resolve handle → Shopify GID); else kind note with heading → title, text → body, link → link. Label from the pin's `aria-label`, else the product title or note title. Keep the base image at its original crop.","pitfalls":["Pixel positions break on every other screen size — always convert to %.","Copying product price into the note text goes stale; reference the product.","Re-cropping the image after placing pins moves every pin. A visual point picker would prevent this (schema gap)."],"notCarried":["category","shopify reference","variants"],"component":{"name":"Image with hotspots","slug":"image-hotspots","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"image","label":"Image","type":"image","helpText":"Pins are placed in % of this photo. Changing the crop moves them.","required":true,"ui":{"tab":{"id":"media","label":"Media"}}},{"key":"mobileImage","label":"Phone photo","type":"image","helpText":"Pins show as a list on this crop.","advanced":true,"ui":{"tab":{"id":"media"}}},{"key":"hotspots","label":"Hotspots","type":"table","config":{"minItems":1,"maxItems":12,"fields":[{"key":"label","label":"Label","type":"text","helpText":"Not shown. Read by screen readers.","required":true,"localized":true,"config":{"maxLength":48}},{"key":"x","label":"Across","type":"number","helpText":"0 is the left edge, 100 the right.","required":true,"config":{"min":0,"max":100,"step":0.5,"unit":"%"},"ui":{"control":"slider"}},{"key":"y","label":"Down","type":"number","helpText":"0 is the top edge, 100 the bottom.","required":true,"config":{"min":0,"max":100,"step":0.5,"unit":"%"},"ui":{"control":"slider"}},{"key":"kind","label":"Opens","type":"select","defaultValue":"product","options":[{"label":"Product","value":"product"},{"label":"Note","value":"note"}],"ui":{"control":"segmented"}},{"key":"product","label":"Product","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"product"}]},"readOnly":true,"lockReason":"A Shopify product id. No Shopify integration backs a reference in BetterCMS yet, so the id is kept as it is."},{"key":"title","label":"Title","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"note"}]},"localized":true,"config":{"maxLength":48}},{"key":"body","label":"Body","type":"longtext","showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"note"}]},"localized":true,"config":{"maxLength":240},"ui":{"rows":3}},{"key":"link","label":"Link","type":"link","showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"note"}]}},{"key":"cardSide","label":"Card opens","type":"select","defaultValue":"auto","advanced":true,"options":[{"label":"Auto","value":"auto"},{"label":"Left","value":"left"},{"label":"Right","value":"right"},{"label":"Above","value":"top"}],"ui":{"control":"segmented"}}]},"ui":{"itemLabel":"Hotspot","icon":"point","preview":{"title":"label","subtitle":"kind","fallback":"Untitled hotspot"},"tab":{"id":"content"}}},{"key":"pinStyle","label":"Pin style","type":"select","defaultValue":"number","config":{"options":[{"label":"Dot","value":"dot"},{"label":"Plus","value":"plus"},{"label":"Number","value":"number"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"usePulse","label":"Pulse pins","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Pins on a photo that open a product card or a note: shop the look, label the parts","category":"content","icon":"focus-2","preview":{"title":"heading","media":"image","icon":"focus-2"}}},
|
|
1411
|
+
{"id":"interactive-3d-lottie-embed","name":"3D model / Lottie / 360° embed","category":"Media & galleries","kind":"block","tags":["embed","conditional","media-heavy","a11y","validation","variants"],"proves":"Embed kind decides file vs allow-listed URL · required still fallback + accessible name · load-on-click for performance","why":"Three very different embeds share one block because they share the hard parts: a REQUIRED still fallback (loading, slow networks, reduced motion), a REQUIRED accessible name, and a load strategy. `kind` decides what is stored — uploaded files for 3D (GLB + optional USDZ for iOS AR) and Lottie, or an iframe URL for hosted scenes. The URL is pattern-validated against an allow-list of hosts so a marketer can't paste an arbitrary site into an iframe; raw HTML/script embeds are deliberately NOT offered here (that's a developer-only code block).","htmlSignals":["`<model-viewer src=\"….glb\" ios-src=\"….usdz\" camera-orbit=… auto-rotate ar>`","`<lottie-player>`, `<dotlottie-player>`, `lottie.loadAnimation({ path: '….json' })` or `data-animation-path`","Iframes from `my.spline.design`, `sketchfab.com/models/…/embed`, `my.matterport.com/show`, `kuula.co/share`","A canvas element (`<canvas>` from three.js/Spline runtime) with a poster image fallback"],"mcp":"`<model-viewer>` → kind model: src → model (download the GLB), ios-src → iosModel, camera-orbit → cameraOrbit, auto-rotate → useAutoRotate, poster → fallback, alt → label. Lottie players → kind lottie: src/path → lottie file, loop/autoplay/hover/scroll triggers → loop/playOn. Allow-listed iframes → kind iframe + embedUrl; any other iframe host → do NOT import into this block — emit a developer task instead. If no poster exists, capture a screenshot of the first frame as fallback and flag it.","pitfalls":["A 40 MB GLB will tank the page — validate size and recommend Draco/meshopt compression.","Iframe embeds from un-vetted hosts are a security hole — the host allow-list is intentional.","Lottie animations that loop forever must stop for reduced-motion users; the renderer shows the fallback instead."],"notCarried":["category","variants"],"component":{"name":"Interactive embed","slug":"interactive-embed","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"richtext","localized":true,"ui":{"tab":{"id":"content"}}},{"key":"kind","label":"Type","type":"select","defaultValue":"model","config":{"options":[{"label":"3D model","value":"model","icon":"box","description":"A .glb file people can spin and view in AR"},{"label":"Lottie animation","value":"lottie","icon":"sparkles","description":"A .json or .lottie file"},{"label":"Hosted scene","value":"iframe","icon":"world","description":"Spline, Sketchfab, Matterport or a 360° tour"}]},"ui":{"control":"cards","tab":{"id":"media","label":"Media"}}},{"key":"model","label":"Model file","type":"file","helpText":"GLB under 8 MB.","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"model"}]},"ui":{"tab":{"id":"media"}}},{"key":"iosModel","label":"iPhone AR file","type":"file","helpText":"USDZ, so iPhones can place it in the room.","showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"model"}]},"ui":{"tab":{"id":"media"}}},{"key":"cameraOrbit","label":"Starting angle","type":"text","helpText":"Rotation, tilt and distance, e.g. 45deg 60deg 2m.","showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"model"}]},"advanced":true,"placeholder":"30deg 75deg 2m","ui":{"tab":{"id":"media"}}},{"key":"useAutoRotate","label":"Rotate slowly","type":"boolean","defaultValue":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"model"}]},"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"lottie","label":"Animation file","type":"file","helpText":"Lottie .json or .lottie, under 500 KB.","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"lottie"}]},"ui":{"tab":{"id":"media"}}},{"key":"loop","label":"Loop the animation","type":"boolean","defaultValue":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"lottie"}]},"ui":{"tab":{"id":"design"}}},{"key":"playOn","label":"Play","type":"select","defaultValue":"view","showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"lottie"}]},"config":{"options":[{"label":"In view","value":"view"},{"label":"On hover","value":"hover"},{"label":"With scroll","value":"scroll"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"embedUrl","label":"Scene link","type":"url","helpText":"The share or embed link from the host.","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"iframe"}]},"helpMore":"Only allow-listed hosts load. Ask a developer to add another.","config":{"pattern":"^https://(my\\.spline\\.design|prod\\.spline\\.design|sketchfab\\.com|my\\.matterport\\.com|kuula\\.co)/","patternMessage":"Use a Spline, Sketchfab, Matterport or Kuula link, like https://my.spline.design/caldera."},"ui":{"tab":{"id":"media"}}},{"key":"fallback","label":"Still image","type":"image","required":true,"ui":{"tab":{"id":"media"}}},{"key":"label","label":"Accessible name","type":"text","helpText":"Not shown. Read by screen readers.","required":true,"localized":true,"config":{"maxLength":120},"ui":{"tab":{"id":"content"}}},{"key":"aspect","label":"Shape","type":"select","defaultValue":"4:3","config":{"options":[{"label":"Square","value":"1:1"},{"label":"4:3","value":"4:3"},{"label":"16:9","value":"16:9"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"loadOn","label":"Load","type":"select","defaultValue":"click","config":{"options":[{"label":"In view","value":"view"},{"label":"On click","value":"click"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A spinnable 3D model, a Lottie animation or a hosted 360°/Spline scene, with a still fallback","category":"content","icon":"box","preview":{"title":"heading","media":"fallback","icon":"box"}}},
|
|
1412
|
+
{"id":"mixed-media-carousel","name":"Mixed-media carousel","category":"Media & galleries","kind":"block","tags":["polymorphic-array","media-heavy","a11y","responsive-layout","references","conditional","commerce"],"proves":"Polymorphic slides (photo · video · quote · product) · per-breakpoint slides visible · interval shown only with autoplay","why":"Slides are a POLYMORPHIC ARRAY (`imageSlide`, `videoSlide`, `quoteSlide`, `productSlide`) because each kind needs different required fields — a video needs a poster, a product needs a live Shopify reference, a quote needs an author. Using small object types (not a slot of full blocks) keeps slides lightweight and the carousel's rendering predictable. Products are REFERENCES so price/stock stay live. Slides-per-view per breakpoint, autoplay, interval (hidden unless autoplay) and controls are design.","htmlSignals":["Carousel libraries: `swiper-slide`, `splide__slide`, `embla__slide`, `glide__slide`, `keen-slider__slide`, or `scroll-snap-type: x mandatory`","Slides with different internal structures (one `<img>`, one `<video>`, one `<blockquote>`, one product card with price)","`slidesPerView`/`perPage` options in inline JS or `data-*` attributes; prev/next buttons, pagination dots"],"mcp":"Classify each slide by content: `<video>`/video iframe → videoSlide (poster required; extract or generate one), `<blockquote>`/quote marks + name → quoteSlide, price + add-to-cart or `/products/` link → productSlide (resolve to a Shopify GID), otherwise imageSlide. Read the library config (`slidesPerView`, breakpoints, `autoplay.delay`, `loop`) → perViewDesktop/perViewMobile, autoplay + interval (ms → s), loop. Pagination/arrows/progress elements → controls. Ignore cloned slides (`swiper-slide-duplicate`, `aria-hidden` clones).","pitfalls":["Loop mode clones slides in the DOM — import only originals.","Autoplaying carousels need a pause control and must stop on focus (WCAG 2.2.2); the renderer adds both."],"notCarried":["category","shopify reference","variants"],"component":{"name":"Mixed-media carousel","slug":"media-carousel","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content","order":0}}},{"key":"heading","label":"Heading","type":"text","recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"slides","label":"Slides","type":"table","config":{"mode":"types","minItems":2,"maxItems":16,"itemTypes":[{"name":"imageSlide","label":"Photo","fields":[{"key":"image","label":"Image","type":"image","required":true},{"key":"caption","label":"Caption","type":"text","localized":true,"config":{"maxLength":120}},{"key":"link","label":"Link","type":"link"}],"icon":"photo","preview":{"title":"caption","media":"image","fallback":"Untitled photo slide"}},{"name":"videoSlide","label":"Video","fields":[{"key":"video","label":"Video","type":"video","required":true},{"key":"poster","label":"Poster","type":"image","required":true},{"key":"captions","label":"Captions","type":"file","helpText":"WebVTT. Required if the clip has speech."},{"key":"caption","label":"Caption","type":"text","localized":true,"config":{"maxLength":120}},{"key":"muted","label":"Start muted","type":"boolean","defaultValue":true}],"icon":"player-play","preview":{"title":"caption","media":"poster","fallback":"Untitled video slide"}},{"name":"quoteSlide","label":"Quote","fields":[{"key":"quote","label":"Quote","type":"longtext","required":true,"recommendedMax":180,"localized":true,"ui":{"rows":3}},{"key":"author","label":"Name","type":"text","required":true},{"key":"role","label":"Role","type":"text","localized":true},{"key":"photo","label":"Photo","type":"image"},{"key":"background","label":"Background image","type":"image"}],"icon":"quote","preview":{"title":"author","subtitle":"quote","media":"photo","fallback":"Untitled quote slide"}},{"name":"productSlide","label":"Product","fields":[{"key":"product","label":"Product","type":"text","required":true,"readOnly":true,"lockReason":"A Shopify product id. No Shopify integration backs a reference in BetterCMS yet, so the id is kept as it is."},{"key":"image","label":"Image","type":"image","helpText":"Leave empty to use the product photo."},{"key":"badge","label":"Badge","type":"text","placeholder":"New","config":{"maxLength":16}}],"icon":"shopping-bag","preview":{"title":"badge","subtitle":"product","media":"image","fallback":"Untitled product slide"}}]},"ui":{"itemLabel":"Slide","tab":{"id":"content"}}},{"key":"autoplay","label":"Play automatically","type":"boolean","ui":{"tab":{"id":"design","label":"Design","order":2}}},{"key":"interval","label":"Seconds per slide","type":"number","defaultValue":6,"showIf":{"match":"all","rules":[{"field":"autoplay","op":"eq","value":"true"}]},"config":{"min":3,"max":15,"step":1,"unit":"s"},"ui":{"control":"slider","tab":{"id":"design"}}},{"key":"loop","label":"Loop back to the start","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design"}}},{"key":"perViewDesktop","label":"Slides visible on desktop","type":"number","helpText":"Halves show a peek of the next slide.","defaultValue":3,"config":{"min":1,"max":5,"step":0.5},"ui":{"control":"slider","tab":{"id":"design"}}},{"key":"perViewMobile","label":"Slides visible on phones","type":"number","defaultValue":1.2,"config":{"min":1,"max":2,"step":0.1},"ui":{"control":"slider","tab":{"id":"design"}}},{"key":"controls","label":"Controls","type":"multiselect","defaultValue":["arrows","dots"],"config":{"options":[{"label":"Arrows","value":"arrows"},{"label":"Dots","value":"dots"},{"label":"Progress bar","value":"progress"}]},"ui":{"tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Slides of different kinds (photo, video, quote, product) in one swipeable row","category":"content","icon":"carousel-horizontal","preview":{"title":"heading","icon":"carousel-horizontal"},"tabs":[{"id":"media","label":"Media","order":1}]}},
|
|
1413
|
+
{"id":"parallax-image-overlay","name":"Full-bleed parallax image with overlay text","category":"Media & galleries","kind":"block","tags":["media-heavy","a11y","responsive-layout","variants","conditional"],"proves":"Art-directed phone image · decorative flag · 9-position text grid shown only when there is text · parallax strength only for the parallax variant","why":"A full-bleed image is mostly DESIGN with a little content: the photo, an optional art-directed phone crop (a 2:1 landscape becomes a sliver on phones), and optional overlay text. Focal points live on the image values so automatic crops keep the subject. `isDecorative` is explicit because empty alt must be a decision, not an accident. Text position, overlay (with a recommended 35–55% range in the help), height and parallax strength are design; position only shows when there is a heading, and parallax strength only for the parallax variant. The renderer disables parallax for reduced motion and on iOS where `background-attachment: fixed` is unsupported.","htmlSignals":["A full-viewport-width section with `background-image` and `background-attachment: fixed`, or an `<img>` translated on scroll (`data-speed`, `data-parallax`, Rellax, GSAP ScrollTrigger)","A gradient/scrim overlay (`linear-gradient(rgba(0,0,0,.5)…)`, an absolutely positioned dark `div`)","`<picture>` with `<source media=\"(max-width: …)\">` providing a different crop for phones","Little or no text — a heading and maybe one button — positioned in a corner"],"mcp":"Background image or `<img>` → image (largest source); a `<picture><source media>` for small screens → mobileImage. Empty `alt=\"\"` or `role=\"presentation\"` → isDecorative true. Scrim opacity → overlay (gradient vs flat from the background value). Text block alignment (flex justify/alignment) → position. Section height in vh → height (≥95 → screen, ≥75 → tall, else medium). `background-attachment: fixed` → variant fixed; scroll-linked transforms → parallax with intensity from the speed factor; none → static. Photo credit text → credit.","pitfalls":["`background-attachment: fixed` doesn't work on iOS Safari — the renderer falls back to static.","Text over a busy photo fails contrast; the overlay help gives a range and the renderer checks contrast.","A background-image has no alt — converting to an `<img>` with alt (or decorative) is part of the import."],"notCarried":["ai","category","variants"],"component":{"name":"Full-bleed image","slug":"parallax-image","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","recommendedMax":160,"localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"image","label":"Image","type":"image","helpText":"At least 2400px wide.","required":true,"ui":{"tab":{"id":"media","label":"Media"}}},{"key":"mobileImage","label":"Phone photo","type":"image","ui":{"tab":{"id":"media"}}},{"key":"isDecorative","label":"Decorative only","type":"boolean","helpText":"Screen readers skip the photo.","ui":{"tab":{"id":"media"}}},{"key":"credit","label":"Photo credit","type":"text","config":{"maxLength":64},"ui":{"tab":{"id":"media"}}},{"key":"position","label":"Text position","type":"select","defaultValue":"bottom-left","showIf":{"match":"all","rules":[{"field":"heading","op":"notEmpty"}]},"config":{"options":[{"label":"Top left","value":"top-left"},{"label":"Top","value":"top-center"},{"label":"Top right","value":"top-right"},{"label":"Left","value":"middle-left"},{"label":"Center","value":"center"},{"label":"Right","value":"middle-right"},{"label":"Bottom left","value":"bottom-left"},{"label":"Bottom","value":"bottom-center"},{"label":"Bottom right","value":"bottom-right"}]},"ui":{"control":"grid","tab":{"id":"design","label":"Design"}}},{"key":"overlay","label":"Overlay","type":"number","defaultValue":40,"config":{"min":0,"max":80,"step":5,"unit":"%"},"ui":{"control":"slider","tab":{"id":"design"}}},{"key":"overlayStyle","label":"Overlay shape","type":"select","defaultValue":"gradient","config":{"options":[{"label":"Flat","value":"flat"},{"label":"Gradient","value":"gradient"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"height","label":"Height","type":"select","helpText":"Share of the browser window's height.","defaultValue":"tall","config":{"options":[{"label":"60%","value":"medium"},{"label":"80%","value":"tall"},{"label":"Full screen","value":"screen"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"intensity","label":"Parallax strength","type":"number","defaultValue":20,"showIf":{"match":"all","rules":[{"field":"_variant","op":"eq","value":"parallax"}]},"config":{"min":5,"max":40,"step":5,"unit":"%"},"ui":{"control":"slider","tab":{"id":"design"}}}],"description":"An edge-to-edge photo with optional parallax and text over it","category":"content","icon":"photo-scan","preview":{"title":"heading","media":"image","icon":"photo-scan"}}},
|
|
1414
|
+
{"id":"ugc-social-feed","name":"Social photo feed (UGC)","category":"Media & galleries","kind":"block","tags":["dynamic-query","embed","conditional","media-heavy","references","commerce","validation"],"proves":"Connected account vs approved hashtag vs hand-picked · developer-locked connection · rights + permalink required per photo","why":"A social feed is a DYNAMIC source, not content: the connected account is developer-locked (credentials live in Integrations), hashtag mode only shows posts approved in the media library, and manual mode stores hand-picked photos. Picked photos carry a required permalink and a required rights flag because UGC reposting needs the creator's permission. Products shown in a photo are references so “Shop this photo” stays live. We never store an embed script — the renderer fetches through the integration and caches.","htmlSignals":["Instagram widget scripts/markup: `elfsight`, `snapwidget`, `curator.io`, `taggbox`, `behold.so`, `juicer.io`, `instafeed`","A grid of square images linking to `instagram.com/p/…` or `tiktok.com/@…/video/…`","A heading with a hashtag or @handle and a “Follow us” button"],"mcp":"If a feed widget script is present: source account (or hashtag if the widget config names one), copy the handle into the developer task to connect the integration — do NOT copy the widget script. If the grid is static images linking to posts: source manual, each tile → a post with permalink from the link, handle from the alt/caption, hasRights false + flag for review. Products tagged in a tile (shop-the-look overlays) → products references.","pitfalls":["Widget scripts from UGC vendors add 200–500 KB and third-party cookies — replace, don't import.","Never mark rightsGranted true on import; only a person can confirm permission."],"notCarried":["category","shopify reference","variants"],"component":{"name":"Social photo feed","slug":"ugc-feed","props":[{"key":"heading","label":"Heading","type":"text","recommendedMax":48,"localized":true,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"followButton","label":"Follow button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"source","label":"Photos from","type":"select","defaultValue":"account","config":{"options":[{"label":"Our account","value":"account","description":"Newest posts from the connected profile"},{"label":"A hashtag","value":"hashtag","description":"Only posts you approve in Media › Social"},{"label":"Picked photos","value":"manual"}]},"ui":{"control":"radio","tab":{"id":"content"}}},{"key":"account","label":"Account","type":"text","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"account"}]},"readOnly":"developer","lockReason":"Connected in Settings › Integrations.","placeholder":"@calderaoutside","ui":{"access":{"edit":"schema.manage"},"tab":{"id":"content"}}},{"key":"hashtag","label":"Hashtag","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"hashtag"}]},"placeholder":"#cookedoncaldera","config":{"pattern":"^#?[A-Za-z0-9_]+$","patternMessage":"Use letters, numbers and underscores, like utm_source."},"ui":{"tab":{"id":"content"}}},{"key":"limit","label":"How many","type":"number","defaultValue":8,"showIf":{"match":"all","rules":[{"field":"source","op":"in","value":["account","hashtag"]}]},"config":{"min":4,"max":24,"step":4},"ui":{"tab":{"id":"content"}}},{"key":"posts","label":"Photos","type":"table","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"manual"}]},"config":{"minItems":4,"maxItems":24,"fields":[{"key":"image","label":"Image","type":"image","required":true},{"key":"handle","label":"Creator","type":"text","required":true,"placeholder":"@trailkitchen"},{"key":"permalink","label":"Original post","type":"url","required":true},{"key":"hasRights","label":"Creator gave permission","type":"boolean","helpText":"Required before it can be published.","required":true},{"key":"products","label":"Products shown","type":"array","readOnly":true,"lockReason":"A Shopify product id. No Shopify integration backs a reference in BetterCMS yet, so the id is kept as it is.","config":{"itemType":"text"},"ui":{"itemTitle":"Product"}}]},"ui":{"itemLabel":"Photo","layout":"grid","icon":"photo","preview":{"title":"handle","subtitle":"permalink","media":"image","fallback":"Untitled photo"},"tab":{"id":"media","label":"Media"}}},{"key":"columns","label":"Columns on desktop","type":"number","defaultValue":4,"config":{"min":3,"max":6,"step":1},"ui":{"control":"slider","tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A grid of customer photos from a connected Instagram account or hashtag, or picked by hand","category":"content","icon":"brand-instagram","preview":{"title":"heading","icon":"brand-instagram"}}},
|
|
1415
|
+
{"id":"commerce-product-grid-query","name":"Product grid (query)","category":"Commerce","kind":"block","tags":["dynamic-query","commerce","conditional","references","repeater","validation","variants","responsive-layout"],"proves":"A saved query (collection · filters · sort · limit) vs hand-picked products · per-breakpoint columns · card display toggles","why":"A grid is a QUERY, not a list: most merch grids should keep themselves current (new arrivals, best sellers, a material), so the default `source` is a saved query object — collection reference + filter rows + sort + limit — and hand-picking is the conditional alternative. Filters are an array of tiny objects (field · rule · value) so they render inline and stay readable; price and stock are never stored here, the card reads them live through each Products entry's Shopify link. Card toggles and columns are design, so they live in the Design group (columns per breakpoint = responsive-layout).","htmlSignals":["a repeated card list (ul/div grid) where each card has an <img>, a product title link to /products/…, and a price with a currency symbol","a struck-through price (<s>, <del>, .compare-at, .price--sale) next to a second price","'Add to cart' / 'Quick add' buttons or forms posting to /cart/add","a heading plus a 'View all' / 'Shop all' link to /collections/…","Shopify markup: data-product-id, .product-card, .card--product, /cdn/shop/products/ image URLs"],"mcp":"Map the heading/eyebrow/intro text. For each card, resolve the /products/<handle> link to a Products entry (create a stub entry linked to the Shopify product if missing) — never store the card's price, title or image as section values. If the cards share an obvious rule (all one collection URL, 'New', 'Best sellers'), emit source:'query' with the matching filter/sort and limit = number of cards; otherwise source:'manual' with the entry ids in DOM order. Count grid-template-columns / Tailwind grid-cols-* at the md+ breakpoint for columnsDesktop and the base breakpoint for columnsMobile. Carousel markup (overflow-x, swiper, flickity, scroll-snap) → _variant:'carousel'.","pitfalls":["Copying prices into the section: they go stale the moment Shopify changes them.","A query that matches nothing renders an empty section — the renderer hides the block and the editor warns, so don't add placeholder products.","Filter values are untyped strings today (price '200' vs 200); the converter must normalize units.","excludeCurrent only applies inside a product template; on a static page it is a no-op."],"notCarried":["ai","category","variants"],"component":{"name":"Product grid","slug":"product-grid","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":160,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"source","label":"Products come from","type":"select","defaultValue":"query","config":{"options":[{"label":"A query","value":"query","description":"Updates on its own as products change"},{"label":"Picked by hand","value":"manual","description":"A fixed list in your order"}]},"ui":{"control":"segmented","tab":{"id":"products","label":"Products"}}},{"key":"query","label":"Query","type":"object","helpText":"Re-runs on every publish and when stock changes.","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"query"}]},"config":{"fields":[{"key":"collection","label":"Collection","type":"reference","required":true,"config":{"targets":["collection"],"filter":"singular == 'Product'"}},{"key":"filters","label":"Filters","type":"table","helpText":"Every filter must match. Leave empty to show the whole collection.","openMode":"inline","fields":[{"key":"field","label":"Field","type":"select","required":true,"options":[{"label":"Material","value":"material"},{"label":"Tag","value":"tags"},{"label":"Product type","value":"productType"},{"label":"Price","value":"price"},{"label":"Rating","value":"rating"},{"label":"Date added","value":"createdAt"}]},{"key":"operator","label":"Condition","type":"select","defaultValue":"eq","options":[{"label":"Is","value":"eq"},{"label":"Is not","value":"neq"},{"label":"Contains","value":"contains"},{"label":"Is at least","value":"gte"},{"label":"Is at most","value":"lte"},{"label":"In the last N days","value":"within"}]},{"key":"value","label":"Value","type":"text","helpText":"Matches exactly. Prices in dollars, dates in days.","required":true,"config":{"maxLength":48}}],"config":{"maxItems":4},"ui":{"itemLabel":"Filter","icon":"filter","preview":{"title":"field","subtitle":"value","fallback":"Untitled filter"}}},{"key":"sort","label":"Sort by","type":"select","defaultValue":"bestselling","options":[{"label":"Best selling","value":"bestselling","description":"Shopify sales, last 30 days"},{"label":"Newest first","value":"newest"},{"label":"Price, low to high","value":"price-asc"},{"label":"Price, high to low","value":"price-desc"},{"label":"Highest rated","value":"rating"},{"label":"Collection order","value":"collection","description":"The order set in Content › Products"}]},{"key":"limit","label":"How many","type":"number","required":true,"defaultValue":8,"config":{"min":1,"max":24,"step":1,"unit":"products","validation":{"min":1,"max":24}}},{"key":"hideSoldOut","label":"Hide sold-out products","type":"boolean","defaultValue":true},{"key":"excludeCurrent","label":"Leave out this page","type":"boolean","defaultValue":true}]},"ui":{"icon":"filter","tab":{"id":"products"}}},{"key":"products","label":"Products","type":"multi-reference","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"manual"}]},"config":{"min":1,"max":12,"contentModelId":"col_products"},"ui":{"tab":{"id":"products"}}},{"key":"viewAllButton","label":"View all button","type":"button","helpText":"Leave empty to hide it.","ui":{"tab":{"id":"content"}}},{"key":"showPrice","label":"Price","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"},"group":{"id":"card","label":"Card","kind":"group","description":"What each product card shows."}}},{"key":"showRating","label":"Show rating","type":"boolean","helpText":"Hidden for products with fewer than 3 reviews.","defaultValue":true,"ui":{"tab":{"id":"design"},"group":{"id":"card"}}},{"key":"showSwatches","label":"Color swatches","type":"boolean","ui":{"tab":{"id":"design"},"group":{"id":"card"}}},{"key":"showQuickAdd","label":"Show quick add","type":"boolean","ui":{"tab":{"id":"design"},"group":{"id":"card"}}},{"key":"imageRatio","label":"Image shape","type":"select","defaultValue":"4:5","config":{"options":[{"label":"Square","value":"1:1"},{"label":"Portrait","value":"4:5"},{"label":"Landscape","value":"4:3"}]},"ui":{"control":"segmented","tab":{"id":"design"},"group":{"id":"card"}}},{"key":"columnsDesktop","label":"Desktop","type":"number","defaultValue":4,"config":{"min":2,"max":5,"step":1},"ui":{"control":"slider","tab":{"id":"design"},"group":{"id":"columns","label":"Columns","kind":"group"}}},{"key":"columnsMobile","label":"Phone","type":"select","defaultValue":"2","config":{"options":[{"label":"1","value":"1"},{"label":"2","value":"2"}]},"ui":{"control":"segmented","tab":{"id":"design"},"group":{"id":"columns"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Products from a live query or picked by hand","category":"conversion","icon":"grid-dots","preview":{"title":"heading","subtitle":"eyebrow","icon":"layout-grid"}}},
|
|
1416
|
+
{"id":"commerce-product-detail","name":"Product detail page","category":"Commerce","kind":"entry","tags":["commerce","deep-nesting","polymorphic-array","conditional","references","media-heavy","repeater","rich-text","validation","seo-structured","a11y"],"proves":"A Products ENTRY (not a block): Shopify-synced vs native commerce · polymorphic gallery tied to options · options → variants matrix · grouped specs · policy accordions","why":"D27: a product page is a Products ENTRY, so this is `kind: 'entry'`, not a block. The one decision that shapes everything is `source`: when Shopify owns commerce, price/options/variants/stock are hidden and the entry only stores the Shopify GID plus EDITORIAL content (story, gallery, highlights, specs, care, policies); the 'native' path shows the full options → variants matrix for stores without Shopify. Options are an array of objects whose values are themselves objects (label + swatch color/image) because swatches need data, not just strings. The gallery is a polymorphic array (image | video | 3D) and each image can be scoped to option values so picking 'Ember red' swaps the photos. Specs are groups of rows (rendered as a table with th scope) instead of rich text so they can be compared, filtered and emitted in Product JSON-LD `additionalProperty`. Policies default to the store-wide ones (one source of truth) with a per-product override.","htmlSignals":["a single page with one <h1> product name, a price block and a form posting to /cart/add with a hidden variant id","option pickers: radio groups / fieldsets named Size, Color, Finish; swatch buttons with background colors or images","a thumbnail strip + main image (or a <video>/<model-viewer>) — the media gallery","a <table> or <dl> of label/value pairs under 'Specs', 'Specifications', 'Details', 'Dimensions'","accordions/<details> titled Shipping, Returns, Care, Warranty","existing JSON-LD with @type Product and offers"],"mcp":"Create/locate the Products entry by the URL handle; set source:'shopify' and shopifyProduct when Shopify markers exist (window.ShopifyAnalytics, /products/<handle>.js, cdn.shopify.com), and then DO NOT import price, compare-at, options, variants or stock — they sync. Otherwise source:'native' and parse options from the pickers, variants from the variant JSON (often a <script type=application/json> with variants[]). Gallery: each media → galleryImage/galleryVideo/gallery3d, alt from the img alt (never the filename), forValues from data-variant/data-color attributes. Specs: each table/dl → one specGroup (caption or preceding heading → title). Shipping/returns accordions: if the text is identical across products, set useStorePolicies:true and put the text in Settings once; otherwise emit policies. Never create meta/og fields — page meta is native.","pitfalls":["Duplicating Shopify price/stock into the entry — two sources drift within a day.","compare-at must be higher than price; the schema can't express a cross-field rule yet (gap), so the editor validates on save.","Variant combinations are typed strings; renaming an option value orphans gallery `forValues` and variant rows.","Video posters are required: autoplaying video without a poster hurts LCP on the product page."],"notCarried":["ai","category","kind:entry (props)","shopify reference"],"component":{"name":"Product","slug":"product","props":[{"key":"title","label":"Product name","type":"text","helpText":"","required":true,"localized":true,"config":{"maxLength":70},"ui":{"tab":{"id":"content","label":"Story","order":0}}},{"key":"slug","label":"URL","type":"slug","required":true,"ui":{"tab":{"id":"content"}}},{"key":"tagline","label":"Tagline","type":"text","recommendedMax":80,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"description","label":"Description","type":"richtext","localized":true,"ui":{"tab":{"id":"content"}}},{"key":"badges","label":"Badges","type":"multiselect","helpText":"Also shown on product cards everywhere.","config":{"options":[{"label":"New","value":"new"},{"label":"Best seller","value":"bestseller"},{"label":"Limited run","value":"limited"},{"label":"Made in USA","value":"made-usa"}]},"ui":{"control":"checkbox","tab":{"id":"content"}}},{"key":"source","label":"Price and stock come from","type":"select","defaultValue":"shopify","config":{"options":[{"label":"Shopify","value":"shopify","description":"Synced every few minutes, read-only here"},{"label":"BetterCMS","value":"native","description":"You manage price, options and stock here"}]},"ui":{"control":"segmented","tab":{"id":"commerce","label":"Options & price","order":2}}},{"key":"shopifyProduct","label":"Shopify product","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"shopify"}]},"readOnly":true,"lockReason":"A Shopify product id. No Shopify integration backs a reference in BetterCMS yet, so the id is kept as it is.","ui":{"tab":{"id":"commerce"}}},{"key":"price","label":"Price","type":"number","helpText":"A variant can override it.","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"native"}]},"config":{"unit":"USD","min":0,"step":0.01,"validation":{"min":0}},"ui":{"tab":{"id":"commerce"},"group":{"id":"price","label":"Price","kind":"group","columns":2}}},{"key":"compareAt","label":"Compare-at price","type":"number","helpText":"Must be higher than the price.","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"native"}]},"config":{"unit":"USD","min":0,"step":0.01},"ui":{"tab":{"id":"commerce"},"group":{"id":"price"}}},{"key":"options","label":"Options","type":"table","helpText":"Up to 3. Each combination becomes a variant.","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"native"}]},"config":{"maxItems":3,"fields":[{"key":"name","label":"Option name","type":"text","required":true,"placeholder":"Color","config":{"maxLength":20}},{"key":"display","label":"Show as","type":"select","defaultValue":"buttons","options":[{"label":"Buttons","value":"buttons"},{"label":"Swatches","value":"swatches"},{"label":"Dropdown","value":"dropdown"}],"ui":{"control":"segmented"}},{"key":"values","label":"Values","type":"table","fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":24}},{"key":"swatch","label":"Swatch color","type":"color"},{"key":"image","label":"Image","type":"image"}],"config":{"minItems":1,"maxItems":12},"ui":{"itemLabel":"Value","itemName":"optionValue","icon":"point","preview":{"title":"label","media":"image","fallback":"Untitled value"}}}]},"ui":{"itemLabel":"Option","icon":"adjustments","preview":{"title":"name","fallback":"Untitled option"},"tab":{"id":"commerce"}}},{"key":"variants","label":"Variants","type":"table","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"native"}]},"helpMore":"Option values exactly as above, joined with \" / \", like \"5.5 qt / Ember red\".","config":{"maxItems":100,"fields":[{"key":"combination","label":"Combination","type":"text","required":true,"placeholder":"5.5 qt / Ember red","config":{"maxLength":60}},{"key":"sku","label":"SKU","type":"text","required":true,"unique":true,"config":{"pattern":"^[A-Z0-9-]{3,24}$","patternMessage":"Use capital letters, numbers and hyphens, like CF-12."}},{"key":"price","label":"Price override","type":"number","helpText":"Leave empty to use the product price.","config":{"unit":"USD","min":0,"step":0.01}},{"key":"inventory","label":"In stock","type":"number","config":{"min":0,"step":1,"unit":"units"}},{"key":"weight","label":"Shipping weight","type":"number","config":{"min":0,"step":0.1,"unit":"lb"}}]},"ui":{"itemLabel":"Variant","icon":"box","preview":{"title":"combination","subtitle":"sku","fallback":"Untitled variant"},"tab":{"id":"commerce"}}},{"key":"inventoryPolicy","label":"When it sells out","type":"select","defaultValue":"deny","config":{"options":[{"label":"Stop selling","value":"deny"},{"label":"Take pre-orders","value":"preorder","description":"Shows the restock date below"},{"label":"Collect back-in-stock emails","value":"notify"}]},"ui":{"tab":{"id":"commerce"},"group":{"id":"stock","label":"Stock","kind":"group","columns":2}}},{"key":"restockDate","label":"Back in stock","type":"date","showIf":{"match":"all","rules":[{"field":"inventoryPolicy","op":"eq","value":"preorder"}]},"ui":{"tab":{"id":"commerce"},"group":{"id":"stock"}}},{"key":"lowStockAt","label":"Low-stock warning at","type":"number","helpText":"0 turns it off.","config":{"min":0,"max":50,"step":1,"unit":"units"},"ui":{"tab":{"id":"commerce"}}},{"key":"gallery","label":"Gallery","type":"table","helpText":"The first item is the main image on cards and in shared links.","config":{"mode":"types","minItems":1,"maxItems":16,"itemTypes":[{"name":"galleryImage","label":"Image","fields":[{"key":"image","label":"Image","type":"image","required":true},{"key":"forValues","label":"Show for","type":"tags","helpText":"Leave empty for every variant."}],"icon":"photo","preview":{"title":"image.altText","media":"image","fallback":"Untitled image"}},{"name":"galleryVideo","label":"Video","fields":[{"key":"video","label":"Video","type":"video","required":true},{"key":"poster","label":"Poster","type":"image","required":true},{"key":"caption","label":"Caption","type":"text","helpText":"Also the video's accessible name.","config":{"maxLength":80}}],"icon":"player-play","preview":{"title":"caption","media":"poster","fallback":"Untitled video"}},{"name":"gallery3d","label":"3D model","fields":[{"key":"model","label":"Model file","type":"file","helpText":"A .glb file under 15 MB.","required":true},{"key":"poster","label":"Poster","type":"image","required":true},{"key":"caption","label":"Caption","type":"text","config":{"maxLength":80}}],"icon":"box","preview":{"title":"caption","media":"poster","fallback":"Untitled 3D model"}}]},"ui":{"itemLabel":"Media","layout":"grid","tab":{"id":"media","label":"Gallery","order":1}}},{"key":"highlights","label":"Highlights","type":"table","openMode":"inline","config":{"maxItems":4,"fields":[{"key":"icon","label":"Icon","type":"icon"},{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":40}}]},"ui":{"itemLabel":"Highlight","icon":"sparkles","preview":{"title":"label","fallback":"Untitled highlight"},"tab":{"id":"details","label":"Specs & care","order":3}}},{"key":"specs","label":"Specs","type":"table","config":{"maxItems":6,"fields":[{"key":"title","label":"Group title","type":"text","required":true,"placeholder":"Dimensions","config":{"maxLength":32}},{"key":"rows","label":"Rows","type":"table","openMode":"inline","fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":32}},{"key":"value","label":"Value","type":"text","required":true,"config":{"maxLength":60}}],"config":{"minItems":1,"maxItems":12},"ui":{"itemLabel":"Row","itemName":"specRow","icon":"table","preview":{"title":"label","subtitle":"value","fallback":"Untitled row"}}}]},"ui":{"itemLabel":"Spec group","itemName":"specGroup","icon":"list-details","preview":{"title":"title","fallback":"Untitled group"},"tab":{"id":"details"}}},{"key":"care","label":"Care","type":"richtext","ui":{"tab":{"id":"details"}}},{"key":"useStorePolicies","label":"Use the store's shipping and returns","type":"boolean","helpText":"Turn off to write product-specific ones.","defaultValue":true,"ui":{"tab":{"id":"policies","label":"Shipping & returns","order":4}}},{"key":"policies","label":"Product policies","type":"table","showIf":{"match":"all","rules":[{"field":"useStorePolicies","op":"eq","value":"false"}]},"config":{"maxItems":4,"fields":[{"key":"title","label":"Title","type":"text","required":true,"config":{"maxLength":40}},{"key":"summary","label":"Summary","type":"text","placeholder":"Free over $80","config":{"maxLength":80}},{"key":"body","label":"Body","type":"richtext","required":true}]},"ui":{"itemLabel":"Policy","icon":"truck-delivery","preview":{"title":"title","subtitle":"summary","fallback":"Untitled policy"},"tab":{"id":"policies"}}},{"key":"related","label":"Pairs well with","type":"multi-reference","unique":true,"config":{"max":4,"contentModelId":"col_products"},"ui":{"itemName":"relatedProduct","itemTitle":"Product","tab":{"id":"details"}}},{"key":"reviewsId","label":"Reviews product ID","type":"text","helpText":"The product's ID in Okendo.","advanced":true,"readOnly":"developer","lockReason":"Set by the reviews app integration","ui":{"access":{"edit":"schema.manage"},"tab":{"id":"details"}}}],"description":"One product page: story, gallery, options, specs and policies","category":"conversion","icon":"package","preview":{"title":"title","subtitle":"tagline","media":"gallery.0.image"}}},
|
|
1417
|
+
{"id":"commerce-featured-product","name":"Featured product spotlight","category":"Commerce","kind":"block","tags":["references","commerce","variants","conditional","media-heavy","repeater"],"proves":"One product reference + per-placement overrides (headline, image) that fall back to the entry · inline add-to-cart vs link","why":"The spotlight is a REFERENCE plus optional overrides: name, price, rating, image and stock all come from the Products entry, and the section only stores what is placement-specific (headline, story, image override, selling points). Every override says 'leave empty to use…' so the fallback is explicit. The action is a conditional pair: 'Add to cart' needs only a label (the renderer adds the variant picker and swaps to Pre-order/Sold out itself); 'Open the product' is a normal button unit.","htmlSignals":["a two-column section with one large product image and one product name + price + add-to-cart/'Shop now'","a single /products/<handle> link or a single add-to-cart form in the section","short check-icon bullet list beside the image","an eyebrow like 'New', 'Featured', 'Product of the month'"],"mcp":"Resolve the single product link to a Products entry → product. Compare the visible name/description/image with the entry: only emit headline/body/image when they DIFFER (that's an override); otherwise leave them empty. A form posting to /cart/add → action:'add' (+ addLabel from the submit text); a plain link → action:'link' with a button unit. Image position decides the variant (image first in DOM / on the left → image-left).","pitfalls":["Copying the product name into `heading` freezes it; renames in Shopify won't flow through.","A deleted or archived product leaves the section empty — the renderer hides it and the editor flags the broken reference."],"notCarried":["ai","category","variants"],"component":{"name":"Featured product","slug":"featured-product","props":[{"key":"product","label":"Product","type":"reference","helpText":"Name, price, rating and stock come from this product.","config":{"contentModelId":"col_products"},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"eyebrow","label":"Eyebrow","type":"text","placeholder":"New for fall","config":{"maxLength":32},"ui":{"tab":{"id":"content"}}},{"key":"heading","label":"Heading","type":"text","helpText":"Leave empty to use the product name.","recommendedMax":70,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","helpText":"Leave empty to use the product tagline.","recommendedMax":280,"localized":true,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"points","label":"Selling points","type":"table","openMode":"inline","config":{"maxItems":4,"fields":[{"key":"icon","label":"Icon","type":"icon"},{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":48}}]},"ui":{"itemLabel":"Point","icon":"check","preview":{"title":"label","fallback":"Untitled point"},"tab":{"id":"content"}}},{"key":"image","label":"Image","type":"image","helpText":"Leave empty to use the product's first gallery image.","ui":{"tab":{"id":"content"}}},{"key":"action","label":"Button does","type":"select","defaultValue":"add","config":{"options":[{"label":"Add to cart","value":"add","description":"Variant picker + add button in place"},{"label":"Open the product","value":"link"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"addLabel","label":"Add button label","type":"text","helpText":"Changes to Pre-order or Sold out on its own.","showIf":{"match":"all","rules":[{"field":"action","op":"eq","value":"add"}]},"placeholder":"Add to cart","config":{"maxLength":24},"ui":{"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","showIf":{"match":"all","rules":[{"field":"action","op":"eq","value":"link"}]},"ui":{"tab":{"id":"content"}}},{"key":"showPrice","label":"Show price","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"showRating","label":"Show rating","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"One product, told as a story, with a buy button","category":"conversion","icon":"star","preview":{"title":"heading","subtitle":"eyebrow","media":"image"}}},
|
|
1418
|
+
{"id":"commerce-collection-header-facets","name":"Collection header + filters","category":"Commerce","kind":"block","tags":["commerce","dynamic-query","references","repeater","conditional","validation","media-heavy"],"proves":"Collection reference with header overrides · facet config array (source × display) with conditional swatch/range settings · sort options","why":"The header is presentation over a collection reference (title/intro fall back to the collection), and the filter sidebar is CONFIGURATION, not content: an array of facet objects, each saying what it filters on (option, field, tag, price, availability) and how it shows (checkbox, swatch, pills, range). Display-specific settings are conditional siblings — swatch colors only for swatches, step only for ranges — so a facet stays a short form. Facet values themselves are never listed: they're computed from the products, so new colors appear without an edit.","htmlSignals":["a page title + description followed by a filter sidebar/drawer with grouped checkbox lists, color swatches and a price slider","a 'Sort by' <select> with options like Featured, Best selling, Price low to high","URL params like ?filter.v.option.color= or ?sort_by= (Shopify), ?min_price= (WooCommerce)","a 'n products' count and pagination/'Load more' below the grid"],"mcp":"Header text → title (only if it differs from the collection name) / description; hero image → banner. For each filter group: label = group heading; infer source from the param name (filter.v.option.* → option + key, filter.p.m.* metafield → field, filter.v.price → price + range, filter.v.availability → availability). Swatch groups → display:'swatch' and copy each swatch's background color into swatches[]. Do NOT import the filter VALUES — they are computed from products. Sort <select> options → sortOptions (map labels to the enum), selected option → defaultSort.","pitfalls":["defaultSort must be one of sortOptions; select options can't depend on another field yet (gap), so it's validated on save.","Importing facet values as content makes new colors invisible until someone edits the page."],"notCarried":["ai","category","variants"],"component":{"name":"Collection header","slug":"collection-header","props":[{"key":"collection","label":"Collection","type":"reference","required":true,"config":{"targets":["collection"]},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"title","label":"Title override","type":"text","helpText":"Leave empty to use the collection name.","localized":true,"config":{"maxLength":60},"ui":{"tab":{"id":"content"}}},{"key":"description","label":"Intro","type":"longtext","recommendedMax":200,"localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"banner","label":"Banner","type":"image","helpText":"Shown at 1600×500.","ui":{"tab":{"id":"content"}}},{"key":"facets","label":"Filters","type":"table","helpText":"Filters with one value hide themselves.","config":{"maxItems":8,"fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":24}},{"key":"source","label":"Filters on","type":"select","required":true,"options":[{"label":"A product option","value":"option","description":"Size, Color, Finish"},{"label":"A product field","value":"field","description":"Material, Capacity"},{"label":"Tags","value":"tag"},{"label":"Price","value":"price"},{"label":"In stock","value":"availability"}]},{"key":"key","label":"Option or field name","type":"text","helpText":"Exactly as named on the products, e.g. Finish.","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"in","value":["option","field"]}]},"config":{"maxLength":32}},{"key":"display","label":"Show as","type":"select","defaultValue":"checkbox","options":[{"label":"Checkboxes","value":"checkbox"},{"label":"Swatches","value":"swatch"},{"label":"Pills","value":"pills"},{"label":"Range","value":"range"}],"ui":{"control":"segmented"}},{"key":"rangeStep","label":"Range step","type":"number","showIf":{"match":"all","rules":[{"field":"display","op":"eq","value":"range"}]},"config":{"min":1,"max":100,"step":1,"unit":"USD"}},{"key":"swatches","label":"Swatch colors","type":"table","helpText":"Values without a color show as a text pill.","showIf":{"match":"all","rules":[{"field":"display","op":"eq","value":"swatch"}]},"openMode":"inline","fields":[{"key":"value","label":"Value","type":"text","required":true,"config":{"maxLength":24}},{"key":"color","label":"Color","type":"color","required":true}],"config":{"maxItems":16},"ui":{"itemLabel":"Swatch","itemName":"swatch","icon":"palette","preview":{"title":"value","fallback":"Untitled swatch"}}},{"key":"isOpen","label":"Open by default","type":"boolean"},{"key":"showCounts","label":"Show counts","type":"boolean","defaultValue":true}]},"ui":{"itemLabel":"Filter","icon":"adjustments-horizontal","preview":{"title":"label","subtitle":"source","fallback":"Untitled filter"},"tab":{"id":"filters","label":"Filters & sort"}}},{"key":"sortOptions","label":"Sort options","type":"multiselect","config":{"min":1,"options":[{"label":"Featured","value":"featured"},{"label":"Best selling","value":"bestselling"},{"label":"Newest","value":"newest"},{"label":"Price, low to high","value":"price-asc"},{"label":"Price, high to low","value":"price-desc"},{"label":"Highest rated","value":"rating"}]},"ui":{"control":"checkbox","tab":{"id":"filters"}}},{"key":"defaultSort","label":"Default sort","type":"select","helpText":"Must be one of the sort options above.","defaultValue":"featured","config":{"options":[{"label":"Featured","value":"featured"},{"label":"Best selling","value":"bestselling"},{"label":"Newest","value":"newest"}]},"ui":{"tab":{"id":"filters"}}},{"key":"perPage","label":"Products per page","type":"number","defaultValue":24,"config":{"min":12,"max":48,"step":12},"ui":{"control":"slider","tab":{"id":"filters"}}},{"key":"showCount","label":"Show result count","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"filters"}}}],"description":"The top of a shop page: title, banner, filters and sort","category":"conversion","icon":"category-2","preview":{"title":"title","subtitle":"description","media":"banner"}}},
|
|
1419
|
+
{"id":"commerce-bundle-builder","name":"Bundle builder (choose N)","category":"Commerce","kind":"block","tags":["commerce","conditional","references","repeater","validation"],"proves":"Eligible products (query or picked) · pick min/max · tiered discount OR fixed bundle price as a conditional pair · tier rows with validation","why":"A bundle is RULES around a product set: the eligible list is references (price and stock stay live), the pick limits are two numbers side by side, and the savings model is a conditional pair — tiered steps (an inline array of quantity/percent/label) or one fixed bundle price — because those two are the deals real stores run. The actual checkout discount lives in Shopify; the section shows its id developer-locked so marketers can see what enforces the promise without breaking it.","htmlSignals":["copy like 'Pick 3', 'Build your bundle', 'Mix & match', 'Save x% when you buy n'","product cards with Add/Remove toggles and a running selection tray or progress bar (1 of 3 selected)","a discount ladder ('2 items: 10% · 3 items: 15%')","a single 'Add bundle to cart' button that stays disabled until enough items are chosen"],"mcp":"Resolve each selectable card to a Products entry → eligible[]. Parse the deal copy: 'n for $X' → pricing:'fixed' + bundlePrice + max=n; ladder copy → pricing:'tiers' with one row per step. min = the first step's quantity (or the disabled-until count). Never invent a discount code — leave discountCode empty for a developer to wire.","pitfalls":["min ≤ max and ascending tier quantities can't be enforced by the schema yet (no cross-field rules).","The page promise and the Shopify discount can disagree; the converter must flag bundles without a discountCode."],"notCarried":["ai","category"],"component":{"name":"Bundle builder","slug":"bundle-builder","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","recommendedMax":160,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"eligibleProducts","label":"Products to choose from","type":"multi-reference","unique":true,"config":{"min":2,"max":12,"contentModelId":"col_products"},"ui":{"itemTitle":"Product","tab":{"id":"content"}}},{"key":"min","label":"At least","type":"number","required":true,"defaultValue":2,"config":{"min":1,"max":10,"step":1,"unit":"items"},"ui":{"tab":{"id":"rules","label":"Rules & savings"},"group":{"id":"pick","label":"How many","kind":"group","columns":2}}},{"key":"max","label":"At most","type":"number","helpText":"Must be at least the minimum.","required":true,"defaultValue":4,"config":{"min":1,"max":12,"step":1,"unit":"items"},"ui":{"tab":{"id":"rules"},"group":{"id":"pick"}}},{"key":"allowRepeats","label":"Allow the same product twice","type":"boolean","ui":{"tab":{"id":"rules"}}},{"key":"pricing","label":"Savings","type":"select","defaultValue":"tiers","config":{"options":[{"label":"Save more as they add","value":"tiers","icon":"stairs-up","description":"e.g. 10% for 2, 15% for 3"},{"label":"One bundle price","value":"fixed","icon":"tag","description":"e.g. any 3 for $199"}]},"ui":{"control":"cards","tab":{"id":"rules"}}},{"key":"tiers","label":"Discount steps","type":"table","helpText":"The highest step reached applies.","showIf":{"match":"all","rules":[{"field":"pricing","op":"eq","value":"tiers"}]},"openMode":"inline","config":{"minItems":1,"maxItems":4,"fields":[{"key":"quantity","label":"Items","type":"number","required":true,"config":{"min":1,"max":12,"step":1}},{"key":"percent","label":"Off","type":"number","required":true,"config":{"min":1,"max":50,"step":1,"unit":"%"}},{"key":"label","label":"Label","type":"text","placeholder":"Save 15%","config":{"maxLength":24}}]},"ui":{"itemLabel":"Step","icon":"discount","preview":{"title":"label","subtitle":"quantity","fallback":"Untitled step"},"tab":{"id":"rules"}}},{"key":"bundlePrice","label":"Bundle price","type":"number","helpText":"Charged for exactly the maximum number of items.","required":true,"showIf":{"match":"all","rules":[{"field":"pricing","op":"eq","value":"fixed"}]},"config":{"unit":"USD","min":0,"step":1},"ui":{"tab":{"id":"rules"}}},{"key":"discountCode","label":"Shopify discount","type":"text","helpText":"The Shopify automatic discount that applies these savings.","readOnly":"developer","lockReason":"Created by the bundle app in Shopify","ui":{"access":{"edit":"schema.manage"},"tab":{"id":"rules"}}},{"key":"addLabel","label":"Add bundle button","type":"text","required":true,"placeholder":"Add kit to cart","config":{"maxLength":28},"ui":{"tab":{"id":"content"}}},{"key":"note","label":"Small print","type":"text","config":{"maxLength":120},"ui":{"tab":{"id":"content"}}}],"description":"Shoppers pick a set of products and save as they add more","category":"conversion","icon":"packages","preview":{"title":"heading","subtitle":"eyebrow"}}},
|
|
1420
|
+
{"id":"commerce-cart-cross-sell","name":"Cart upsell & cross-sell","category":"Commerce","kind":"block","tags":["commerce","references","conditional","deep-nesting","personalization","repeater","validation"],"proves":"Rules array: when (condition type) → then (offer references) with condition-specific inputs · fallback list · placement","why":"Cart offers are RULES, not a product list: an ordered array where each rule is `when` (one select) → condition-specific input (product refs, a tag, or an amount, each a conditional sibling) → `offer` (a small drill-in object of product refs, a message with a {{remaining}} token, and an optional discount). First match wins, which is how merchandisers reason about it, and a fallbackOffers list covers the rest. Rules can be paused with `isActive` so experiments don't destroy setup.","htmlSignals":["inside a cart drawer/page (form action=/cart, .cart-drawer): a heading like 'You may also like', 'Complete the set', 'Goes well with'","a small horizontal product list with '+ Add' buttons inside the cart","free-shipping progress copy: 'You're $12 away from free shipping'","data attributes from upsell apps (Rebuy, ReConvert, Shopify product recommendations intent=complementary)"],"mcp":"Static cross-sell markup only shows ONE state, so emit a single fallback list from the visible products and, if a free-shipping bar exists, one 'subtotal-below' rule with the threshold parsed from the copy. If an app config is reachable (Rebuy JSON, recommendations endpoint intent=complementary), translate its rules into rules[]. Never fabricate rules from a single screenshot; leave rules empty and set fallback.","pitfalls":["Rule order matters (first match wins); the converter must preserve source order.","percentOff needs a matching Shopify discount — otherwise the price promised in the cart won't apply at checkout.","A rule's trigger can't reference products already in the offer set; 'hideInCart' handles it at render."],"notCarried":["category"],"component":{"name":"Cart offers","slug":"cart-offers","props":[{"key":"heading","label":"Heading","type":"text","required":true,"localized":true,"placeholder":"Goes well with","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"placements","label":"Shows in","type":"multiselect","config":{"min":1,"options":[{"label":"Cart drawer","value":"drawer"},{"label":"Cart page","value":"page"}]},"ui":{"control":"checkbox","tab":{"id":"content"}}},{"key":"maxShown","label":"Show at most","type":"number","defaultValue":3,"config":{"min":1,"max":4,"step":1,"unit":"products"},"ui":{"control":"slider","tab":{"id":"content"}}},{"key":"hideInCart","label":"Hide products already in the cart","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"content"}}},{"key":"rules","label":"Rules","type":"table","helpText":"Checked top to bottom. The first match decides the offers.","config":{"maxItems":10,"fields":[{"key":"name","label":"Rule name","type":"text","helpText":"Only your team sees this.","required":true,"config":{"maxLength":48}},{"key":"when","label":"When the cart","type":"select","required":true,"options":[{"label":"contains a product","value":"contains-product"},{"label":"contains a product tagged","value":"contains-tag"},{"label":"is under an amount","value":"subtotal-below","description":"Good for free-shipping nudges"}]},{"key":"triggerProducts","label":"Any of these products","type":"multi-reference","showIf":{"match":"all","rules":[{"field":"when","op":"eq","value":"contains-product"}]},"config":{"min":1,"max":6,"contentModelId":"col_products"},"ui":{"itemTitle":"Product"}},{"key":"triggerTag","label":"Tag","type":"text","helpText":"Exactly as written on the products.","required":true,"showIf":{"match":"all","rules":[{"field":"when","op":"eq","value":"contains-tag"}]},"config":{"maxLength":32}},{"key":"threshold","label":"Amount","type":"number","helpText":"The offer shows while the subtotal is below this.","required":true,"showIf":{"match":"all","rules":[{"field":"when","op":"eq","value":"subtotal-below"}]},"config":{"unit":"USD","min":1,"step":1}},{"key":"offer","label":"Then suggest","type":"object","openMode":"drill","fields":[{"key":"products","label":"Products","type":"multi-reference","config":{"min":1,"max":4,"contentModelId":"col_products"},"ui":{"itemName":"offerProduct"}},{"key":"message","label":"Message","type":"text","helpText":"{{remaining}} becomes the amount left.","placeholder":"You're {{remaining}} from free shipping","config":{"maxLength":80}},{"key":"percentOff","label":"Discount on these","type":"number","helpText":"0 = full price.","config":{"min":0,"max":30,"step":5,"unit":"%"}}],"ui":{"icon":"gift"}},{"key":"isActive","label":"Active","type":"boolean","defaultValue":true}]},"ui":{"itemLabel":"Rule","icon":"arrow-guide","preview":{"title":"name","subtitle":"when","fallback":"Untitled rule"},"tab":{"id":"rules","label":"Rules"}}},{"key":"fallbackOffers","label":"When no rule matches","type":"multi-reference","helpText":"Leave empty to show nothing.","config":{"max":4,"contentModelId":"col_products"},"ui":{"itemName":"fallbackProduct","itemTitle":"Product","tab":{"id":"rules"}}}],"description":"Suggestions in the cart, chosen by what's already in it","category":"conversion","icon":"shopping-cart-plus","preview":{"title":"heading","subtitle":"placements"}}},
|
|
1421
|
+
{"id":"commerce-promo-countdown","name":"Promo banner with code + countdown","category":"Commerce","kind":"block","tags":["commerce","conditional","validation","personalization","variants"],"proves":"Discount code with pattern validation · fixed-deadline vs per-visitor countdown (conditional) · what happens after it ends · schedule + audience","why":"Three ideas, each a conditional cluster: the CODE (pattern-validated string, plus 'apply for them' only when a code exists — `notEmpty` condition), the COUNTDOWN (none / a real deadline / a per-visitor evergreen timer, each showing only its own input), and what happens AFTER it ends (hide or swap the message). Schedule and audience are separate from the countdown on purpose: the banner can be visible before the timer starts. The evergreen help text keeps it honest — fake urgency is a dark pattern.","htmlSignals":["a thin full-width bar above the header with sale copy and a code in <code>/<strong>/a 'Copy' button","a ticking timer: elements for days/hours/minutes/seconds, data-countdown / data-end attributes or a script with a target timestamp","a close (×) button with aria-label='Close announcement'","a link that applies a discount: /discount/<CODE>?redirect=…"],"mcp":"Copy text → message; the code → code (upper-case it); a /discount/<CODE> link → isAutoApplied: true. A timer with a fixed timestamp in markup/script → countdown:'deadline' + endsAt (keep the offset); a timer seeded from localStorage/cookie → 'evergreen' + durationHours. Position above the nav → _variant:'bar', otherwise 'band'. Close button → isDismissible: true.","pitfalls":["Time zones: an ISO string without an offset ends the sale at the wrong hour for half the country.","hideAfter earlier than endsAt can't be caught by the schema (no cross-field rules).","Evergreen timers that reset on refresh are deceptive; the renderer persists the start per visitor."],"notCarried":["ai","category","variants"],"component":{"name":"Promo banner","slug":"promo-banner","props":[{"key":"message","label":"Message","type":"text","required":true,"recommendedMax":60,"localized":true,"config":{"maxLength":90},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"code","label":"Discount code","type":"text","helpText":"Must already exist in Shopify.","config":{"pattern":"^[A-Z0-9]{3,20}$","patternMessage":"Use 3–20 capital letters and numbers, like SAVE20."},"ui":{"tab":{"id":"content"}}},{"key":"isAutoApplied","label":"Apply the code for them","type":"boolean","showIf":{"match":"all","rules":[{"field":"code","op":"notEmpty"}]},"ui":{"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"countdown","label":"Countdown","type":"select","defaultValue":"none","config":{"options":[{"label":"None","value":"none"},{"label":"To a date","value":"deadline","description":"Everyone sees the same end time"},{"label":"Per visitor","value":"evergreen","description":"Starts on their first visit"}]},"ui":{"control":"segmented","tab":{"id":"timing","label":"Timing"}}},{"key":"endsAt","label":"Ends","type":"datetime","required":true,"showIf":{"match":"all","rules":[{"field":"countdown","op":"eq","value":"deadline"}]},"ui":{"tab":{"id":"timing"}}},{"key":"durationHours","label":"Counts down from","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"countdown","op":"eq","value":"evergreen"}]},"config":{"min":1,"max":72,"step":1,"unit":"hours"},"ui":{"tab":{"id":"timing"}}},{"key":"afterEnd","label":"When the timer ends","type":"select","defaultValue":"hide","showIf":{"match":"all","rules":[{"field":"countdown","op":"in","value":["deadline","evergreen"]}]},"config":{"options":[{"label":"Hide the banner","value":"hide"},{"label":"Show a different message","value":"message"}]},"ui":{"tab":{"id":"timing"}}},{"key":"endedMessage","label":"Message after it ends","type":"text","showIf":{"match":"all","rules":[{"field":"afterEnd","op":"eq","value":"message"}]},"placeholder":"The sale's over. Free shipping still applies over $80.","config":{"maxLength":90},"ui":{"tab":{"id":"timing"}}},{"key":"startsAt","label":"Show from","type":"datetime","ui":{"tab":{"id":"timing"},"group":{"id":"schedule","label":"Schedule","kind":"group"}}},{"key":"hideAfter","label":"Hide after","type":"datetime","helpText":"Leave empty to keep it until you remove it.","ui":{"tab":{"id":"timing"},"group":{"id":"schedule"}}},{"key":"audience","label":"Who sees it","type":"select","defaultValue":"everyone","config":{"options":[{"label":"Everyone","value":"everyone"},{"label":"First-time visitors","value":"new"},{"label":"Returning visitors","value":"returning"},{"label":"Visitors from a campaign link","value":"utm"}]},"ui":{"tab":{"id":"timing"}}},{"key":"utmCampaign","label":"Campaign","type":"text","helpText":"Matches utm_campaign in the link.","required":true,"showIf":{"match":"all","rules":[{"field":"audience","op":"eq","value":"utm"}]},"config":{"maxLength":48},"ui":{"tab":{"id":"timing"}}},{"key":"isDismissible","label":"Visitors can close it","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A sale message with a code to copy and a countdown","category":"conversion","icon":"discount","preview":{"title":"message","subtitle":"code","icon":"discount"}}},
|
|
1422
|
+
{"id":"commerce-shop-the-look","name":"Shop the look (hotspots)","category":"Commerce","kind":"block","tags":["media-heavy","references","deep-nesting","repeater","commerce","a11y","validation"],"proves":"Looks › look › hotspots › hotspot: image coordinates as numbers + a product reference per point · accessible labels for every dot","why":"Four levels: section › looks › look › hotspots › hotspot. A hotspot is a product REFERENCE plus two percentage coordinates (so a re-crop or responsive resize keeps the dot on the object) plus a REQUIRED screen-reader label — a dot is a button with no visible text, so the label is not optional. `showProductList` guarantees the products are reachable without precise tapping on a phone.","htmlSignals":["an image wrapper with absolutely positioned buttons/dots (style='left:42%;top:58%')","popovers/tooltips that show a product name, price and 'Shop' link on dot click","a 'Shop the look' / 'Get the look' heading with a product list beside the image","data attributes like data-hotspot, data-x/data-y, .lookbook__dot"],"mcp":"Each image with positioned dots → one look. For each dot: read left/top percentages (convert px to % using the image's natural size), resolve the popover's product link to a Products entry, and write label from the dot's aria-label or the product name + where it is. Never drop a dot because it lacks an aria-label — generate one and flag it for review.","pitfalls":["Pixel coordinates break on resize; always store percentages.","Coordinates are raw numbers today — there's no visual point picker bound to the image yet (gap).","Dots without labels fail WCAG 4.1.2; the label is required for that reason."],"notCarried":["ai","category","variants"],"component":{"name":"Shop the look","slug":"shop-the-look","props":[{"key":"heading","label":"Heading","type":"text","recommendedMax":48},{"key":"looks","label":"Looks","type":"table","config":{"minItems":1,"maxItems":6,"fields":[{"key":"title","label":"Look name","type":"text","config":{"maxLength":40}},{"key":"image","label":"Image","type":"image","required":true},{"key":"hotspots","label":"Dots","type":"table","fields":[{"key":"product","label":"Product","type":"reference","config":{"contentModelId":"col_products"}},{"key":"label","label":"Label for screen readers","type":"text","helpText":"Not shown. Read by screen readers.","required":true,"config":{"maxLength":60}},{"key":"x","label":"Across","type":"number","helpText":"From the left edge of the photo.","required":true,"config":{"min":0,"max":100,"step":0.5,"unit":"%"},"ui":{"control":"slider"}},{"key":"y","label":"Down","type":"number","helpText":"From the top edge of the photo.","required":true,"config":{"min":0,"max":100,"step":0.5,"unit":"%"},"ui":{"control":"slider"}},{"key":"variant","label":"Variant","type":"text","helpText":"Leave empty for the default.","config":{"maxLength":40}}],"config":{"minItems":1,"maxItems":8},"ui":{"itemLabel":"Dot","icon":"point","preview":{"title":"label","subtitle":"product","fallback":"Untitled dot"}}}]},"ui":{"itemLabel":"Look","layout":"grid","icon":"photo","preview":{"title":"title","subtitle":"image.altText","media":"image","fallback":"Untitled look"}}},{"key":"showProductList","label":"List the products beside the photo","type":"boolean","defaultValue":true}],"description":"Photos with dots that open the products in them","category":"conversion","icon":"hanger","preview":{"title":"heading","media":"looks.0.image"}}},
|
|
1423
|
+
{"id":"commerce-size-guide","name":"Size guide table","category":"Commerce","kind":"block","tags":["repeater","validation","a11y","references","media-heavy"],"proves":"`table` type: sizes as row headers, a column group per unit (both measurements stored, never converted) · measuring steps · applies-to references","why":"Each size table is a `table` (CPO D42): sizes are row headers (th scope=row), and every measurement appears once per unit inside a column group (Inches, Centimetres → th scope=colgroup), so the unit switch only shows or hides a group and never computes conversions (rounding a size chart is how returns happen). Cells are keyed by column, so reordering never misaligns a size. The caption is required because a <table> without a caption is a guessing game for screen readers. The guide is referenced FROM products via `appliesTo`, so one guide serves the whole range and the product page links to it next to the size picker.","htmlSignals":["a <table> with a first column of sizes (XS/S/M/L/XL or numbers) and measurement columns","a 'Size guide' / 'Size chart' link next to the size option that opens a modal/drawer","an in/cm or US/EU toggle switching the table values","a 'How to measure' illustration with numbered instructions"],"mcp":"For each table run the table detector (docs/TABLES.md): caption from <caption> or the preceding heading; first column → row headers (`headerColumn: true`); measurement headers → columns. If a unit toggle exists, read both states (data attributes often hold the other unit) and emit one column group per unit with the same measurements in the same order; never convert numerically. Images in the guide → measureImage with descriptive alt.","pitfalls":["RESOLVED (D42): cells are keyed by column, so a missing value is an empty cell, never a shifted row.","The unit switch is the renderer's job (show the active column group); both groups stay in the HTML for search and AI.","Auto-converting inches to cm produces values the brand never published.","A size guide pasted as an image fails accessibility; the converter should OCR it into rows and keep the image only as the diagram."],"notCarried":["category"],"component":{"name":"Size guide","slug":"size-guide","props":[{"key":"title","label":"Title","type":"text","required":true,"config":{"maxLength":48},"ui":{"tab":{"id":"content","label":"Tables"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":160,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"appliesTo","label":"Used on","type":"multi-reference","config":{"max":20,"contentModelId":"col_products"},"ui":{"itemName":"guideProduct","itemTitle":"Product","tab":{"id":"content"}}},{"key":"units","label":"Units","type":"select","defaultValue":"both","config":{"options":[{"label":"Inches only","value":"in"},{"label":"Centimeters only","value":"cm"},{"label":"Both, with a switch","value":"both"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"tables","label":"Tables","type":"table","config":{"minItems":1,"maxItems":4,"fields":[{"key":"table","label":"Sizes","type":"table","helpText":"Give each measurement in inches and centimeters.","required":true,"helpMore":"Ranges like 3.5–3.75 are fine.","config":{"mode":"data","maxColumns":9,"maxRows":10,"columnTypes":["text","number"],"allowGroups":false,"settings":{"totals":false,"highlight":false}}}]},"ui":{"itemLabel":"Table","itemName":"sizeTable","icon":"table","preview":{"title":"table.caption","fallback":"Untitled table"},"tab":{"id":"content"}}},{"key":"measureImage","label":"Measuring diagram","type":"image","ui":{"tab":{"id":"measure","label":"How to measure"}}},{"key":"steps","label":"Steps","type":"table","openMode":"inline","config":{"maxItems":4,"fields":[{"key":"label","label":"What","type":"text","required":true,"config":{"maxLength":32}},{"key":"body","label":"Body","type":"longtext","config":{"maxLength":160},"ui":{"rows":2}}]},"ui":{"itemLabel":"Step","icon":"list-numbers","preview":{"title":"label","subtitle":"body","fallback":"Untitled step"},"tab":{"id":"measure"}}},{"key":"fitNote","label":"Between sizes?","type":"richtext","ui":{"tab":{"id":"measure"}}}],"description":"Size tables with inch and centimetre values, and how to measure","category":"conversion","icon":"ruler-measure","preview":{"title":"title","subtitle":"intro"}}},
|
|
1424
|
+
{"id":"commerce-store-locator","name":"Store locator","category":"Commerce","kind":"block","tags":["references","dynamic-query","conditional","deep-nesting","repeater","validation","embed","seo-structured"],"proves":"Locations collection query vs written-here list · geopoint · weekly hours + holiday exceptions with HH:MM validation · map provider settings","why":"Locations are real-world entities that deserve their own pages, so the default source is a Locations collection; the inline list exists for small brands and is what the sample shows so the full shape is visible. Hours are modelled the way people say them (grouped days + opens/closes in 24-hour strings, pattern-validated) plus dated exceptions, which is exactly what LocalBusiness `openingHoursSpecification` needs — so rich results come from content, not a second copy. Map provider/center/zoom are settings; the API key is never a content field.","htmlSignals":["an embedded map (Mapbox GL canvas, maps.googleapis.com iframe/script) next to a scrolling list of addresses","a 'Find a store' / 'Stockists' heading with a ZIP/city search input and 'Use my location'","address blocks with phone links (tel:) and opening hours tables (Mon–Fri 10:00–18:00)","JSON-LD LocalBusiness/Store, or data-lat/data-lng on list items","store-locator app markers (Stockist, Storemapper, Storepoint)"],"mcp":"If >4 locations or each has its own URL, create Locations entries and set source:'collection'; else inline. Parse each address block; geocode only if lat/lng are absent from data attributes/JSON-LD (flag geocoded pins for review). Normalise hours to grouped days + HH:MM (12h '6pm' → '18:00'). Holiday notices → exceptions with ISO dates. Map vendor from the script URL → provider; never import API keys.","pitfalls":["Time is a string with a pattern because there is no time-of-day primitive (gap).","Overnight hours (closes 01:00) need care: closes < opens means past midnight.","The Locations collection does not exist in the demo seed, so the collection path can't be previewed here."],"notCarried":["category","variants"],"component":{"name":"Store locator","slug":"store-locator","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":160,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"source","label":"Locations come from","type":"select","defaultValue":"collection","config":{"options":[{"label":"Locations collection","value":"collection","description":"Each shop has its own page"},{"label":"Written here","value":"inline","description":"For two or three shops"}]},"ui":{"control":"segmented","tab":{"id":"locations","label":"Locations"}}},{"key":"types","label":"Show these types","type":"multiselect","helpText":"Leave empty for every location.","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"collection"}]},"config":{"options":[{"label":"Caldera stores","value":"flagship"},{"label":"Stockists","value":"stockist"},{"label":"Outfitters with demo kits","value":"outfitter"}]},"ui":{"control":"checkbox","tab":{"id":"locations"}}},{"key":"locations","label":"Locations","type":"table","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"inline"}]},"config":{"minItems":1,"maxItems":12,"fields":[{"key":"name","label":"Name","type":"text","required":true,"config":{"maxLength":48}},{"key":"type","label":"Type","type":"select","options":[{"label":"Caldera store","value":"flagship"},{"label":"Stockist","value":"stockist"},{"label":"Outfitter","value":"outfitter"}]},{"key":"address","label":"Address","type":"longtext","helpText":"As it should appear, on two lines.","required":true,"ui":{"rows":2}},{"key":"point","label":"Map pin","type":"location","helpText":"Drop it on the door.","required":true},{"key":"phone","label":"Phone","type":"link"},{"key":"hours","label":"Opening hours","type":"table","helpText":"Days not listed show as closed.","openMode":"inline","fields":[{"key":"days","label":"Days","type":"multiselect","options":[{"label":"Mon","value":"mon"},{"label":"Tue","value":"tue"},{"label":"Wed","value":"wed"},{"label":"Thu","value":"thu"},{"label":"Fri","value":"fri"},{"label":"Sat","value":"sat"},{"label":"Sun","value":"sun"}],"config":{"min":1}},{"key":"opens","label":"Opens","type":"text","required":true,"placeholder":"09:00","config":{"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","patternMessage":"Use 24-hour time, like 09:00."}},{"key":"closes","label":"Closes","type":"text","required":true,"placeholder":"18:00","config":{"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","patternMessage":"Use 24-hour time, like 18:00."}}],"config":{"maxItems":7},"ui":{"itemLabel":"Hours row","itemName":"hoursRow","itemTitle":"Hours","icon":"clock","preview":{"title":"opens","subtitle":"closes","fallback":"Untitled hours"}}},{"key":"exceptions","label":"Holiday hours","type":"table","openMode":"inline","fields":[{"key":"date","label":"Date","type":"date","required":true},{"key":"label","label":"Label","type":"text","placeholder":"Thanksgiving","config":{"maxLength":32}},{"key":"isClosed","label":"Closed all day","type":"boolean","defaultValue":true}],"config":{"maxItems":12},"ui":{"itemLabel":"Date","icon":"calendar-off","preview":{"title":"label","subtitle":"date","fallback":"Untitled date"}}},{"key":"services","label":"Services","type":"tags"}]},"ui":{"itemLabel":"Location","icon":"building-store","preview":{"title":"name","subtitle":"address","fallback":"Untitled location"},"tab":{"id":"locations"}}},{"key":"provider","label":"Map","type":"select","defaultValue":"mapbox","config":{"options":[{"label":"Mapbox","value":"mapbox"},{"label":"Google Maps","value":"google"}]},"ui":{"tab":{"id":"map","label":"Map & search"}}},{"key":"center","label":"Start at","type":"location","helpText":"Where the map opens before a search.","ui":{"tab":{"id":"map"}}},{"key":"zoom","label":"Start zoom","type":"number","defaultValue":6,"config":{"min":2,"max":14,"step":1},"ui":{"control":"slider","tab":{"id":"map"}}},{"key":"showSearch","label":"Show search by city or ZIP","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"map"}}},{"key":"radius","label":"Search radius","type":"select","defaultValue":"50","showIf":{"match":"all","rules":[{"field":"showSearch","op":"eq","value":"true"}]},"config":{"options":[{"label":"25 miles","value":"25"},{"label":"50 miles","value":"50"},{"label":"100 miles","value":"100"}]},"ui":{"tab":{"id":"map"}}},{"key":"useRichResults","label":"Local business rich results","type":"boolean","helpText":"Shows address and hours in search results.","defaultValue":true,"ui":{"tab":{"id":"map"}}}],"description":"A searchable map of shops and stockists, with hours","category":"conversion","icon":"map-pin","preview":{"title":"heading","subtitle":"source","icon":"map-pin"}}},
|
|
1425
|
+
{"id":"commerce-product-comparison","name":"Product comparison (2–4)","category":"Commerce","kind":"block","tags":["references","commerce","deep-nesting","repeater","validation","conditional","a11y","responsive-layout"],"proves":"2–4 product references as columns · a `table` keyed by product id (reorder-safe) · row groups · Yes / no columns with per-row and per-cell types","why":"Columns are product REFERENCES (name, photo, price and Add button come from the entry); the specs are one `table` (CPO D42) whose product columns are KEYED BY THE PRODUCT ID, so reordering `products` re-orders the columns with their cells. Rows are editorial and grouped (Cooking, Carry → tbody + th scope=rowgroup). Product columns are Yes / no, so the renderer writes ✓ with the words 'Included' / 'Not included' (never a bare icon); a row can be Text or Number ('Best for', 'Weight') and one cell can differ ('No lid — use foil'). The highlighted column is a column flag, not an index.","htmlSignals":["a <table> whose header row has 2–4 product images/names with prices and buttons","rows of ✓/✕ icons or 'Yes/No' with an occasional text value","a 'Compare' heading, 'Which one is right for you?' copy, or a 'Hide identical' toggle","a highlighted column with a ribbon like 'Best seller' / 'Our pick'"],"mcp":"Header cells → resolve each product link to a Products entry; emit `products` in column order and give each spec column `_key` = that entry id. Body via the table detector (docs/TABLES.md): <tbody> per group or a full-width heading row → `rowGroups`; first cell → row header; ✓/✕ icons, aria-labels and sr-only text → Yes / no; a row of words or numbers → that row's `type`; a partial icon with a note → a Text cell. The ribbon column → `highlight: true` on that column, its words → highlightLabel.","pitfalls":["RESOLVED (D42): cells are keyed by product id, not position; the highlight is a column flag.","Keeping `products` and the spec columns in step (add a product → add its column) is the editor's job today; a column source ('one column per item of products') is the follow-up (gap 3).","Icon-only cells need text alternatives; Yes / no cells render ✓ plus the words, which is what search and AI read."],"notCarried":["ai","category"],"component":{"name":"Product comparison","slug":"product-comparison","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":160,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"products","label":"Products","type":"multi-reference","unique":true,"config":{"min":2,"max":4,"contentModelId":"col_products"},"ui":{"itemName":"comparedProduct","tab":{"id":"content"}}},{"key":"highlightLabel","label":"Highlight label","type":"text","placeholder":"Our pick","config":{"maxLength":20},"ui":{"tab":{"id":"content"}}},{"key":"specs","label":"Specs","type":"table","required":true,"config":{"mode":"data","maxColumns":5,"maxRows":40,"columnTypes":["text","boolean","number","money","rating"],"jsonLd":["ItemList"],"settings":{"totals":false,"sticky":false}},"ui":{"tab":{"id":"content"}}},{"key":"hideSame","label":"Offer Hide identical rows","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"isHeaderSticky","label":"Keep the header row in view","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design"}}}],"description":"Two to four products side by side, spec by spec","category":"conversion","icon":"columns-3","preview":{"title":"heading","subtitle":"intro"}}},
|
|
1426
|
+
{"id":"pricing-tiers-billing-toggle","name":"Plans with monthly / yearly switch","category":"Pricing & plans","kind":"block","tags":["commerce","conditional","validation","repeater","variants"],"proves":"Both cycle prices written per tier (no computed discount) · per-seat pricing as a conditional · 'Everything in X, plus' inheritance · developer-locked payment price IDs","why":"Unlike the core Pricing block (one amount + a global yearly-discount slider), real SaaS pricing pages publish BOTH cycle prices per plan because they rarely match one tidy percentage ($49 → $39 is 20%, $25 → $20 is 20%, but legacy plans drift). So each plan stores monthly + yearly as explicit numbers, conditional on a 4-way pricing select (free / flat / per seat / custom), with per-seat adding a minimum seats field. 'Everything in X, plus' is modelled as an `inherits` label + a features list of only the deltas, which is how readers scan tiers. Stripe price IDs sit in a collapsed, developer-locked fieldset: the page and checkout share one plan row, so a marketer can change the copy but never which price is charged.","htmlSignals":["a Monthly/Yearly (Annual) segmented switch or toggle above 2–4 plan columns, often with a 'Save 20%' pill","each column: plan name, price with '/mo' or '/user/month', a CTA, a ✓ feature list","data-monthly / data-yearly attributes or two price spans toggled by the switch","'Everything in Pro, plus' lead-in lines","checkout links containing price_… / plan= / priceId= parameters"],"mcp":"Read BOTH states of the switch (data attributes, hidden spans, or the script's price map) — never derive yearly from monthly with a percentage. Price text with '/user' or '/seat' → pricing:'seat'; '$0' or 'Free' → 'free'; 'Contact'/'Custom'/'Let's talk' → 'custom' + customLabel. 'Everything in X' → inherits:'X' and only the following bullets → features. Badge ribbons → badge. Stripe/Paddle IDs found in checkout URLs → stripeMonthly/stripeYearly (they're developer-locked afterwards). The switch's initially checked state → toggle.defaultCycle.","pitfalls":["A 'Save 20%' badge that isn't true for every plan is misleading; the schema can't check it yet (no computed fields).","`inherits` is a free string, not a reference to a sibling plan — renaming a plan leaves stale 'Everything in' lines (gap: references within the same entry).","Per-seat prices must say 'per seat' near the number, not only in the footnote."],"notCarried":["ai","category","variants"],"component":{"name":"Pricing plans","slug":"plan-tiers","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":160,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"toggle","label":"Billing switch","type":"object","openMode":"inline","config":{"fields":[{"key":"defaultCycle","label":"Opens on","type":"select","defaultValue":"yearly","options":[{"label":"Monthly","value":"monthly"},{"label":"Yearly","value":"yearly"}],"ui":{"control":"segmented"}},{"key":"badge","label":"Yearly badge","type":"text","placeholder":"Save 20%","config":{"maxLength":16}}]},"ui":{"icon":"toggle-right","tab":{"id":"content"}}},{"key":"plans","label":"Plans","type":"table","config":{"minItems":2,"maxItems":4,"fields":[{"key":"name","label":"Plan name","type":"text","required":true,"config":{"maxLength":20}},{"key":"audience","label":"Who it's for","type":"text","recommendedMax":60,"placeholder":"For teams shipping to production"},{"key":"pricing","label":"Price","type":"select","defaultValue":"flat","options":[{"label":"Free","value":"free"},{"label":"Flat","value":"flat"},{"label":"Per seat","value":"seat"},{"label":"Custom","value":"custom"}],"ui":{"control":"segmented"}},{"key":"monthly","label":"Price","type":"number","helpText":"Per month, when paid monthly.","required":true,"showIf":{"match":"all","rules":[{"field":"pricing","op":"in","value":["flat","seat"]}]},"config":{"unit":"USD","min":0,"step":1},"ui":{"group":{"id":"monthly","label":"Billed monthly","kind":"group"}}},{"key":"yearly","label":"Price a month","type":"number","helpText":"Per month, when paid yearly.","required":true,"showIf":{"match":"all","rules":[{"field":"pricing","op":"in","value":["flat","seat"]}]},"config":{"unit":"USD","min":0,"step":1},"ui":{"group":{"id":"yearly","label":"Billed yearly","kind":"group"}}},{"key":"minSeats","label":"Minimum seats","type":"number","helpText":"The price above is per seat.","showIf":{"match":"all","rules":[{"field":"pricing","op":"eq","value":"seat"}]},"config":{"min":1,"max":50,"step":1,"unit":"seats"}},{"key":"customLabel","label":"Price label","type":"text","showIf":{"match":"all","rules":[{"field":"pricing","op":"eq","value":"custom"}]},"placeholder":"Let's talk","config":{"maxLength":20}},{"key":"button","label":"Button","type":"button","required":true},{"key":"badge","label":"Badge","type":"text","placeholder":"Most popular","config":{"maxLength":18}},{"key":"inherits","label":"Everything in","type":"text","helpText":"The previous plan's name. Shows \"Everything in Pro, plus:\".","config":{"maxLength":20}},{"key":"features","label":"Features","type":"array","helpText":"Only what's new when \"Everything in\" is set.","config":{"itemType":"text","maxItems":8},"ui":{"itemLabel":"Feature"}},{"key":"stripeMonthly","label":"Monthly price ID","type":"text","showIf":{"match":"all","rules":[{"field":"pricing","op":"in","value":["flat","seat"]}]},"readOnly":"developer","lockReason":"Set by the developer in Stripe","config":{"pattern":"^price_[A-Za-z0-9]+$","patternMessage":"Use a Stripe price ID, like price_1PqRsT."},"ui":{"access":{"edit":"schema.manage"},"group":{"id":"ids","label":"Payment IDs","kind":"group","collapsed":true,"description":"Set by a developer. Checkout uses these, not the numbers above."}}},{"key":"stripeYearly","label":"Yearly price ID","type":"text","showIf":{"match":"all","rules":[{"field":"pricing","op":"in","value":["flat","seat"]}]},"readOnly":"developer","lockReason":"Set by the developer in Stripe","config":{"pattern":"^price_[A-Za-z0-9]+$","patternMessage":"Use a Stripe price ID, like price_1PqRsT."},"ui":{"access":{"edit":"schema.manage"},"group":{"id":"ids"}}}]},"ui":{"itemLabel":"Plan","icon":"badge","preview":{"title":"name","subtitle":"audience","fallback":"Untitled plan"},"tab":{"id":"plans","label":"Plans"}}},{"key":"footnote","label":"Footnote","type":"longtext","config":{"maxLength":200},"ui":{"rows":2,"tab":{"id":"settings","label":"Settings"}}},{"key":"currency","label":"Currency","type":"select","helpText":"Doesn't convert prices.","defaultValue":"USD","config":{"options":[{"label":"US dollar","value":"USD"},{"label":"Euro","value":"EUR"},{"label":"Pound sterling","value":"GBP"}]},"ui":{"tab":{"id":"settings"}}}],"description":"Plan columns with a monthly / yearly switch","category":"conversion","icon":"currency-dollar","preview":{"title":"heading","subtitle":"toggle.defaultCycle"}}},
|
|
1427
|
+
{"id":"pricing-usage-calculator","name":"Usage-based price calculator","category":"Pricing & plans","kind":"block","tags":["deep-nesting","repeater","validation","conditional","commerce"],"proves":"Slider inputs keyed for a formula · rates › graduated tiers per input (4 levels) · base price + included units · contact-sales threshold","why":"A calculator is INPUTS + a PRICE MODEL, and the model must be data, not a formula string a marketer can break: sliders are keyed objects (key developer-locked, label/hint/range editable), and each rate points at a slider key and holds included units, a bundle size and graduated steps — section › rates › rate › tiers › tier. That covers the common SaaS shapes (base fee + included allowance + graduated overage, flat add-on per block) without code. The 'Talk to sales above' threshold is conditional with its own button so very large estimates route to a human, which is both honest and how volume deals actually close.","htmlSignals":["<input type=range> sliders with live-updating number labels and a total price that changes as they move","copy like 'Estimate your cost', 'Pricing calculator', '$0.40 per 100k'","a breakdown list (base + overage lines) under a big monthly total","a script with a rate table / tiers array (look for upTo, unitPrice, included)"],"mcp":"Each range input → one slider (min/max/step from attributes, start from value; label from <label for>). Detect log scales from non-linear value mapping in the script (Math.pow/Math.log). Rates: prefer reading the rate table from the page script or the pricing table text ('first 2M included, then $0.40 per 100k up to 10M'); graduated steps → tiers with upTo, the last step without upTo. Never encode the formula as a string field. If the page shows 'Contact us' past a value, set contactAbove.","pitfalls":["No computed/formula field exists, so the total is calculated by the renderer from the rate data — the schema can't preview the number in the editor (gap).","`rate.input` should be a select of slider keys from the same block (gap: options from a sibling array).","min < start < max is a cross-field rule the schema can't express yet.","Graduated vs volume pricing are different maths; this model is graduated only — say so in notes for volume-priced brands."],"notCarried":["category"],"component":{"name":"Price calculator","slug":"usage-calculator","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":160,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"inputs","label":"Sliders","type":"table","config":{"minItems":1,"maxItems":4,"fields":[{"key":"key","label":"Key","type":"text","required":true,"readOnly":"developer","lockReason":"Used by the rates below","config":{"pattern":"^[a-z][a-zA-Z0-9]*$","patternMessage":"Use camelCase letters and numbers, like hoursPerRun."},"ui":{"access":{"edit":"schema.manage"}}},{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":40}},{"key":"help","label":"Hint","type":"text","config":{"maxLength":90}},{"key":"unit","label":"Unit","type":"text","placeholder":"events","config":{"maxLength":16}},{"key":"min","label":"Lowest","type":"number","required":true},{"key":"max","label":"Highest","type":"number","required":true},{"key":"start","label":"Starts at","type":"number","required":true},{"key":"scale","label":"Scale","type":"select","defaultValue":"linear","options":[{"label":"Even steps","value":"linear"},{"label":"Grows faster","value":"log","description":"For ranges like 100k to 100M"}],"ui":{"control":"segmented"}}]},"ui":{"itemLabel":"Slider","icon":"adjustments-horizontal","preview":{"title":"label","subtitle":"unit","fallback":"Untitled slider"},"tab":{"id":"inputs","label":"Sliders"}}},{"key":"basePrice","label":"Platform fee","type":"number","helpText":"Monthly, before usage. 0 for pay-as-you-go.","config":{"unit":"USD","min":0,"step":1},"ui":{"tab":{"id":"rates","label":"Rates"}}},{"key":"rates","label":"Rates","type":"table","config":{"minItems":1,"maxItems":4,"fields":[{"key":"input","label":"For slider","type":"text","helpText":"The slider's key, e.g. events.","required":true},{"key":"included","label":"Included free","type":"number","helpText":"Units covered by the platform fee.","config":{"min":0,"step":1}},{"key":"per","label":"Priced per","type":"number","helpText":"Bundle size, e.g. 100000 for \"per 100k events\".","defaultValue":1,"config":{"min":1,"step":1}},{"key":"tiers","label":"Price steps","type":"table","helpText":"Each step prices only the units inside it.","openMode":"inline","fields":[{"key":"upTo","label":"Up to","type":"number","helpText":"Leave empty on the last step for \"and above\".","config":{"min":1,"step":1}},{"key":"price","label":"Price","type":"number","required":true,"config":{"unit":"USD","min":0,"step":0.01}}],"config":{"minItems":1,"maxItems":6},"ui":{"itemLabel":"Step","icon":"stairs","preview":{"title":"upTo","subtitle":"price","fallback":"Untitled step"}}}]},"ui":{"itemLabel":"Rate","icon":"receipt","preview":{"title":"input","subtitle":"included","fallback":"Untitled rate"},"tab":{"id":"rates"}}},{"key":"showBreakdown","label":"Show the breakdown","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"settings","label":"Settings"}}},{"key":"contactAbove","label":"Talk to sales above","type":"number","helpText":"Above this, a sales button replaces the price. Leave empty for never.","config":{"unit":"USD","min":0,"step":100},"ui":{"tab":{"id":"settings"}}},{"key":"salesButton","label":"Sales button","type":"button","showIf":{"match":"all","rules":[{"field":"contactAbove","op":"notEmpty"}]},"ui":{"tab":{"id":"settings"}}},{"key":"button","label":"Button","type":"button","required":true,"ui":{"tab":{"id":"content"}}},{"key":"disclaimer","label":"Disclaimer","type":"text","config":{"maxLength":140},"ui":{"tab":{"id":"settings"}}},{"key":"currency","label":"Currency","type":"select","helpText":"Doesn't convert prices.","defaultValue":"USD","config":{"options":[{"label":"US dollar","value":"USD"},{"label":"Euro","value":"EUR"},{"label":"Pound sterling","value":"GBP"}]},"ui":{"tab":{"id":"settings"}}}],"description":"Sliders for usage, with the monthly estimate worked out live","category":"conversion","icon":"calculator","preview":{"title":"heading","subtitle":"intro"}}},
|
|
1428
|
+
{"id":"pricing-feature-matrix","name":"Full feature comparison matrix","category":"Pricing & plans","kind":"block","tags":["deep-nesting","repeater","conditional","validation","responsive-layout","a11y"],"proves":"Plans as keyed columns · one `table` keyed by plan key · Yes / no columns with per-row (limits) and per-cell (partial, add-on, soon) types · row groups · price row → Offers JSON-LD · sticky header + mobile mode","why":"A pricing matrix was the deepest section most sites have (groups › rows › cells, 5 drill levels). Now it is one `table` (CPO D42): plan columns are KEYED by the plan key, so a missing cell is a dash, not a shifted column, and adding a plan never scrambles rows. Plan columns are Yes / no (✓ with the words 'Included' / 'Not included'); a limit row is Text; a partial, add-on or 'coming soon' is one cell with its own type. Groups are row groups (tbody + th scope=rowgroup). The price row is Money, which is what the opt-in Offers JSON-LD reads. The plan cards above still own name, price line and button (`plans`). Phone behavior is a design choice, not a layout hack.","htmlSignals":["a large <table> (or role=table grid) under the plan cards with a header row of plan names + prices + buttons","row group headings spanning all columns (<th colspan>, <tbody> per group, or accordion sections)","cells with ✓/✕ icons, short limits ('2M', '30 days'), '+$29/mo' add-on text, 'Coming soon' pills","info-icon tooltips next to feature names","position: sticky on the header row"],"mcp":"Header cells → plans[] (lowercase keys from plan names) AND the matrix's plan columns with `_key` = that key, type Yes / no. Run the table detector (docs/TABLES.md): group heading rows / tbody per group → rowGroups; feature cell → row header, its tooltip (title / aria-describedby / data-tooltip) → a footnote; ✓ → true, ✕ / dash / empty → false; a row of limits ('2M', '30 days') → row type Text; '+$29/mo' / 'Add-on' / 'Soon' / a caveat → that cell's own Text type. A header price line → a Money 'Price a month' row (Offers JSON-LD reads it). Collapsed groups are a renderer setting.","pitfalls":["RESOLVED (D42): cells are keyed by plan key; nothing is aligned by position.","Plan columns and `plans` must share keys; keeping them in step is the editor's job until a column source exists (gap 3). The validator should flag a column key with no plan.","Collapsed row groups and feature tooltips are presentation; tooltips become footnotes so their words stay on the page for search and AI.","Price lines in the header duplicate the plan cards above — they should reference one plan source (gap: plans as CMS items).","Icon-only cells need words: Yes / no cells render ✓ / – plus visually hidden 'Included' / 'Not included'."],"notCarried":["category"],"component":{"name":"Plan comparison","slug":"plan-comparison","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"plans","label":"Columns","type":"table","config":{"minItems":2,"maxItems":5,"fields":[{"key":"key","label":"Key","type":"text","required":true,"unique":true,"readOnly":"developer","lockReason":"The matrix's plan columns use this key","config":{"pattern":"^[a-z]+$","patternMessage":"Use lowercase letters, like email."},"ui":{"access":{"edit":"schema.manage"}}},{"key":"name","label":"Plan name","type":"text","required":true,"config":{"maxLength":20}},{"key":"price","label":"Price line","type":"text","placeholder":"$39/mo","config":{"maxLength":24}},{"key":"button","label":"Button","type":"button"}]},"ui":{"itemLabel":"Column","itemName":"column","icon":"columns","preview":{"title":"name","subtitle":"price","fallback":"Untitled column"},"tab":{"id":"content"}}},{"key":"matrix","label":"Matrix","type":"table","required":true,"config":{"mode":"data","maxColumns":6,"maxRows":80,"columnTypes":["text","boolean","money","link","richtext-inline"],"jsonLd":["Offers"],"settings":{"phones":[],"totals":false,"sticky":false}},"ui":{"tab":{"id":"content"}}},{"key":"isHeaderSticky","label":"Keep the header row in view","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design"}}},{"key":"mobile","label":"On phones","type":"select","defaultValue":"one-plan","config":{"options":[{"label":"One plan at a time","value":"one-plan","icon":"device-mobile","description":"A plan switcher above the table"},{"label":"Scroll sideways","value":"scroll","icon":"arrows-horizontal","description":"Feature names stay pinned"}]},"ui":{"control":"cards","tab":{"id":"design"}}}],"description":"Every feature across every plan, grouped, with a sticky header","category":"conversion","icon":"table-options","preview":{"title":"heading"}}},
|
|
1429
|
+
{"id":"pricing-single-plan-addons","name":"Single plan with add-ons","category":"Pricing & plans","kind":"block","tags":["commerce","repeater","conditional","validation","rich-text"],"proves":"One plan object + add-on array with units, quantities and preselection · eligibility flags · live total","why":"Single-plan businesses (clinics, studios, memberships) sell one plan plus extras, so the shape is one drill-in `plan` object and an `addons` array — each add-on carrying its unit (per session / month / once), whether it's a checkbox or a quantity stepper (conditional max), and eligibility flags that matter to buyers (HSA/FSA). Preselected add-ons are allowed but the help text keeps them honest. The minimum term is its own field so it can sit next to the price instead of being buried in the small print.","htmlSignals":["one prominent plan card with a price per month and an 'includes' list","a list of optional extras with checkboxes or +/- quantity steppers and individual prices","a running total that updates as extras are ticked ('Your monthly total')","small print about minimum terms, cancellation, insurance/HSA"],"mcp":"Plan card → plan (name, price, interval from '/mo', includes from its list). Each optional extra → addons[] with price and unit parsed from '/session', '/mo', 'one-time'; a stepper input → allowQuantity: true + maxQuantity from its max attribute; a pre-checked checkbox → isPreselected: true. Terms paragraphs → finePrint (rich text, keep links). Do not compute the total — the renderer does.","pitfalls":["Pre-checked paid add-ons can be a dark pattern; keep them rare and visible.","A minimum term hidden in small print is a trust killer — it has its own field on purpose.","Mixed units (per month + per session) can't be summed into one honest monthly number without an assumption; show the breakdown."],"notCarried":["category"],"component":{"name":"Plan with add-ons","slug":"plan-with-addons","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":180,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"plan","label":"Plan","type":"object","openMode":"drill","config":{"fields":[{"key":"name","label":"Plan name","type":"text","required":true,"config":{"maxLength":32}},{"key":"price","label":"Price","type":"number","required":true,"config":{"unit":"USD","min":0,"step":1},"ui":{"group":{"id":"price","label":"Price","kind":"group","columns":2}}},{"key":"interval","label":"Billed","type":"select","defaultValue":"month","options":[{"label":"Per month","value":"month"},{"label":"Per year","value":"year"},{"label":"Once","value":"once"}],"ui":{"group":{"id":"price"}}},{"key":"commitment","label":"Minimum term","type":"text","placeholder":"3 months, then month to month","config":{"maxLength":40}},{"key":"includes","label":"Includes","type":"array","config":{"itemType":"text","minItems":1,"maxItems":8},"ui":{"itemLabel":"Benefit","itemName":"benefit","itemTitle":"Benefit"}}]},"ui":{"icon":"id-badge","tab":{"id":"content"}}},{"key":"addons","label":"Add-ons","type":"table","config":{"maxItems":8,"fields":[{"key":"name","label":"Name","type":"text","required":true,"config":{"maxLength":40}},{"key":"description","label":"Description","type":"text","recommendedMax":90},{"key":"price","label":"Price","type":"number","required":true,"config":{"unit":"USD","min":0,"step":1}},{"key":"unit","label":"Charged","type":"select","defaultValue":"session","options":[{"label":"Per session","value":"session"},{"label":"Per month","value":"month"},{"label":"Once","value":"once"}]},{"key":"allowQuantity","label":"Let them choose how many","type":"boolean"},{"key":"maxQuantity","label":"Up to","type":"number","showIf":{"match":"all","rules":[{"field":"allowQuantity","op":"eq","value":"true"}]},"config":{"min":2,"max":12,"step":1}},{"key":"isPreselected","label":"Selected by default","type":"boolean"},{"key":"isHsaEligible","label":"HSA or FSA eligible","type":"boolean"}]},"ui":{"itemLabel":"Add-on","icon":"circle-plus","preview":{"title":"name","subtitle":"price","fallback":"Untitled add-on"},"tab":{"id":"addons","label":"Add-ons"}}},{"key":"totalLabel","label":"Total label","type":"text","placeholder":"Your monthly total","config":{"maxLength":32},"ui":{"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","required":true,"ui":{"tab":{"id":"content"}}},{"key":"finePrint","label":"Small print","type":"richtext","ui":{"tab":{"id":"content"}}}],"description":"One membership or plan, with optional extras and a running total","category":"conversion","icon":"playlist-add","preview":{"title":"plan.name","subtitle":"heading"}}},
|
|
1430
|
+
{"id":"pricing-enterprise-card","name":"Enterprise contact card","category":"Pricing & plans","kind":"block","tags":["conditional","references","form","repeater","a11y","media-heavy"],"proves":"Action select → form reference (opens in place) / calendar URL / plain link · compliance badges · customer logos with alt · inline quote","why":"The enterprise card's only job is the next step, so the core field is `action`: open a FORM (a reference to a form in Forms — fields, notifications and spam rules live there once, not per section), BOOK a call (a validated scheduling URL), or go to a page (a normal button unit). Each path shows only its own inputs. Proof is grouped separately: compliance as a multiselect of real certifications (help text forbids aspirational ticks), logos with the company name doubling as alt text, and a small inline quote.","htmlSignals":["a dark or bordered panel after/beside the plan columns with 'Enterprise', 'Custom', 'Contact sales'","a button that opens a modal form, a Calendly/Cal.com embed or links to /contact-sales","SOC 2 / ISO / HIPAA badge images or text","a small logo row and a single quote inside the panel"],"mcp":"A button opening a modal with a <form> → action:'form'; match the form's field names against existing forms in Forms and reference the best match, otherwise create a form with those fields (never inline them into the section). Calendly/Cal.com/HubSpot meeting links → action:'calendar' + calendarUrl. Badges → compliance (map by text/alt). Logo <img alt> → logos[].name.","pitfalls":["Inlining the contact-sales fields into the section creates a second form with separate submissions and rules.","Compliance badges are claims; the converter imports them but flags them for a human to confirm."],"notCarried":["ai","category"],"component":{"name":"Enterprise card","slug":"enterprise-card","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","recommendedMax":220,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"points","label":"What's included","type":"table","openMode":"inline","config":{"maxItems":6,"fields":[{"key":"icon","label":"Icon","type":"icon"},{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":48}}]},"ui":{"itemLabel":"Point","icon":"check","preview":{"title":"label","fallback":"Untitled point"},"tab":{"id":"content"}}},{"key":"action","label":"The button","type":"select","defaultValue":"form","config":{"options":[{"label":"Opens a form","value":"form","icon":"forms","description":"Submissions arrive in Forms"},{"label":"Books a call","value":"calendar","icon":"calendar","description":"A scheduling page opens in place"},{"label":"Goes to a page","value":"link","icon":"link"}]},"ui":{"control":"cards","tab":{"id":"content"}}},{"key":"form","label":"Form","type":"form","helpText":"Shared: every placement uses the same form.","required":true,"showIf":{"match":"all","rules":[{"field":"action","op":"eq","value":"form"}]},"ui":{"tab":{"id":"content"}}},{"key":"calendarUrl","label":"Scheduling page","type":"url","required":true,"showIf":{"match":"all","rules":[{"field":"action","op":"eq","value":"calendar"}]},"config":{"pattern":"^https://(cal\\.com|calendly\\.com|meetings\\.hubspot\\.com|savvycal\\.com)/","patternMessage":"Use a Cal.com, Calendly, HubSpot or SavvyCal link, like https://cal.com/caldera/fitting."},"ui":{"tab":{"id":"content"}}},{"key":"buttonLabel","label":"Button label","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"action","op":"in","value":["form","calendar"]}]},"placeholder":"Talk to sales","config":{"maxLength":24},"ui":{"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","showIf":{"match":"all","rules":[{"field":"action","op":"eq","value":"link"}]},"ui":{"tab":{"id":"content"}}},{"key":"responseTime","label":"Reply promise","type":"text","placeholder":"We reply within one business day","config":{"maxLength":48},"ui":{"tab":{"id":"content"}}},{"key":"compliance","label":"Compliance","type":"multiselect","config":{"options":[{"label":"SOC 2 Type II","value":"soc2"},{"label":"ISO 27001","value":"iso27001"},{"label":"HIPAA","value":"hipaa"},{"label":"GDPR","value":"gdpr"},{"label":"PCI DSS","value":"pci"}]},"ui":{"control":"checkbox","tab":{"id":"proof","label":"Proof"}}},{"key":"logos","label":"Customer logos","type":"table","config":{"maxItems":6,"fields":[{"key":"name","label":"Company","type":"text","helpText":"Not shown. Used as the logo's alt text.","required":true,"config":{"maxLength":32}},{"key":"logo","label":"Logo","type":"image","helpText":"SVG or a transparent PNG, one color.","required":true}]},"ui":{"itemLabel":"Logo","layout":"grid","icon":"building","preview":{"title":"name","media":"logo","fallback":"Untitled logo"},"tab":{"id":"proof"}}},{"key":"quote","label":"Quote","type":"object","openMode":"drill","config":{"fields":[{"key":"text","label":"Quote","type":"longtext","recommendedMax":200,"ui":{"rows":3}},{"key":"name","label":"Name","type":"text","config":{"maxLength":40}},{"key":"role","label":"Role and company","type":"text","config":{"maxLength":60}}]},"ui":{"icon":"quote","tab":{"id":"proof"}}}],"description":"The \"talk to us\" panel for large customers","category":"conversion","icon":"building","preview":{"title":"heading","subtitle":"action"}}},
|
|
1431
|
+
{"id":"pricing-faq","name":"Pricing FAQ","category":"Pricing & plans","kind":"block","tags":["rich-text","seo-structured","conditional","references","repeater","validation"],"proves":"Grouped questions (written here or FAQs collection) · price tokens in answers so copy never drifts from the plans · FAQPage rich results","why":"A pricing FAQ is different from a general FAQ in two ways: it's GROUPED (plans, payments, security) — so groups › items, one level deeper than the core FAQ example — and its answers quote prices, which is where pricing pages most often contradict themselves. Answers therefore support {{plan.cycle}} tokens resolved from the Plans block on the same page, documented in helpMore. Written-here vs FAQs-collection stays a conditional pair (the collection reference is filtered to billing questions), and FAQPage rich results are a toggle because two FAQ blocks on one page must not both emit it.","htmlSignals":["a 'Pricing FAQ', 'Billing questions' or 'Frequently asked' heading right after the plans","<details>/<summary> or aria-expanded accordions, sometimes with group sub-headings","answers that mention the same prices as the plan cards","existing FAQPage JSON-LD"],"mcp":"Group sub-headings → groups; each question/answer → items (answer as rich text, keep links). When an answer contains a price that equals a plan price on the same page, replace it with the matching token ({{pro.monthly}}) and note it in the conversion receipt. If the site has an FAQs collection with the same questions, prefer source:'collection' with references. Existing FAQPage JSON-LD → useRichResults: true (and remove the hand-written script).","pitfalls":["Tokens are plain strings inside rich text — typos aren't caught until preview (gap: typed inline references to plan fields).","Two FAQ blocks emitting FAQPage on one URL is invalid; the toggle exists for that."],"notCarried":["ai","category","variants"],"component":{"name":"Pricing FAQ","slug":"pricing-faq","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"localized":true,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"source","label":"Questions come from","type":"select","defaultValue":"inline","config":{"options":[{"label":"Written here","value":"inline"},{"label":"FAQs collection","value":"collection"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"groups","label":"Groups","type":"table","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"inline"}]},"config":{"minItems":1,"maxItems":5,"fields":[{"key":"title","label":"Group title","type":"text","required":true,"config":{"maxLength":32}},{"key":"items","label":"Questions","type":"table","fields":[{"key":"question","label":"Question","type":"text","required":true,"localized":true,"config":{"maxLength":120}},{"key":"answer","label":"Answer","type":"richtext","helpText":"Use {{pro.yearly}}-style tokens so prices stay in step with the plans.","required":true,"helpMore":"Tokens read the Plans block on this page: {{key.monthly}}, {{key.yearly}} or {{key.name}}, where key is the plan name in lowercase.","localized":true}],"config":{"minItems":1,"maxItems":10},"ui":{"itemLabel":"Question","itemName":"faqItem","icon":"help-circle","preview":{"title":"question","subtitle":"answer","fallback":"Untitled question"}}}]},"ui":{"itemLabel":"Group","itemName":"faqGroup","icon":"folder","preview":{"title":"title","fallback":"Untitled group"},"tab":{"id":"content"}}},{"key":"questions","label":"FAQs to show","type":"multi-reference","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"collection"}]},"config":{"max":12,"contentModelId":"col_faq","filter":"category == 'billing'"},"ui":{"itemName":"faqRef","itemTitle":"FAQ","tab":{"id":"content"}}},{"key":"contactButton","label":"Contact button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"useRichResults","label":"FAQ rich results","type":"boolean","helpText":"Turn off if another FAQ on this page already sends it.","defaultValue":true,"ui":{"tab":{"id":"settings","label":"Settings"}}},{"key":"isFirstOpen","label":"Open the first question","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"settings"}}}],"description":"Billing questions, grouped, with prices that stay in sync","category":"conversion","icon":"help-circle","preview":{"title":"heading"}}},
|
|
1432
|
+
{"id":"pricing-annual-savings-banner","name":"Pay-yearly savings banner","category":"Pricing & plans","kind":"block","tags":["personalization","conditional","validation","commerce"],"proves":"Savings expressed three ways (percent / months free / amount) as a conditional unit · audience targeting (members on monthly) · placement","why":"Unlike a sale banner, this one is about the BILLING CYCLE and only makes sense to some people, so audience is first-class (members on monthly need a signed-in portal; visitors see the public version). The saving can be told three ways — percent, months free, dollar amount — and each is a conditional numeric with its own unit and range, substituted into the message through a single {{saving}} token, so the copy and the number can't disagree.","htmlSignals":["a slim notice on a pricing/billing page: 'Switch to annual and save 20%', '2 months free'","links with ?cycle=yearly / ?billing=annual / /billing/switch","rendered only for logged-in users (inside an account/portal layout)"],"mcp":"Copy → message with the saving replaced by {{saving}}; detect the unit ('%' → percent, 'months free' → months, '$' → amount). A banner inside an authenticated layout → audience:'monthly-members'; on a public pricing page → 'visitors' or 'everyone'. Its URL path → placements.","pitfalls":["The saving isn't computed from the actual plan prices (no computed fields) — a price change can make the banner untrue.","Targeting signed-in members needs the portal integration; without it the banner silently shows to no one."],"notCarried":["ai","category"],"component":{"name":"Savings banner","slug":"savings-banner","props":[{"key":"message","label":"Message","type":"text","helpText":"{{saving}} becomes the saving.","required":true,"recommendedMax":70,"config":{"maxLength":90},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"savingType","label":"Saving shown as","type":"select","defaultValue":"months","config":{"options":[{"label":"Percent","value":"percent"},{"label":"Months free","value":"months"},{"label":"Amount","value":"amount"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"percent","label":"Percent","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"savingType","op":"eq","value":"percent"}]},"config":{"min":1,"max":50,"step":1,"unit":"%"},"ui":{"tab":{"id":"content"}}},{"key":"months","label":"Months free","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"savingType","op":"eq","value":"months"}]},"config":{"min":1,"max":4,"step":1,"unit":"months"},"ui":{"tab":{"id":"content"}}},{"key":"amount","label":"Amount","type":"number","helpText":"In dollars.","required":true,"showIf":{"match":"all","rules":[{"field":"savingType","op":"eq","value":"amount"}]},"config":{"unit":"USD","min":1,"step":1},"ui":{"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","required":true,"ui":{"tab":{"id":"content"}}},{"key":"audience","label":"Who sees it","type":"select","defaultValue":"monthly-members","config":{"options":[{"label":"Everyone","value":"everyone"},{"label":"Visitors who aren't members","value":"visitors"},{"label":"Members paying monthly","value":"monthly-members","description":"Needs them signed in to the member portal"}]},"ui":{"tab":{"id":"targeting","label":"Who and where"}}},{"key":"placements","label":"Shows on","type":"multiselect","config":{"min":1,"options":[{"label":"Membership page","value":"pricing"},{"label":"Member portal home","value":"portal"},{"label":"After booking confirmation","value":"booking"}]},"ui":{"control":"checkbox","tab":{"id":"targeting"}}},{"key":"until","label":"Show until","type":"date","helpText":"Leave empty for an always-on offer.","ui":{"tab":{"id":"targeting"}}},{"key":"isDismissible","label":"Visitors can close it","type":"boolean","helpText":"Closed banners stay hidden for 30 days.","defaultValue":true,"ui":{"tab":{"id":"targeting"}}}],"description":"A nudge to switch to yearly billing, shown to the right people","category":"conversion","icon":"pig-money","preview":{"title":"message","subtitle":"audience","icon":"pig-money"}}},
|
|
1433
|
+
{"id":"conversion-cta-band","name":"CTA band","category":"Conversion & forms","kind":"block","tags":["variants","conditional","media-heavy"],"proves":"Variants that swap layout without losing content · two button units · background as a conditional design cluster (color vs photo + overlay)","why":"The CTA band is the most-reused section on any site, so the schema optimises for swapping: three variants share one content shape, and fields one layout doesn't use (image on Centered) are kept rather than deleted so switching back loses nothing. Buttons are button units (label + link + style). Background is design, modelled as a conditional cluster — a color, or a photo with an overlay slider whose help gives the readable range.","htmlSignals":["a full-width section with high-contrast background, one heading, at most one sentence and 1–2 buttons","placed just before the footer or between long sections","a background-image with a semi-transparent overlay (::before with rgba/opacity or bg-black/40)","no form inputs (otherwise it's a newsletter or lead form)"],"mcp":"Heading/eyebrow/body text → fields; each <a>/<button> styled as a button → primaryButton/secondaryButton units (style from class: solid → primary, bordered → outline). Pick the variant from layout: centered text → centered, a sibling <img> column → split (+ image), flex row with buttons at the end → inline. Background image → background:'photo' + photo + overlay from the overlay opacity; solid color → map to the nearest brand token.","pitfalls":["Fields can't be conditioned on the variant (gap) — `image` is always visible and its help explains why.","Hard-coded hex colors break dark mode and brand updates; map to tokens."],"notCarried":["ai","category","variants"],"component":{"name":"Call to action band","slug":"call-to-action","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":50,"localized":true,"config":{"maxLength":80},"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","helpText":"Hidden on phones in the Inline layout.","recommendedMax":140,"localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"primaryButton","label":"Primary button","type":"button","required":true,"ui":{"tab":{"id":"content"}}},{"key":"secondaryButton","label":"Secondary button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"image","label":"Image","type":"image","helpText":"Split layout only.","ui":{"tab":{"id":"content"}}},{"key":"background","label":"Background","type":"select","defaultValue":"color","config":{"options":[{"label":"Color","value":"color"},{"label":"Photo","value":"photo"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"color","label":"Background color","type":"color","showIf":{"match":"all","rules":[{"field":"background","op":"eq","value":"color"}]},"ui":{"tab":{"id":"design"}}},{"key":"photo","label":"Background photo","type":"image","helpText":"At least 2000px wide.","showIf":{"match":"all","rules":[{"field":"background","op":"eq","value":"photo"}]},"ui":{"tab":{"id":"design"}}},{"key":"overlay","label":"Darken photo","type":"number","showIf":{"match":"all","rules":[{"field":"background","op":"eq","value":"photo"}]},"config":{"min":0,"max":80,"step":5,"unit":"%"},"ui":{"control":"slider","tab":{"id":"design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A full-width band with one message and one or two buttons","category":"conversion","icon":"click","preview":{"title":"heading","subtitle":"body","media":"image"}}},
|
|
1434
|
+
{"id":"conversion-newsletter-consent","name":"Newsletter sign-up with consent","category":"Conversion & forms","kind":"block","tags":["form","references","conditional","validation","a11y","repeater"],"proves":"The simple inline form: visible label + placeholder + submit label · interest checkboxes that tag subscribers · consent mode (checkbox vs notice) · success with an optional code","why":"The newsletter is the 'simple inline form' case: the visitor sees one email field, but submissions still go to a form (the reference), so double opt-in, list sync and suppression live in one place. The section owns only presentation: a REQUIRED visible label (a placeholder is not a label — WCAG 3.3.2), the placeholder, the submit label (a string, not a button unit, because submit isn't a link), optional interest checkboxes that map to subscriber tags, and the consent mode — an unticked checkbox or a notice — with rich-text wording so the privacy link is a real link.","htmlSignals":["a <form> with exactly one type=email input (maybe a first-name input) and one submit button","copy like 'Subscribe', 'Join the list', 'Get the newsletter', 'No spam'","a consent checkbox or a small line mentioning privacy/unsubscribe","action URLs or scripts from Klaviyo, Mailchimp (list-manage.com), ConvertKit, Beehiiv"],"mcp":"Match the form to an existing form in Forms by its action/list id; otherwise create one with an email field (and first name if present) and reference it. Visible <label> → emailLabel (if only a placeholder exists, generate 'Email address' and flag it); button text → submitLabel; checkbox groups → interests with tags from their value attributes. A consent checkbox → consent:'checkbox' + its label as consentText; a fine-print line → 'notice'. Never inline the list id into the section.","pitfalls":["Placeholder-only fields fail accessibility; emailLabel is required for that reason.","A pre-ticked consent box is not valid consent under GDPR — the option says 'Unticked'.","Submit is a string, not `type: button`: our button unit is label + LINK, and a submit action has no link (gap: action buttons)."],"notCarried":["ai","category","variants"],"component":{"name":"Newsletter sign-up","slug":"newsletter","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"body","label":"Body","type":"longtext","recommendedMax":140,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"image","label":"Image","type":"image","helpText":"Card layout only.","ui":{"tab":{"id":"content"}}},{"key":"form","label":"Form","type":"form","helpText":"Sign-ups go to this form's list. Pick the one connected to your email tool.","required":true,"ui":{"tab":{"id":"content"}}},{"key":"emailLabel","label":"Field label","type":"text","helpText":"Read by screen readers. The Inline layout hides it.","required":true,"defaultValue":"Email address","config":{"maxLength":32},"ui":{"tab":{"id":"form","label":"Form & consent"}}},{"key":"placeholder","label":"Placeholder","type":"text","placeholder":"you@example.com","config":{"maxLength":40},"ui":{"tab":{"id":"form"}}},{"key":"submitLabel","label":"Button label","type":"text","required":true,"placeholder":"Subscribe","config":{"maxLength":20},"ui":{"tab":{"id":"form"}}},{"key":"interests","label":"Interests","type":"table","helpText":"Each one adds a tag to the subscriber.","openMode":"inline","config":{"maxItems":5,"fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":32}},{"key":"tag","label":"Tag","type":"text","helpText":"Exactly as named in your email tool.","required":true,"config":{"pattern":"^[a-z0-9-]+$","patternMessage":"Use lowercase letters, numbers and hyphens, like camp-kitchen."}}]},"ui":{"itemLabel":"Interest","icon":"tag","preview":{"title":"label","subtitle":"tag","fallback":"Untitled interest"},"tab":{"id":"form"}}},{"key":"consent","label":"Consent","type":"select","defaultValue":"checkbox","config":{"options":[{"label":"Unticked checkbox","value":"checkbox","description":"Required for the EU, UK and Canada"},{"label":"A notice under the button","value":"notice","description":"Signing up is the consent"}]},"ui":{"control":"radio","tab":{"id":"form"}}},{"key":"consentText","label":"Consent wording","type":"richtext","required":true,"ui":{"tab":{"id":"form"}}},{"key":"successMessage","label":"Thank-you message","type":"text","required":true,"placeholder":"You're in. Check your inbox to confirm.","config":{"maxLength":120},"ui":{"tab":{"id":"success","label":"After sign-up"}}},{"key":"welcomeCode","label":"Welcome code","type":"text","helpText":"Must exist in Shopify.","config":{"pattern":"^[A-Z0-9]{3,20}$","patternMessage":"Use capital letters and numbers, like SAVE20."},"ui":{"tab":{"id":"success"}}}],"description":"An email field, a promise and a clear consent line","category":"conversion","icon":"mail","preview":{"title":"heading","subtitle":"body","icon":"mail"}}},
|
|
1435
|
+
{"id":"conversion-lead-capture","name":"Lead capture form + success state","category":"Conversion & forms","kind":"block","tags":["form","references","conditional","repeater","validation","rich-text"],"proves":"Form reference + section-level presentation · success: message / redirect / file download as a conditional trio · hidden attribution fields with tokens","why":"The FORM (fields, validation, notifications, CRM sync, spam protection) is a form in Forms referenced here; everything the section adds is presentation and context: the pitch, the reasons, an optional submit-label override for this placement, hidden attribution fields with submit-time tokens, and the success state. Success is a conditional trio — message, redirect (best for analytics) or download (message + file) — because those are the three things marketers actually choose between. Hidden fields are advanced: most editors never need them.","htmlSignals":["a two-column section: persuasive copy + bullets on one side, a multi-field <form> on the other","form fields beyond email (company, phone, store type, message) and a submit button with an action verb","<input type=hidden> fields named utm_*, lead_source, page_url","a thank-you state in markup (hidden div), a redirect in the submit handler, or a gated PDF link","HubSpot (hsforms), Marketo (mktoForm), Typeform or Formspree embeds/actions"],"mcp":"Extract the form's fields into a form in Forms (or match an existing one by field names) and reference it — never copy fields into the section. Copy and bullets → heading/body/points. Hidden inputs → hiddenFields, converting server-rendered values to tokens where they're clearly UTM/page values. Detect the success path: a hidden success div → message; window.location / redirect param → redirect; a PDF link revealed after submit → download.","pitfalls":["A per-section copy of the fields forks submissions and notification rules.","Redirect pages must exist and be noindexed natively — never add a robots field to the section.","Token names in hidden fields are free strings; a typo silently sends an empty value."],"notCarried":["ai","category","variants"],"component":{"name":"Lead form","slug":"lead-form","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":56,"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"richtext","ui":{"tab":{"id":"content"}}},{"key":"points","label":"Reasons to apply","type":"table","openMode":"inline","config":{"maxItems":4,"fields":[{"key":"icon","label":"Icon","type":"icon"},{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":56}}]},"ui":{"itemLabel":"Reason","icon":"check","preview":{"title":"label","fallback":"Untitled reason"},"tab":{"id":"content"}}},{"key":"form","label":"Form","type":"form","helpText":"Fields, notifications and submissions are managed in Forms.","required":true,"ui":{"tab":{"id":"form","label":"Form"}}},{"key":"submitLabel","label":"Button label","type":"text","helpText":"Overrides the form's own label here only.","config":{"maxLength":24},"ui":{"tab":{"id":"form"}}},{"key":"hiddenFields","label":"Hidden fields","type":"table","helpText":"Not shown. Sent with every submission from this page.","helpMore":"Tokens: {{utm_source}}, {{utm_campaign}}, {{page_url}}, {{referrer}}.","advanced":true,"openMode":"inline","config":{"maxItems":6,"fields":[{"key":"name","label":"Field key","type":"text","required":true,"config":{"pattern":"^[a-z_]+$","patternMessage":"Use lowercase letters and underscores, like analytics_storage."}},{"key":"value","label":"Value","type":"text","required":true,"config":{"maxLength":80}}]},"ui":{"itemLabel":"Hidden field","icon":"eye-off","preview":{"title":"name","subtitle":"value","fallback":"Untitled hidden field"},"tab":{"id":"form"}}},{"key":"onSuccess","label":"Then","type":"select","defaultValue":"message","config":{"options":[{"label":"Show a message","value":"message","icon":"message-check"},{"label":"Go to a page","value":"redirect","icon":"arrow-right","description":"Best for tracking conversions"},{"label":"Start a download","value":"download","icon":"download"}]},"ui":{"control":"cards","tab":{"id":"success","label":"After sending"}}},{"key":"successMessage","label":"Message","type":"richtext","required":true,"showIf":{"match":"all","rules":[{"field":"onSuccess","op":"in","value":["message","download"]}]},"ui":{"tab":{"id":"success"}}},{"key":"redirect","label":"Page","type":"link","required":true,"showIf":{"match":"all","rules":[{"field":"onSuccess","op":"eq","value":"redirect"}]},"ui":{"tab":{"id":"success"}}},{"key":"file","label":"File","type":"file","helpText":"A PDF under 10 MB. The link is also emailed to them.","required":true,"showIf":{"match":"all","rules":[{"field":"onSuccess","op":"eq","value":"download"}]},"ui":{"tab":{"id":"success"}}},{"key":"privacyNote","label":"Privacy note","type":"richtext","ui":{"tab":{"id":"form"}}}],"description":"A pitch beside a form, with what happens after they send it","category":"conversion","icon":"forms","preview":{"title":"heading","subtitle":"form","icon":"forms"}}},
|
|
1436
|
+
{"id":"conversion-multi-step-form","name":"Multi-step form","category":"Conversion & forms","kind":"block","tags":["form","polymorphic-array","deep-nesting","conditional","validation","references","a11y"],"proves":"form grouped into steps by field key OR steps › polymorphic fields › options built here · visitor-side showIf branching · progress UI","why":"Two honest paths. The recommended one references a form in Forms and only GROUPS its fields into steps by key (presentation stays here, data stays in Forms). The 'Built here' path exists for one-off forms and shows the full inline shape: steps › fields (a polymorphic array of text | choice | scale | consent) › answers — 4 drill levels, each field type carrying only the settings it needs. Branching ('ask for the surgery date only if they had surgery') is visitor-side DATA in `showIf`, deliberately separate from schema `hidden`, which only controls what the editor sees. The sample is a clinic intake, so 'Remember answers' is off — health data shouldn't sit in a browser.","htmlSignals":["one <form> split into fieldsets/panels with Next/Back buttons and only one visible at a time","a progress bar, step indicator (1 · 2 · 3) or 'Step 2 of 3'","fields that appear when an earlier answer changes (data-show-if, x-show, conditional rendering)","Typeform/Jotform/Tally-style one-question-per-screen layouts"],"mcp":"Prefer source:'form': create or match a form in Forms with ALL fields (flattened), then emit formSteps with each panel's field names as fieldKeys. Use source:'inline' only when asked for a one-off. Map inputs: text/email/tel/date/textarea → textField with format; radio/checkbox groups → choiceField (+ options with value keys); range inputs → scaleField; consent checkboxes → consentField. Translate show/hide scripts into showIf on the dependent field (equals / not / answered). Health, financial or ID fields → set saveProgress:false and flag the form for a compliance review.","pitfalls":["Schema conditions can't read a parent's value (gap), so the two step arrays sit side by side at the root instead of one array whose items change shape.","`showIf.field` is a string key, not a reference to an earlier field (gap: references within the same entry) — renaming a key breaks the branch silently.","Inline-built forms fork submissions and notifications from the Forms area; keep them rare.","Persisting answers in localStorage is a privacy risk for health data."],"notCarried":["category"],"component":{"name":"Multi-step form","slug":"multi-step-form","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":160,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"source","label":"Fields come from","type":"select","defaultValue":"form","config":{"options":[{"label":"A form in Forms","value":"form","icon":"forms","description":"Recommended: one set of fields everywhere"},{"label":"Built here","value":"inline","icon":"pencil","description":"Only for a one-off form"}]},"ui":{"control":"cards","tab":{"id":"steps","label":"Steps"}}},{"key":"form","label":"Form","type":"form","helpText":"Its fields are grouped into the steps below by key.","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"form"}]},"ui":{"tab":{"id":"steps"}}},{"key":"formSteps","label":"Steps","type":"table","helpText":"Fields not placed in a step go in the last one.","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"form"}]},"config":{"minItems":2,"maxItems":6,"fields":[{"key":"title","label":"Step title","type":"text","required":true,"config":{"maxLength":32}},{"key":"description","label":"Description","type":"text","config":{"maxLength":100}},{"key":"fieldKeys","label":"Fields","type":"array","helpText":"The form's field keys, in order, e.g. first_name.","config":{"itemType":"text","minItems":1},"ui":{"itemLabel":"Field"}}]},"ui":{"itemLabel":"Step","icon":"list-numbers","preview":{"title":"title","subtitle":"fieldKeys","fallback":"Untitled step"},"tab":{"id":"steps"}}},{"key":"steps","label":"Steps","type":"table","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"inline"}]},"config":{"minItems":2,"maxItems":6,"fields":[{"key":"title","label":"Step title","type":"text","required":true,"config":{"maxLength":32}},{"key":"description","label":"Description","type":"text","config":{"maxLength":100}},{"key":"fields","label":"Fields","type":"table","config":{"mode":"types","minItems":1,"maxItems":8,"itemTypes":[{"name":"textField","label":"Text","fields":[{"key":"key","label":"Key","type":"text","helpText":"The column name in submissions.","required":true,"config":{"pattern":"^[a-z_]+$","patternMessage":"Use lowercase letters and underscores, like analytics_storage."}},{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":60}},{"key":"format","label":"Format","type":"select","defaultValue":"text","options":[{"label":"Text","value":"text"},{"label":"Email","value":"email"},{"label":"Phone","value":"phone"},{"label":"Date","value":"date"},{"label":"Paragraph","value":"long"}]},{"key":"isRequired","label":"Required","type":"boolean"},{"key":"showIf","label":"Only show if","type":"object","helpText":"Leave empty to always show it.","openMode":"popover","fields":[{"key":"field","label":"Answer to","type":"text","helpText":"The key of an earlier question or field.","config":{"maxLength":32}},{"key":"operator","label":"Condition","type":"select","defaultValue":"equals","options":[{"label":"Is","value":"equals"},{"label":"Is not","value":"not"},{"label":"Has any answer","value":"answered"}]},{"key":"value","label":"Value","type":"text","helpText":"The answer's key, e.g. self-pay.","showIf":{"match":"all","rules":[{"field":"operator","op":"in","value":["equals","not"]}]},"config":{"maxLength":48}}],"ui":{"icon":"git-branch"}}],"icon":"forms","preview":{"title":"label","subtitle":"key","fallback":"Untitled text field"}},{"name":"choiceField","label":"Choice","fields":[{"key":"key","label":"Key","type":"text","required":true,"config":{"pattern":"^[a-z_]+$","patternMessage":"Use lowercase letters and underscores, like analytics_storage."}},{"key":"label","label":"Question","type":"text","required":true,"config":{"maxLength":80}},{"key":"allowMultiple","label":"Allow several answers","type":"boolean"},{"key":"options","label":"Answers","type":"table","openMode":"inline","fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":48}},{"key":"value","label":"Key","type":"text","required":true,"config":{"pattern":"^[a-z0-9-]+$","patternMessage":"Use lowercase letters, numbers and hyphens, like camp-kitchen."}}],"config":{"minItems":2,"maxItems":12},"ui":{"itemLabel":"Answer","itemName":"choice","icon":"point","preview":{"title":"label","subtitle":"value","fallback":"Untitled answer"}}},{"key":"isRequired","label":"Required","type":"boolean"},{"key":"showIf","label":"Only show if","type":"object","helpText":"Leave empty to always show it.","openMode":"popover","fields":[{"key":"field","label":"Answer to","type":"text","helpText":"The key of an earlier question or field.","config":{"maxLength":32}},{"key":"operator","label":"Condition","type":"select","defaultValue":"equals","options":[{"label":"Is","value":"equals"},{"label":"Is not","value":"not"},{"label":"Has any answer","value":"answered"}]},{"key":"value","label":"Value","type":"text","helpText":"The answer's key, e.g. self-pay.","showIf":{"match":"all","rules":[{"field":"operator","op":"in","value":["equals","not"]}]},"config":{"maxLength":48}}],"ui":{"icon":"git-branch"}}],"icon":"list-check","preview":{"title":"label","subtitle":"key","fallback":"Untitled choice field"}},{"name":"scaleField","label":"Scale","fields":[{"key":"key","label":"Key","type":"text","required":true,"config":{"pattern":"^[a-z_]+$","patternMessage":"Use lowercase letters and underscores, like analytics_storage."}},{"key":"label","label":"Question","type":"text","required":true,"config":{"maxLength":80}},{"key":"min","label":"From","type":"number","defaultValue":0},{"key":"max","label":"To","type":"number","defaultValue":10},{"key":"minLabel","label":"Low end means","type":"text","placeholder":"No pain","config":{"maxLength":24}},{"key":"maxLabel","label":"High end means","type":"text","placeholder":"Worst pain imaginable","config":{"maxLength":24}},{"key":"showIf","label":"Only show if","type":"object","helpText":"Leave empty to always show it.","openMode":"popover","fields":[{"key":"field","label":"Answer to","type":"text","helpText":"The key of an earlier question or field.","config":{"maxLength":32}},{"key":"operator","label":"Condition","type":"select","defaultValue":"equals","options":[{"label":"Is","value":"equals"},{"label":"Is not","value":"not"},{"label":"Has any answer","value":"answered"}]},{"key":"value","label":"Value","type":"text","helpText":"The answer's key, e.g. self-pay.","showIf":{"match":"all","rules":[{"field":"operator","op":"in","value":["equals","not"]}]},"config":{"maxLength":48}}],"ui":{"icon":"git-branch"}}],"icon":"adjustments-horizontal","preview":{"title":"label","subtitle":"key","fallback":"Untitled scale field"}},{"name":"consentField","label":"Consent","fields":[{"key":"key","label":"Key","type":"text","required":true,"config":{"pattern":"^[a-z_]+$","patternMessage":"Use lowercase letters and underscores, like analytics_storage."}},{"key":"text","label":"Wording","type":"richtext","required":true},{"key":"isRequired","label":"Must be ticked to send","type":"boolean","defaultValue":true}],"icon":"shield-check","preview":{"title":"key","fallback":"Untitled consent field"}}]},"ui":{"itemLabel":"Field"}}]},"ui":{"itemLabel":"Step","icon":"list-numbers","preview":{"title":"title","subtitle":"description","fallback":"Untitled step"},"tab":{"id":"steps"}}},{"key":"progress","label":"Progress","type":"select","defaultValue":"steps","config":{"options":[{"label":"Bar","value":"bar"},{"label":"Step names","value":"steps"},{"label":"Step 2 of 4","value":"count"}]},"ui":{"control":"segmented","tab":{"id":"settings","label":"Settings"}}},{"key":"useSaveProgress","label":"Remember answers","type":"boolean","helpText":"Keeps answers on this device for 24 hours.","defaultValue":true,"ui":{"tab":{"id":"settings"}}},{"key":"submitLabel","label":"Last button label","type":"text","required":true,"placeholder":"Send","config":{"maxLength":24},"ui":{"tab":{"id":"settings"}}},{"key":"successMessage","label":"Thank-you message","type":"richtext","required":true,"ui":{"tab":{"id":"settings"}}}],"description":"A long form split into short steps with a progress bar","category":"conversion","icon":"list-check","preview":{"title":"heading","subtitle":"source","icon":"list-numbers"}}},
|
|
1437
|
+
{"id":"conversion-booking-embed","name":"Booking / reservations embed","category":"Conversion & forms","kind":"block","tags":["embed","conditional","repeater","validation","media-heavy","rich-text"],"proves":"Provider select → provider-specific ids (conditional) · experiences array with party-size range and deposits · large-party handoff","why":"Booking is an EMBED with content around it. The provider select drives exactly one conditional id field (Resy venue number, OpenTable rid, Tock slug, Cal.com link — each validated in its own format) so editors never paste raw script tags. Experiences are editorial cards (photo, length, party range, price, deposit) that open the widget on a developer-locked booking type. Large parties are handed off to a private-dining page above a threshold, which is how restaurants actually avoid six-top no-shows.","htmlSignals":["scripts/iframes from widgets.resy.com, opentable.com/widget, exploretock.com, cal.com/embed, calendly.com","a 'Reserve', 'Book a table' or 'Book an appointment' heading with a date/party-size picker","cards for different seatings or services with durations and per-person prices","cancellation/deposit policy text near the widget"],"mcp":"Identify the provider from the script/iframe host and extract ONLY the id (data-venue-id / rid / slug / cal link) — never store the script. Seating/service cards → experiences (price per person and party size parsed from copy). A 'larger parties' link → largePartyFrom + largePartyButton. Policy paragraphs → policy (rich text).","pitfalls":["Storing the widget script as HTML makes it uneditable and a security risk; store ids only.","partyMin ≤ partyMax is a cross-field rule the schema can't express (gap).","Widget heights vary by provider; the renderer reserves space to avoid layout shift."],"notCarried":["category"],"component":{"name":"Booking","slug":"booking","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":180,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"provider","label":"Provider","type":"select","required":true,"config":{"options":[{"label":"Resy","value":"resy"},{"label":"OpenTable","value":"opentable"},{"label":"Tock","value":"tock"},{"label":"Cal.com","value":"calcom","description":"For appointments and consultations"}]},"ui":{"tab":{"id":"provider","label":"Booking provider"}}},{"key":"resyVenueId","label":"Resy venue ID","type":"number","helpText":"The number in your Resy widget code, e.g. 58421.","required":true,"showIf":{"match":"all","rules":[{"field":"provider","op":"eq","value":"resy"}]},"ui":{"tab":{"id":"provider"}}},{"key":"openTableRid","label":"OpenTable restaurant ID","type":"number","helpText":"The rid in your OpenTable widget code.","required":true,"showIf":{"match":"all","rules":[{"field":"provider","op":"eq","value":"opentable"}]},"ui":{"tab":{"id":"provider"}}},{"key":"tockSlug","label":"Tock page","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"provider","op":"eq","value":"tock"}]},"config":{"pattern":"^[a-z0-9-]+$","patternMessage":"Use the part after exploretock.com/, like caldera-kitchen."},"ui":{"tab":{"id":"provider"}}},{"key":"calLink","label":"Cal.com link","type":"url","required":true,"showIf":{"match":"all","rules":[{"field":"provider","op":"eq","value":"calcom"}]},"config":{"pattern":"^https://cal\\.com/","patternMessage":"Use a cal.com link, like https://cal.com/caldera/demo."},"ui":{"tab":{"id":"provider"}}},{"key":"display","label":"Shows as","type":"select","defaultValue":"inline","config":{"options":[{"label":"Inline widget","value":"inline"},{"label":"Button that opens it","value":"button"}]},"ui":{"control":"segmented","tab":{"id":"provider"}}},{"key":"experiences","label":"Experiences","type":"table","config":{"maxItems":6,"fields":[{"key":"name","label":"Name","type":"text","required":true,"config":{"maxLength":40}},{"key":"description","label":"Description","type":"longtext","recommendedMax":160,"ui":{"rows":2}},{"key":"image","label":"Image","type":"image"},{"key":"duration","label":"Length","type":"number","config":{"min":15,"max":360,"step":15,"unit":"min"}},{"key":"partyMin","label":"Smallest party","type":"number","config":{"min":1,"max":20,"step":1,"unit":"guests"}},{"key":"partyMax","label":"Largest party","type":"number","config":{"min":1,"max":40,"step":1,"unit":"guests"}},{"key":"pricePerPerson","label":"Price per person","type":"number","helpText":"0 or empty for à la carte.","config":{"unit":"USD","min":0,"step":1}},{"key":"hasDeposit","label":"Takes a deposit","type":"boolean"},{"key":"bookingType","label":"Booking type ID","type":"text","helpText":"Opens the widget straight on this experience.","readOnly":"developer","lockReason":"Matches the provider's booking type","ui":{"access":{"edit":"schema.manage"}}}]},"ui":{"itemLabel":"Experience","icon":"tools-kitchen-2","preview":{"title":"name","subtitle":"duration","media":"image","fallback":"Untitled experience"},"tab":{"id":"experiences","label":"What can be booked"}}},{"key":"largePartyFrom","label":"Large parties from","type":"number","helpText":"Parties this size or bigger see the button below instead.","config":{"min":6,"max":40,"step":1,"unit":"guests"},"ui":{"tab":{"id":"content"}}},{"key":"largePartyButton","label":"Large party button","type":"button","showIf":{"match":"all","rules":[{"field":"largePartyFrom","op":"notEmpty"}]},"ui":{"tab":{"id":"content"}}},{"key":"policy","label":"Booking policy","type":"richtext","ui":{"tab":{"id":"content"}}},{"key":"phone","label":"Phone","type":"link","ui":{"tab":{"id":"content"}}}],"description":"Reservations or appointments from your booking provider","category":"conversion","icon":"calendar-time","preview":{"title":"heading","subtitle":"provider","icon":"calendar-event"}}},
|
|
1438
|
+
{"id":"conversion-waitlist-referral","name":"Waitlist with referral rewards","category":"Conversion & forms","kind":"block","tags":["form","references","conditional","repeater","validation","media-heavy"],"proves":"Reuses one form with a list tag · referral reward ladder · share channels + message token · an honest signup counter with a minimum","why":"A waitlist is a sign-up with a GOAL. Instead of a new form per launch, it reuses the newsletter form and stamps a `listTag`, so launch emails go only to this list while consent and sync stay central. Referral rewards are a small inline ladder (friends → reward), share channels a multiselect, and the share message carries a {{link}} token. The counter is a toggle with a minimum, never a typed number — social proof that's live and true or not shown at all.","htmlSignals":["'Join the waitlist', 'Get early access', 'Notify me' with an email form for a product not yet on sale","a referral panel after signup: personal link, share buttons, 'You're #1,284 in line'","a reward ladder ('Refer 3 friends to…')","a 'n people have joined' counter","scripts from Viral Loops, Prefinery, GetWaitlist, KickoffLabs"],"mcp":"Match the form to the existing newsletter/marketing form and set listTag from the list/segment/tag in the embed config (or slugify the product name). Never import a hard-coded 'n people joined' number — set counter:true and let the renderer count; if the count is a fake static number, flag it. Reward copy → rewards with friends parsed from the number.","pitfalls":["One form per launch fragments consent records; tag the shared list instead.","Static 'join 12,000 others' numbers are often invented; the schema doesn't allow typing one.","{{position}} only makes sense with referrals on; the editor warns when it's used without them."],"notCarried":["ai","category"],"component":{"name":"Waitlist","slug":"waitlist","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","recommendedMax":220,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"image","label":"Image","type":"image","required":true,"ui":{"tab":{"id":"content"}}},{"key":"launch","label":"Launch window","type":"text","helpText":"Words, not a date, until the date is certain.","placeholder":"Spring 2027","config":{"maxLength":32},"ui":{"tab":{"id":"content"}}},{"key":"form","label":"Form","type":"form","helpText":"Sign-ups join this form's list with the tag below.","required":true,"ui":{"tab":{"id":"form","label":"Sign-up"}}},{"key":"listTag","label":"List tag","type":"text","helpText":"Tags everyone who joins here.","required":true,"config":{"pattern":"^[a-z0-9-]+$","patternMessage":"Use lowercase letters, numbers and hyphens, like camp-kitchen."},"ui":{"tab":{"id":"form"}}},{"key":"submitLabel","label":"Button label","type":"text","required":true,"placeholder":"Join the waitlist","config":{"maxLength":24},"ui":{"tab":{"id":"form"}}},{"key":"showCounter","label":"Show how many have joined","type":"boolean","helpText":"A live count from the form's submissions.","ui":{"tab":{"id":"form"}}},{"key":"counterMin","label":"Only once it passes","type":"number","helpText":"Hides the count while it's below this.","showIf":{"match":"all","rules":[{"field":"showCounter","op":"eq","value":"true"}]},"config":{"min":0,"step":50,"unit":"sign-ups"},"ui":{"tab":{"id":"form"}}},{"key":"useReferrals","label":"Referral rewards","type":"boolean","ui":{"tab":{"id":"referral","label":"Referrals"}}},{"key":"rewards","label":"Rewards","type":"table","showIf":{"match":"all","rules":[{"field":"useReferrals","op":"eq","value":"true"}]},"openMode":"inline","config":{"minItems":1,"maxItems":4,"fields":[{"key":"friends","label":"Friends who join","type":"number","required":true,"config":{"min":1,"max":50,"step":1}},{"key":"reward","label":"Reward","type":"text","required":true,"config":{"maxLength":48}}]},"ui":{"itemLabel":"Reward","icon":"gift","preview":{"title":"reward","subtitle":"friends","fallback":"Untitled reward"},"tab":{"id":"referral"}}},{"key":"channels","label":"Share buttons","type":"multiselect","showIf":{"match":"all","rules":[{"field":"useReferrals","op":"eq","value":"true"}]},"config":{"options":[{"label":"Copy link","value":"copy"},{"label":"Email","value":"email"},{"label":"Text message","value":"sms"},{"label":"WhatsApp","value":"whatsapp"},{"label":"X","value":"x"}]},"ui":{"control":"checkbox","tab":{"id":"referral"}}},{"key":"shareMessage","label":"Share message","type":"longtext","helpText":"{{link}} becomes their personal link.","showIf":{"match":"all","rules":[{"field":"useReferrals","op":"eq","value":"true"}]},"config":{"maxLength":200},"ui":{"rows":2,"tab":{"id":"referral"}}},{"key":"successMessage","label":"After joining","type":"richtext","helpText":"{{position}} shows their place in line when referrals are on.","required":true,"ui":{"tab":{"id":"content"}}}],"description":"Join-the-list for a launch, with rewards for sharing","category":"conversion","icon":"user-plus","preview":{"title":"heading","subtitle":"launch","media":"image"}}},
|
|
1439
|
+
{"id":"conversion-contact-section","name":"Contact section","category":"Conversion & forms","kind":"block","tags":["form","references","repeater","conditional","embed","variants","a11y"],"proves":"Channel cards with typed links (email / phone / chat / visit) · form reference · optional map pin + address · layout variants","why":"A contact section is a directory, a form and a place. Channels are an array of small objects with a typed `link` (email, phone, section, page — the link unit already knows mailto/tel), plus hours and an honest reply time. The form is a reference (topic routing and notifications belong to the form in Forms, not to this block, so no `topics` field here) and is optional for the channels-only layout. The map is an opt-in cluster: a geopoint for the pin and a written address, because a map alone is inaccessible.","htmlSignals":["mailto: and tel: links grouped with labels like 'Customer service', 'Press', 'Wholesale'","a contact <form> with name/email/message (often a topic select)","an embedded map iframe (google.com/maps/embed) or static map image with an address block","opening hours and 'we reply within…' copy"],"mcp":"Each mailto:/tel:/chat trigger → one channel (kind from the scheme, label from the nearest heading, detail = visible text). The <form> → match/create a form in Forms and reference it; a topic <select> becomes a field ON that form (with routing set in Forms), not a section field. Map iframes → showMap:true, parse lat/lng from the embed URL (q= or !3d!4d) into location, and copy the address text.","pitfalls":["Email addresses and phone numbers duplicated across header, footer and contact drift apart — prefer site settings + references (gap: global values referenced from sections).","Address must be text as well as a pin; maps aren't accessible on their own."],"notCarried":["category","variants"],"component":{"name":"Contact","slug":"contact-section","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":160,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"channels","label":"Channels","type":"table","config":{"minItems":1,"maxItems":5,"fields":[{"key":"kind","label":"Type","type":"select","defaultValue":"email","options":[{"label":"Email","value":"email","icon":"mail"},{"label":"Phone","value":"phone","icon":"phone"},{"label":"Chat","value":"chat","icon":"message"},{"label":"Visit","value":"visit","icon":"map-pin"}],"ui":{"control":"segmented"}},{"key":"label","label":"Label","type":"text","required":true,"placeholder":"Orders and returns","config":{"maxLength":32}},{"key":"link","label":"Link","type":"link","helpText":"An email, phone number or page. #chat opens the chat widget.","required":true},{"key":"detail","label":"Detail","type":"text","config":{"maxLength":60}},{"key":"hours","label":"Hours","type":"text","placeholder":"Mon–Fri, 8am–5pm PT","config":{"maxLength":48}},{"key":"replyTime","label":"Reply time","type":"text","placeholder":"Within a day","config":{"maxLength":32}}]},"ui":{"itemLabel":"Channel","icon":"message-circle","preview":{"title":"label","subtitle":"detail","fallback":"Untitled channel"},"tab":{"id":"channels","label":"Channels"}}},{"key":"form","label":"Form","type":"form","helpText":"Edit its fields and who gets notified in Forms.","ui":{"tab":{"id":"content"}}},{"key":"showMap","label":"Show a map","type":"boolean","ui":{"tab":{"id":"visit","label":"Visit us"}}},{"key":"location","label":"Map pin","type":"location","required":true,"showIf":{"match":"all","rules":[{"field":"showMap","op":"eq","value":"true"}]},"ui":{"tab":{"id":"visit"}}},{"key":"address","label":"Address","type":"longtext","showIf":{"match":"all","rules":[{"field":"showMap","op":"eq","value":"true"}]},"ui":{"rows":3,"tab":{"id":"visit"}}}],"description":"Ways to reach you, a form and where to find you","category":"conversion","icon":"address-book","preview":{"title":"heading","subtitle":"intro","icon":"address-book"}}},
|
|
1440
|
+
{"id":"conversion-quiz-product-finder","name":"Quiz / product finder","category":"Conversion & forms","kind":"block","tags":["deep-nesting","conditional","references","repeater","validation","form","personalization","media-heavy","commerce"],"proves":"Six drill levels: questions › answers › scores › product refs · visitor-side branching (showIf) · results mapped to products with a fallback · optional email gate","why":"The deepest conversion pattern: section › questions › question › answers › answer › scores › score (+ a product reference inside). Scoring beats decision trees for product finders because answers can push several products up or rule one out (negative points), and it survives adding a product without rewriting branches. Branching is still possible with a visitor-side `showIf` on a question. Results are COPY keyed by product (products without copy fall back to their tagline) plus a required fallback, so the quiz never ends empty. The email gate is optional and never blocks results.","htmlSignals":["a 'Take the quiz' / 'Find your…' start screen, then one question per screen with large answer buttons or picture cards","a progress indicator (Question 2 of 4) and Back button","a results screen with 1–3 recommended products and 'why it suits you' copy","scripts from Octane AI, RevenueHunt, Typeform, Jebbit, or a JSON config with questions/answers/weights"],"mcp":"If a quiz config is reachable (Octane/RevenueHunt JSON, inline script), map it directly: questions → questions with generated keys, answer weights/tags → scores with product references (resolve product handles to Products entries). If only markup is visible, import questions and answers and leave scores EMPTY for a merchandiser to fill — never guess weights. Result screens → results (copy per product). Email steps → useEmailGate + a Forms reference.","pitfalls":["`showIf.field` and answer keys are strings, not references; renaming a key silently breaks branching (gap: references within the same entry, with rename-tracking).","Picture-card images can't be shown/hidden by the PARENT question's style (gap: parent-scoped conditions), so the image field is always visible with an explaining hint.","A gate that hides results until an email is given is a dark pattern; results always show."],"notCarried":["ai","category"],"component":{"name":"Product finder quiz","slug":"product-quiz","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"ui":{"tab":{"id":"content","label":"Intro"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":160,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"image","label":"Image","type":"image","ui":{"tab":{"id":"content"}}},{"key":"startLabel","label":"Start button label","type":"text","required":true,"placeholder":"Find my pan","config":{"maxLength":24},"ui":{"tab":{"id":"content"}}},{"key":"questions","label":"Questions","type":"table","config":{"minItems":2,"maxItems":8,"fields":[{"key":"key","label":"Key","type":"text","helpText":"Used by \"Only show if\" on later questions.","required":true,"unique":true,"config":{"pattern":"^[a-z_]+$","patternMessage":"Use lowercase letters and underscores, like analytics_storage."}},{"key":"question","label":"Question","type":"text","required":true,"config":{"maxLength":90}},{"key":"help","label":"Hint","type":"text","config":{"maxLength":90}},{"key":"style","label":"Answers look like","type":"select","defaultValue":"buttons","options":[{"label":"Buttons","value":"buttons"},{"label":"Picture cards","value":"images"}],"ui":{"control":"segmented"}},{"key":"allowMultiple","label":"Allow several answers","type":"boolean"},{"key":"answers","label":"Answers","type":"table","fields":[{"key":"key","label":"Key","type":"text","required":true,"config":{"pattern":"^[a-z0-9-]+$","patternMessage":"Use lowercase letters, numbers and hyphens, like camp-kitchen."}},{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":48}},{"key":"image","label":"Image","type":"image","helpText":"Used when answers show as picture cards."},{"key":"scores","label":"Points","type":"table","helpText":"The highest total wins.","openMode":"inline","fields":[{"key":"product","label":"Product","type":"reference","required":true,"config":{"contentModelId":"col_products"}},{"key":"points","label":"Points","type":"number","helpText":"Negative points rule a product out.","defaultValue":1,"config":{"min":-5,"max":5,"step":1}}],"config":{"maxItems":6},"ui":{"itemLabel":"Score","icon":"plus-minus","preview":{"title":"product","subtitle":"points","fallback":"Untitled score"}}}],"config":{"minItems":2,"maxItems":6},"ui":{"itemLabel":"Answer","icon":"point","preview":{"title":"label","subtitle":"key","media":"image","fallback":"Untitled answer"}}},{"key":"showIf","label":"Only show if","type":"object","helpText":"Leave empty to always show it.","openMode":"popover","fields":[{"key":"field","label":"Answer to","type":"text","helpText":"The key of an earlier question or field.","config":{"maxLength":32}},{"key":"operator","label":"Condition","type":"select","defaultValue":"equals","options":[{"label":"Is","value":"equals"},{"label":"Is not","value":"not"},{"label":"Has any answer","value":"answered"}]},{"key":"value","label":"Value","type":"text","helpText":"The answer's key, e.g. self-pay.","showIf":{"match":"all","rules":[{"field":"operator","op":"in","value":["equals","not"]}]},"config":{"maxLength":48}}],"ui":{"icon":"git-branch"}}]},"ui":{"itemLabel":"Question","icon":"help-circle","preview":{"title":"question","subtitle":"key","fallback":"Untitled question"},"tab":{"id":"questions","label":"Questions"}}},{"key":"resultsHeading","label":"Results heading","type":"text","placeholder":"Your match","config":{"maxLength":48},"ui":{"tab":{"id":"results","label":"Results"}}},{"key":"showTop","label":"Products to show","type":"number","defaultValue":1,"config":{"min":1,"max":3,"step":1},"ui":{"control":"slider","tab":{"id":"results"}}},{"key":"results","label":"Result copy","type":"table","config":{"maxItems":12,"fields":[{"key":"product","label":"Product","type":"reference","required":true,"config":{"contentModelId":"col_products"}},{"key":"heading","label":"Heading","type":"text","config":{"maxLength":60}},{"key":"why","label":"Why it fits","type":"richtext"}]},"ui":{"itemLabel":"Result","icon":"award","preview":{"title":"heading","subtitle":"product","fallback":"Untitled result"},"tab":{"id":"results"}}},{"key":"fallback","label":"If nothing scores","type":"reference","helpText":"Recommended when every product scores zero or less.","required":true,"config":{"contentModelId":"col_products"},"ui":{"tab":{"id":"results"}}},{"key":"retakeLabel","label":"Retake label","type":"text","placeholder":"Start over","config":{"maxLength":24},"ui":{"tab":{"id":"results"}}},{"key":"useEmailGate","label":"Ask for an email before results","type":"boolean","helpText":"Results are always shown, even if they skip.","ui":{"tab":{"id":"capture","label":"Email"}}},{"key":"form","label":"Form","type":"form","helpText":"Answers are saved with the email on this form.","required":true,"showIf":{"match":"all","rules":[{"field":"useEmailGate","op":"eq","value":"true"}]},"ui":{"tab":{"id":"capture"}}},{"key":"emailPrompt","label":"Prompt","type":"text","showIf":{"match":"all","rules":[{"field":"useEmailGate","op":"eq","value":"true"}]},"placeholder":"Want your results and a care guide by email?","config":{"maxLength":90},"ui":{"tab":{"id":"capture"}}}],"description":"A few questions that recommend the right product","category":"conversion","icon":"zoom-question","preview":{"title":"heading","subtitle":"intro","media":"image"}}},
|
|
1441
|
+
{"id":"conversion-exit-intent-popup","name":"Exit-intent popup","category":"Conversion & forms","kind":"global","tags":["conditional","personalization","form","references","validation","a11y","media-heavy"],"proves":"Trigger rules (exit / delay / scroll / click) with per-trigger inputs · page targeting · frequency capping · offer as code / form / link","why":"A popup isn't a page section — it's a site-wide GLOBAL with rules, so `kind: 'global'`. Three rule clusters, each a select with conditional inputs: the trigger (exit / delay / scroll / click — the click selector is developer-locked), targeting (included and excluded paths as string lists, audience, devices; checkout excluded by default), and frequency capping (per visit / every N days / once). The offer is a conditional trio — email-for-a-code (a Forms reference + the code revealed after sign-up), a bare code, or a button. The decline text is required and its help bans confirmshaming.","htmlSignals":["a hidden role=dialog / aria-modal element with an overlay, revealed by a script on mouseleave/visibilitychange/scroll/timeout","copy like 'Wait!', 'Before you go', 'Get 10% off your first order'","an email input + a code reveal after submit","a decline link ('No thanks') and a close button","scripts from Klaviyo forms, Privy, Justuno, OptinMonster, Sumo"],"mcp":"Create ONE popup global per dialog. Read trigger settings from the script/app config (exitIntent, delay ms → seconds, scroll %) and targeting from its URL rules; frequency from cookie/localStorage expiry (days). Email forms → match the form in Forms and reference it. Confirmshaming decline copy should be imported as-is but flagged with a suggested neutral rewrite.","pitfalls":["Full-screen popups on phones count as intrusive interstitials for Google; the size help says so.","Never show on /checkout — it's in the default excludes.","Focus must move into the dialog and return on close; that's the renderer's job, so there's no field for it."],"notCarried":["ai","category","kind:global"],"component":{"name":"Popup","slug":"popup","props":[{"key":"name","label":"Popup name","type":"text","helpText":"Only your team sees this.","required":true,"config":{"maxLength":48},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"image","label":"Image","type":"image","ui":{"tab":{"id":"content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":40,"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","recommendedMax":140,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"offer","label":"Offer","type":"select","defaultValue":"form","config":{"options":[{"label":"Email for a code","value":"form"},{"label":"Show a code","value":"code"},{"label":"A button","value":"link"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"form","label":"Form","type":"form","helpText":"They get the code after signing up through this form.","required":true,"showIf":{"match":"all","rules":[{"field":"offer","op":"eq","value":"form"}]},"ui":{"tab":{"id":"content"}}},{"key":"code","label":"Discount code","type":"text","helpText":"Must exist in Shopify. With a form, it's shown after sign-up.","required":true,"showIf":{"match":"all","rules":[{"field":"offer","op":"in","value":["form","code"]}]},"config":{"pattern":"^[A-Z0-9]{3,20}$","patternMessage":"Use capital letters and numbers, like SAVE20."},"ui":{"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","showIf":{"match":"all","rules":[{"field":"offer","op":"eq","value":"link"}]},"ui":{"tab":{"id":"content"}}},{"key":"declineLabel","label":"Close link text","type":"text","required":true,"defaultValue":"No thanks","config":{"maxLength":24},"ui":{"tab":{"id":"content"}}},{"key":"trigger","label":"Show when","type":"select","defaultValue":"exit","config":{"options":[{"label":"They're about to leave","value":"exit","icon":"door-exit","description":"Mouse heads for the tab bar; on phones, a fast scroll up"},{"label":"After some time","value":"delay","icon":"clock"},{"label":"After scrolling","value":"scroll","icon":"arrow-down"},{"label":"They click something","value":"click","icon":"click"}]},"ui":{"control":"cards","tab":{"id":"trigger","label":"When"}}},{"key":"delaySeconds","label":"After","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"trigger","op":"eq","value":"delay"}]},"config":{"min":5,"max":120,"step":5,"unit":"seconds"},"ui":{"tab":{"id":"trigger"}}},{"key":"scrollPercent","label":"After scrolling","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"trigger","op":"eq","value":"scroll"}]},"config":{"min":10,"max":90,"step":10,"unit":"%"},"ui":{"control":"slider","tab":{"id":"trigger"}}},{"key":"clickSelector","label":"Opens from","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"trigger","op":"eq","value":"click"}]},"readOnly":"developer","lockReason":"A CSS selector set by the developer","ui":{"access":{"view":"schema.view","edit":"schema.manage"},"tab":{"id":"trigger"}}},{"key":"includedPages","label":"Only on pages","type":"array","helpText":"Paths like /products/*. Leave empty for every page.","config":{"itemType":"text"},"ui":{"itemLabel":"Path","itemName":"path","itemTitle":"Path","tab":{"id":"targeting","label":"Where and who"}}},{"key":"excludedPages","label":"Never on pages","type":"array","helpText":"Paths like /checkout/*.","defaultValue":["/cart","/checkout/*"],"config":{"itemType":"text"},"ui":{"itemLabel":"Path","itemName":"path","itemTitle":"Path","tab":{"id":"targeting"}}},{"key":"audience","label":"Who sees it","type":"select","defaultValue":"not-subscribed","config":{"options":[{"label":"Everyone","value":"everyone"},{"label":"First-time visitors","value":"new"},{"label":"People not on the list yet","value":"not-subscribed"},{"label":"People who haven't ordered","value":"no-orders"}]},"ui":{"tab":{"id":"targeting"}}},{"key":"devices","label":"Devices","type":"multiselect","config":{"min":1,"options":[{"label":"Desktop","value":"desktop"},{"label":"Tablet","value":"tablet"},{"label":"Phone","value":"phone"}]},"ui":{"control":"checkbox","tab":{"id":"targeting"}}},{"key":"frequency","label":"How often","type":"select","defaultValue":"days","config":{"options":[{"label":"Once per visit","value":"session"},{"label":"Once every few days","value":"days"},{"label":"Only once, ever","value":"once"}]},"ui":{"tab":{"id":"targeting"}}},{"key":"everyDays","label":"Every","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"frequency","op":"eq","value":"days"}]},"config":{"min":1,"max":90,"step":1,"unit":"days"},"ui":{"tab":{"id":"targeting"}}},{"key":"size","label":"Size","type":"select","helpText":"Full screen on phones can hurt search rankings.","defaultValue":"medium","config":{"options":[{"label":"Corner card","value":"small"},{"label":"Centered","value":"medium"},{"label":"Full screen","value":"full"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"A modal shown by a trigger, to the right people, not too often","category":"conversion","icon":"app-window","preview":{"title":"heading","subtitle":"trigger","media":"image"}}},
|
|
1442
|
+
{"id":"conversion-trust-badges","name":"Trust badges & guarantees row","category":"Conversion & forms","kind":"block","tags":["repeater","a11y","variants","conditional","validation","media-heavy"],"proves":"Typed badges (guarantee / shipping / certification…) with logo only when certified · payment icons as a multiselect · a synced review score that can't be typed","why":"Trust rows are claims, so the schema keeps them honest and accessible. Each badge is TYPED: guarantees, shipping, returns and origin use an icon, while certifications require the official logo (with alt text naming it) — the condition swaps icon for logo. Payment methods are a multiselect of known marks (the renderer owns the correct SVGs; nobody uploads a blurry Visa logo). The review score is a toggle that pulls a live number from the connected app — there is no field to type 4.9.","htmlSignals":["a row of 3–6 small icon + short text items near the buy button or above the footer ('Free shipping', '30-day returns', 'Lifetime warranty')","payment brand SVGs (Visa, Mastercard, Amex, Apple Pay, Shop Pay, PayPal) in a list","certification marks (B Corp, 1% for the Planet, Climate Neutral) as images","a star rating with '4.9 from 2,400 reviews' text, often from a reviews widget"],"mcp":"Each icon+text item → badge (kind from keywords: ship → shipping, return/refund → returns, warranty/guarantee → guarantee, 'made in'/'poured in' → origin; known certification marks → certification with the image as logo). Payment SVGs → payments by brand (match file names/titles/aria-labels). A star rating from a reviews widget → showRating:true; never import the number. Text-only certifications without a logo stay 'certification' and are flagged for a logo.","pitfalls":["Typed review numbers go stale and can mislead; only the live score is allowed.","Payment icons for methods not enabled at checkout are a common trust-breaker.","Icon-only badges need text; the title is required."],"notCarried":["category","variants"],"component":{"name":"Trust row","slug":"trust-row","props":[{"key":"badges","label":"Badges","type":"table","config":{"minItems":2,"maxItems":6,"fields":[{"key":"kind","label":"Type","type":"select","defaultValue":"guarantee","options":[{"label":"Guarantee","value":"guarantee"},{"label":"Shipping","value":"shipping"},{"label":"Returns","value":"returns"},{"label":"Certification","value":"certification","description":"B Corp, 1% for the Planet…"},{"label":"Made in","value":"origin"}]},{"key":"icon","label":"Icon","type":"icon","showIf":{"match":"all","rules":[{"field":"kind","op":"in","value":["guarantee","shipping","returns","origin"]}]}},{"key":"logo","label":"Certification logo","type":"image","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"certification"}]}},{"key":"title","label":"Title","type":"text","required":true,"config":{"maxLength":28}},{"key":"detail","label":"Detail","type":"text","helpText":"Shown on the Cards layout and as a tooltip elsewhere.","config":{"maxLength":80}},{"key":"link","label":"Link","type":"link"}]},"ui":{"itemLabel":"Badge","icon":"rosette","preview":{"title":"title","subtitle":"detail","media":"logo","fallback":"Untitled badge"},"tab":{"id":"content","label":"Content"}}},{"key":"payments","label":"Payment icons","type":"multiselect","config":{"options":[{"label":"Visa","value":"visa"},{"label":"Mastercard","value":"mastercard"},{"label":"American Express","value":"amex"},{"label":"Apple Pay","value":"apple-pay"},{"label":"Google Pay","value":"google-pay"},{"label":"Shop Pay","value":"shop-pay"},{"label":"PayPal","value":"paypal"},{"label":"Afterpay","value":"afterpay"}]},"ui":{"control":"checkbox","tab":{"id":"content"}}},{"key":"showRating","label":"Show rating","type":"boolean","ui":{"tab":{"id":"content"}}},{"key":"ratingSource","label":"Reviews from","type":"select","showIf":{"match":"all","rules":[{"field":"showRating","op":"eq","value":"true"}]},"readOnly":"developer","lockReason":"Connected in Settings › Integrations","config":{"options":[{"label":"Okendo","value":"okendo"},{"label":"Judge.me","value":"judgeme"},{"label":"Google reviews","value":"google"}]},"ui":{"access":{"edit":"schema.manage"},"tab":{"id":"content"}}},{"key":"alignment","label":"Alignment","type":"select","defaultValue":"center","config":{"options":[{"label":"Left","value":"left"},{"label":"Center","value":"center"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"showDivider","label":"Lines above and below","type":"boolean","ui":{"tab":{"id":"design"}}}],"description":"Guarantees, shipping promises, certifications and how people pay","category":"conversion","icon":"shield-check","preview":{"title":"badges.0.title","subtitle":"badges.1.title","icon":"shield-check"}}},
|
|
1443
|
+
{"id":"blog-index","name":"Blog index","category":"Listings & feeds","kind":"block","tags":["dynamic-query","references","conditional","variants","validation"],"proves":"A listing stores a query, not copies · featured pick as a conditional reference · pagination modes","why":"Posts are Journal entries; the index is a section on the Journal static page that holds only a query (topics, authors, order, page size), so a new post appears without anyone editing the page. The collection is developer-locked because the card template is wired to that collection's fields. The editorial pick is the one exception — a single optional reference shown only when Featured story = Pick one — and excludeFeatured stops it showing twice. Card visibility and image ratio are design, so they live in the Design group, not on each post.","htmlSignals":["Three or more sibling <article> cards with the same shape: image, h2/h3 link, <time>, short paragraph","Card hrefs share a prefix (/blog/, /journal/, /news/) — that prefix names the collection","One card visibly larger or first in its own row before the grid (featured)","<nav aria-label=\"Pagination\"> with numbered links or rel=\"next\", or a 'Load more' button","Category chips or a 'Filed under' label on each card (topics reference)"],"mcp":"1) Find or create the collection from the card shape: title (card heading), slug (last href segment), excerpt (card paragraph), coverImage (card image + alt), publishedAt (<time datetime>), author, topics. 2) Import each card as an entry; follow the href for the body. 3) Create ONE blogIndex section with posts.collection = that collection. Set topics only when the page URL or heading scopes it (/blog/category/camp → topic 'camp'). 4) featuredMode: 'pick' + featuredPost when the first card is larger, else 'latest'. 5) pagination from the DOM (numbered nav → numbered; 'Load more' → loadMore; none visible → none). pageSize = cards on page 1 minus the featured card, rounded to columns. Never write card titles into the section.","pitfalls":["Copying the 9 visible cards into an inline array freezes the list — new posts never appear.","Featured post listed again in the grid when excludeFeatured is off.","Infinite scroll with no numbered fallback leaves old posts unreachable for crawlers.","pageSize that isn't a multiple of columns leaves a ragged last row.","A topic-scoped index (/journal/topic/camp) is a page template on the Topics collection, not a copy of this section per topic."],"notCarried":["ai","category","variants"],"component":{"name":"Blog index","slug":"blog-index","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","placeholder":"The Journal","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":200,"localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"featuredMode","label":"Featured story","type":"select","helpText":"Newest featured updates itself when a new featured post goes live.","defaultValue":"latest","config":{"options":[{"label":"Newest featured","value":"latest","description":"The newest post marked Featured"},{"label":"Pick one","value":"pick"},{"label":"None","value":"none"}]},"ui":{"control":"segmented","tab":{"id":"content"},"group":{"id":"featured","label":"Featured story","kind":"group","description":"The large card above the grid."}}},{"key":"featuredPost","label":"Post to feature","type":"reference","required":true,"showIf":{"match":"all","rules":[{"field":"featuredMode","op":"eq","value":"pick"}]},"config":{"contentModelId":"col_journal"},"ui":{"tab":{"id":"content"},"group":{"id":"featured"}}},{"key":"posts","label":"Posts to list","type":"object","helpText":"New posts that match appear on their own.","config":{"fields":[{"key":"collection","label":"Collection","type":"select","helpText":"","defaultValue":"col_journal","readOnly":"developer","lockReason":"The collection is wired to the page template in code.","options":[{"label":"Journal","value":"col_journal"}],"ui":{"access":{"edit":"schema.manage"}}},{"key":"topics","label":"Only these topics","type":"multi-reference","helpText":"Leave empty for every post.","config":{"max":6,"contentModelId":"col_topics"},"ui":{"itemTitle":"Topic"}},{"key":"authors","label":"Only these authors","type":"multi-reference","helpText":"Leave empty for every author.","config":{"max":6,"contentModelId":"col_authors"},"ui":{"itemTitle":"Author"}},{"key":"hideFeatured","label":"Skip the featured post","type":"boolean","defaultValue":true},{"key":"sort","label":"Order","type":"select","helpText":"","defaultValue":"newest","options":[{"label":"Newest first","value":"newest"},{"label":"Oldest first","value":"oldest"},{"label":"Recently updated","value":"updated","description":"Uses the Updated date, then the publish date"},{"label":"A to Z","value":"title"}]},{"key":"pageSize","label":"Posts per page","type":"number","defaultValue":9,"config":{"min":3,"max":24,"step":3},"ui":{"control":"slider"}}]},"ui":{"icon":"database","tab":{"id":"query","label":"Items","icon":"database"}}},{"key":"pagination","label":"More posts","type":"select","helpText":"","defaultValue":"numbered","config":{"options":[{"label":"Numbered pages","value":"numbered","description":"Each page gets its own address, /journal/page/2"},{"label":"Load more button","value":"loadMore"},{"label":"Infinite scroll","value":"infinite"},{"label":"First page only","value":"none"}]},"ui":{"control":"radio","tab":{"id":"query"}}},{"key":"loadMoreLabel","label":"Button label","type":"text","helpText":"","defaultValue":"Load more stories","showIf":{"match":"all","rules":[{"field":"pagination","op":"eq","value":"loadMore"}]},"config":{"maxLength":32},"ui":{"tab":{"id":"query"}}},{"key":"emptyHeading","label":"Empty heading","type":"text","helpText":"Shown when the filters match no posts.","defaultValue":"Nothing here yet","config":{"maxLength":60},"ui":{"tab":{"id":"query"},"group":{"id":"empty","label":"When nothing matches","kind":"group","collapsed":true}}},{"key":"emptyButton","label":"Empty button","type":"button","ui":{"tab":{"id":"query"},"group":{"id":"empty"}}},{"key":"columns","label":"Columns on desktop","type":"number","helpText":"","defaultValue":3,"config":{"min":2,"max":4,"step":1},"ui":{"control":"slider","tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"imageRatio","label":"Card image","type":"select","defaultValue":"4:3","config":{"options":[{"label":"16:9","value":"16:9"},{"label":"4:3","value":"4:3"},{"label":"Square","value":"1:1"},{"label":"No image","value":"none"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"cardShows","label":"Card shows","type":"multiselect","helpText":"","defaultValue":["excerpt","date","topic"],"config":{"options":[{"label":"Excerpt","value":"excerpt"},{"label":"Date","value":"date"},{"label":"Author","value":"author"},{"label":"Reading time","value":"readingTime"},{"label":"First topic","value":"topic"}]},"ui":{"control":"checkbox","tab":{"id":"design"}}},{"key":"anchor","label":"Anchor","type":"text","advanced":true,"config":{"pattern":"^[a-z0-9-]+$","patternMessage":"Use lowercase letters, numbers and hyphens, like our-story."},"ui":{"tab":{"id":"design"}}}],"description":"A featured story, then a paged grid of Journal posts","category":"content","icon":"news","preview":{"title":"heading","subtitle":"intro","icon":"layout-grid"}}},
|
|
1444
|
+
{"id":"category-filter-bar","name":"Category filter bar","category":"Listings & feeds","kind":"block","tags":["references","dynamic-query","conditional","variants","responsive-layout","a11y"],"proves":"Chips from a taxonomy collection (all or picked) · filters a list on the page or navigates · mobile behavior per breakpoint","why":"The chips are a projection of the Topics collection, so the section holds either 'every topic' (a query) or an ordered list of topic references — never typed labels, which drift from the real taxonomy. Whether a chip filters in place or navigates is behavior the marketer chooses; the URL parameter is wired in code (locked). It points at the list it filters by anchor because the schema has no 'section on this page' reference. Mobile treatment is per-breakpoint design.","htmlSignals":["<nav> or role=\"tablist\" of 3–15 short links/buttons just above a card grid","First item reads 'All' / 'Everything' and has aria-current or an active class","Hrefs like ?category=x, ?topic=x or /category/x/","Horizontal overflow-x: auto on small screens (scroll), or a <select> fallback"],"mcp":"Map each chip label to an existing taxonomy entry (create Topics if missing; slug from the href). If the chips equal the full taxonomy, set source 'all'; otherwise 'pick' in DOM order. action = 'filter' when hrefs are query strings or buttons, 'navigate' when they are /category/x paths. targetList = anchor of the next listing section (add one to it if missing). Don't store chip labels as strings.","pitfalls":["Typed chip labels drift from topic names after a rename — always references.","A filter bar with no targetList silently does nothing.","Counts that include drafts or future posts — counts must use the published query.","Wrapping 12 chips on a phone pushes the list below the fold."],"notCarried":["category","variants"],"component":{"name":"Category filter bar","slug":"category-filter","props":[{"key":"source","label":"Chips","type":"select","helpText":"Every topic, new ones included, A to Z.","defaultValue":"all","config":{"options":[{"label":"Every topic","value":"all"},{"label":"Pick topics","value":"pick"}]},"ui":{"control":"segmented","tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"topics","label":"Topics","type":"multi-reference","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"pick"}]},"config":{"min":2,"max":10,"contentModelId":"col_topics"},"ui":{"tab":{"id":"content"}}},{"key":"allLabel","label":"First chip","type":"text","helpText":"Leave empty to drop the chip.","defaultValue":"All","localized":true,"config":{"maxLength":20},"ui":{"tab":{"id":"content"}}},{"key":"hideEmpty","label":"Hide empty topics","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"content"}}},{"key":"showCounts","label":"Show counts","type":"boolean","ui":{"tab":{"id":"content"}}},{"key":"action","label":"Clicking a chip","type":"select","helpText":"","defaultValue":"filter","config":{"options":[{"label":"Filters the list on this page","value":"filter","description":"No page load; the URL keeps the choice"},{"label":"Opens the topic's page","value":"navigate","description":"/journal/topic/camp"}]},"ui":{"control":"radio","tab":{"id":"content"}}},{"key":"targetList","label":"List to filter","type":"text","helpText":"The anchor of the list section below, e.g. stories.","required":true,"showIf":{"match":"all","rules":[{"field":"action","op":"eq","value":"filter"}]},"config":{"pattern":"^[a-z0-9-]+$","patternMessage":"Use the list section's anchor, like stories."},"ui":{"tab":{"id":"content"}}},{"key":"param","label":"URL parameter","type":"text","helpText":"Keeps the filter in shared links, e.g. ?topic=camp.","defaultValue":"topic","showIf":{"match":"all","rules":[{"field":"action","op":"eq","value":"filter"}]},"advanced":true,"ui":{"access":{"view":"schema.view","edit":"schema.manage"},"tab":{"id":"content"}}},{"key":"ariaLabel","label":"Name for screen readers","type":"text","helpText":"Not shown. Read by screen readers before the chips.","required":true,"defaultValue":"Filter stories by topic","config":{"maxLength":60},"ui":{"tab":{"id":"content"}}},{"key":"mobile","label":"On phones","type":"select","defaultValue":"scroll","config":{"options":[{"label":"Scroll sideways","value":"scroll"},{"label":"Wrap","value":"wrap"},{"label":"Dropdown","value":"dropdown"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"isSticky","label":"Stick under the header","type":"boolean","ui":{"tab":{"id":"design"}}},{"key":"alignment","label":"Alignment","type":"select","helpText":"","defaultValue":"start","config":{"options":[{"label":"Left","value":"start"},{"label":"Center","value":"center"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Topic chips that filter a list or link to topic pages","category":"content","icon":"filter","preview":{"title":"allLabel","subtitle":"targetList","icon":"filter"}}},
|
|
1445
|
+
{"id":"related-posts","name":"Related posts","category":"Listings & feeds","kind":"block","tags":["conditional","references","dynamic-query","validation","variants"],"proves":"Automatic, hand-picked or mixed (pins first, then auto) · match rules · fallback when nothing matches","why":"Related posts live on the post template (one placement for every post), so the default must work with zero editing: automatic matching on shared topics. Editors who care can pin a few (mixed) or hand-pick all (manual). The three-way mode is the condition that shows picks vs. match rules — the same field never means two things. References carry the picks so a renamed post stays linked; the current post is excluded by the renderer, not by validation.","htmlSignals":["Heading like 'Related', 'Keep reading', 'You might also like', 'More from' near the article end","2–4 small cards after </article> or before the comments/footer","Cards link to the same URL prefix as the current page","Sometimes inside <aside> with a smaller heading level (h2/h3)"],"mcp":"Place one relatedPosts on the post TEMPLATE, not per post. If the source CMS shows the same cards for posts with the same category, use mode 'auto' with matchOn ['topics']. If a post shows cards that don't share its topics, convert those to picks on that post's instance (mode 'mixed'). count = cards shown.","pitfalls":["Manual picks on a template apply to every post — picks belong on the post's own instance.","Auto with no fallback renders an empty heading on a post with a unique topic.","Picking the current post (self-reference) — the renderer must skip it.","Unpublished picks: the reference resolves only to published entries on the live site."],"notCarried":["category","variants"],"component":{"name":"Related posts","slug":"related-posts","props":[{"key":"heading","label":"Heading","type":"text","defaultValue":"Keep reading","localized":true,"config":{"maxLength":48},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"mode","label":"Which posts","type":"select","helpText":"","defaultValue":"auto","config":{"options":[{"label":"Automatic","value":"auto","description":"Shares topics with this post, newest first"},{"label":"Pick them","value":"manual"},{"label":"Pick some, fill the rest","value":"mixed","description":"Your picks first, then automatic"}]},"ui":{"control":"radio","tab":{"id":"content"}}},{"key":"picks","label":"Posts","type":"multi-reference","helpText":"The current post is skipped.","showIf":{"match":"all","rules":[{"field":"mode","op":"in","value":["manual","mixed"]}]},"unique":true,"config":{"min":1,"max":4,"polymorphic":true,"allowedModelSlugs":["col_journal","col_recipes"]},"ui":{"itemName":"post","tab":{"id":"content"}}},{"key":"matchOn","label":"Match on","type":"multiselect","helpText":"More shared values rank higher. Ties go to the newest.","defaultValue":["topics"],"showIf":{"match":"all","rules":[{"field":"mode","op":"in","value":["auto","mixed"]}]},"config":{"options":[{"label":"Topics","value":"topics"},{"label":"Authors","value":"authors"},{"label":"Reviewed product","value":"product"}]},"ui":{"control":"checkbox","tab":{"id":"content"}}},{"key":"showRecipes","label":"Include recipes","type":"boolean","showIf":{"match":"all","rules":[{"field":"mode","op":"in","value":["auto","mixed"]}]},"ui":{"tab":{"id":"content"}}},{"key":"count","label":"How many","type":"number","helpText":"","defaultValue":3,"config":{"min":2,"max":6,"step":1},"ui":{"control":"slider","tab":{"id":"content"}}},{"key":"fallback","label":"When nothing matches","type":"select","helpText":"","defaultValue":"newest","showIf":{"match":"all","rules":[{"field":"mode","op":"in","value":["auto","mixed"]}]},"config":{"options":[{"label":"Show the newest posts","value":"newest"},{"label":"Hide the section","value":"hide"}]},"ui":{"tab":{"id":"content"}}},{"key":"showImages","label":"Show images","type":"boolean","helpText":"","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"showDivider","label":"Line above","type":"boolean","ui":{"tab":{"id":"design"}}}],"description":"More stories at the end of a post, matched or picked","category":"content","icon":"arrows-shuffle","preview":{"title":"heading","subtitle":"mode","icon":"arrows-shuffle"}}},
|
|
1446
|
+
{"id":"resource-library","name":"Resource library","category":"Listings & feeds","kind":"block","tags":["dynamic-query","repeater","conditional","references","form","validation","responsive-layout"],"proves":"Multi-facet filters defined as data (field · control · order) · gated downloads by type · pinned items above the query","why":"Resources are a collection; the library is a query plus a list of facets. Facets are DATA (label · which field · control · order) so marketers can add 'Industry' without code — but the field each facet reads is developer-locked, because it must be an indexed field. Gating is a page-level decision by resource type, not a flag copied onto every resource, and the form is a reference so submissions land in one place.","htmlSignals":["A left column (or top bar) of <fieldset>/<details> groups with checkboxes and counts in parentheses","A search <input type=search> above a card grid","Cards with a type badge (Guide, Report, Template) and a 'Download' or 'Read' action","URL query like ?type=guide&topic=close updating as filters change","A modal or redirect with a form before a PDF opens (gated)"],"mcp":"Create col_resources with fields title, summary, type (select from the badges), topics, industry, file/url, cover. Import every card. Each filter group becomes one facet: label = group legend; field = the collection field whose values match the checkbox labels (create the field if missing, then ask the developer to index it); control from the DOM (checkbox list → checkbox, pill buttons → chips, <select> → dropdown). If downloads open a form, set gate 'form' and create/attach the form; restrict gateTypes to the badges that trigger it.","pitfalls":["Facet values typed into the section drift from entry values — values always come from the collection.","A facet on a non-indexed field works in the demo, then times out at 5,000 resources.","Gating every type (including blog-like guides) hurts organic traffic; gate by type.","Pinned items that also match the filters appear twice unless the renderer de-duplicates."],"notCarried":["ai","category","variants"],"component":{"name":"Resource library","slug":"resource-library","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"localized":true,"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"intro","label":"Intro","type":"longtext","helpText":"","localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"pinned","label":"Pinned","type":"multi-reference","unique":true,"config":{"max":3,"contentModelId":"col_resources"},"ui":{"itemName":"resource","itemTitle":"Resource","tab":{"id":"content"}}},{"key":"query","label":"Resources to list","type":"object","config":{"fields":[{"key":"collection","label":"Collection","type":"select","helpText":"","defaultValue":"col_resources","readOnly":"developer","lockReason":"The collection is wired to the page template in code.","options":[{"label":"Resources","value":"col_resources"}],"ui":{"access":{"edit":"schema.manage"}}},{"key":"types","label":"Only these types","type":"multiselect","helpText":"Leave empty for every type.","options":[{"label":"Guides","value":"guide"},{"label":"Reports","value":"report"},{"label":"Templates","value":"template"},{"label":"Webinars","value":"webinar"},{"label":"Case studies","value":"case-study"}]},{"key":"sort","label":"Order","type":"select","helpText":"Most downloaded uses the last 90 days.","defaultValue":"newest","options":[{"label":"Newest first","value":"newest"},{"label":"Most downloaded","value":"popular"},{"label":"A to Z","value":"title"}]},{"key":"pageSize","label":"Per page","type":"number","helpText":"","defaultValue":12,"config":{"min":6,"max":36,"step":6},"ui":{"control":"slider"}}]},"ui":{"tab":{"id":"query","label":"Items","icon":"database"}}},{"key":"showSearch","label":"Show search box","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"filters","label":"Filters","icon":"adjustments"}}},{"key":"searchPlaceholder","label":"Search placeholder","type":"text","helpText":"","defaultValue":"Search guides, reports and templates","showIf":{"match":"all","rules":[{"field":"showSearch","op":"eq","value":"true"}]},"config":{"maxLength":48},"ui":{"tab":{"id":"filters"}}},{"key":"facets","label":"Filters","type":"table","helpText":"Each filter reads one field of the Resources collection.","config":{"maxItems":5,"fields":[{"key":"label","label":"Label","type":"text","required":true,"localized":true,"config":{"maxLength":24}},{"key":"field","label":"Filters by","type":"select","helpText":"","required":true,"readOnly":"developer","lockReason":"Facets map to indexed fields; the developer adds new ones.","options":[{"label":"Type","value":"type"},{"label":"Topic","value":"topics"},{"label":"Industry","value":"industry"},{"label":"Role","value":"role"},{"label":"Format","value":"format"}],"ui":{"access":{"edit":"schema.manage"}}},{"key":"control","label":"Shown as","type":"select","helpText":"","defaultValue":"checkbox","options":[{"label":"Checkboxes","value":"checkbox"},{"label":"Chips","value":"chips"},{"label":"Dropdown","value":"dropdown"}],"ui":{"control":"segmented"}},{"key":"allowMultiple","label":"Allow several","type":"boolean","helpText":"Results match any of the ticked values.","defaultValue":true},{"key":"order","label":"Value order","type":"select","helpText":"","defaultValue":"count","options":[{"label":"Most results first","value":"count"},{"label":"A to Z","value":"alpha"}]},{"key":"isCollapsed","label":"Start collapsed","type":"boolean"}]},"ui":{"itemLabel":"Filter","icon":"adjustments","preview":{"title":"label","subtitle":"field","fallback":"Untitled filter"},"tab":{"id":"filters"}}},{"key":"useUrlSync","label":"Keep filters in the URL","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"filters"}}},{"key":"gate","label":"Downloads","type":"select","helpText":"","defaultValue":"open","config":{"options":[{"label":"Open to everyone","value":"open"},{"label":"Ask for details first","value":"form","description":"Shows a form before the file"}]},"ui":{"control":"radio","tab":{"id":"content"}}},{"key":"gateForm","label":"Form","type":"form","helpText":"Visitors fill it once; later downloads skip it.","required":true,"showIf":{"match":"all","rules":[{"field":"gate","op":"eq","value":"form"}]},"ui":{"tab":{"id":"content"}}},{"key":"gateTypes","label":"Ask only for","type":"multiselect","helpText":"Leave empty to gate every type.","showIf":{"match":"all","rules":[{"field":"gate","op":"eq","value":"form"}]},"config":{"options":[{"label":"Guides","value":"guide"},{"label":"Reports","value":"report"},{"label":"Templates","value":"template"}]},"ui":{"tab":{"id":"content"}}},{"key":"columns","label":"Columns on desktop","type":"number","helpText":"","defaultValue":3,"config":{"min":2,"max":4,"step":1},"ui":{"control":"slider","tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"mobileFilters","label":"Filters on phones","type":"select","defaultValue":"sheet","config":{"options":[{"label":"Bottom sheet","value":"sheet"},{"label":"Above results","value":"inline"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Guides, reports and templates with search and filters","category":"content","icon":"books","preview":{"title":"heading","subtitle":"intro","icon":"books"}}},
|
|
1447
|
+
{"id":"podcast-episode-list","name":"Podcast episode list","category":"Listings & feeds","kind":"block","tags":["dynamic-query","media-heavy","references","conditional","repeater","embed","a11y"],"proves":"Show-level details + platform links (repeater) · episodes by query with season filter · inline player vs episode page","why":"Episodes are entries (they have their own page, audio file, show notes, transcript — see the Podcast episode entry), so the list is a query with a season and type filter. Show-level facts (name, art, hosts, platforms) are said once on the section because there is one show per page; if a brand runs several shows, promote them to a Shows collection and swap these fields for a show reference. Platform links are a small repeater of service + link so badges render with the right logo and order.","htmlSignals":["A square cover image next to 'Listen on' badges (Apple Podcasts, Spotify, YouTube logos)","Repeated rows with episode number, date, duration (e.g. 48 min) and a play button","<audio> elements or an embedded player iframe (Transistor, Buzzsprout, Simplecast, Spotify)","A link to an RSS feed (application/rss+xml) or 'Subscribe' button"],"mcp":"Read the show's RSS feed if linked — it is the cleanest source: channel → show fields; each <item> → an episode entry (enclosure url → audio, itunes:duration, itunes:episode/season, content:encoded → show notes). Badges → platforms (match by logo alt/href host). Place one podcastEpisodes section with the collection query; set featured 'latest' when the top episode is larger.","pitfalls":["Copying the embed iframe as the 'episode' loses the audio file and show notes — import from RSS.","Show art under 1400 px is rejected by Apple Podcasts.","Two players playing at once when several inline players exist — the renderer must use one shared player.","Trailers at the top of a 'newest' list unless types excludes them."],"notCarried":["category","variants"],"component":{"name":"Podcast episodes","slug":"podcast-episodes","props":[{"key":"showTitle","label":"Show name","type":"text","required":true,"config":{"maxLength":60},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"},"group":{"id":"show","label":"The show","kind":"group","description":"Shown above the episode list."}}},{"key":"showDescription","label":"About the show","type":"longtext","helpText":"Podcast apps show the same text.","recommendedMax":280,"localized":true,"ui":{"rows":3,"tab":{"id":"content"},"group":{"id":"show"}}},{"key":"showArt","label":"Show art","type":"image","helpText":"Square, 3000 × 3000. Apps reject anything under 1400.","required":true,"ui":{"tab":{"id":"content"},"group":{"id":"show"}}},{"key":"hosts","label":"Hosts","type":"multi-reference","helpText":"","config":{"max":3,"contentModelId":"col_authors"},"ui":{"tab":{"id":"content"},"group":{"id":"show"}}},{"key":"platforms","label":"Listen on","type":"table","openMode":"inline","config":{"maxItems":7,"fields":[{"key":"service","label":"Service","type":"select","helpText":"","required":true,"options":[{"label":"Apple Podcasts","value":"apple"},{"label":"Spotify","value":"spotify"},{"label":"YouTube","value":"youtube"},{"label":"Overcast","value":"overcast"},{"label":"Pocket Casts","value":"pocketcasts"},{"label":"RSS feed","value":"rss"}]},{"key":"link","label":"Link","type":"link","required":true}]},"ui":{"itemLabel":"Platform","icon":"headphones","preview":{"title":"service","subtitle":"link.href"},"tab":{"id":"content"}}},{"key":"episodes","label":"Episodes to list","type":"object","config":{"fields":[{"key":"collection","label":"Collection","type":"select","helpText":"","defaultValue":"col_episodes","readOnly":"developer","lockReason":"The collection is wired to the page template in code.","options":[{"label":"Podcast episodes","value":"col_episodes"}],"ui":{"access":{"edit":"schema.manage"}}},{"key":"season","label":"Season","type":"number","helpText":"Leave empty for every season.","config":{"min":1,"step":1}},{"key":"types","label":"Episode types","type":"multiselect","helpText":"","defaultValue":["full","bonus"],"options":[{"label":"Full","value":"full"},{"label":"Bonus","value":"bonus"},{"label":"Trailer","value":"trailer"}]},{"key":"sort","label":"Order","type":"select","defaultValue":"newest","options":[{"label":"Newest first","value":"newest"},{"label":"Episode number","value":"episode"}]},{"key":"limit","label":"How many","type":"number","helpText":"","defaultValue":10,"config":{"min":3,"max":30,"step":1},"ui":{"control":"slider"}}]},"ui":{"tab":{"id":"query","label":"Items","icon":"database"}}},{"key":"highlight","label":"Highlight","type":"select","defaultValue":"latest","config":{"options":[{"label":"Latest episode","value":"latest"},{"label":"Pick one","value":"pick"},{"label":"None","value":"none"}]},"ui":{"control":"segmented","tab":{"id":"query"}}},{"key":"featuredEpisode","label":"Episode","type":"reference","required":true,"showIf":{"match":"all","rules":[{"field":"highlight","op":"eq","value":"pick"}]},"config":{"contentModelId":"col_episodes"},"ui":{"tab":{"id":"query"}}},{"key":"play","label":"Play button","type":"select","helpText":"","defaultValue":"inline","config":{"options":[{"label":"Plays here","value":"inline","description":"A sticky mini player keeps playing across pages"},{"label":"Opens the episode page","value":"page"}]},"ui":{"control":"radio","tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"showNotesPreview","label":"Show notes preview","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design"}}},{"key":"showDuration","label":"Show length","type":"boolean","helpText":"","defaultValue":true,"ui":{"tab":{"id":"design"}}},{"key":"subscribeButton","label":"Subscribe button","type":"button","ui":{"tab":{"id":"content"}}}],"description":"The show, where to listen, and its latest episodes","category":"content","icon":"microphone","preview":{"title":"showTitle","subtitle":"showDescription","media":"showArt"}}},
|
|
1448
|
+
{"id":"changelog","name":"Changelog","category":"Listings & feeds","kind":"block","tags":["deep-nesting","repeater","conditional","rich-text","validation","dynamic-query","variants"],"proves":"Releases → changes → type tags · breaking/deprecated changes reveal a migration note · collection or written-here source","why":"A changelog is releases → changes, and each change carries a TYPE that drives its badge, the type filter and — for Breaking/Deprecated only — a required migration note and removal date (conditional on the change's own type). Small products write releases right in the section; once releases need their own URLs, an RSS feed or reuse in the dashboard, switch the source to the Releases collection (same item shape) and the section becomes a query. Version is validated and unique because it doubles as the anchor.","htmlSignals":["Repeated date + version headings (h2/h3 like 'v2.14.0' or 'September 29, 2026')","Colored badges per line: New, Improved, Fixed, Deprecated, Breaking","A vertical line/timeline down the left with dots per release","Links to an RSS/Atom feed or 'Subscribe to updates'","Anchors per version (#v2-14-0) for deep links"],"mcp":"Split on version/date headings → releases. Within each, every list item or sub-heading → a change; type from its badge text (map 'Added'→new, 'Changed'→improved, 'Removed'→breaking); the first sentence → title, the rest → details. If a change says 'deprecated'/'breaking' and names a replacement, put that sentence in migration. Use source 'collection' when the old site gives each release its own URL; otherwise 'inline'.","pitfalls":["Free-text badges ('Bugfix', 'Fix', 'Fixed') break the type filter — always map to the select.","Breaking changes without a migration note — validation must block publish.","Version strings with spaces or dates as versions collide as anchors.","60+ releases inline makes the section slow to edit — move to the collection."],"notCarried":["category","segmented fallback","variants"],"component":{"name":"Changelog","slug":"changelog","props":[{"key":"heading","label":"Heading","type":"text","required":true,"localized":true,"config":{"maxLength":48},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"intro","label":"Intro","type":"longtext","helpText":"","ui":{"rows":2,"tab":{"id":"content"}}},{"key":"source","label":"Releases come from","type":"select","defaultValue":"collection","config":{"options":[{"label":"Releases collection","value":"collection"},{"label":"Written here","value":"inline"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"query","label":"Releases to list","type":"object","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"collection"}]},"openMode":"inline","config":{"fields":[{"key":"collection","label":"Collection","type":"select","helpText":"","defaultValue":"col_releases","readOnly":"developer","lockReason":"The collection is wired to the page template in code.","options":[{"label":"Releases","value":"col_releases"}],"ui":{"access":{"edit":"schema.manage"}}},{"key":"products","label":"Products","type":"multiselect","helpText":"Leave empty for every product.","options":[{"label":"API","value":"api"},{"label":"Dashboard","value":"dashboard"},{"label":"SDKs","value":"sdk"}]},{"key":"limit","label":"How many","type":"number","helpText":"","defaultValue":10,"config":{"min":5,"max":50,"step":5},"ui":{"control":"slider"}}]},"ui":{"tab":{"id":"content"}}},{"key":"releases","label":"Releases","type":"table","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"inline"}]},"config":{"maxItems":60,"fields":[{"key":"version","label":"Version","type":"text","helpText":"Also becomes the anchor, e.g. #v2-14-0.","required":true,"unique":true,"placeholder":"2.14.0","config":{"pattern":"^v?\\d+\\.\\d+(\\.\\d+)?(-[a-z0-9.]+)?$","patternMessage":"Use a version like 2.14.0 or v2.14.0-beta.1."}},{"key":"date","label":"Release date","type":"date","helpText":"","required":true},{"key":"title","label":"Headline","type":"text","recommendedMax":70},{"key":"screenshot","label":"Screenshot","type":"image"},{"key":"changes","label":"Changes","type":"table","fields":[{"key":"type","label":"Type","type":"select","helpText":"","required":true,"options":[{"label":"New","value":"new"},{"label":"Improved","value":"improved"},{"label":"Fixed","value":"fixed"},{"label":"Deprecated","value":"deprecated"},{"label":"Breaking","value":"breaking"},{"label":"Security","value":"security"}]},{"key":"title","label":"What changed","type":"text","required":true,"recommendedMax":90},{"key":"details","label":"Details","type":"richtext"},{"key":"areas","label":"Areas","type":"multiselect","helpText":"","options":[{"label":"Payments","value":"payments"},{"label":"Ledger","value":"ledger"},{"label":"Reports","value":"reports"},{"label":"Auth","value":"auth"},{"label":"Webhooks","value":"webhooks"}]},{"key":"migration","label":"How to migrate","type":"richtext","required":true,"showIf":{"match":"all","rules":[{"field":"type","op":"in","value":["breaking","deprecated"]}]}},{"key":"removalDate","label":"Removed on","type":"date","helpText":"When the deprecated behavior stops working.","showIf":{"match":"all","rules":[{"field":"type","op":"eq","value":"deprecated"}]}},{"key":"docs","label":"Docs link","type":"link"}],"config":{"minItems":1,"maxItems":40},"ui":{"itemLabel":"Change","icon":"point","preview":{"title":"title","subtitle":"type","fallback":"Untitled change"}}}]},"ui":{"itemLabel":"Release","icon":"tag","preview":{"title":"version","subtitle":"title","fallback":"Untitled release"},"tab":{"id":"content"}}},{"key":"hasRssFeed","label":"Publish an RSS feed","type":"boolean","helpText":"Publishes /changelog/feed.xml.","defaultValue":true,"ui":{"tab":{"id":"content"}}},{"key":"subscribeForm","label":"Email updates","type":"form","ui":{"tab":{"id":"content"}}},{"key":"showTypeFilter","label":"Show type filter","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"groupBy","label":"Group by","type":"select","helpText":"","defaultValue":"release","config":{"options":[{"label":"Release","value":"release"},{"label":"Month","value":"month"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Versions and what changed in each","category":"content","icon":"history","preview":{"title":"heading","subtitle":"intro","icon":"history"}}},
|
|
1449
|
+
{"id":"public-roadmap","name":"Public roadmap","category":"Listings & feeds","kind":"block","tags":["deep-nesting","repeater","conditional","references","form","validation"],"proves":"Columns → items with status, quarter and read-only vote counts · shipped items link to a release · voting + suggestion form","why":"A board is columns → items, three levels deep (section → column → item → its release). Each column has a STAGE (a select, not just a title) so styling and filtering don't depend on wording. Votes are read-only data the site writes — editors see them but can't fake them. A shipped item must link to its release, which is a reference so it follows renames. Feedback settings sit in their own group because they are a different job from editing the plan.","htmlSignals":["3–5 side-by-side columns headed Planned / In progress / Shipped (kanban)","Cards with an upvote arrow and a number","Quarter labels like 'Q1 2027' or 'Later'","A 'Suggest a feature' / 'Submit an idea' button opening a form","Often an embedded Canny / Productboard / Featurebase widget (iframe) — import its items as data"],"mcp":"Columns from the board headers (stage from the header text). Each card → item: title, summary (first paragraph), area (tag), quarter, votes (read-only — import the number once, then the site owns it). Cards in a 'Done/Shipped' column get shipped=true; try to match them to a release by name, else leave release empty and flag it. A Canny-style iframe: fetch its public JSON if available, otherwise keep it as an embed and say so.","pitfalls":["Item stage inferred from column title text ('Now', 'Next', 'Later') — map it to the stage select explicitly.","The release condition lives on the item (shipped flag) because a field can't see its parent column's stage.","Editable vote counts invite fake numbers — keep votes read-only.","Dates as promises ('Ships Oct 3') — use quarters."],"notCarried":["category","variants"],"component":{"name":"Public roadmap","slug":"roadmap","props":[{"key":"heading","label":"Heading","type":"text","required":true,"config":{"maxLength":48},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"intro","label":"Intro","type":"longtext","helpText":"","ui":{"rows":2,"tab":{"id":"content"}}},{"key":"columns","label":"Columns","type":"table","config":{"minItems":2,"maxItems":5,"fields":[{"key":"title","label":"Title","type":"text","required":true,"config":{"maxLength":24}},{"key":"stage","label":"Stage","type":"select","required":true,"options":[{"label":"Considering","value":"considering"},{"label":"Planned","value":"planned"},{"label":"In progress","value":"building"},{"label":"In beta","value":"beta"},{"label":"Shipped","value":"shipped"}]},{"key":"description","label":"Description","type":"text","config":{"maxLength":90}},{"key":"items","label":"Items","type":"table","fields":[{"key":"title","label":"Title","type":"text","required":true,"recommendedMax":60},{"key":"summary","label":"Summary","type":"longtext","recommendedMax":240,"ui":{"rows":3}},{"key":"area","label":"Area","type":"select","helpText":"","options":[{"label":"Payments","value":"payments"},{"label":"Ledger","value":"ledger"},{"label":"Reports","value":"reports"},{"label":"Integrations","value":"integrations"}]},{"key":"quarter","label":"Target","type":"text","helpText":"Shown as a rough target, never a promise.","placeholder":"Q1 2027","config":{"pattern":"^(Q[1-4] 20\\d\\d|H[12] 20\\d\\d|Later)$","patternMessage":"Use a quarter or half, like Q3 2026, H1 2027 or Later."}},{"key":"isShipped","label":"Shipped","type":"boolean","helpText":"Links the card to its release notes."},{"key":"release","label":"Release","type":"reference","required":true,"showIf":{"match":"all","rules":[{"field":"isShipped","op":"eq","value":"true"}]},"config":{"contentModelId":"col_releases"}},{"key":"votes","label":"Votes","type":"number","helpText":"Counted from visitor votes. Not editable.","readOnly":true},{"key":"discussion","label":"Discussion link","type":"link"}],"config":{"maxItems":25},"ui":{"itemLabel":"Item","icon":"point","preview":{"title":"title","subtitle":"quarter","fallback":"Untitled item"}}}]},"ui":{"itemLabel":"Column","icon":"layout-columns","preview":{"title":"title","subtitle":"stage"},"tab":{"id":"content"}}},{"key":"allowVoting","label":"Let visitors vote","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"feedback","label":"Feedback","icon":"message-plus"}}},{"key":"useEmailToVote","label":"Ask for an email to vote","type":"boolean","helpText":"One vote per email.","showIf":{"match":"all","rules":[{"field":"allowVoting","op":"eq","value":"true"}]},"ui":{"tab":{"id":"feedback"}}},{"key":"suggest","label":"Suggestion form","type":"form","ui":{"tab":{"id":"feedback"}}},{"key":"suggestButton","label":"Suggestion button","type":"button","showIf":{"match":"all","rules":[{"field":"suggest","op":"notEmpty"}]},"ui":{"tab":{"id":"feedback"}}},{"key":"sortItems","label":"Sort items by","type":"select","helpText":"","defaultValue":"votes","config":{"options":[{"label":"Votes","value":"votes"},{"label":"Your order","value":"manual"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"showVotes","label":"Show vote counts","type":"boolean","helpText":"Off still lets people vote; the number stays private.","defaultValue":true,"ui":{"tab":{"id":"design"}}}],"description":"What's planned, in progress and shipped, with votes","category":"content","icon":"map-2","preview":{"title":"heading","subtitle":"intro","icon":"map-2"}}},
|
|
1450
|
+
{"id":"search-results-page","name":"Search results page","category":"Listings & feeds","kind":"block","tags":["dynamic-query","references","conditional","repeater","validation","a11y"],"proves":"Configure search, don't store results · sources with weights · pinned results per keyword · empty states","why":"A results page never stores results; it stores how search behaves on this site. Sources map to indexes that are built in code, so the collection is locked while the label, boost and page size are editorial. Pinned results are the one hand-curated part — keyword tags → a reference — with an optional end date so seasonal pins don't rot. The empty state is its own group because it is what most people judge search by.","htmlSignals":["A page at /search with ?q= or ?query= in the URL and a prominent <input type=search>","Result tabs or filters by type (All, Products, Recipes, Articles) with counts","Highlighted <mark> or <em> around matching words in snippets","A 'No results for …' message with suggestions"],"mcp":"Don't import results. Create one searchResults section on the /search page. Tabs → sources (match each tab to a collection; ask the developer to index any that aren't). Popular or suggested chips → popularSearches. Visible 'Featured' or 'Top result' cards for a query you can reproduce → pinned. Copy the no-results text into the empty group with {query} for the echoed term.","pitfalls":["Pins pointing at unpublished items show nothing — validate on publish.","Typo tolerance 2 on short words (≤4 letters) returns noise.","A label-less search input fails WCAG 1.3.1 / 4.1.2 — label is required.","Search pages must be noindex — the platform does it natively; never add a field for it."],"notCarried":["category","shopify reference","variants"],"component":{"name":"Search results","slug":"search-results","props":[{"key":"placeholder","label":"Search placeholder","type":"text","helpText":"","defaultValue":"Search recipes, gear and stories","localized":true,"config":{"maxLength":48},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"label","label":"Name for screen readers","type":"text","helpText":"Not shown. Read by screen readers.","required":true,"defaultValue":"Search Caldera","config":{"maxLength":40},"ui":{"tab":{"id":"content"}}},{"key":"popularSearches","label":"Popular searches","type":"array","config":{"itemType":"text","maxItems":8},"ui":{"itemLabel":"Search","itemName":"search","itemTitle":"Search","tab":{"id":"content"}}},{"key":"sources","label":"What's searched","type":"table","config":{"minItems":1,"maxItems":6,"fields":[{"key":"collection","label":"Collection","type":"select","helpText":"","required":true,"readOnly":"developer","lockReason":"Search indexes are built per collection in code.","options":[{"label":"Products","value":"shopify"},{"label":"Recipes","value":"col_recipes"},{"label":"Journal","value":"col_journal"},{"label":"FAQs","value":"col_faq"},{"label":"Pages","value":"page"}],"ui":{"access":{"edit":"schema.manage"}}},{"key":"tabLabel","label":"Tab label","type":"text","required":true,"localized":true,"config":{"maxLength":20}},{"key":"weight","label":"Boost","type":"number","helpText":"Higher ranks this source's matches first.","defaultValue":3,"config":{"min":1,"max":5,"step":1},"ui":{"control":"slider"}},{"key":"perPage","label":"Results per page","type":"number","helpText":"","defaultValue":12,"config":{"min":4,"max":40,"step":4}}]},"ui":{"itemLabel":"Source","icon":"database","preview":{"title":"tabLabel","subtitle":"collection"},"tab":{"id":"ranking","label":"Ranking","icon":"sort-descending"}}},{"key":"pinned","label":"Pinned results","type":"table","config":{"maxItems":20,"fields":[{"key":"keywords","label":"When someone searches","type":"tags","helpText":"Exact searches, any case. Add plurals and misspellings.","required":true,"config":{"min":1,"max":10}},{"key":"result","label":"Show first","type":"reference","required":true,"config":{"polymorphic":true,"allowedModelSlugs":["col_recipes","col_journal"],"targets":["page"]}},{"key":"until","label":"Until","type":"date","helpText":"The pin stops after this date."}]},"ui":{"itemLabel":"Pin","itemName":"pin","icon":"pin","preview":{"title":"keywords","subtitle":"result"},"tab":{"id":"ranking"}}},{"key":"typos","label":"Forgive typos","type":"select","helpText":"Lets \"skilet\" find skillet.","defaultValue":"1","config":{"options":[{"label":"Off","value":"0"},{"label":"1 letter","value":"1"},{"label":"2 letters","value":"2"}]},"ui":{"control":"segmented","tab":{"id":"ranking"}}},{"key":"emptyHeading","label":"Heading","type":"text","helpText":"{query} becomes what they typed.","defaultValue":"Nothing for “{query}”","config":{"maxLength":60},"ui":{"tab":{"id":"empty","label":"No results","icon":"mood-empty"}}},{"key":"emptyBody","label":"Text","type":"longtext","helpText":"","ui":{"rows":2,"tab":{"id":"empty"}}},{"key":"emptyShow","label":"Suggest instead","type":"select","helpText":"","defaultValue":"popular","config":{"options":[{"label":"Popular searches","value":"popular"},{"label":"Links you choose","value":"links"},{"label":"Nothing","value":"none"}]},"ui":{"control":"radio","tab":{"id":"empty"}}},{"key":"emptyLinks","label":"Links","type":"array","showIf":{"match":"all","rules":[{"field":"emptyShow","op":"eq","value":"links"}]},"config":{"maxItems":4,"itemType":"link"},"ui":{"itemName":"link","tab":{"id":"empty"}}},{"key":"emptyButton","label":"Empty button","type":"button","ui":{"tab":{"id":"empty"}}},{"key":"showMatchHighlights","label":"Highlight matches","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"showCounts","label":"Count on tabs","type":"boolean","helpText":"","defaultValue":true,"ui":{"tab":{"id":"design"}}}],"description":"The /search page: what's searched, how results look, what shows when nothing matches","category":"content","icon":"search","preview":{"title":"placeholder","icon":"search"}}},
|
|
1451
|
+
{"id":"rich-blog-post","name":"Rich blog post","category":"Long-form & docs","kind":"entry","tags":["rich-text","references","media-heavy","embed","conditional","validation","seo-structured","localized"],"proves":"One rich-text body holds key takeaways, callouts, tables, code, images, video, pull quotes, toggles, footnotes and linked components · sponsor disclosure is conditional","why":"The post is a Journal ENTRY; the page template around it is fixed sections (post header from title/dek/byline/coverImage → body with optional ToC → closingButton → Author bio → Related posts). Everything readers scan inside the article — tables, callouts, code, video, FAQ toggles, linked components — lives in ONE rich-text body because its order is editorial and different every time. What the site queries (authors, topics, dates, isFeatured) are real fields. Sponsorship is three fields revealed by one toggle so the disclosure label can't be forgotten. Reading time is derived, so it's read-only.","htmlSignals":["<article> with one <h1>, a byline (rel=author link, <time datetime>) and a large hero <img>","Body with h2/h3, <figure>/<figcaption>, <table>, <pre><code>, <blockquote>, <details>","Boxed asides with an icon (Tip / Note / Warning) → callouts","Mid-article product or newsletter card unlike the prose → component instance","'Sponsored', 'In partnership with', 'Paid partnership' near the byline"],"mcp":"Create the entry: title (h1), slug (URL), dek (subtitle/first <p class=lede>), coverImage (hero img + alt + figcaption), authors/topics (match or create entries), publishedAt (<time datetime> or article:published_time), updatedAt (dateModified). Body: convert the article content to the rich-text format — h2/h3 kept, figure→data-rt=image, table→data-rt=table with thead, aside.note/tip/warning→data-rt=callout with tone, iframes from YouTube/Vimeo→data-rt=video, details→data-rt=toggle; repeated mid-article cards that match a Blocks component→data-rt=component with overrides for the differing text. Strip share bars, author box and related cards (template sections). Never copy <meta> into fields.","pitfalls":["An H1 inside the body duplicates the title — the renderer demotes it, the converter should too.","Copying the author box into the body instead of referencing the author.","Pasting a CTA card as HTML loses the link to the component — use an instance.","Sponsored links without rel=sponsored — set by the sponsorLink default.","updatedAt bumped for typo fixes makes every post look fresh and erodes trust."],"notCarried":["ai","category","kind:entry (props)"],"component":{"name":"Journal post","slug":"journal-post","props":[{"key":"title","label":"Title","type":"text","helpText":"The page's only H1.","required":true,"recommendedMax":70,"localized":true,"config":{"maxLength":90},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"slug","label":"Slug","type":"slug","required":true,"ui":{"tab":{"id":"content"}}},{"key":"dek","label":"Dek","type":"longtext","helpText":"Also shown on cards.","required":true,"recommendedMax":160,"localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"authors","label":"Authors","type":"multi-reference","helpText":"The first author leads the byline.","config":{"contentModelId":"col_authors","min":1,"max":3},"ui":{"tab":{"id":"content"},"group":{"id":"byline","label":"Byline","kind":"group"}}},{"key":"topics","label":"Topics","type":"multi-reference","helpText":"The first topic is the label on cards.","config":{"contentModelId":"col_topics","min":1,"max":4},"ui":{"tab":{"id":"content"},"group":{"id":"byline"}}},{"key":"publishedAt","label":"Publish date","type":"datetime","required":true,"ui":{"tab":{"id":"content"},"group":{"id":"dates","label":"Dates","kind":"group"}}},{"key":"updatedAt","label":"Updated","type":"datetime","helpText":"Only for real corrections or updates.","ui":{"tab":{"id":"content"},"group":{"id":"dates"}}},{"key":"readingTime","label":"Reading time","type":"number","helpText":"Worked out from the body word count.","readOnly":true,"config":{"unit":"min"},"ui":{"tab":{"id":"content"},"group":{"id":"dates"}}},{"key":"body","label":"Body","type":"richtext","helpText":"Type / for images, tables, callouts, code, video and components.","required":true,"helpMore":"H2 and H3 build the table of contents. Components stay shared with Blocks; a field edited here changes only this place.","localized":true,"ui":{"tab":{"id":"content"}}},{"key":"closingButton","label":"Closing button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"coverImage","label":"Cover image","type":"image","helpText":"1600 × 900 or larger. Used on cards and when shared.","required":true,"ui":{"tab":{"id":"media","label":"Media","icon":"photo"}}},{"key":"coverCaption","label":"Cover caption","type":"text","config":{"maxLength":140},"ui":{"tab":{"id":"media"}}},{"key":"isSponsored","label":"Sponsored","type":"boolean","helpText":"Adds a \"Paid partnership\" label and marks partner links sponsored.","ui":{"tab":{"id":"settings","label":"Settings","icon":"settings"},"group":{"id":"sponsor","label":"Sponsorship","kind":"group","collapsed":true}}},{"key":"sponsorName","label":"Partner","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"isSponsored","op":"eq","value":"true"}]},"config":{"maxLength":40},"ui":{"tab":{"id":"settings"},"group":{"id":"sponsor"}}},{"key":"sponsorLink","label":"Partner link","type":"link","defaultValue":{"type":"url","href":"","rel":"sponsored","newTab":true},"showIf":{"match":"all","rules":[{"field":"isSponsored","op":"eq","value":"true"}]},"ui":{"tab":{"id":"settings"},"group":{"id":"sponsor"}}},{"key":"isFeatured","label":"Featured","type":"boolean","ui":{"tab":{"id":"settings"}}},{"key":"showContents","label":"Show table of contents","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"settings"}}},{"key":"related","label":"Related stories","type":"select","helpText":"","defaultValue":"auto","config":{"options":[{"label":"Automatic","value":"auto","description":"Same topics, newest first"},{"label":"Pick them","value":"manual"},{"label":"Don't show","value":"none"}]},"ui":{"control":"radio","tab":{"id":"settings"}}},{"key":"relatedPosts","label":"Stories","type":"multi-reference","showIf":{"match":"all","rules":[{"field":"related","op":"eq","value":"manual"}]},"unique":true,"config":{"polymorphic":true,"allowedModelSlugs":["col_journal","col_recipes"],"min":1,"max":3},"ui":{"itemTitle":"Storie","tab":{"id":"settings"}}},{"key":"layout","label":"Layout","type":"select","helpText":"","defaultValue":"standard","config":{"options":[{"label":"Standard","value":"standard","icon":"file-text","description":"Text column, inline images"},{"label":"Wide","value":"wide","icon":"layout-columns","description":"Full-width images"},{"label":"Photo essay","value":"essay","icon":"library-photo","description":"Images first, short captions"}]},"ui":{"control":"cards","tab":{"id":"settings"}}}],"description":"A Journal story with a rich body","category":"content","icon":"news","preview":{"title":"title","subtitle":"dek","media":"coverImage"}}},
|
|
1452
|
+
{"id":"author-bio","name":"Author bio","category":"Long-form & docs","kind":"block","tags":["references","conditional","variants","dynamic-query","a11y"],"proves":"One reference drives photo, name, role, bio and socials · per-placement bio override · latest posts by that author","why":"Authors are a collection (name, slug, role, photo with alt, short bio, long bio, socials array, expertise topics); the bio box is a thin section that renders one of them, so a new headshot updates every post. The reference can be empty on the post template and fall back to the post's first author. A per-placement bio override is a conditional rich-text field — rare, but guest posts need it — rather than a second 'guest author' type. 'Their latest posts' is a tiny query (author = this, exclude current).","htmlSignals":["A box after the article with a round headshot, name, a one-paragraph bio and social icons","rel=\"author\" links or links to /author/{slug}","'Written by', 'About the author', 'More from {name}'","Person JSON-LD with sameAs social URLs (native output from the author entry)"],"mcp":"Create/match the author entry from the box: name, role (line under the name), photo (+ alt 'Portrait of {name}'), bio, socials (each icon link → platform + url). Place ONE authorBio on the post template with author empty (falls back to the post's first author). Only set author + bioSource 'custom' when a specific page shows a different bio.","pitfalls":["Typing the author's name and photo into each post — they drift on the first rebrand.","Headshot alt text 'image' or empty — use 'Portrait of {name}'.","'Latest posts' listing the current post.","Two authors: the box must render both or say why only the first."],"notCarried":["category","variants"],"component":{"name":"Author bio","slug":"author-bio","props":[{"key":"heading","label":"Heading","type":"text","helpText":"Leave empty on author pages.","defaultValue":"About the author","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"author","label":"Author","type":"reference","helpText":"Leave empty on a post to use its first author.","config":{"contentModelId":"col_authors"},"ui":{"tab":{"id":"content"}}},{"key":"bioSource","label":"Bio","type":"select","defaultValue":"profile","config":{"options":[{"label":"From their profile","value":"profile"},{"label":"Written here","value":"custom"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"bio","label":"Bio for this placement","type":"richtext","required":true,"showIf":{"match":"all","rules":[{"field":"bioSource","op":"eq","value":"custom"}]},"ui":{"tab":{"id":"content"}}},{"key":"showSocial","label":"Show social links","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"content"}}},{"key":"showLatest","label":"Show their latest posts","type":"boolean","ui":{"tab":{"id":"content"}}},{"key":"latestCount","label":"How many","type":"number","helpText":"","defaultValue":3,"showIf":{"match":"all","rules":[{"field":"showLatest","op":"eq","value":"true"}]},"config":{"min":2,"max":6,"step":1},"ui":{"control":"slider","tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","placeholder":"All posts by {name}","ui":{"tab":{"id":"content"}}},{"key":"photoShape","label":"Photo","type":"select","helpText":"","defaultValue":"circle","config":{"options":[{"label":"Circle","value":"circle"},{"label":"Rounded","value":"rounded"},{"label":"No photo","value":"none"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design","icon":"palette"}}}],"description":"Who wrote it, with a link to everything else they wrote","category":"content","icon":"user-circle","preview":{"title":"heading","subtitle":"author","icon":"user-circle"}}},
|
|
1453
|
+
{"id":"docs-page","name":"Docs page","category":"Long-form & docs","kind":"entry","tags":["rich-text","references","global-synced","conditional","embed","validation"],"proves":"Sidebar position by reference to a nav group · auto ToC with depth · prev/next auto or manual · deprecation reveals a notice","why":"A docs page is an entry; the template around it is: shared sidebar (a global built from Doc groups — each page only says which group and position), breadcrumbs, the body, an auto 'On this page' list from H2/H3, prev/next, feedback, and the footer. Prev/next defaults to the sidebar order so editors rarely touch it; manual is there for tutorials that jump. Status is a select whose non-stable values reveal a required notice and, for deprecated, a replacement reference — so the warning can't be half-done. Code that readers copy goes in Code tabs instances, not raw <pre> blocks.","htmlSignals":["Three columns: left nav tree, article, right 'On this page' list of anchors","Breadcrumbs and 'Previous' / 'Next' cards at the bottom","Admonitions (.admonition, .callout, :::note) and tabbed code blocks","'Edit this page on GitHub' link; 'Last updated on' date","A version switcher or 'Beta' / 'Deprecated' banners"],"mcp":"Rebuild the sidebar tree as Doc groups (one per top-level nav heading) and record each page's group + position from the nav DOM order. Body: MDX/HTML → rich text; admonitions → callouts (note/info→info, tip→tip, caution/danger/warning→warning); tabbed code → codeTabs component instances (one block per tab group, see code-tabs); plain <pre> stays a code block. Don't import the right-hand ToC — it's derived. 'Beta'/'Deprecated' banners → status + statusNote.","pitfalls":["Hard-coding prev/next per page — they break when pages are reordered; leave pager on auto.","Importing the ToC as content duplicates headings.","Docusaurus/Mintlify tabs flattened into one <pre> lose the other languages.","Deprecated without a replacement link — the reader is stranded.","Relative links (../guides/x) must be rewritten to page references on import."],"notCarried":["category","kind:entry (props)"],"component":{"name":"Docs page","slug":"doc-page","props":[{"key":"title","label":"Title","type":"text","required":true,"localized":true,"config":{"maxLength":70},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"slug","label":"Slug","type":"slug","required":true,"ui":{"tab":{"id":"content"}}},{"key":"summary","label":"Summary","type":"longtext","required":true,"recommendedMax":160,"localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"richtext","helpText":"Type / for callouts, tables, Code tabs and images.","required":true,"helpMore":"H2 and H3 build the \"On this page\" list.","localized":true,"ui":{"tab":{"id":"content"}}},{"key":"sidebarGroup","label":"Sidebar group","type":"reference","helpText":"The sidebar itself is shared.","required":true,"config":{"contentModelId":"col_doc_groups"},"ui":{"tab":{"id":"navigation","label":"Navigation","icon":"list-tree"}}},{"key":"sidebarLabel","label":"Sidebar label","type":"text","helpText":"Leave empty to use the title.","config":{"maxLength":28},"ui":{"tab":{"id":"navigation"}}},{"key":"sidebarPosition","label":"Position","type":"number","helpText":"Lower comes first.","config":{"min":1,"step":1},"ui":{"tab":{"id":"navigation"}}},{"key":"tocDepth","label":"On this page","type":"select","helpText":"","defaultValue":"h3","config":{"options":[{"label":"Off","value":"off"},{"label":"H2 only","value":"h2"},{"label":"H2 + H3","value":"h3"}]},"ui":{"control":"segmented","tab":{"id":"navigation"}}},{"key":"pager","label":"Previous / next","type":"select","helpText":"","defaultValue":"auto","config":{"options":[{"label":"Follow the sidebar order","value":"auto"},{"label":"Choose them","value":"manual"},{"label":"Hide","value":"off"}]},"ui":{"control":"radio","tab":{"id":"navigation"}}},{"key":"prev","label":"Previous page","type":"reference","showIf":{"match":"all","rules":[{"field":"pager","op":"eq","value":"manual"}]},"config":{"contentModelId":"col_docs"},"ui":{"tab":{"id":"navigation"}}},{"key":"next","label":"Next page","type":"reference","showIf":{"match":"all","rules":[{"field":"pager","op":"eq","value":"manual"}]},"config":{"contentModelId":"col_docs"},"ui":{"tab":{"id":"navigation"}}},{"key":"status","label":"Status","type":"select","helpText":"","defaultValue":"stable","config":{"options":[{"label":"Stable","value":"stable"},{"label":"Beta","value":"beta"},{"label":"Deprecated","value":"deprecated"}]},"ui":{"control":"segmented","tab":{"id":"settings","label":"Settings","icon":"settings"},"group":{"id":"status","label":"Status","kind":"group","description":"Beta and deprecated pages show a notice at the top."}}},{"key":"statusNote","label":"Notice","type":"richtext","required":true,"showIf":{"match":"all","rules":[{"field":"status","op":"in","value":["beta","deprecated"]}]},"ui":{"tab":{"id":"settings"},"group":{"id":"status"}}},{"key":"replacement","label":"Use instead","type":"reference","showIf":{"match":"all","rules":[{"field":"status","op":"eq","value":"deprecated"}]},"config":{"contentModelId":"col_docs"},"ui":{"tab":{"id":"settings"},"group":{"id":"status"}}},{"key":"apiVersion","label":"API version","type":"select","helpText":"The version the examples on this page use.","config":{"options":[{"label":"2026-09-01 (latest)","value":"2026-09-01"},{"label":"2026-03-01","value":"2026-03-01"},{"label":"2025-09-01","value":"2025-09-01"}]},"ui":{"tab":{"id":"settings"}}},{"key":"lastReviewed","label":"Last reviewed","type":"date","ui":{"tab":{"id":"settings"}}},{"key":"editUrl","label":"Source file","type":"url","helpText":"Powers the \"Edit on GitHub\" link.","advanced":true,"ui":{"access":{"view":"schema.view","edit":"schema.manage"},"tab":{"id":"settings"}}}],"description":"A guide or reference page in the docs","category":"content","icon":"book","preview":{"title":"title","subtitle":"summary","icon":"book"}}},
|
|
1454
|
+
{"id":"code-tabs","name":"Code tabs","category":"Long-form & docs","kind":"block","tags":["embed","repeater","validation","variants"],"proves":"One example in N languages · language is a select, not a free label · tabs sync across the page by key","why":"Docs show the same call in several languages; each snippet is an item with a LANGUAGE select (drives highlighting, the default tab name and the sync), an optional label override ('Node.js'), filename and highlight ranges. It is a block so it can sit inside a docs body as a component instance or on its own. syncKey lets every code-tabs block on the page switch together when a reader picks Python once.","htmlSignals":["A tab bar of language names (cURL, Node, Python…) above a <pre><code class=\"language-*\">","Several <pre> siblings where only one is visible at a time (role=tabpanel)","A copy-to-clipboard button in the code header","data-lang / data-language attributes; localStorage keys like 'preferred-language'"],"mcp":"One codeTabs per tab group. For each panel: language from class=\"language-x\" / tab text (normalize: 'Node'/'JS'→javascript, 'Shell'/'Bash'→curl if it calls curl), code from <code> textContent (keep indentation, decode entities), filename from the panel header. Lines with a highlight class → highlight ranges. Same tab labels across groups → same syncKey.","pitfalls":["innerHTML of highlighted code carries <span> soup — store textContent only.","Free-text language labels ('Node 18') break highlighting — map to the select, keep the label separately.","API keys in sample code — replace real-looking secrets with $ENV placeholders.","The `code` field's built-in language list (html/css/js/liquid/json) doesn't cover API languages — the select carries it."],"notCarried":["category","variants"],"component":{"name":"Code tabs","slug":"code-tabs","props":[{"key":"title","label":"Title","type":"text","placeholder":"Create a journal entry","config":{"maxLength":60},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"tabs","label":"Languages","type":"table","config":{"minItems":1,"maxItems":8,"fields":[{"key":"language","label":"Language","type":"select","required":true,"options":[{"label":"cURL","value":"curl"},{"label":"JavaScript","value":"javascript"},{"label":"TypeScript","value":"typescript"},{"label":"Python","value":"python"},{"label":"Ruby","value":"ruby"},{"label":"Go","value":"go"},{"label":"Java","value":"java"},{"label":"PHP","value":"php"},{"label":"JSON","value":"json"}]},{"key":"label","label":"Tab name","type":"text","helpText":"Leave empty to use the language name.","config":{"maxLength":16}},{"key":"filename","label":"File name","type":"text","helpText":"","placeholder":"create-entry.ts","config":{"maxLength":40}},{"key":"code","label":"Code","type":"code","required":true,"openMode":"dialog"},{"key":"highlight","label":"Highlight lines","type":"text","helpText":"","placeholder":"3, 7-9","config":{"pattern":"^\\d+(-\\d+)?(,\\s*\\d+(-\\d+)?)*$","patternMessage":"Use line numbers like 3, 7-9."}}]},"ui":{"itemLabel":"Language","itemName":"snippet","itemTitle":"Snippet","icon":"code","preview":{"title":"language","subtitle":"filename"},"tab":{"id":"content"}}},{"key":"syncKey","label":"Switch together","type":"text","helpText":"Code tabs with the same key switch language together.","defaultValue":"sdk","config":{"pattern":"^[a-z0-9-]+$","patternMessage":"Lowercase letters, numbers and hyphens only."},"ui":{"tab":{"id":"content"}}},{"key":"showLineNumbers","label":"Show line numbers","type":"boolean","helpText":"","ui":{"tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"showCopyButton","label":"Show copy button","type":"boolean","helpText":"","defaultValue":true,"ui":{"tab":{"id":"design"}}},{"key":"maxHeight","label":"Max height","type":"number","helpText":"Longer code scrolls inside the block.","config":{"unit":"px","min":160,"max":800,"step":40},"ui":{"tab":{"id":"design"}}}],"description":"The same example in several languages","category":"content","icon":"code","preview":{"title":"title","subtitle":"syncKey","icon":"code"}}},
|
|
1455
|
+
{"id":"kb-article","name":"Knowledge-base article","category":"Long-form & docs","kind":"entry","tags":["rich-text","form","conditional","references","validation"],"proves":"'Was this helpful?' modelled as settings, not markup · 'No' path is quick reasons, a form or a contact button · review ownership","why":"Help articles are entries in a KB collection with a required category (it drives the help-center tree and breadcrumbs). The feedback widget is part of the template, but its BEHAVIOUR is per-article content — the question, the thanks line and what happens after 'No' (quick reasons, a form reference, or a contact button), each revealed by one select. lastReviewed is required and owner is a reference so stale articles get routed to a person. Internal notes never render.","htmlSignals":["'Was this article helpful?' with Yes/No or thumbs buttons at the end","Breadcrumbs Help Center › Category › Article; 'Updated N days ago'","Numbered steps with UI screenshots and bolded menu paths","Zendesk/Intercom/HelpScout markup (article-body, .article-votes)"],"mcp":"Category from the breadcrumb (create KB categories). Body: keep ordered lists, bold UI paths and screenshots (alt from the step text). The vote widget → feedback settings; if 'No' opens a reason list, copy the reasons; if it links to a contact form, set onNo 'form' and attach/create the form. 'Updated' date → lastReviewed. Don't import vote counts — they're analytics.","pitfalls":["Vote buttons copied as HTML into the body (they can't record anything).","Screenshots with alt 'screenshot' — describe what the reader should look for.","No owner → stale answers nobody reviews.","Plan badges as body text drift from pricing — use appliesTo."],"notCarried":["category","kind:entry (props)"],"component":{"name":"Help article","slug":"kb-article","props":[{"key":"title","label":"Title","type":"text","required":true,"localized":true,"config":{"maxLength":90},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"slug","label":"Slug","type":"slug","required":true,"ui":{"tab":{"id":"content"}}},{"key":"category","label":"Category","type":"reference","required":true,"config":{"contentModelId":"col_kb_categories"},"ui":{"tab":{"id":"content"}}},{"key":"summary","label":"Short answer","type":"longtext","helpText":"Shown above the steps.","recommendedMax":200,"localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"richtext","required":true,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"appliesTo","label":"Applies to","type":"multiselect","helpText":"Leave empty for every plan.","config":{"options":[{"label":"Starter","value":"starter"},{"label":"Growth","value":"growth"},{"label":"Scale","value":"scale"},{"label":"Enterprise","value":"enterprise"}]},"ui":{"tab":{"id":"content"}}},{"key":"related","label":"Related articles","type":"multi-reference","unique":true,"config":{"contentModelId":"col_kb","max":5},"ui":{"tab":{"id":"content"}}},{"key":"showFeedback","label":"Ask \"Was this helpful?\"","type":"boolean","helpText":"Answers are counted in Growth › Analytics.","defaultValue":true,"ui":{"tab":{"id":"feedback","label":"Feedback","icon":"thumb-up"}}},{"key":"question","label":"Question","type":"text","helpText":"","defaultValue":"Was this article helpful?","showIf":{"match":"all","rules":[{"field":"showFeedback","op":"eq","value":"true"}]},"config":{"maxLength":48},"ui":{"tab":{"id":"feedback"}}},{"key":"thanks","label":"After Yes","type":"text","helpText":"","defaultValue":"Thanks — glad it helped.","showIf":{"match":"all","rules":[{"field":"showFeedback","op":"eq","value":"true"}]},"config":{"maxLength":80},"ui":{"tab":{"id":"feedback"}}},{"key":"onNo","label":"After No","type":"select","helpText":"","defaultValue":"reasons","showIf":{"match":"all","rules":[{"field":"showFeedback","op":"eq","value":"true"}]},"config":{"options":[{"label":"Ask why, with quick reasons","value":"reasons"},{"label":"Show a form","value":"form"},{"label":"Offer to contact support","value":"contact"}]},"ui":{"control":"radio","tab":{"id":"feedback"}}},{"key":"reasons","label":"Reasons","type":"array","showIf":{"match":"all","rules":[{"field":"onNo","op":"eq","value":"reasons"}]},"config":{"itemType":"text","minItems":2,"maxItems":5},"ui":{"itemLabel":"Reason","tab":{"id":"feedback"}}},{"key":"noForm","label":"Form","type":"form","required":true,"showIf":{"match":"all","rules":[{"field":"onNo","op":"eq","value":"form"}]},"ui":{"tab":{"id":"feedback"}}},{"key":"contactButton","label":"Contact button","type":"button","showIf":{"match":"all","rules":[{"field":"onNo","op":"eq","value":"contact"}]},"ui":{"tab":{"id":"feedback"}}},{"key":"owner","label":"Owner","type":"reference","helpText":"Gets the \"needs review\" reminder and the No answers.","config":{"contentModelId":"col_people"},"ui":{"tab":{"id":"settings","label":"Settings","icon":"settings"}}},{"key":"lastReviewed","label":"Last reviewed","type":"date","helpText":"Articles over 6 months old are flagged.","required":true,"ui":{"tab":{"id":"settings"}}},{"key":"internalNotes","label":"Internal notes","type":"longtext","helpText":"Never shown on the site.","ui":{"rows":3,"tab":{"id":"settings"}}}],"description":"A help-center answer with feedback","category":"content","icon":"lifebuoy","preview":{"title":"title","subtitle":"category","icon":"lifebuoy"}}},
|
|
1456
|
+
{"id":"glossary","name":"Glossary A–Z","category":"Long-form & docs","kind":"block","tags":["dynamic-query","repeater","conditional","references","seo-structured","a11y","validation"],"proves":"Terms written here or from a Glossary collection · auto letter index · aliases and see-also links · each term deep-linkable","why":"A glossary is a list of term objects sorted by the renderer, with the A–Z bar derived from first letters (never stored). Small sites write terms in the section; once terms should be linked from articles, have their own URLs or be reused, switch the source to the Glossary collection (same item shape) and the section becomes a query. Aliases feed the filter box and search; see-also is plain tags because a reference can't point at a sibling array item — the collection version uses real references.","htmlSignals":["A row of A–Z letter links (#a, #b…) above alphabetised groups","<dl> with <dt> terms and <dd> definitions, or h3 + p pairs","Each term has an id for deep links","DefinedTermSet / DefinedTerm JSON-LD in the source (native output on our side)"],"mcp":"Each <dt>/<h3> → term; following <dd>/<p> → definition; element id → anchor. 'Also known as …' lines → aliases. Inline 'See: X' links that point at other terms → seeAlso (by name). Drop the letter bar and letter headings — they're derived. If the site has /glossary/{term} pages, use source 'collection' and import each as an entry.","pitfalls":["Storing the letter headings ('A', 'B') as terms.","See-also tags that don't match a term exactly render as dead text.","300 inline terms is the ceiling — beyond that, use the collection.","Localised term lists re-sort per language (Ä, Ø, Ł) — sort with the locale's collation."],"notCarried":["category","variants"],"component":{"name":"Glossary","slug":"glossary","props":[{"key":"heading","label":"Heading","type":"text","required":true,"localized":true,"config":{"maxLength":48},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"intro","label":"Intro","type":"longtext","helpText":"","localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"source","label":"Terms come from","type":"select","defaultValue":"inline","config":{"options":[{"label":"Written here","value":"inline"},{"label":"Glossary collection","value":"collection"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"topics","label":"Only these topics","type":"multi-reference","helpText":"Leave empty for every term.","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"collection"}]},"config":{"contentModelId":"col_topics","max":4},"ui":{"tab":{"id":"content"}}},{"key":"terms","label":"Terms","type":"table","helpText":"Sorted A to Z on the page.","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"inline"}]},"config":{"minItems":1,"maxItems":300,"fields":[{"key":"term","label":"Term","type":"text","required":true,"localized":true,"unique":true,"config":{"maxLength":48}},{"key":"anchor","label":"Anchor","type":"text","helpText":"Made from the term if empty.","config":{"pattern":"^[a-z0-9-]+$","patternMessage":"Use lowercase letters, numbers and hyphens, like camp-kitchen."}},{"key":"definition","label":"Definition","type":"richtext","required":true,"localized":true},{"key":"aliases","label":"Also called","type":"tags","helpText":"Other names people search for.","config":{"max":5}},{"key":"seeAlso","label":"See also","type":"tags","helpText":"Other terms in this list, spelled exactly as written.","config":{"max":4}},{"key":"link","label":"Learn more","type":"link"}]},"ui":{"itemLabel":"Term","icon":"abc","preview":{"title":"term","subtitle":"definition","fallback":"Untitled term"},"tab":{"id":"content"}}},{"key":"showLetterNav","label":"Show A to Z bar","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"isNavSticky","label":"Keep the A to Z bar in view","type":"boolean","helpText":"","showIf":{"match":"all","rules":[{"field":"showLetterNav","op":"eq","value":"true"}]},"ui":{"tab":{"id":"design"}}},{"key":"showSearch","label":"Show search box","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design"}}}],"description":"Terms and definitions, A to Z","category":"content","icon":"abc","preview":{"title":"heading","subtitle":"intro","icon":"abc"}}},
|
|
1457
|
+
{"id":"recipe","name":"Recipe","category":"Long-form & docs","kind":"entry","tags":["deep-nesting","repeater","seo-structured","media-heavy","conditional","references","validation","localized"],"proves":"Ingredient groups → items with amount/unit/prep · steps with images and optional timers · nutrition · Recipe JSON-LD from fields","why":"Recipes are entries because every part is queried or validated: course, method and dietary drive filters; times and servings feed scaling and the Recipe JSON-LD (prepTime, cookTime, totalTime, recipeYield, recipeIngredient, recipeInstructions as HowToStep with image, nutrition, aggregateRating) — all emitted natively from these fields. Ingredients are groups → items (four drill levels: recipe → group → item → product) with numeric amount + unit select, so scaling and unit conversion are possible; typed lines like '2 cups flour' can't scale. Step timers are optional and revealed per step. Template: hero (title, summary, image, times) → ingredients + steps two-column → notes → nutrition → author bio → related recipes.","htmlSignals":["Recipe JSON-LD in the source (the most reliable signal) or WPRM/Tasty Recipes plugin markup (.wprm-recipe)","A 'Prep · Cook · Total' strip and 'Serves N' / servings stepper","<ul> of ingredients with amounts and units; group sub-headings like 'For the glaze'","<ol> of steps, often with step photos; 'Jump to recipe' / 'Print' buttons","A nutrition table (Calories, Protein, Fat, Carbs)"],"mcp":"Prefer the page's Recipe JSON-LD: recipeIngredient strings → parse into amount (handle ½, 1/2, ranges '2–3' → amount + amountMax), unit (normalize 'tablespoons'→tbsp), name, prep (text after a comma). recipeInstructions → steps (HowToSection → separate steps with the section name as title). ISO-8601 durations PT25M → minutes. Without JSON-LD, use the WPRM/Tasty classes. Group headings inside the ingredient list → ingredientGroups. Never import the 'Jump to recipe'/'Print' buttons.","pitfalls":["Ingredient lines as plain strings — scaling and unit switches become impossible.","Unicode fractions (½ ⅓) and ranges parsed as one number.","Total time typed by hand disagrees with prep + cook — keep it derived.","Dietary claims set at recipe level while an optional ingredient breaks them.","Timer fields filled but hasTimer off — hidden values still ship unless cleared on save."],"notCarried":["category","kind:entry (props)","shopify reference"],"component":{"name":"Recipe","slug":"recipe","props":[{"key":"title","label":"Title","type":"text","required":true,"localized":true,"config":{"maxLength":80},"ui":{"tab":{"id":"overview","label":"Overview","icon":"file-text"}}},{"key":"slug","label":"Slug","type":"slug","required":true,"ui":{"tab":{"id":"overview"}}},{"key":"summary","label":"Summary","type":"longtext","required":true,"recommendedMax":160,"localized":true,"ui":{"rows":2,"tab":{"id":"overview"}}},{"key":"image","label":"Image","type":"image","helpText":"1600 × 1200 or larger. Google shows it in recipe results.","required":true,"ui":{"tab":{"id":"overview"}}},{"key":"video","label":"Video","type":"video","ui":{"tab":{"id":"overview"}}},{"key":"authors","label":"Authors","type":"multi-reference","config":{"contentModelId":"col_authors","min":1,"max":2},"ui":{"tab":{"id":"overview"}}},{"key":"course","label":"Course","type":"select","helpText":"","required":true,"config":{"options":[{"label":"Breakfast","value":"breakfast"},{"label":"Main","value":"main"},{"label":"Side","value":"side"},{"label":"Bread","value":"bread"},{"label":"Dessert","value":"dessert"}]},"ui":{"tab":{"id":"overview"}}},{"key":"methods","label":"Cooked on","type":"multiselect","config":{"options":[{"label":"Campfire","value":"campfire"},{"label":"Coals","value":"coals"},{"label":"Grill","value":"grill"},{"label":"Stovetop","value":"stove"},{"label":"Oven","value":"oven"}]},"ui":{"tab":{"id":"overview"}}},{"key":"difficulty","label":"Difficulty","type":"select","helpText":"","config":{"options":[{"label":"Easy","value":"easy"},{"label":"Medium","value":"medium"},{"label":"Hard","value":"hard"}]},"ui":{"control":"segmented","tab":{"id":"overview"}}},{"key":"dietary","label":"Dietary","type":"multiselect","config":{"options":[{"label":"Vegetarian","value":"vegetarian"},{"label":"Vegan","value":"vegan"},{"label":"Gluten-free","value":"gluten-free"},{"label":"Dairy-free","value":"dairy-free"}]},"ui":{"tab":{"id":"overview"}}},{"key":"prepMinutes","label":"Prep","type":"number","required":true,"config":{"unit":"min","min":0,"step":5},"ui":{"tab":{"id":"overview"},"group":{"id":"times","label":"Times","kind":"group","columns":2}}},{"key":"cookMinutes","label":"Cook","type":"number","required":true,"config":{"unit":"min","min":0,"step":5},"ui":{"tab":{"id":"overview"},"group":{"id":"times"}}},{"key":"restMinutes","label":"Rest","type":"number","helpText":"In minutes. Counts toward the total.","config":{"unit":"min","min":0,"step":5},"ui":{"tab":{"id":"overview"},"group":{"id":"times"}}},{"key":"totalMinutes","label":"Total","type":"number","helpText":"In minutes: prep + cook + rest.","readOnly":true,"config":{"unit":"min"},"ui":{"tab":{"id":"overview"},"group":{"id":"times"}}},{"key":"servings","label":"Servings","type":"number","helpText":"Readers can scale amounts from this.","required":true,"config":{"min":1,"max":40,"step":1},"ui":{"tab":{"id":"overview"},"group":{"id":"yield","label":"Makes","kind":"group"}}},{"key":"yieldLabel","label":"Yield","type":"text","helpText":"Shown instead of \"Serves 8\".","placeholder":"one 10-inch skillet","config":{"maxLength":32},"ui":{"tab":{"id":"overview"},"group":{"id":"yield"}}},{"key":"equipment","label":"Cooked in","type":"array","helpText":"Caldera gear used. Shown with a link to each product.","readOnly":true,"lockReason":"A Shopify product id. No Shopify integration backs a reference in BetterCMS yet, so the id is kept as it is.","config":{"itemType":"text","maxItems":4},"ui":{"tab":{"id":"overview"}}},{"key":"ingredientGroups","label":"Ingredients","type":"table","config":{"minItems":1,"maxItems":8,"fields":[{"key":"title","label":"Group name","type":"text","helpText":"Leave empty for a single list.","placeholder":"For the glaze","config":{"maxLength":40}},{"key":"items","label":"Items","type":"table","fields":[{"key":"amount","label":"Amount","type":"number","helpText":"Leave empty for \"to taste\".","config":{"min":0,"step":0.25}},{"key":"amountMax","label":"Up to","type":"number","helpText":"For ranges like 2–3 cloves.","showIf":{"match":"all","rules":[{"field":"amount","op":"notEmpty"}]},"config":{"min":0,"step":0.25}},{"key":"unit","label":"Unit","type":"select","helpText":"Converts for readers who switch between metric and imperial.","defaultValue":"","options":[{"label":"None","value":""},{"label":"g","value":"g"},{"label":"kg","value":"kg"},{"label":"ml","value":"ml"},{"label":"l","value":"l"},{"label":"tsp","value":"tsp"},{"label":"tbsp","value":"tbsp"},{"label":"cup","value":"cup"},{"label":"oz","value":"oz"},{"label":"lb","value":"lb"},{"label":"clove","value":"clove"},{"label":"pinch","value":"pinch"}]},{"key":"name","label":"Ingredient","type":"text","required":true,"localized":true,"config":{"maxLength":60}},{"key":"prep","label":"Prep","type":"text","helpText":"","localized":true,"placeholder":"melted and cooled","config":{"maxLength":40}},{"key":"isOptional","label":"Optional","type":"boolean","helpText":""},{"key":"product","label":"Shop link","type":"text","helpText":"Caldera pantry items only.","advanced":true,"readOnly":true,"lockReason":"A Shopify product id. No Shopify integration backs a reference in BetterCMS yet, so the id is kept as it is."}],"config":{"minItems":1,"maxItems":30},"ui":{"itemLabel":"Ingredient","itemName":"ingredient","icon":"point","preview":{"title":"name","subtitle":"amount","fallback":"Untitled ingredient"}}}]},"ui":{"itemLabel":"Group","icon":"list","preview":{"title":"title","fallback":"Untitled ingredient group"},"tab":{"id":"ingredients","label":"Ingredients","icon":"list"}}},{"key":"steps","label":"Steps","type":"table","config":{"minItems":1,"maxItems":20,"fields":[{"key":"title","label":"Short name","type":"text","config":{"maxLength":40}},{"key":"instruction","label":"Instruction","type":"richtext","required":true,"localized":true},{"key":"image","label":"Image","type":"image"},{"key":"hasTimer","label":"Add a timer","type":"boolean"},{"key":"timerMinutes","label":"Timer","type":"number","helpText":"","required":true,"showIf":{"match":"all","rules":[{"field":"hasTimer","op":"eq","value":"true"}]},"config":{"unit":"min","min":1,"max":720,"step":1}},{"key":"timerLabel","label":"Timer label","type":"text","helpText":"","showIf":{"match":"all","rules":[{"field":"hasTimer","op":"eq","value":"true"}]},"placeholder":"Bake","config":{"maxLength":20}},{"key":"tip","label":"Tip","type":"longtext","helpText":"Shown as a note under the step.","config":{"maxLength":200},"ui":{"rows":2}}]},"ui":{"itemLabel":"Step","icon":"list-numbers","preview":{"title":"title","subtitle":"instruction","media":"image","fallback":"Untitled step"},"tab":{"id":"method","label":"Method","icon":"list-numbers"}}},{"key":"notes","label":"Cook's notes","type":"richtext","ui":{"tab":{"id":"method"}}},{"key":"showNutrition","label":"Show nutrition","type":"boolean","helpText":"Shown as estimates, with a disclaimer.","defaultValue":true,"ui":{"tab":{"id":"nutrition","label":"Nutrition","icon":"apple"}}},{"key":"calories","label":"Calories","type":"number","showIf":{"match":"all","rules":[{"field":"showNutrition","op":"eq","value":"true"}]},"config":{"unit":"kcal","min":0},"ui":{"tab":{"id":"nutrition"},"group":{"id":"facts","label":"Per serving","kind":"group","columns":2}}},{"key":"protein","label":"Protein","type":"number","showIf":{"match":"all","rules":[{"field":"showNutrition","op":"eq","value":"true"}]},"config":{"unit":"g","min":0},"ui":{"tab":{"id":"nutrition"},"group":{"id":"facts"}}},{"key":"fat","label":"Fat","type":"number","showIf":{"match":"all","rules":[{"field":"showNutrition","op":"eq","value":"true"}]},"config":{"unit":"g","min":0},"ui":{"tab":{"id":"nutrition"},"group":{"id":"facts"}}},{"key":"carbs","label":"Carbohydrates","type":"number","showIf":{"match":"all","rules":[{"field":"showNutrition","op":"eq","value":"true"}]},"config":{"unit":"g","min":0},"ui":{"tab":{"id":"nutrition"},"group":{"id":"facts"}}},{"key":"fiber","label":"Fibre","type":"number","showIf":{"match":"all","rules":[{"field":"showNutrition","op":"eq","value":"true"}]},"config":{"unit":"g","min":0},"ui":{"tab":{"id":"nutrition"},"group":{"id":"facts"}}},{"key":"sodium","label":"Sodium","type":"number","showIf":{"match":"all","rules":[{"field":"showNutrition","op":"eq","value":"true"}]},"config":{"unit":"mg","min":0},"ui":{"tab":{"id":"nutrition"},"group":{"id":"facts"}}},{"key":"rating","label":"Reader rating","type":"rating","helpText":"Average of reader ratings. Not editable.","readOnly":true,"config":{"step":0.1},"ui":{"tab":{"id":"nutrition"}}}],"description":"A recipe with structured ingredients, steps and nutrition","category":"content","icon":"chef-hat","preview":{"title":"title","subtitle":"summary","media":"image"}}},
|
|
1458
|
+
{"id":"course-curriculum","name":"Course curriculum","category":"Long-form & docs","kind":"block","tags":["deep-nesting","repeater","conditional","references","validation","media-heavy","seo-structured"],"proves":"Modules → lessons → resources → file (4 drill levels) · lesson type reveals video / reading / quiz / live fields · free previews","why":"The curriculum sits on the course page and references the Course entry (title, price, instructors, enrolment). Modules → lessons → resources is the deepest editorial structure on the site, so every level drills in with clear previews ('Video · 12 min'). The lesson TYPE is a select that reveals exactly the fields that type needs (video + captions, reading, quiz reference, live start) — no lesson carries empty video and quiz fields at once. Lengths are numbers so totals are computed, not typed. Course JSON-LD (syllabusSections, timeRequired) is derived natively.","htmlSignals":["Accordion of 'Module 1 / Week 1' headings with lesson rows inside","Per-row icons for video / article / quiz and durations like '12 min'","'Preview' or 'Free' badges on some lessons; lock icons on others","Totals like '6 modules · 42 lessons · 9h 30m'"],"mcp":"Each accordion header → module (title, summary from its subtitle). Each row → lesson: type from the row icon/label, minutes from '12:34' or '12 min' (round up), isFreePreview from a Preview/Free badge. Downloadable attachments under a lesson → resources (kind file); external links → kind link. Ignore the totals line — it's derived. Don't import lock icons; set unlock 'previous' when the source says lessons unlock in order.","pitfalls":["Durations typed as text ('1h 05m') can't be summed.","Video lessons without captions — the captions field should be checked before publish (a11y).","Live lesson times stored in local time without a zone.","Five levels inline (course → module → lesson → resource → file) — keep resources tiny and inline."],"notCarried":["category","segmented fallback","variants"],"component":{"name":"Course curriculum","slug":"curriculum","props":[{"key":"heading","label":"Heading","type":"text","defaultValue":"What you'll learn","localized":true,"config":{"maxLength":48},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"course","label":"Course","type":"reference","helpText":"Totals, enrolment and the course page come from here.","required":true,"config":{"contentModelId":"col_courses"},"ui":{"tab":{"id":"content"}}},{"key":"modules","label":"Modules","type":"table","config":{"minItems":1,"maxItems":16,"fields":[{"key":"title","label":"Title","type":"text","required":true,"localized":true,"config":{"maxLength":60}},{"key":"summary","label":"Summary","type":"longtext","recommendedMax":200,"localized":true,"ui":{"rows":2}},{"key":"unlock","label":"Opens","type":"select","helpText":"","defaultValue":"now","options":[{"label":"At enrolment","value":"now"},{"label":"After the last module","value":"previous"},{"label":"On a date","value":"date"}],"ui":{"control":"segmented"}},{"key":"unlockDate","label":"Opens on","type":"date","required":true,"showIf":{"match":"all","rules":[{"field":"unlock","op":"eq","value":"date"}]}},{"key":"lessons","label":"Lessons","type":"table","fields":[{"key":"title","label":"Title","type":"text","required":true,"localized":true,"config":{"maxLength":80}},{"key":"type","label":"Type","type":"select","helpText":"","required":true,"defaultValue":"video","options":[{"label":"Video","value":"video","icon":"player-play"},{"label":"Reading","value":"reading","icon":"file-text"},{"label":"Quiz","value":"quiz","icon":"checklist"},{"label":"Assignment","value":"assignment","icon":"pencil"},{"label":"Live","value":"live","icon":"broadcast"}]},{"key":"minutes","label":"Length","type":"number","helpText":"In minutes. Adds up to the module and course totals.","required":true,"config":{"unit":"min","min":1,"max":240,"step":1}},{"key":"video","label":"Video","type":"video","required":true,"showIf":{"match":"all","rules":[{"field":"type","op":"eq","value":"video"}]}},{"key":"captions","label":"Captions","type":"file","helpText":"A .vtt file. Required on every video lesson.","showIf":{"match":"all","rules":[{"field":"type","op":"eq","value":"video"}]}},{"key":"reading","label":"Reading","type":"richtext","required":true,"showIf":{"match":"all","rules":[{"field":"type","op":"in","value":["reading","assignment"]}]}},{"key":"quiz","label":"Quiz","type":"reference","required":true,"showIf":{"match":"all","rules":[{"field":"type","op":"eq","value":"quiz"}]},"config":{"contentModelId":"col_quizzes"}},{"key":"liveAt","label":"Starts","type":"datetime","helpText":"Shown in each student's own time zone.","required":true,"showIf":{"match":"all","rules":[{"field":"type","op":"eq","value":"live"}]}},{"key":"isFreePreview","label":"Free preview","type":"boolean","helpText":"Visitors can open it before enrolling."},{"key":"resources","label":"Resources","type":"table","openMode":"inline","fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":48}},{"key":"kind","label":"Kind","type":"select","helpText":"","defaultValue":"file","options":[{"label":"File","value":"file"},{"label":"Link","value":"link"}],"ui":{"control":"segmented"}},{"key":"file","label":"File","type":"file","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"file"}]}},{"key":"link","label":"Link","type":"link","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"link"}]}}],"config":{"maxItems":8},"ui":{"itemLabel":"Resource","icon":"paperclip","preview":{"title":"label","subtitle":"kind"}}}],"config":{"minItems":1,"maxItems":20},"ui":{"itemLabel":"Lesson","icon":"player-play","preview":{"title":"title","subtitle":"type","fallback":"Untitled lesson"}}}]},"ui":{"itemLabel":"Module","icon":"folder","preview":{"title":"title","subtitle":"summary","fallback":"Untitled module"},"tab":{"id":"content"}}},{"key":"isFirstOpen","label":"Open the first module","type":"boolean","helpText":"","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"showLengths","label":"Show lengths","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design"}}},{"key":"showNumbers","label":"Number lessons","type":"boolean","helpText":"1.1, 1.2 … restarting each module.","defaultValue":true,"ui":{"tab":{"id":"design"}}}],"description":"Modules and lessons, with free previews and resources","category":"content","icon":"school","preview":{"title":"heading","subtitle":"course","icon":"school"}}},
|
|
1459
|
+
{"id":"legal-policy","name":"Legal / policy page","category":"Long-form & docs","kind":"page","tags":["deep-nesting","repeater","rich-text","validation","conditional","a11y","localized"],"proves":"Numbered sections → clauses with optional plain-English summaries · effective date + version history with archived copies","why":"Policies are static PAGES (one per policy, in the System folder), but their text is structured: sections → clauses, so numbering, anchors and the contents list are generated and stay correct when a clause is inserted. Each clause may carry a plain-English note (conditional), the pattern readers trust. The effective date and version are required; past versions are a repeater with an archived PDF so you can prove what a customer agreed to. policyType is developer-locked because footer and checkout links resolve by it.","htmlSignals":["Headings numbered '1.', '1.1', 'Section 4' or roman numerals","'Effective date', 'Last updated', 'Version' near the title","A contents list of #anchors at the top","Two-column 'In plain English' or 'TL;DR' asides beside clauses","Links to previous versions or PDFs"],"mcp":"Strip typed numbers from headings ('4.2 Returns window' → 'Returns window'); top-level numbered headings → sections, sub-numbers → clauses. Keep existing element ids as anchors (links in emails depend on them). Side notes / TL;DR boxes → hasSummary + plain. 'Last updated' → effectiveDate (ask if the page shows both). Older version links → versions items with archive files downloaded.","pitfalls":["Changing an anchor breaks links in sent emails and order confirmations.","Typed numbering drifts on insert — numbering is generated.","'Last updated' bumped for typos implies the terms changed.","Localized policies must be reviewed per locale, not machine-translated — flag localized fields."],"notCarried":["category","kind:entry (props)"],"component":{"name":"Policy page","slug":"policy-page","props":[{"key":"title","label":"Title","type":"text","required":true,"localized":true,"config":{"maxLength":60},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"slug","label":"Slug","type":"slug","required":true,"ui":{"tab":{"id":"content"}}},{"key":"policyType","label":"Policy","type":"select","helpText":"Footer and checkout links use this.","readOnly":"developer","lockReason":"Set by the developer. Footer and checkout links depend on it.","config":{"options":[{"label":"Terms of service","value":"terms"},{"label":"Privacy","value":"privacy"},{"label":"Cookies","value":"cookies"},{"label":"Returns","value":"returns"},{"label":"Warranty","value":"warranty"},{"label":"Accessibility","value":"accessibility"}]},"ui":{"access":{"edit":"schema.manage"},"tab":{"id":"content"}}},{"key":"effectiveDate","label":"Effective from","type":"date","required":true,"ui":{"tab":{"id":"content"},"group":{"id":"dates","label":"Dates","kind":"group"}}},{"key":"version","label":"Version","type":"text","helpText":"Bump the first number for material changes.","required":true,"config":{"pattern":"^\\d+\\.\\d+$","patternMessage":"Use a version like 2.1."},"ui":{"tab":{"id":"content"},"group":{"id":"dates"}}},{"key":"summary","label":"In short","type":"richtext","localized":true,"ui":{"tab":{"id":"content"}}},{"key":"sections","label":"Sections","type":"table","helpText":"Numbered automatically in this order.","config":{"minItems":1,"maxItems":40,"fields":[{"key":"heading","label":"Heading","type":"text","required":true,"localized":true,"config":{"maxLength":80}},{"key":"anchor","label":"Anchor","type":"text","helpText":"Changing it once live breaks existing links.","config":{"pattern":"^[a-z0-9-]+$","patternMessage":"Use lowercase letters, numbers and hyphens, like camp-kitchen."}},{"key":"clauses","label":"Clauses","type":"table","fields":[{"key":"heading","label":"Heading","type":"text","localized":true,"config":{"maxLength":80}},{"key":"body","label":"Body","type":"richtext","required":true,"localized":true},{"key":"hasSummary","label":"Add a plain-English note","type":"boolean"},{"key":"plain","label":"Plain-English note","type":"longtext","helpText":"","required":true,"showIf":{"match":"all","rules":[{"field":"hasSummary","op":"eq","value":"true"}]},"localized":true,"config":{"maxLength":240},"ui":{"rows":2}}],"config":{"minItems":1,"maxItems":30},"ui":{"itemLabel":"Clause","icon":"align-left","preview":{"title":"heading","subtitle":"body","fallback":"Untitled clause"}}}]},"ui":{"itemLabel":"Section","icon":"section","preview":{"title":"heading","subtitle":"anchor","fallback":"Untitled section"},"tab":{"id":"content"}}},{"key":"contactEmail","label":"Questions go to","type":"email","required":true,"ui":{"tab":{"id":"content"}}},{"key":"versions","label":"Version history","type":"table","config":{"maxItems":30,"fields":[{"key":"version","label":"Version","type":"text","required":true,"config":{"pattern":"^\\d+\\.\\d+$","patternMessage":"Use a version like 2.1."}},{"key":"effectiveDate","label":"Effective from","type":"date","required":true},{"key":"changes","label":"What changed","type":"longtext","required":true,"config":{"maxLength":300},"ui":{"rows":2}},{"key":"archive","label":"Archived copy","type":"file","helpText":"A PDF of the full text as it stood."}]},"ui":{"itemLabel":"Version","itemName":"pastVersion","itemTitle":"Past version","icon":"history","preview":{"title":"version","subtitle":"effectiveDate"},"tab":{"id":"versions","label":"Versions","icon":"history"}}},{"key":"numbering","label":"Numbering","type":"select","helpText":"","defaultValue":"decimal","config":{"options":[{"label":"1. 1.1","value":"decimal"},{"label":"I, A","value":"roman"},{"label":"None","value":"none"}]},"ui":{"control":"segmented","tab":{"id":"settings","label":"Settings","icon":"settings"}}},{"key":"showContents","label":"Show table of contents","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"settings"}}},{"key":"showChangeBanner","label":"Show a banner when it changes","type":"boolean","helpText":"Shows a site banner for 30 days after a new effective date.","ui":{"tab":{"id":"settings"}}}],"description":"Terms, privacy, warranty or returns, numbered and versioned","category":"content","icon":"scale","preview":{"title":"title","subtitle":"version","icon":"scale"}}},
|
|
1460
|
+
{"id":"case-study","name":"Case study","category":"Long-form & docs","kind":"entry","tags":["rich-text","repeater","references","conditional","media-heavy","validation"],"proves":"Customer facts as an object · headline metrics repeater · challenge/solution/results as three rich-text bodies · quote written here or referenced","why":"Case studies are entries in a Customers collection so the customer logo wall, the filterable customer list (industry, size, region, product) and pricing-page proof all query them. Customer facts are real fields in their own group, not prose. The story is three required rich-text bodies — challenge, solution, results — because every case study must answer all three and the template styles each part. Headline metrics are a 2–4 item repeater (value, label, comparison). The quote can be written here or referenced from Testimonials when it's reused elsewhere — one select decides.","htmlSignals":["A customer logo and big numbers row near the top ('3 days', '92%')","Sections headed Challenge / Solution / Results (or Problem / Approach / Outcome)","A large pull quote with name, title and headshot","A side panel: Industry, Company size, Location, Products used","URL under /customers/ or /case-studies/"],"mcp":"Side panel facts → customer group fields (map 'Industry: SaaS' to the select). The numbers row → metrics (value as written, label underneath, 'from X' → context). Split the body at Challenge/Solution/Results headings into the three richtext fields (drop the headings themselves). A pull quote that also appears on other pages → create a Testimonial and use quoteSource 'testimonial'. Download links to a PDF → pdf.","pitfalls":["Metrics without approval — help text says so; numbers are what legal checks.","One undivided body: the Challenge/Solution/Results layout can't be enforced.","Customer logos as JPG with a white box — ask for SVG/transparent PNG.","Duplicate quotes typed in three places drift — reference Testimonials."],"notCarried":["ai","category","kind:entry (props)"],"component":{"name":"Case study","slug":"case-study","props":[{"key":"title","label":"Title","type":"text","required":true,"localized":true,"config":{"maxLength":90},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"slug","label":"Slug","type":"slug","required":true,"ui":{"tab":{"id":"content"}}},{"key":"summary","label":"Summary","type":"longtext","required":true,"recommendedMax":180,"localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"metrics","label":"Headline results","type":"table","openMode":"inline","config":{"minItems":2,"maxItems":4,"fields":[{"key":"value","label":"Number","type":"text","required":true,"placeholder":"3 days","config":{"maxLength":10}},{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":40}},{"key":"context","label":"Compared with","type":"text","placeholder":"down from 11","config":{"maxLength":40}}]},"ui":{"itemLabel":"Result","icon":"chart-bar","preview":{"title":"value","subtitle":"label"},"tab":{"id":"content"}}},{"key":"challenge","label":"The challenge","type":"richtext","required":true,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"solution","label":"The solution","type":"richtext","required":true,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"results","label":"The results","type":"richtext","required":true,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"quoteSource","label":"Quote","type":"select","helpText":"Testimonials are reused on pricing and the homepage.","defaultValue":"written","config":{"options":[{"label":"Write it here","value":"written"},{"label":"From Testimonials","value":"testimonial"},{"label":"None","value":"none"}]},"ui":{"control":"segmented","tab":{"id":"content"},"group":{"id":"quote","label":"Quote","kind":"group"}}},{"key":"quote","label":"Quote","type":"longtext","required":true,"showIf":{"match":"all","rules":[{"field":"quoteSource","op":"eq","value":"written"}]},"recommendedMax":220,"ui":{"rows":3,"tab":{"id":"content"},"group":{"id":"quote"}}},{"key":"quoteName","label":"Name","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"quoteSource","op":"eq","value":"written"}]},"ui":{"tab":{"id":"content"},"group":{"id":"quote"}}},{"key":"quoteRole","label":"Role","type":"text","showIf":{"match":"all","rules":[{"field":"quoteSource","op":"eq","value":"written"}]},"ui":{"tab":{"id":"content"},"group":{"id":"quote"}}},{"key":"quotePhoto","label":"Photo","type":"image","showIf":{"match":"all","rules":[{"field":"quoteSource","op":"eq","value":"written"}]},"ui":{"tab":{"id":"content"},"group":{"id":"quote"}}},{"key":"testimonial","label":"Testimonial","type":"reference","required":true,"showIf":{"match":"all","rules":[{"field":"quoteSource","op":"eq","value":"testimonial"}]},"config":{"contentModelId":"col_testimonials"},"ui":{"tab":{"id":"content"},"group":{"id":"quote"}}},{"key":"productsUsed","label":"Products used","type":"multiselect","helpText":"Also filters the customer list.","config":{"options":[{"label":"Close","value":"close"},{"label":"Reconcile","value":"reconcile"},{"label":"Spend","value":"spend"},{"label":"Reports","value":"reports"}]},"ui":{"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"customerName","label":"Company","type":"text","required":true,"config":{"maxLength":40},"ui":{"tab":{"id":"customer","label":"Customer","icon":"building"}}},{"key":"customerLogo","label":"Logo","type":"image","helpText":"SVG or a transparent PNG.","required":true,"ui":{"tab":{"id":"customer"}}},{"key":"industry","label":"Industry","type":"select","helpText":"","config":{"options":[{"label":"Software","value":"saas"},{"label":"E-commerce","value":"ecommerce"},{"label":"Marketplace","value":"marketplace"},{"label":"Fintech","value":"fintech"},{"label":"Healthcare","value":"healthcare"}]},"ui":{"tab":{"id":"customer"}}},{"key":"companySize","label":"Company size","type":"select","helpText":"","config":{"options":[{"label":"1–50","value":"1-50"},{"label":"51–200","value":"51-200"},{"label":"201–1,000","value":"201-1000"},{"label":"1,000+","value":"1000+"}]},"ui":{"tab":{"id":"customer"}}},{"key":"region","label":"Region","type":"select","helpText":"","config":{"options":[{"label":"North America","value":"na"},{"label":"Europe & Middle East","value":"emea"},{"label":"Asia Pacific","value":"apac"}]},"ui":{"tab":{"id":"customer"}}},{"key":"website","label":"Website","type":"url","ui":{"tab":{"id":"customer"}}},{"key":"coverImage","label":"Cover image","type":"image","required":true,"ui":{"tab":{"id":"media","label":"Media","icon":"photo"}}},{"key":"pdf","label":"PDF version","type":"file","helpText":"Offered as a download on the page.","ui":{"tab":{"id":"media"}}}],"description":"A customer story with results","category":"content","icon":"briefcase","preview":{"title":"title","subtitle":"customerName","media":"customerLogo"}}},
|
|
1461
|
+
{"id":"podcast-episode","name":"Podcast episode","category":"Long-form & docs","kind":"entry","tags":["media-heavy","embed","references","repeater","seo-structured","a11y","validation","conditional"],"proves":"Audio file + show notes + timestamped chapters + transcript (a11y) · guests by reference · feeds the RSS and PodcastEpisode JSON-LD","why":"Episodes are entries: each has a URL, audio, notes and a transcript, and the same entry feeds three outputs — the episode page, the podcast RSS feed (enclosure, itunes:duration, itunes:episode/season/episodeType, content:encoded from show notes) and PodcastEpisode JSON-LD — all native. Duration is read from the file (read-only) so the feed is never wrong. Chapters are a tiny inline repeater with a time pattern. Episode number is required only for full episodes (bonus and trailers aren't numbered). The transcript is either rich text or a caption file — one select reveals one field.","htmlSignals":["An <audio> player or a Transistor/Buzzsprout/Simplecast iframe as the page's main media","'Episode 42', 'S3 E42', a duration like '48:54'","Timestamps (12:30) linking to positions in the player","'Show notes', 'Mentioned in this episode', 'Transcript' sections","Guest cards with headshots and links"],"mcp":"Pull the episode from the show's RSS <item> when available (enclosure → audio, itunes:* → numbering/type/duration/isExplicit, content:encoded → showNotes, podcast:chapters/podcast:transcript → chapters/transcript). From the page: '12:30 — Topic' lines → chapters; 'Transcript' section → transcript (speaker labels bolded). Guests → people entries. Don't store the embed iframe; store the audio file.","pitfalls":["Episode number typed into the title ('Ep. 42 —') duplicates in apps.","Summary with HTML/links — some apps show raw tags.","Missing transcript fails accessibility and loses search traffic.","Changing the audio file URL after publish re-downloads in every subscriber's app — replace in place."],"notCarried":["category","kind:entry (props)"],"component":{"name":"Podcast episode","slug":"podcast-episode","props":[{"key":"title","label":"Title","type":"text","helpText":"Without the episode number. Apps add it.","required":true,"config":{"maxLength":90},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"slug","label":"Slug","type":"slug","required":true,"ui":{"tab":{"id":"content"}}},{"key":"episodeType","label":"Type","type":"select","helpText":"","defaultValue":"full","config":{"options":[{"label":"Full","value":"full"},{"label":"Bonus","value":"bonus"},{"label":"Trailer","value":"trailer"}]},"ui":{"control":"segmented","tab":{"id":"content"},"group":{"id":"numbering","label":"Numbering","kind":"group","columns":2}}},{"key":"season","label":"Season","type":"number","config":{"min":1,"step":1},"ui":{"tab":{"id":"content"},"group":{"id":"numbering"}}},{"key":"episodeNumber","label":"Episode","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"episodeType","op":"eq","value":"full"}]},"config":{"min":1,"step":1},"ui":{"tab":{"id":"content"},"group":{"id":"numbering"}}},{"key":"publishedAt","label":"Publish date","type":"datetime","helpText":"Also the release time in podcast apps.","required":true,"ui":{"tab":{"id":"content"}}},{"key":"summary","label":"Summary","type":"longtext","helpText":"The first lines podcast apps show. No links.","required":true,"recommendedMax":250,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"showNotes","label":"Show notes","type":"richtext","helpText":"Also goes into the RSS feed.","required":true,"ui":{"tab":{"id":"content"}}},{"key":"hosts","label":"Hosts","type":"multi-reference","config":{"contentModelId":"col_authors","min":1,"max":3},"ui":{"tab":{"id":"content"}}},{"key":"guests","label":"Guests","type":"multi-reference","helpText":"Each guest gets a card and links back from their profile.","config":{"contentModelId":"col_people","max":4},"ui":{"tab":{"id":"content"}}},{"key":"chapters","label":"Chapters","type":"table","openMode":"inline","config":{"maxItems":30,"fields":[{"key":"start","label":"Starts at","type":"text","required":true,"placeholder":"12:30","config":{"pattern":"^(\\d{1,2}:)?[0-5]?\\d:[0-5]\\d$","patternMessage":"Use mm:ss or h:mm:ss, like 12:40."}},{"key":"title","label":"Title","type":"text","required":true,"config":{"maxLength":60}}]},"ui":{"itemLabel":"Chapter","icon":"clock","preview":{"title":"title","subtitle":"start"},"tab":{"id":"content"}}},{"key":"audio","label":"Audio file","type":"file","helpText":"MP3 or M4A. The player and the RSS feed both use it.","required":true,"ui":{"tab":{"id":"audio","label":"Audio","icon":"headphones"}}},{"key":"durationSeconds","label":"Length","type":"number","helpText":"Read from the audio file.","readOnly":true,"config":{"unit":"s"},"ui":{"tab":{"id":"audio"}}},{"key":"isExplicit","label":"Explicit content","type":"boolean","helpText":"Marks it explicit in podcast apps.","ui":{"tab":{"id":"audio"}}},{"key":"art","label":"Episode art","type":"image","helpText":"Square, 3000 × 3000. Leave empty to use the show art.","ui":{"tab":{"id":"audio"}}},{"key":"video","label":"Video version","type":"video","ui":{"tab":{"id":"audio"}}},{"key":"transcriptSource","label":"Transcript","type":"select","defaultValue":"text","config":{"options":[{"label":"Write or paste","value":"text"},{"label":"Upload file","value":"file"}]},"ui":{"control":"segmented","tab":{"id":"transcript","label":"Transcript","icon":"file-text"}}},{"key":"transcript","label":"Transcript text","type":"richtext","helpText":"Bold each speaker's name at the start of their turn.","showIf":{"match":"all","rules":[{"field":"transcriptSource","op":"eq","value":"text"}]},"ui":{"tab":{"id":"transcript"}}},{"key":"transcriptFile","label":"Transcript file","type":"file","helpText":"A .vtt or .srt file. Also used as player captions.","showIf":{"match":"all","rules":[{"field":"transcriptSource","op":"eq","value":"file"}]},"ui":{"tab":{"id":"transcript"}}}],"description":"One episode of the show, with audio, notes and transcript","category":"content","icon":"microphone","preview":{"title":"title","subtitle":"episodeNumber","media":"art"}}},
|
|
1462
|
+
{"id":"team-grid","name":"Team grid","category":"Company & people","kind":"block","tags":["references","dynamic-query","conditional","variants","a11y","validation"],"proves":"Everyone by query or hand-picked people · department filter tabs · card fields as toggles · optional 'We're hiring' card","why":"People are a collection (name, slug, role, department, location, photo with alt, pronouns, short bio, socials, start/end dates) because the same person is an author, an event speaker and a team-page face. The grid is either everyone (a query: departments, hide alumni, order) or a hand-picked, ordered list of references — the mode select shows one or the other. Card fields and photo treatment are design. The hiring card is three fields behind one toggle.","htmlSignals":["A grid of square/portrait headshots with a name and a smaller role line under each","Department tabs or headings (Engineering, Design, Operations)","Hover states revealing a bio, fun fact or LinkedIn icon","A final card that's not a person: 'We're hiring' / 'Join us'"],"mcp":"Each face → a People entry (name, role, photo with alt 'Portrait of {name}', department from the enclosing heading/tab, socials from icons). If the grid shows everyone in the company, use source 'query' with the department order; if it's a curated subset (e.g. 'Meet the founders'), use 'pick' in DOM order. The non-person card → hiring fields.","pitfalls":["Retyping people per page — a new photo must update everywhere.","Portraits with empty alt; decorative hover images need alt=\"\" on the duplicate.","Former employees lingering — set end dates, keep excludeAlumni on.","Department tabs with one person each look broken — hide tabs under ~12 people."],"notCarried":["category","variants"],"component":{"name":"Team grid","slug":"team-grid","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"intro","label":"Intro","type":"longtext","helpText":"","localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"source","label":"People","type":"select","helpText":"Everyone adds new starters when their profile is published.","defaultValue":"query","config":{"options":[{"label":"Everyone","value":"query"},{"label":"Pick people","value":"pick"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"query","label":"Who to show","type":"object","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"query"}]},"config":{"fields":[{"key":"collection","label":"Collection","type":"select","helpText":"","defaultValue":"col_people","readOnly":"developer","lockReason":"The collection is wired to the card template in code.","options":[{"label":"People","value":"col_people"}],"ui":{"access":{"edit":"schema.manage"}}},{"key":"departments","label":"Departments","type":"multiselect","helpText":"Leave empty for every department.","options":[{"label":"Workshop","value":"workshop"},{"label":"Design","value":"design"},{"label":"Customer care","value":"care"},{"label":"Operations","value":"ops"},{"label":"Marketing","value":"marketing"},{"label":"Leadership","value":"leadership"}]},{"key":"hideAlumni","label":"Hide people who've left","type":"boolean","helpText":"Uses the end date on each person.","defaultValue":true},{"key":"sort","label":"Order","type":"select","helpText":"","defaultValue":"department","options":[{"label":"By department, then name","value":"department"},{"label":"A to Z","value":"name"},{"label":"Longest-serving first","value":"start"}]}]},"ui":{"tab":{"id":"content"}}},{"key":"people","label":"People","type":"multi-reference","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"pick"}]},"unique":true,"config":{"min":1,"max":48,"contentModelId":"col_people"},"ui":{"itemName":"person","itemTitle":"Person","tab":{"id":"content"}}},{"key":"showDepartmentTabs","label":"Show department tabs","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"content"}}},{"key":"showHiringCard","label":"Show a hiring card","type":"boolean","ui":{"tab":{"id":"content"},"group":{"id":"hiring","label":"Hiring card","kind":"group","collapsed":true,"description":"An extra card at the end of the grid."}}},{"key":"hiringHeading","label":"Card heading","type":"text","helpText":"","defaultValue":"This could be you","showIf":{"match":"all","rules":[{"field":"showHiringCard","op":"eq","value":"true"}]},"config":{"maxLength":40},"ui":{"tab":{"id":"content"},"group":{"id":"hiring"}}},{"key":"hiringButton","label":"Hiring card button","type":"button","showIf":{"match":"all","rules":[{"field":"showHiringCard","op":"eq","value":"true"}]},"ui":{"tab":{"id":"content"},"group":{"id":"hiring"}}},{"key":"cardShows","label":"Card shows","type":"multiselect","helpText":"","defaultValue":["role"],"config":{"options":[{"label":"Role","value":"role"},{"label":"Location","value":"location"},{"label":"Pronouns","value":"pronouns"},{"label":"Social links","value":"social"},{"label":"Fun fact on hover","value":"funFact"}]},"ui":{"control":"checkbox","tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"photoTreatment","label":"Photos","type":"select","defaultValue":"color","config":{"options":[{"label":"Color","value":"color"},{"label":"Black & white","value":"mono"},{"label":"Brand duotone","value":"duotone"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"columns","label":"Columns on desktop","type":"number","helpText":"","defaultValue":4,"config":{"min":2,"max":6,"step":1},"ui":{"control":"slider","tab":{"id":"design"}}}],"description":"Faces and roles, filterable by department","category":"content","icon":"users","preview":{"title":"heading","subtitle":"source","icon":"users"}}},
|
|
1463
|
+
{"id":"leadership-bios","name":"Leadership with bios","category":"Company & people","kind":"block","tags":["references","conditional","rich-text","repeater","variants","a11y"],"proves":"Groups (Leadership, Board, Advisors) → person references with per-placement title and bio overrides · bio in modal, drawer or inline","why":"Leaders are People entries, so their photo and bio are shared with the team grid, press kit and event pages. The section is groups → member objects; each member is a reference plus per-placement overrides (title here, affiliation for outside board members, an optional page-specific bio behind a toggle). The same person can sit in two groups (CEO and board chair) without duplicating them. Where the bio opens is a design choice; dialogs and drawers must restore focus (a11y).","htmlSignals":["Larger portraits than a team grid with name + C-level title, often in a separate band","'Read bio' buttons opening a modal/dialog (role=dialog, aria-modal)","Sub-headings 'Leadership', 'Board of directors', 'Advisors', 'Investors'","Board members listed with their firm ('Partner, Accel')"],"mcp":"Create/match People entries for each person (photo, name, title, bio from the modal content). Sub-headings → groups. If the modal bio differs from the person's main bio (or no person page exists), put it on the member with ownBio true. Board titles that differ from the job title → titleOverride; firm names → affiliation. bioOpens from the interaction (modal → dialog, accordion → inline).","pitfalls":["Duplicating the CEO as a second person for the board group — reference twice instead.","Modal bios that don't trap and return focus.","Bios in first person on one page and third on another — the override exists for this.","Investor logos mixed into this section — they belong in a logo wall."],"notCarried":["category","variants"],"component":{"name":"Leadership","slug":"leadership","props":[{"key":"heading","label":"Heading","type":"text","required":true,"localized":true,"config":{"maxLength":48},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"intro","label":"Intro","type":"longtext","helpText":"","ui":{"rows":2,"tab":{"id":"content"}}},{"key":"groups","label":"Groups","type":"table","config":{"minItems":1,"maxItems":4,"fields":[{"key":"title","label":"Group name","type":"text","required":true,"localized":true,"placeholder":"Board of directors","config":{"maxLength":32}},{"key":"members","label":"People","type":"table","fields":[{"key":"person","label":"Person","type":"reference","required":true,"config":{"contentModelId":"col_people"}},{"key":"titleOverride","label":"Title here","type":"text","helpText":"Only if it differs from their job title.","config":{"maxLength":48}},{"key":"affiliation","label":"Affiliation","type":"text","config":{"maxLength":48}},{"key":"useOwnBio","label":"Write a bio for this page","type":"boolean","helpText":"Otherwise their profile bio is used."},{"key":"bio","label":"Bio","type":"richtext","required":true,"showIf":{"match":"all","rules":[{"field":"useOwnBio","op":"eq","value":"true"}]},"recommendedMax":900,"localized":true}],"config":{"minItems":1,"maxItems":16},"ui":{"itemLabel":"Person","icon":"user","preview":{"title":"person","subtitle":"titleOverride","fallback":"Untitled person"}}}]},"ui":{"itemLabel":"Group","itemName":"peopleGroup","icon":"users-group","preview":{"title":"title","fallback":"Untitled group"},"tab":{"id":"content"}}},{"key":"bioOpens","label":"Full bio opens in","type":"select","defaultValue":"dialog","config":{"options":[{"label":"Dialog","value":"dialog"},{"label":"Side drawer","value":"drawer"},{"label":"Expands in place","value":"inline"},{"label":"Their page","value":"page"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"showLinkedIn","label":"LinkedIn link","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design"}}},{"key":"columns","label":"Columns on desktop","type":"number","helpText":"","defaultValue":3,"config":{"min":2,"max":4,"step":1},"ui":{"control":"slider","tab":{"id":"design"}}}],"description":"Leaders, board and advisors with full bios","category":"content","icon":"user-star","preview":{"title":"heading","icon":"user-star"}}},
|
|
1464
|
+
{"id":"values-culture","name":"Values & culture","category":"Company & people","kind":"block","tags":["repeater","media-heavy","variants","validation","a11y"],"proves":"Values with a real example each · perks as a light repeater · culture photo wall with required alt","why":"Values are a short repeater of title → meaning → a real example (the field that separates a credible values page from a poster). Perks are a light inline repeater (icon, label, detail). The photo wall is an array of images with alt required, laid out as a grid. Layout and background are variants/design. Nothing here is shared, so it stays a section — no collection needed.","htmlSignals":["3–8 short headed blocks with an icon or number ('01', '02') and a paragraph each","Headings like 'Our values', 'What we believe', 'How we work'","A grid of benefit icons with one-line labels (perks)","A collage of candid team photos"],"mcp":"Each headed block → a value (icon from its SVG name if recognizable, else leave empty). A second sentence that tells a concrete story → example. Benefits list → perks. Team photo collage → photos (write alt from context; never 'team photo'). Numbered styling → variant 'numbered'.","pitfalls":["Photos without alt (or alt='team') — describe who's doing what.","Perks with prices or policies that change — keep them short and dated in HR, not here.","Nine-value lists — validation caps at eight; past five, nobody remembers them."],"notCarried":["ai","category","variants"],"component":{"name":"Values and culture","slug":"values","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"intro","label":"Intro","type":"longtext","helpText":"","localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"values","label":"Values","type":"table","config":{"minItems":3,"maxItems":8,"fields":[{"key":"icon","label":"Icon","type":"icon"},{"key":"title","label":"Value","type":"text","required":true,"localized":true,"config":{"maxLength":40}},{"key":"body","label":"Body","type":"longtext","required":true,"recommendedMax":200,"localized":true,"ui":{"rows":3}},{"key":"example","label":"In practice","type":"longtext","recommendedMax":160,"localized":true,"ui":{"rows":2}},{"key":"image","label":"Image","type":"image","helpText":"Alternating layout only."}]},"ui":{"itemLabel":"Value","icon":"heart","preview":{"title":"title","subtitle":"body","media":"image","fallback":"Untitled value"},"tab":{"id":"content"}}},{"key":"perks","label":"Perks","type":"table","openMode":"inline","config":{"maxItems":12,"fields":[{"key":"icon","label":"Icon","type":"icon"},{"key":"label","label":"Perk","type":"text","required":true,"config":{"maxLength":32}},{"key":"detail","label":"Detail","type":"text","config":{"maxLength":80}}]},"ui":{"itemLabel":"Perk","icon":"gift","preview":{"title":"label","subtitle":"detail"},"tab":{"id":"culture","label":"Culture","icon":"photo"}}},{"key":"photos","label":"Photo wall","type":"array","config":{"maxItems":9,"itemType":"image","item":{"required":true}},"ui":{"layout":"gallery","itemTitle":"Photo","tab":{"id":"culture"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"theme","label":"Background","type":"select","helpText":"","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design","icon":"palette"}}}],"description":"What we believe, how it shows up, and what it's like to work here","category":"content","icon":"heart-handshake","preview":{"title":"heading","subtitle":"intro","icon":"heart-handshake"}}},
|
|
1465
|
+
{"id":"careers-list","name":"Careers list","category":"Company & people","kind":"block","tags":["dynamic-query","conditional","references","variants","embed"],"proves":"Open roles from the Jobs collection or a hiring system (Greenhouse, Lever, Ashby) · grouped by department or location · filters","why":"Roles are entries (each with its own page and JobPosting data), so the list is a query — or, for companies already on an ATS, a synced source whose provider and board are developer-locked. One select decides which settings show. Grouping and the filter set are editorial; filters hide themselves below six roles because three dropdowns over two roles reads as broken. The empty state is mandatory content: most of the year a small company has no roles.","htmlSignals":["Department headings with rows of job titles, each with location and 'Remote'/'Hybrid' tags","Links to boards.greenhouse.io, jobs.lever.co, jobs.ashbyhq.com, or an embedded board iframe","Filter dropdowns for Department / Location / Team","'No open positions' message with a general application link"],"mcp":"If job links go to an ATS host, set source 'ats', provider from the host and board from the path; don't import jobs (they sync). If jobs have pages on the site, import each as a Jobs entry (see job-posting) and use source 'cms'. Department headings → groupBy 'department'; location headings → 'location'. Copy the empty-state text if visible.","pitfalls":["Copying job rows into the section — they're stale the day a role closes.","Closed roles still listed — the query hard-filters on status.","ATS iframes are unstyled and invisible to search — prefer the sync.","Salary on the row but not on the job page (or vice versa) — one source: the job entry."],"notCarried":["category","variants"],"component":{"name":"Careers list","slug":"careers-list","props":[{"key":"heading","label":"Heading","type":"text","required":true,"localized":true,"config":{"maxLength":48},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"intro","label":"Intro","type":"longtext","helpText":"","ui":{"rows":2,"tab":{"id":"content"}}},{"key":"source","label":"Roles come from","type":"select","helpText":"","defaultValue":"cms","config":{"options":[{"label":"Jobs collection","value":"cms","description":"Written in BetterCMS, each with its own page"},{"label":"Hiring system","value":"ats","description":"Synced from Greenhouse, Lever or Ashby"}]},"ui":{"control":"radio","tab":{"id":"source","label":"Roles","icon":"database"}}},{"key":"query","label":"Which roles","type":"object","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"cms"}]},"config":{"fields":[{"key":"collection","label":"Collection","type":"select","helpText":"","defaultValue":"col_jobs","readOnly":"developer","lockReason":"The collection is wired to the card template in code.","options":[{"label":"Jobs","value":"col_jobs"}],"ui":{"access":{"edit":"schema.manage"}}},{"key":"departments","label":"Departments","type":"multi-reference","helpText":"Leave empty for every department.","config":{"contentModelId":"col_departments"}},{"key":"locations","label":"Locations","type":"multi-reference","helpText":"Leave empty for every location.","config":{"contentModelId":"col_locations"}},{"key":"showOpenOnly","label":"Only open roles","type":"boolean","defaultValue":true,"readOnly":true}]},"ui":{"tab":{"id":"source"}}},{"key":"ats","label":"Hiring system","type":"object","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"ats"}]},"openMode":"inline","config":{"fields":[{"key":"provider","label":"Provider","type":"select","helpText":"","required":true,"readOnly":"developer","lockReason":"Set by the developer when the job board is connected.","options":[{"label":"Greenhouse","value":"greenhouse"},{"label":"Lever","value":"lever"},{"label":"Ashby","value":"ashby"},{"label":"Workable","value":"workable"}],"ui":{"access":{"edit":"schema.manage"}}},{"key":"board","label":"Job board name","type":"text","helpText":"The name in the board URL, e.g. boards.greenhouse.io/caldera.","required":true,"readOnly":"developer","lockReason":"From the provider's public board URL.","ui":{"access":{"edit":"schema.manage"}}},{"key":"applyOn","label":"Apply button opens","type":"select","defaultValue":"site","options":[{"label":"Our job page","value":"site"},{"label":"Their hosted page","value":"ats"}],"ui":{"control":"segmented"}}]},"ui":{"tab":{"id":"source"}}},{"key":"groupBy","label":"Group by","type":"select","helpText":"","defaultValue":"department","config":{"options":[{"label":"Department","value":"department"},{"label":"Location","value":"location"},{"label":"No groups","value":"none"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"filters","label":"Filters","type":"multiselect","helpText":"Hidden when there are fewer than 6 roles.","defaultValue":["department","location"],"config":{"options":[{"label":"Department","value":"department"},{"label":"Location","value":"location"},{"label":"Remote / hybrid / on-site","value":"workplace"},{"label":"Full-time / part-time","value":"type"}]},"ui":{"control":"checkbox","tab":{"id":"content"}}},{"key":"emptyHeading","label":"Heading","type":"text","helpText":"","defaultValue":"No open roles right now","config":{"maxLength":48},"ui":{"tab":{"id":"content"},"group":{"id":"empty","label":"When there are no roles","kind":"group","collapsed":true}}},{"key":"emptyBody","label":"Text","type":"longtext","helpText":"","ui":{"rows":2,"tab":{"id":"content"},"group":{"id":"empty"}}},{"key":"emptyButton","label":"Empty button","type":"button","ui":{"tab":{"id":"content"},"group":{"id":"empty"}}},{"key":"rowShows","label":"Each role shows","type":"multiselect","helpText":"","defaultValue":["location","workplace"],"config":{"options":[{"label":"Location","value":"location"},{"label":"Workplace","value":"workplace"},{"label":"Employment type","value":"type"},{"label":"Salary range","value":"salary"},{"label":"Posted date","value":"posted"}]},"ui":{"control":"checkbox","tab":{"id":"design","label":"Design","icon":"palette"}}}],"description":"Open roles, grouped and filterable","category":"content","icon":"id-badge-2","preview":{"title":"heading","subtitle":"source","icon":"briefcase"}}},
|
|
1466
|
+
{"id":"job-posting","name":"Job posting","category":"Company & people","kind":"entry","tags":["seo-structured","conditional","references","repeater","validation","form"],"proves":"Workplace reveals remote regions or office days · salary range with currency and period · apply by form, link or email · JobPosting JSON-LD","why":"A job is an entry because it needs its own URL, it's listed by the careers query, and it emits JobPosting JSON-LD natively (title, description from summary + about, datePosted from publish, validThrough, employmentType, jobLocation from location references or applicantLocationRequirements + jobLocationType TELECOMMUTE for remote, baseSalary from the range). The select values are the schema.org enums on purpose. Workplace reveals exactly one of locations / office days / remote regions; the apply method reveals exactly one of form / URL / email. Responsibilities and requirements are arrays of strings, not one rich-text blob, so the template can count and style them.","htmlSignals":["Headings 'About the role', 'What you'll do', 'What you bring', 'Nice to have', 'Benefits'","A meta strip: Department · Location · Full-time · Remote/Hybrid","A salary range like '$52,000 – $61,000 a year' (pay transparency)","An 'Apply' button to an ATS or an embedded application form","JobPosting JSON-LD in the source"],"mcp":"Use the page's JobPosting JSON-LD when present (map employmentType, baseSalary.value.minValue/maxValue/unitText, jobLocationType). Bullet lists under 'What you'll do' → responsibilities, 'What you bring/Requirements' → requirements, 'Nice to have/Bonus' → bonusPoints (one string per <li>). Benefit lists are company-wide → turn on showBenefits and import them once into the shared block. Apply button host → applyMethod url; mailto: → email; on-page form → create a form and reference it.","pitfalls":["salaryMax lower than salaryMin — cross-field validation isn't in the schema language yet; the renderer must refuse it.","Remote roles with a physical location set confuse Google — the conditional hides it.","Closed roles deleted instead of closed: their URLs 404 and lose links. Close them.","Benefits retyped on every job drift — use the shared block."],"notCarried":["category","kind:entry (props)"],"component":{"name":"Job","slug":"job","props":[{"key":"title","label":"Job title","type":"text","required":true,"config":{"maxLength":70},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"slug","label":"Slug","type":"slug","required":true,"ui":{"tab":{"id":"content"}}},{"key":"status","label":"Status","type":"select","helpText":"Paused and closed roles leave the list.","defaultValue":"open","config":{"options":[{"label":"Open","value":"open"},{"label":"Paused","value":"paused"},{"label":"Closed","value":"closed"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"summary","label":"Summary","type":"longtext","required":true,"recommendedMax":300,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"about","label":"About the role","type":"richtext","required":true,"ui":{"tab":{"id":"content"}}},{"key":"responsibilities","label":"You'll","type":"array","config":{"minItems":3,"maxItems":10,"itemType":"text","item":{"required":true,"maxLength":140}},"ui":{"itemLabel":"Responsibility","itemName":"item","itemTitle":"Responsibility","tab":{"id":"content"}}},{"key":"requirements","label":"You have","type":"array","config":{"minItems":2,"maxItems":8,"itemType":"text","item":{"required":true,"maxLength":140}},"ui":{"itemLabel":"Requirement","itemName":"item","itemTitle":"Requirement","tab":{"id":"content"}}},{"key":"bonusPoints","label":"Bonus points","type":"array","config":{"maxItems":6,"itemType":"text","item":{"maxLength":140}},"ui":{"itemLabel":"Bonus point","tab":{"id":"content"}}},{"key":"showBenefits","label":"Show company benefits","type":"boolean","helpText":"The shared Benefits block, edited once in Blocks › Global.","defaultValue":true,"ui":{"tab":{"id":"content"}}},{"key":"department","label":"Department","type":"reference","required":true,"config":{"contentModelId":"col_departments"},"ui":{"tab":{"id":"details","label":"Details","icon":"list-details"}}},{"key":"hiringManager","label":"Hiring manager","type":"reference","config":{"contentModelId":"col_people"},"ui":{"tab":{"id":"details"}}},{"key":"employmentType","label":"Type","type":"select","helpText":"","required":true,"config":{"options":[{"label":"Full-time","value":"FULL_TIME"},{"label":"Part-time","value":"PART_TIME"},{"label":"Contract","value":"CONTRACTOR"},{"label":"Temporary","value":"TEMPORARY"},{"label":"Internship","value":"INTERN"}]},"ui":{"tab":{"id":"details"}}},{"key":"workplace","label":"Workplace","type":"select","helpText":"","required":true,"defaultValue":"onsite","config":{"options":[{"label":"On-site","value":"onsite"},{"label":"Hybrid","value":"hybrid"},{"label":"Remote","value":"remote"}]},"ui":{"control":"segmented","tab":{"id":"details"},"group":{"id":"where","label":"Where","kind":"group","order":1}}},{"key":"locations","label":"Locations","type":"multi-reference","showIf":{"match":"all","rules":[{"field":"workplace","op":"in","value":["onsite","hybrid"]}]},"config":{"min":1,"max":5,"contentModelId":"col_locations"},"ui":{"tab":{"id":"details"},"group":{"id":"where"}}},{"key":"officeDays","label":"Days in the office","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"workplace","op":"eq","value":"hybrid"}]},"config":{"unit":"days / week","min":1,"max":4,"step":1},"ui":{"tab":{"id":"details"},"group":{"id":"where"}}},{"key":"remoteRegions","label":"Can work from","type":"multiselect","required":true,"showIf":{"match":"all","rules":[{"field":"workplace","op":"eq","value":"remote"}]},"config":{"options":[{"label":"United States","value":"US"},{"label":"Canada","value":"CA"},{"label":"Mexico","value":"MX"},{"label":"United Kingdom","value":"GB"}]},"ui":{"tab":{"id":"details"},"group":{"id":"where"}}},{"key":"showSalary","label":"Show the pay range","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"details"},"group":{"id":"pay","label":"Pay","kind":"group","description":"Shown when the range is on. Several states require it.","columns":2,"order":0}}},{"key":"salaryMin","label":"From","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"showSalary","op":"eq","value":"true"}]},"config":{"min":0,"step":1000},"ui":{"tab":{"id":"details"},"group":{"id":"pay"}}},{"key":"salaryMax","label":"To","type":"number","helpText":"Must be at least the From amount.","required":true,"showIf":{"match":"all","rules":[{"field":"showSalary","op":"eq","value":"true"}]},"config":{"min":0,"step":1000},"ui":{"tab":{"id":"details"},"group":{"id":"pay"}}},{"key":"currency","label":"Currency","type":"select","helpText":"","defaultValue":"USD","showIf":{"match":"all","rules":[{"field":"showSalary","op":"eq","value":"true"}]},"config":{"options":[{"label":"USD","value":"USD"},{"label":"CAD","value":"CAD"},{"label":"GBP","value":"GBP"},{"label":"EUR","value":"EUR"}]},"ui":{"tab":{"id":"details"},"group":{"id":"pay"}}},{"key":"period","label":"Per","type":"select","helpText":"","defaultValue":"YEAR","showIf":{"match":"all","rules":[{"field":"showSalary","op":"eq","value":"true"}]},"config":{"options":[{"label":"Hour","value":"HOUR"},{"label":"Month","value":"MONTH"},{"label":"Year","value":"YEAR"}]},"ui":{"tab":{"id":"details"},"group":{"id":"pay"}}},{"key":"validThrough","label":"Apply by","type":"date","helpText":"Google stops showing the job after this date. Leave empty if open-ended.","ui":{"tab":{"id":"apply","label":"Applying","icon":"send"}}},{"key":"applyMethod","label":"People apply","type":"select","helpText":"","defaultValue":"form","config":{"options":[{"label":"With a form on this page","value":"form"},{"label":"On another site","value":"url","description":"Greenhouse, Lever, Workable…"},{"label":"By email","value":"email"}]},"ui":{"control":"radio","tab":{"id":"apply"}}},{"key":"applyForm","label":"Application form","type":"form","helpText":"Submissions arrive in Growth › Forms, tagged with this job.","required":true,"showIf":{"match":"all","rules":[{"field":"applyMethod","op":"eq","value":"form"}]},"ui":{"tab":{"id":"apply"}}},{"key":"applyUrl","label":"Application link","type":"url","required":true,"showIf":{"match":"all","rules":[{"field":"applyMethod","op":"eq","value":"url"}]},"ui":{"tab":{"id":"apply"}}},{"key":"applyEmail","label":"Send applications to","type":"email","helpText":"Opens an email with the job title filled in.","required":true,"showIf":{"match":"all","rules":[{"field":"applyMethod","op":"eq","value":"email"}]},"ui":{"tab":{"id":"apply"}}},{"key":"applyLabel","label":"Button label","type":"text","helpText":"","defaultValue":"Apply for this role","config":{"maxLength":28},"ui":{"tab":{"id":"apply"}}}],"description":"One open role with its own page","category":"content","icon":"id-badge-2","preview":{"title":"title","subtitle":"department","icon":"briefcase"}}},
|
|
1467
|
+
{"id":"about-timeline","name":"About & mission with timeline","category":"Company & people","kind":"block","tags":["repeater","media-heavy","rich-text","references","variants","validation"],"proves":"Mission + story (rich text) · founders by reference · stats · dated milestones with optional photo and link · order and orientation as design","why":"The about page's story is one section with three jobs: a mission sentence (plain text, required — it's quoted elsewhere), a short rich-text story, and a timeline. Founders are references to People so their portraits match the team page. Milestones are an unordered repeater sorted by year + month at render time, so inserting 2020 doesn't mean dragging. Orientation and order are variants/design, never content.","htmlSignals":["Years as big numerals (2017, 2019…) along a vertical or horizontal line","Headings like 'Our story', 'Our journey', 'How it started', 'Milestones'","A one-sentence mission statement set large near the top","Founder portraits with names and 'Co-founder' roles"],"mcp":"Each year marker + caption → a milestone (year as a number, month if shown, first line → title, remainder → body, adjacent image → image). The large standalone sentence → mission; surrounding paragraphs → story. Founder photos → People references (create if missing). Number rows ('212k pans') → stats. Don't reproduce the line/dots.","pitfalls":["Years stored as strings ('2017–2019') can't sort — use the start year and put the range in the title.","A timeline of 30 tiny events — the cap is 30; most stories need 6–10.","Founder bios duplicated here — reference people instead."],"notCarried":["ai","category","variants"],"component":{"name":"Story and timeline","slug":"about-story","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":56,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"mission","label":"Mission","type":"longtext","required":true,"recommendedMax":160,"localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"story","label":"Story","type":"richtext","localized":true,"ui":{"tab":{"id":"content"}}},{"key":"founders","label":"Founders","type":"multi-reference","helpText":"Photos and names come from their profiles.","config":{"max":4,"contentModelId":"col_people"},"ui":{"tab":{"id":"content"}}},{"key":"stats","label":"Numbers","type":"table","openMode":"inline","config":{"maxItems":4,"fields":[{"key":"value","label":"Number","type":"text","required":true,"placeholder":"212k","config":{"maxLength":10}},{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":40}}]},"ui":{"itemLabel":"Number","icon":"chart-bar","preview":{"title":"value","subtitle":"label"},"tab":{"id":"content"}}},{"key":"milestones","label":"Milestones","type":"table","helpText":"Sorted by date on the page.","config":{"minItems":2,"maxItems":30,"fields":[{"key":"year","label":"Year","type":"number","required":true,"config":{"min":1900,"max":2100,"step":1}},{"key":"month","label":"Month","type":"select","helpText":"Orders milestones within a year.","options":[{"label":"Jan","value":"1"},{"label":"Feb","value":"2"},{"label":"Mar","value":"3"},{"label":"Apr","value":"4"},{"label":"May","value":"5"},{"label":"Jun","value":"6"},{"label":"Jul","value":"7"},{"label":"Aug","value":"8"},{"label":"Sep","value":"9"},{"label":"Oct","value":"10"},{"label":"Nov","value":"11"},{"label":"Dec","value":"12"}]},{"key":"title","label":"What happened","type":"text","required":true,"localized":true,"config":{"maxLength":60}},{"key":"body","label":"Body","type":"longtext","recommendedMax":200,"localized":true,"ui":{"rows":2}},{"key":"image","label":"Image","type":"image"},{"key":"link","label":"Link","type":"link"}]},"ui":{"itemLabel":"Milestone","icon":"flag","preview":{"title":"title","subtitle":"year","media":"image","fallback":"Untitled milestone"},"tab":{"id":"timeline","label":"Timeline","icon":"timeline"}}},{"key":"order","label":"Order","type":"select","helpText":"","defaultValue":"oldest","config":{"options":[{"label":"Oldest first","value":"oldest"},{"label":"Newest first","value":"newest"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"theme","label":"Background","type":"select","helpText":"","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"}]},"ui":{"control":"segmented","tab":{"id":"design"}}}],"description":"Why we exist and how we got here","category":"content","icon":"timeline","preview":{"title":"heading","subtitle":"mission","icon":"timeline"}}},
|
|
1468
|
+
{"id":"press-kit","name":"Press & brand kit","category":"Company & people","kind":"block","tags":["deep-nesting","repeater","media-heavy","conditional","validation","a11y"],"proves":"Asset groups → assets → formats → file (4 levels) · kind reveals photo credit or logo background · boilerplate, facts, contact and coverage","why":"A press page is mostly downloads, so the core is asset groups → assets → format variants → file: four drill levels, each with a clear preview (logo thumbnail, 'SVG · 4 KB'). The asset KIND reveals what that kind needs — a background for logos (so a cream logo isn't shown on white), a required credit for photos. Boilerplate is plain text with a hard limit because it's pasted into articles. Coverage is a small repeater of outlet/title/date/link. Nothing is shared elsewhere, so it stays a section; a brand with several sub-brands would promote assets to the media library with tags.","htmlSignals":["Logo thumbnails on light and dark tiles with 'SVG' / 'PNG' / 'EPS' download links","A 'Download press kit (.zip)' button","An 'About {company}' paragraph and a 'Fast facts' list (Founded, HQ, Founders)","'Press contact' with an email; 'As seen in' / 'In the press' article list"],"mcp":"Download every linked asset into the media library (keep original filenames). Group by on-page headings (Logos, Product photos, Founders). Each tile → asset: name from its caption, kind from the group/file type, preview from the tile image (alt describes it), formats from each download link's extension. Tile background dark → background 'dark'. Photo credit lines → credit. Article list → mentions (outlet from the logo alt or text).","pitfalls":["Download links that point at the old CDN — re-upload, don't hotlink.","Logos previewed on the wrong background become invisible.","Photos without credits get printed uncredited — credit is required for photos.","A stale .zip that no longer matches the assets shown."],"notCarried":["category","variants"],"component":{"name":"Press kit","slug":"press-kit","props":[{"key":"heading","label":"Heading","type":"text","required":true,"config":{"maxLength":48},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text","order":0}}},{"key":"boilerplate","label":"About Caldera","type":"longtext","helpText":"Gets a Copy button.","required":true,"localized":true,"config":{"maxLength":700},"ui":{"rows":4,"tab":{"id":"content"}}},{"key":"facts","label":"Fast facts","type":"table","openMode":"inline","config":{"maxItems":8,"fields":[{"key":"label","label":"Label","type":"text","required":true,"placeholder":"Founded","config":{"maxLength":24}},{"key":"value","label":"Value","type":"text","required":true,"placeholder":"2017, Bend, Oregon","config":{"maxLength":60}}]},"ui":{"itemLabel":"Fact","icon":"info-circle","preview":{"title":"label","subtitle":"value"},"tab":{"id":"content"}}},{"key":"contactName","label":"Name","type":"text","ui":{"tab":{"id":"content"},"group":{"id":"contact","label":"Press contact","kind":"group"}}},{"key":"contactEmail","label":"Email","type":"email","required":true,"ui":{"tab":{"id":"content"},"group":{"id":"contact"}}},{"key":"assetGroups","label":"Asset groups","type":"table","config":{"minItems":1,"maxItems":6,"fields":[{"key":"title","label":"Group name","type":"text","required":true,"placeholder":"Logos","config":{"maxLength":32}},{"key":"description","label":"Usage notes","type":"longtext","recommendedMax":200,"ui":{"rows":2}},{"key":"assets","label":"Assets","type":"table","fields":[{"key":"name","label":"Name","type":"text","required":true,"config":{"maxLength":48}},{"key":"kind","label":"Kind","type":"select","helpText":"","required":true,"defaultValue":"logo","options":[{"label":"Logo","value":"logo"},{"label":"Photo","value":"photo"},{"label":"Document","value":"document"}],"ui":{"control":"segmented"}},{"key":"preview","label":"Preview","type":"image","required":true},{"key":"background","label":"Made for","type":"select","defaultValue":"light","showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"logo"}]},"options":[{"label":"Light backgrounds","value":"light"},{"label":"Dark backgrounds","value":"dark"}],"ui":{"control":"segmented"}},{"key":"credit","label":"Photo credit","type":"text","helpText":"Journalists must print it.","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"photo"}]},"config":{"maxLength":60}},{"key":"formats","label":"Downloads","type":"table","openMode":"inline","fields":[{"key":"format","label":"Format","type":"select","helpText":"","required":true,"options":[{"label":"SVG","value":"svg"},{"label":"PNG","value":"png"},{"label":"EPS","value":"eps"},{"label":"JPG","value":"jpg"},{"label":"PDF","value":"pdf"}]},{"key":"file","label":"File","type":"file","required":true}],"config":{"minItems":1,"maxItems":5},"ui":{"itemLabel":"Format","icon":"file-download","preview":{"title":"format","subtitle":"file.name"}}}],"config":{"minItems":1,"maxItems":24},"ui":{"itemLabel":"Asset","layout":"grid","icon":"photo","preview":{"title":"name","subtitle":"kind","media":"preview"}}}]},"ui":{"itemLabel":"Group","icon":"folder","preview":{"title":"title","subtitle":"description","fallback":"Untitled group"},"tab":{"id":"assets","label":"Assets","icon":"photo","order":1}}},{"key":"downloadAll","label":"Download everything","type":"file","helpText":"One .zip of every asset.","ui":{"tab":{"id":"assets"}}},{"key":"mentions","label":"Press mentions","type":"table","config":{"maxItems":12,"fields":[{"key":"outlet","label":"Publication","type":"text","required":true,"config":{"maxLength":40}},{"key":"title","label":"Article title","type":"text","required":true,"config":{"maxLength":120}},{"key":"date","label":"Date","type":"date"},{"key":"link","label":"Link","type":"link","required":true}]},"ui":{"itemLabel":"Article","icon":"news","preview":{"title":"title","subtitle":"outlet"},"tab":{"id":"coverage","label":"Coverage","icon":"news","order":2}}}],"description":"Logos, photos, facts and a press contact","category":"content","icon":"download","preview":{"title":"heading","icon":"download"},"tabs":[{"id":"design","label":"Design","icon":"palette","order":3}]}},
|
|
1469
|
+
{"id":"events-list","name":"Events list","category":"Events & places","kind":"block","tags":["dynamic-query","conditional","variants","references","a11y"],"proves":"Upcoming / past split from one query · past events offer recordings · time-zone display rule · list, cards, calendar or map","why":"Events are entries (each has a page, a time, a place and Event JSON-LD), so the list is a query with types/locations filters. The upcoming/past split is computed from each event's end time, never a status someone has to flip — the section only decides what to show and how many. Recordings only make sense for past events, so that toggle appears only when past events are shown. Time display is an explicit rule because 'Thursday 6pm' is ambiguous the moment an event is online.","htmlSignals":["Rows with a big day number + short month (12 NOV), title, time and city","Tabs 'Upcoming' / 'Past', or 'Past events' heading further down","'Register', 'Get tickets' or 'Watch recording' buttons per row","Embedded Eventbrite/Luma/Meetup links or widgets"],"mcp":"Import each row as an Events entry (follow the link for details — see event-detail). Place one eventsList with show 'both' if a Past section exists. If rows link to Eventbrite/Luma pages, keep the ticket URL on each event as its button link, not on the list. Filters from visible dropdowns.","pitfalls":["Past events lingering as 'upcoming' because a status field was never flipped — the split is computed.","Times shown without a zone on online events.","Deleting past events loses their recordings and links — keep them, the list hides them.","Calendar variant with three events looks empty — use the list until there are ~8 a month."],"notCarried":["category","variants"],"component":{"name":"Events list","slug":"events-list","props":[{"key":"heading","label":"Heading","type":"text","required":true,"localized":true,"config":{"maxLength":48},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"intro","label":"Intro","type":"longtext","helpText":"","ui":{"rows":2,"tab":{"id":"content"}}},{"key":"query","label":"Which events","type":"object","config":{"fields":[{"key":"collection","label":"Collection","type":"select","helpText":"","defaultValue":"col_events","readOnly":"developer","lockReason":"The collection is wired to the card template in code.","options":[{"label":"Events","value":"col_events"}],"ui":{"access":{"edit":"schema.manage"}}},{"key":"types","label":"Types","type":"multiselect","helpText":"Leave empty for every type.","options":[{"label":"Workshops","value":"workshop"},{"label":"Cookouts","value":"cookout"},{"label":"Pop-ups","value":"popup"},{"label":"Talks","value":"talk"}]},{"key":"locations","label":"Locations","type":"multi-reference","helpText":"Leave empty for everywhere.","config":{"contentModelId":"col_locations"}},{"key":"showOnlineEvents","label":"Include online events","type":"boolean","helpText":"","defaultValue":true}]},"ui":{"tab":{"id":"query","label":"Events","icon":"database"}}},{"key":"show","label":"Show","type":"select","defaultValue":"both","config":{"options":[{"label":"Upcoming only","value":"upcoming"},{"label":"Upcoming, with a Past tab","value":"both"},{"label":"Past only","value":"past","description":"For an archive page"}]},"ui":{"control":"radio","tab":{"id":"query"}}},{"key":"upcomingLimit","label":"Upcoming to show","type":"number","helpText":"","defaultValue":12,"showIf":{"match":"all","rules":[{"field":"show","op":"in","value":["upcoming","both"]}]},"config":{"min":3,"max":24,"step":1},"ui":{"control":"slider","tab":{"id":"query"}}},{"key":"pastLimit","label":"Past to show","type":"number","defaultValue":12,"showIf":{"match":"all","rules":[{"field":"show","op":"in","value":["both","past"]}]},"config":{"min":3,"max":48,"step":3},"ui":{"control":"slider","tab":{"id":"query"}}},{"key":"hasRecordings","label":"Offer recordings","type":"boolean","helpText":"Past events with a recording get a Watch button.","defaultValue":true,"showIf":{"match":"all","rules":[{"field":"show","op":"in","value":["both","past"]}]},"ui":{"tab":{"id":"query"}}},{"key":"filters","label":"Filters","type":"multiselect","helpText":"","defaultValue":["type","location"],"config":{"options":[{"label":"Type","value":"type"},{"label":"Location","value":"location"},{"label":"Month","value":"month"},{"label":"In person / online","value":"format"}]},"ui":{"control":"checkbox","tab":{"id":"content"}}},{"key":"timeDisplay","label":"Show times in","type":"select","helpText":"","defaultValue":"event","config":{"options":[{"label":"The event's local time","value":"event","description":"Best for in-person events"},{"label":"The visitor's time zone","value":"visitor","description":"Best for online events"}]},"ui":{"control":"radio","tab":{"id":"content"}}},{"key":"emptyHeading","label":"Heading","type":"text","helpText":"","defaultValue":"Nothing on the calendar yet","config":{"maxLength":48},"ui":{"tab":{"id":"content"},"group":{"id":"empty","label":"When nothing is scheduled","kind":"group","collapsed":true}}},{"key":"emptyButton","label":"Empty button","type":"button","ui":{"tab":{"id":"content"},"group":{"id":"empty"}}},{"key":"showDateBadge","label":"Show date badge","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design","label":"Design","icon":"palette"}}}],"description":"Upcoming and past events with filters","category":"content","icon":"calendar-event","preview":{"title":"heading","subtitle":"show","icon":"calendar-event"}}},
|
|
1470
|
+
{"id":"event-detail","name":"Event detail (conference)","category":"Events & places","kind":"entry","tags":["deep-nesting","references","seo-structured","conditional","commerce","repeater","validation"],"proves":"Days → sessions → speakers (references) · attendance mode reveals venue or stream · ticket tiers with sale windows · status maps to Event JSON-LD","why":"An event is an entry: it has a page and emits Event JSON-LD natively (startDate/endDate with the zone, eventStatus from Status, eventAttendanceMode from Attendance, location from the venue reference or a VirtualLocation from the stream, offers from tiers with validFrom/price/availability, performer from speakers). The agenda is days → sessions → speaker references — three nested levels plus the speaker CMS items — and a session's FORMAT reveals speakers, abstract, track and level (breaks have none). Venues and speakers are collections because they recur across years. Ticket tiers are commerce data with sale windows that switch themselves off.","htmlSignals":["An agenda table/grid with time ranges (09:00–09:45), rooms/tracks and speaker avatars","Day tabs (Day 1 / Day 2) or dated headings","Ticket cards with prices, 'Early bird ends …', 'Sold out' badges","A venue block with address and map; 'Watch online' or 'Livestream' mentions","Event JSON-LD with eventStatus / eventAttendanceMode"],"mcp":"Speakers → Speakers entries (photo, name, title, company, bio from the speaker modal). Venue → Venues entry. Agenda: each day tab → day; each row → session (start/end parsed to 24-hour HH:MM in the event's zone, format from labels like 'Keynote'/'Break'/'Lunch', speakers matched by name). Ticket cards → tiers (price as a number; 'Sold out' → availability). If tickets are sold on Tito/Eventbrite, use those URLs as each tier's button link.","pitfalls":["Agenda times in the visitor's zone instead of the event's — store HH:MM + the event zone.","Speakers typed into session titles ('… with Mira Tanaka') instead of referenced.","End time before start (cross-field) — not expressible in validation yet; the renderer must refuse it.","Cancelled events deleted rather than marked — Google keeps showing the old date.","Sold-out early-bird tiers left 'On sale' after salesEnd — use the window, not the badge."],"notCarried":["category","kind:entry (props)"],"component":{"name":"Event","slug":"event","props":[{"key":"title","label":"Title","type":"text","required":true,"config":{"maxLength":70},"ui":{"tab":{"id":"overview","label":"Overview","icon":"file-text"}}},{"key":"slug","label":"Slug","type":"slug","required":true,"ui":{"tab":{"id":"overview"}}},{"key":"tagline","label":"Tagline","type":"text","recommendedMax":90,"ui":{"tab":{"id":"overview"}}},{"key":"startDate","label":"Starts","type":"datetime","required":true,"ui":{"tab":{"id":"overview"},"group":{"id":"when","label":"When","kind":"group"}}},{"key":"endDate","label":"Ends","type":"datetime","helpText":"Must be after the start.","required":true,"ui":{"tab":{"id":"overview"},"group":{"id":"when"}}},{"key":"timezone","label":"Time zone","type":"select","helpText":"Agenda times and calendar files use this zone.","required":true,"config":{"options":[{"label":"Pacific (Los Angeles)","value":"America/Los_Angeles"},{"label":"Mountain (Denver)","value":"America/Denver"},{"label":"Central (Chicago)","value":"America/Chicago"},{"label":"Eastern (New York)","value":"America/New_York"},{"label":"London","value":"Europe/London"},{"label":"Berlin","value":"Europe/Berlin"}]},"ui":{"tab":{"id":"overview"},"group":{"id":"when"}}},{"key":"status","label":"Status","type":"select","helpText":"Anything but Scheduled shows a banner and tells Google.","defaultValue":"scheduled","config":{"options":[{"label":"Scheduled","value":"scheduled"},{"label":"Postponed","value":"postponed","description":"New date not known yet"},{"label":"Rescheduled","value":"rescheduled"},{"label":"Moved online","value":"moved-online"},{"label":"Cancelled","value":"cancelled"}]},"ui":{"tab":{"id":"overview"}}},{"key":"statusNote","label":"Banner text","type":"longtext","required":true,"showIf":{"match":"all","rules":[{"field":"status","op":"in","value":["postponed","rescheduled","moved-online","cancelled"]}]},"config":{"maxLength":240},"ui":{"rows":2,"tab":{"id":"overview"}}},{"key":"coverImage","label":"Cover image","type":"image","required":true,"ui":{"tab":{"id":"overview"}}},{"key":"about","label":"About","type":"richtext","required":true,"ui":{"tab":{"id":"overview"}}},{"key":"featuredSpeakers","label":"Featured speakers","type":"multi-reference","unique":true,"config":{"max":8,"contentModelId":"col_speakers"},"ui":{"itemName":"speaker","itemTitle":"Speaker","tab":{"id":"overview"}}},{"key":"days","label":"Days","type":"table","config":{"maxItems":7,"fields":[{"key":"date","label":"Date","type":"date","required":true},{"key":"label","label":"Label","type":"text","helpText":"","placeholder":"Day 1 · Build","config":{"maxLength":32}},{"key":"sessions","label":"Sessions","type":"table","fields":[{"key":"start","label":"Starts","type":"text","helpText":"","required":true,"placeholder":"09:30","config":{"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","patternMessage":"Use 24-hour time, like 09:00 or 17:30."}},{"key":"end","label":"Ends","type":"text","helpText":"","required":true,"placeholder":"10:15","config":{"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","patternMessage":"Use 24-hour time, like 09:00 or 17:30."}},{"key":"title","label":"Title","type":"text","required":true,"config":{"maxLength":100}},{"key":"format","label":"Format","type":"select","helpText":"Breaks and meals span every track and have no speakers.","required":true,"defaultValue":"talk","options":[{"label":"Keynote","value":"keynote"},{"label":"Talk","value":"talk"},{"label":"Panel","value":"panel"},{"label":"Workshop","value":"workshop"},{"label":"Break","value":"break"},{"label":"Meal","value":"meal"}]},{"key":"speakers","label":"Speakers","type":"multi-reference","showIf":{"match":"all","rules":[{"field":"format","op":"in","value":["keynote","talk","panel","workshop"]}]},"unique":true,"config":{"min":1,"max":6,"contentModelId":"col_speakers"}},{"key":"track","label":"Track","type":"select","helpText":"","showIf":{"match":"all","rules":[{"field":"format","op":"in","value":["talk","panel","workshop"]}]},"options":[{"label":"Main stage","value":"main"},{"label":"Build","value":"build"},{"label":"Scale","value":"scale"}]},{"key":"room","label":"Room","type":"text","config":{"maxLength":32}},{"key":"abstract","label":"Abstract","type":"richtext","showIf":{"match":"all","rules":[{"field":"format","op":"in","value":["keynote","talk","panel","workshop"]}]}},{"key":"level","label":"Level","type":"select","helpText":"","showIf":{"match":"all","rules":[{"field":"format","op":"in","value":["talk","workshop"]}]},"options":[{"label":"Introductory","value":"intro"},{"label":"Intermediate","value":"intermediate"},{"label":"Advanced","value":"advanced"}]},{"key":"capacity","label":"Seats","type":"number","helpText":"Caps workshop bookings.","showIf":{"match":"all","rules":[{"field":"format","op":"eq","value":"workshop"}]},"config":{"min":1,"step":1}},{"key":"recording","label":"Recording","type":"video"}],"config":{"minItems":1,"maxItems":40},"ui":{"itemLabel":"Session","icon":"microphone","preview":{"title":"title","subtitle":"start","fallback":"Untitled session"}}}]},"ui":{"itemLabel":"Day","itemName":"agendaDay","icon":"calendar","preview":{"title":"label","subtitle":"date","fallback":"Untitled day"},"tab":{"id":"agenda","label":"Agenda","icon":"list-details"}}},{"key":"attendance","label":"Attendance","type":"select","helpText":"","required":true,"defaultValue":"in-person","config":{"options":[{"label":"In person","value":"in-person"},{"label":"Online","value":"online"},{"label":"Hybrid","value":"hybrid"}]},"ui":{"control":"segmented","tab":{"id":"place","label":"Venue","icon":"map-pin"}}},{"key":"venue","label":"Venue","type":"reference","helpText":"Address, map pin and directions come from the venue.","required":true,"showIf":{"match":"all","rules":[{"field":"attendance","op":"in","value":["in-person","hybrid"]}]},"config":{"contentModelId":"col_venues"},"ui":{"tab":{"id":"place"}}},{"key":"gettingThere","label":"Getting there","type":"richtext","showIf":{"match":"all","rules":[{"field":"attendance","op":"in","value":["in-person","hybrid"]}]},"ui":{"tab":{"id":"place"}}},{"key":"streamPlatform","label":"Stream on","type":"select","helpText":"","showIf":{"match":"all","rules":[{"field":"attendance","op":"in","value":["online","hybrid"]}]},"config":{"options":[{"label":"YouTube Live","value":"youtube"},{"label":"Zoom Webinar","value":"zoom"},{"label":"RingCentral Events","value":"hopin"},{"label":"Our own player","value":"custom"}]},"ui":{"tab":{"id":"place"}}},{"key":"streamUrl","label":"Stream link","type":"url","helpText":"Shown to ticket holders on the day.","showIf":{"match":"all","rules":[{"field":"attendance","op":"in","value":["online","hybrid"]}]},"ui":{"tab":{"id":"place"}}},{"key":"currency","label":"Currency","type":"select","helpText":"","defaultValue":"USD","config":{"options":[{"label":"USD","value":"USD"},{"label":"EUR","value":"EUR"},{"label":"GBP","value":"GBP"}]},"ui":{"tab":{"id":"tickets","label":"Tickets","icon":"ticket"}}},{"key":"tiers","label":"Ticket tiers","type":"table","config":{"maxItems":6,"fields":[{"key":"name","label":"Name","type":"text","required":true,"config":{"maxLength":32}},{"key":"price","label":"Price","type":"number","helpText":"0 shows as Free.","required":true,"config":{"min":0,"step":1}},{"key":"salesStart","label":"On sale from","type":"datetime"},{"key":"salesEnd","label":"On sale until","type":"datetime","helpText":"Early-bird tiers switch off on their own at this time."},{"key":"availability","label":"Availability","type":"select","helpText":"","defaultValue":"on-sale","options":[{"label":"On sale","value":"on-sale"},{"label":"Sold out","value":"sold-out"},{"label":"Coming soon","value":"soon"}],"ui":{"control":"segmented"}},{"key":"includes","label":"Includes","type":"array","config":{"maxItems":8,"itemType":"text","item":{"maxLength":60}},"ui":{"itemLabel":"Perk","itemName":"perk","itemTitle":"Perk"}},{"key":"button","label":"Button","type":"button","required":true,"showIf":{"match":"all","rules":[{"field":"availability","op":"eq","value":"on-sale"}]}},{"key":"isHighlighted","label":"Most popular","type":"boolean"}]},"ui":{"itemLabel":"Tier","icon":"ticket","preview":{"title":"name","subtitle":"price","fallback":"Untitled tier"},"tab":{"id":"tickets"}}}],"description":"A conference or event with agenda, venue and tickets","category":"content","icon":"calendar-star","preview":{"title":"title","subtitle":"startDate","media":"coverImage"}}},
|
|
1471
|
+
{"id":"webinar-registration","name":"Webinar registration","category":"Events & places","kind":"block","tags":["form","conditional","references","embed","validation","variants"],"proves":"One section across the lifecycle: upcoming (form) → live (stream) → on demand (recording, optionally gated) · CRM campaign locked","why":"A webinar page lives through three states, and the same URL must work in each: before (time + registration form), during (embedded stream), after (recording, optionally behind the same form). One STAGE select reveals the fields each state needs, so nobody builds a second page for the replay. The form is a reference (submissions land in Forms and sync to the locked CRM campaign). Speakers are People references. Webinars with many sessions graduate to the Event entry (event-detail).","htmlSignals":["A date/time with time zone and a registration form beside a speaker list","'Register now', 'Save my seat', 'Watch on demand', 'Watch the recording'","HubSpot/Marketo/Pardot form embeds; Zoom/ON24/Livestorm links","'What you'll learn' bullet list"],"mcp":"Stage from the page: a form + future date → upcoming; a video + 'on demand' → on-demand (isGated true if a form shows first). Rebuild the embedded HubSpot/Marketo form as a BetterCMS form with the same fields and reference it; keep the original campaign id for the developer. Bullets → takeaways. Speaker cards → People.","pitfalls":["A second '/webinar-replay' page splits links and SEO — switch the stage instead.","Times without a zone ('Thursday 10am').","Leaving stage 'live' after the event — the page shows a dead stream.","Gating on-demand recordings of public talks lowers reach; gate deliberately."],"notCarried":["ai","category","variants"],"component":{"name":"Webinar registration","slug":"webinar-registration","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","defaultValue":"Live webinar","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text","order":0}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"ui":{"tab":{"id":"content"}}},{"key":"summary","label":"Summary","type":"longtext","recommendedMax":280,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"takeaways","label":"You'll learn","type":"array","config":{"maxItems":6,"itemType":"text","item":{"maxLength":100}},"ui":{"itemLabel":"Point","itemName":"point","itemTitle":"Point","tab":{"id":"content"}}},{"key":"speakers","label":"Speakers","type":"multi-reference","config":{"min":1,"max":4,"contentModelId":"col_people"},"ui":{"tab":{"id":"content"}}},{"key":"stage","label":"Stage","type":"select","defaultValue":"upcoming","config":{"options":[{"label":"Upcoming","value":"upcoming"},{"label":"Live now","value":"live"},{"label":"On demand","value":"on-demand"}]},"ui":{"control":"segmented","tab":{"id":"stage","label":"Before · live · after","icon":"broadcast","order":1}}},{"key":"startsAt","label":"Starts","type":"datetime","helpText":"Shown in each visitor's time zone.","required":true,"showIf":{"match":"all","rules":[{"field":"stage","op":"in","value":["upcoming","live"]}]},"ui":{"tab":{"id":"stage"}}},{"key":"minutes","label":"Length","type":"number","helpText":"","defaultValue":45,"showIf":{"match":"all","rules":[{"field":"stage","op":"in","value":["upcoming","live"]}]},"config":{"unit":"min","min":15,"max":180,"step":15},"ui":{"tab":{"id":"stage"}}},{"key":"form","label":"Registration form","type":"form","required":true,"showIf":{"match":"all","rules":[{"field":"stage","op":"eq","value":"upcoming"}]},"ui":{"tab":{"id":"stage"}}},{"key":"showCalendarLinks","label":"Show add-to-calendar links","type":"boolean","defaultValue":true,"showIf":{"match":"all","rules":[{"field":"stage","op":"eq","value":"upcoming"}]},"ui":{"tab":{"id":"stage"}}},{"key":"streamUrl","label":"Stream","type":"url","helpText":"A YouTube Live, Vimeo or Zoom webinar link.","required":true,"showIf":{"match":"all","rules":[{"field":"stage","op":"eq","value":"live"}]},"ui":{"tab":{"id":"stage"}}},{"key":"recording","label":"Recording","type":"video","required":true,"showIf":{"match":"all","rules":[{"field":"stage","op":"eq","value":"on-demand"}]},"ui":{"tab":{"id":"stage"}}},{"key":"isGated","label":"Ask for details first","type":"boolean","showIf":{"match":"all","rules":[{"field":"stage","op":"eq","value":"on-demand"}]},"ui":{"tab":{"id":"stage"}}},{"key":"slides","label":"Slides","type":"file","showIf":{"match":"all","rules":[{"field":"stage","op":"eq","value":"on-demand"}]},"ui":{"tab":{"id":"stage"}}},{"key":"confirmHeading","label":"Heading","type":"text","helpText":"","defaultValue":"You're in","config":{"maxLength":40},"ui":{"tab":{"id":"stage"},"group":{"id":"confirm","label":"After registering","kind":"group","collapsed":true}}},{"key":"confirmBody","label":"Text","type":"longtext","ui":{"rows":2,"tab":{"id":"stage"},"group":{"id":"confirm"}}},{"key":"campaign","label":"CRM campaign","type":"text","helpText":"Registrations sync to this campaign.","advanced":true,"readOnly":"developer","lockReason":"Set by marketing ops; registrations sync to it.","ui":{"access":{"edit":"schema.manage"},"tab":{"id":"stage"}}}],"description":"Register before, watch live, or watch the recording","category":"content","icon":"presentation","preview":{"title":"heading","subtitle":"stage","icon":"presentation"},"tabs":[{"id":"design","label":"Design","icon":"palette","order":2}]}},
|
|
1472
|
+
{"id":"locations-directory","name":"Locations directory","category":"Events & places","kind":"block","tags":["dynamic-query","conditional","repeater","deep-nesting","validation","a11y","responsive-layout"],"proves":"Map + list from a Locations query or written here · per-day hours array (isClosed reveals nothing) · services filter · near-me search","why":"Each location is a pin, an address, a phone, per-day hours and services. Small brands write them right in the section; once a location needs its own page (location-page) or appears in events and job posts, the source flips to the Locations collection with the same item shape. Hours are an array of day objects where `isClosed` reveals or hides open/close times — the item shape both the directory and LocalBusiness openingHoursSpecification need. Map behavior (view, zoom, clustering) is its own group; phone layout is design.","htmlSignals":["A map (Google Maps / Mapbox / Leaflet container) beside a scrollable list of addresses","'Find a store', 'Store locator', 'Stockists', 'Locations' headings; a ZIP/city search box","Hours tables Mon–Sun, 'Closed' on some days, 'Open now' badges","data-lat / data-lng attributes or a locations JSON blob in a <script>","'Get directions' links to maps.google.com"],"mcp":"Look for the locations JSON the map is fed (script tag or XHR) — it usually has lat/lng, hours and services; prefer it over scraping. Each location → an item (or a Locations entry if it has its own page; then use source 'collection'). Parse hours into seven day objects: 'Mon–Fri 10–6' expands to five days with 10:00/18:00; 'Closed' → isClosed true. Pin = lat/lng from data attributes or geocode the address.","pitfalls":["closed must default to false or the time fields stay hidden on new rows.","Hours as one text line ('Tue–Sat 10–6') can't drive 'Open now' or JSON-LD.","Overnight hours (18:00–02:00): closes earlier than opens means next day — say so in help, handle in the renderer.","Pins at the geocoded centroid of a mall instead of the door.","A map without a list fails keyboard and screen-reader users — the list is the primary UI."],"notCarried":["category","variants"],"component":{"name":"Locations directory","slug":"locations-directory","props":[{"key":"heading","label":"Heading","type":"text","required":true,"localized":true,"config":{"maxLength":48},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"intro","label":"Intro","type":"longtext","helpText":"","ui":{"rows":2,"tab":{"id":"content"}}},{"key":"source","label":"Locations come from","type":"select","defaultValue":"collection","config":{"options":[{"label":"Locations collection","value":"collection"},{"label":"Written here","value":"inline"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"query","label":"Which locations","type":"object","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"collection"}]},"openMode":"inline","config":{"fields":[{"key":"collection","label":"Collection","type":"select","helpText":"","defaultValue":"col_locations","readOnly":"developer","lockReason":"The collection is wired to the card template in code.","options":[{"label":"Locations","value":"col_locations"}],"ui":{"access":{"edit":"schema.manage"}}},{"key":"types","label":"Types","type":"multiselect","helpText":"Leave empty for every type.","options":[{"label":"Flagship stores","value":"flagship"},{"label":"Workshops","value":"workshop"},{"label":"Stockists","value":"stockist"}]},{"key":"regions","label":"Regions","type":"multiselect","helpText":"","options":[{"label":"Pacific Northwest","value":"pnw"},{"label":"Mountain","value":"mountain"},{"label":"Northeast","value":"northeast"}]}]},"ui":{"tab":{"id":"content"}}},{"key":"locations","label":"Locations","type":"table","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"inline"}]},"config":{"minItems":1,"maxItems":30,"fields":[{"key":"name","label":"Name","type":"text","required":true,"config":{"maxLength":48}},{"key":"type","label":"Type","type":"select","helpText":"","options":[{"label":"Flagship store","value":"flagship"},{"label":"Workshop","value":"workshop"},{"label":"Stockist","value":"stockist"}]},{"key":"address","label":"Address","type":"longtext","helpText":"As it should appear, one line per row.","required":true,"ui":{"rows":3}},{"key":"pin","label":"Map pin","type":"location","helpText":"Drop it on the entrance.","required":true},{"key":"phone","label":"Phone","type":"text","config":{"pattern":"^\\+?[0-9 ()-]{7,20}$","patternMessage":"Use digits, spaces and + ( ) -, like +1 (541) 555-0182."}},{"key":"hours","label":"Opening hours","type":"table","helpText":"Days left out show as closed.","openMode":"inline","fields":[{"key":"day","label":"Day","type":"select","helpText":"","required":true,"options":[{"label":"Monday","value":"mon"},{"label":"Tuesday","value":"tue"},{"label":"Wednesday","value":"wed"},{"label":"Thursday","value":"thu"},{"label":"Friday","value":"fri"},{"label":"Saturday","value":"sat"},{"label":"Sunday","value":"sun"}]},{"key":"isClosed","label":"Closed","type":"boolean","helpText":"","defaultValue":false},{"key":"opens","label":"Opens","type":"text","helpText":"","required":true,"showIf":{"match":"all","rules":[{"field":"isClosed","op":"eq","value":"false"}]},"placeholder":"10:00","config":{"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","patternMessage":"Use 24-hour time, like 09:00 or 17:30."}},{"key":"closes","label":"Closes","type":"text","helpText":"After midnight? Use the next morning's time, e.g. 01:00.","required":true,"showIf":{"match":"all","rules":[{"field":"isClosed","op":"eq","value":"false"}]},"placeholder":"18:00","config":{"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","patternMessage":"Use 24-hour time, like 09:00 or 17:30."}}],"config":{"maxItems":7},"ui":{"itemLabel":"Day","itemName":"day","icon":"clock","preview":{"title":"day","subtitle":"opens"}}},{"key":"services","label":"Services","type":"multiselect","options":[{"label":"Workshops","value":"workshops"},{"label":"Re-seasoning","value":"reseasoning"},{"label":"Engraving","value":"engraving"},{"label":"Click & collect","value":"pickup"},{"label":"Warranty repairs","value":"repairs"}]},{"key":"image","label":"Image","type":"image"},{"key":"page","label":"Location page","type":"link"}]},"ui":{"itemLabel":"Location","icon":"map-pin","preview":{"title":"name","subtitle":"address","media":"image","fallback":"Untitled location"},"tab":{"id":"content"}}},{"key":"searchPlaceholder","label":"Search placeholder","type":"text","helpText":"","defaultValue":"City or ZIP code","config":{"maxLength":32},"ui":{"tab":{"id":"content"}}},{"key":"showNearMe","label":"Show Near me button","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"content"}}},{"key":"showServiceFilter","label":"Show services filter","type":"boolean","helpText":"","defaultValue":true,"ui":{"tab":{"id":"content"}}},{"key":"center","label":"Starting view","type":"location","helpText":"Leave empty to fit all pins.","ui":{"tab":{"id":"map","label":"Map","icon":"earth-americas"}}},{"key":"zoom","label":"Starting zoom","type":"number","helpText":"3 shows a continent, 15 a few streets.","defaultValue":5,"config":{"min":3,"max":15,"step":1},"ui":{"control":"slider","tab":{"id":"map"}}},{"key":"useClustering","label":"Group nearby pins","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"map"}}},{"key":"mobileView","label":"On phones show","type":"select","defaultValue":"list","config":{"options":[{"label":"List first","value":"list"},{"label":"Map first","value":"map"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design","icon":"palette"}}}],"description":"Every store or workshop on a map and in a list","category":"content","icon":"map-pins","preview":{"title":"heading","subtitle":"source","icon":"map-pins"}}},
|
|
1473
|
+
{"id":"restaurant-menu","name":"Restaurant menu","category":"Events & places","kind":"block","tags":["deep-nesting","repeater","commerce","conditional","variants","localized","validation","seo-structured"],"proves":"Menus → sections → items → price variants (4 levels) · fixed, variant or market price · dietary and allergen tags · 86'd items gray out","why":"A menu is the canonical deep repeater: menus → sections → dishes → size variants. Price is a MODE (one price, sizes, or market price) so 'MP' never sits in a number field and sizes never get crammed into a description. Dietary and allergen values are fixed option lists (legend + filter + Menu JSON-LD suitableForDiet). 'Available today' grays a dish out instead of deleting it, so a sold-out special comes back tomorrow with one tap. It stays a section shared to restaurants by reference; a group with many locations and shared dishes would promote dishes to a collection.","htmlSignals":["Section headings (Antipasti, Mains, Wine) with dish name + price on the same line, dotted leaders","Small letter codes after names (V, VG, GF) and a key at the bottom","Two prices per row with labels ('Glass / Bottle', 'Small / Large')","'MP' or 'Market price' instead of a number","Tabs or anchors for Lunch / Dinner / Drinks; a 'Download PDF menu' link"],"mcp":"Tabs/anchors → menus (copy service hours if shown). Headings → sections. Each row → dish: name, description (the smaller line), price (strip currency symbols; 'MP' → priceType market; two labeled prices → variants), dietary from the letter codes via the page's key, spice from chilli icons. A PDF-only menu: extract its text the same way and attach the PDF. Never keep prices inside descriptions.","pitfalls":["'$18 / $32' in one field — sizes must be variants.","Allergen data that disagrees with the kitchen sheet is a safety issue — owners must sign off.","Deleting sold-out dishes loses them; use available.","Prices typed with symbols ('$18') break sorting and JSON-LD.","Four nesting levels: keep sizes inline (openMode inline) so the deepest level isn't another drill."],"notCarried":["category","variants"],"component":{"name":"Restaurant menu","slug":"restaurant-menu","props":[{"key":"heading","label":"Heading","type":"text","localized":true,"config":{"maxLength":48},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"locations","label":"Served at","type":"multi-reference","helpText":"Leave empty if every restaurant serves this menu.","config":{"contentModelId":"col_locations"},"ui":{"itemTitle":"Restaurant","tab":{"id":"content"}}},{"key":"currency","label":"Currency","type":"select","helpText":"","defaultValue":"USD","config":{"options":[{"label":"$ USD","value":"USD"},{"label":"€ EUR","value":"EUR"},{"label":"£ GBP","value":"GBP"}]},"ui":{"tab":{"id":"content"}}},{"key":"menus","label":"Menus","type":"table","config":{"minItems":1,"maxItems":6,"fields":[{"key":"name","label":"Name","type":"text","required":true,"localized":true,"placeholder":"Dinner","config":{"maxLength":24}},{"key":"servedFrom","label":"From","type":"text","helpText":"","placeholder":"17:00","config":{"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","patternMessage":"Use 24-hour time, like 09:00 or 17:30."}},{"key":"servedUntil","label":"Until","type":"text","helpText":"","placeholder":"22:00","config":{"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","patternMessage":"Use 24-hour time, like 09:00 or 17:30."}},{"key":"days","label":"Days","type":"multiselect","helpText":"Leave empty for every day we're open.","options":[{"label":"Monday","value":"mon"},{"label":"Tuesday","value":"tue"},{"label":"Wednesday","value":"wed"},{"label":"Thursday","value":"thu"},{"label":"Friday","value":"fri"},{"label":"Saturday","value":"sat"},{"label":"Sunday","value":"sun"}]},{"key":"note","label":"Note","type":"text","localized":true,"placeholder":"Kitchen closes 30 minutes before the restaurant","config":{"maxLength":120}},{"key":"sections","label":"Sections","type":"table","fields":[{"key":"title","label":"Title","type":"text","required":true,"localized":true,"placeholder":"Antipasti","config":{"maxLength":32}},{"key":"description","label":"Description","type":"text","localized":true,"config":{"maxLength":120}},{"key":"items","label":"Dishes","type":"table","fields":[{"key":"name","label":"Name","type":"text","required":true,"localized":true,"config":{"maxLength":60}},{"key":"description","label":"Description","type":"longtext","helpText":"Ingredients, lowercase, comma-separated.","recommendedMax":140,"localized":true,"ui":{"rows":2}},{"key":"priceType","label":"Price","type":"select","helpText":"","defaultValue":"fixed","options":[{"label":"One price","value":"fixed"},{"label":"Sizes","value":"variants"},{"label":"Market price","value":"market"}],"ui":{"control":"segmented"}},{"key":"price","label":"Price","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"priceType","op":"eq","value":"fixed"}]},"config":{"min":0,"step":0.5}},{"key":"variants","label":"Sizes","type":"table","showIf":{"match":"all","rules":[{"field":"priceType","op":"eq","value":"variants"}]},"openMode":"inline","fields":[{"key":"label","label":"Label","type":"text","required":true,"placeholder":"Glass","config":{"maxLength":16}},{"key":"price","label":"Price","type":"number","required":true,"config":{"min":0,"step":0.5}}],"config":{"minItems":2,"maxItems":4},"ui":{"itemLabel":"Size","icon":"ruler","preview":{"title":"label","subtitle":"price"}}},{"key":"dietary","label":"Dietary","type":"multiselect","helpText":"Shown as letters after the name, with a key at the bottom.","options":[{"label":"Vegetarian","value":"v"},{"label":"Vegan","value":"vg"},{"label":"Gluten-free","value":"gf"},{"label":"Dairy-free","value":"df"}]},{"key":"allergens","label":"Contains","type":"multiselect","options":[{"label":"Gluten","value":"gluten"},{"label":"Milk","value":"dairy"},{"label":"Egg","value":"egg"},{"label":"Tree nuts","value":"nuts"},{"label":"Peanut","value":"peanut"},{"label":"Fish","value":"fish"},{"label":"Shellfish","value":"shellfish"},{"label":"Sesame","value":"sesame"},{"label":"Soy","value":"soy"}]},{"key":"spice","label":"Heat","type":"number","helpText":"0 is none. Shown as chilli icons.","defaultValue":0,"config":{"min":0,"max":3,"step":1},"ui":{"control":"slider"}},{"key":"image","label":"Image","type":"image"},{"key":"isSignature","label":"House favorite","type":"boolean"},{"key":"isAvailable","label":"Available today","type":"boolean","helpText":"Off shows it grayed out as \"Sold out\".","defaultValue":true}],"config":{"minItems":1,"maxItems":30},"ui":{"itemLabel":"Dish","itemName":"menuItem","icon":"salad","preview":{"title":"name","subtitle":"description","media":"image","fallback":"Untitled dish"}}}],"config":{"minItems":1,"maxItems":12},"ui":{"itemLabel":"Section","itemName":"menuSection","icon":"list","preview":{"title":"title","subtitle":"description","fallback":"Untitled section"}}}]},"ui":{"itemLabel":"Menu","icon":"book-2","preview":{"title":"name","subtitle":"servedFrom","fallback":"Untitled menu"},"tab":{"id":"content"}}},{"key":"showLegend","label":"Show dietary key","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"content"},"group":{"id":"small","label":"Small print","kind":"group"}}},{"key":"footnote","label":"Footnote","type":"longtext","localized":true,"ui":{"rows":2,"tab":{"id":"content"},"group":{"id":"small"}}},{"key":"showPhotos","label":"Dish photos","type":"boolean","ui":{"tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"pdf","label":"Printable PDF","type":"file","helpText":"Adds a \"Download menu\" link.","ui":{"tab":{"id":"design"}}}],"description":"Lunch, dinner and drinks with prices and dietary tags","category":"content","icon":"tools-kitchen-2","preview":{"title":"heading","icon":"tools-kitchen-2"}}},
|
|
1474
|
+
{"id":"location-page","name":"Single location page","category":"Events & places","kind":"entry","tags":["seo-structured","repeater","conditional","references","media-heavy","a11y","validation","deep-nesting"],"proves":"Address object + pin · per-day hours with split shifts · holiday overrides · booking provider reveals its own fields · Restaurant JSON-LD","why":"Each restaurant is an entry because it has its own page and emits Restaurant JSON-LD natively: address (PostalAddress from the address fields), geo, telephone, priceRange, servesCuisine, hasMenu (menu references), acceptsReservations (from the provider), and openingHoursSpecification from the hours array — with special dates mapped to specialOpeningHoursSpecification. Hours are days → shifts, so lunch + dinner and 'closed Mondays' are both data, and a day's shifts appear only when it isn't closed. The booking provider reveals exactly the field it needs (a venue ID for Resy/OpenTable/Tock, a tel: button for phone). Address parts are separate fields because search engines and maps need them separately.","htmlSignals":["An address block, phone link (tel:), and a map or 'Get directions' link","An hours table with day names and one or two time ranges per day; 'Closed' on some days","A Resy/OpenTable/Tock widget or button (resy.com, opentable.com, exploretock.com URLs)","'Holiday hours' notices; 'Private dining' section with a form or PDF","LocalBusiness / Restaurant JSON-LD in the source"],"mcp":"Prefer the page's Restaurant JSON-LD for address, geo, phone, priceRange and openingHoursSpecification. Expand compressed hours ('Tue–Thu 5–10pm' → three days, one shift each; 'Fri–Sat 11:30–2:30, 5–11' → two shifts). Holiday notices → specialHours. Booking widget host → bookingProvider and venue ID from its URL/params. Menus → Menus entries/sections (see restaurant-menu) referenced here. Photos → gallery with alt.","pitfalls":["Overnight closes (01:00) read as earlier than opens — mean next day; the renderer must handle it.","Hours duplicated on Google Business Profile drift — this entry should be the source you sync from.","Special hours never cleaned up — past dates hide themselves but stay as history.","A booking widget without a phone fallback excludes people who can't use it.","closed defaults to false or new days hide their shifts."],"notCarried":["category","kind:entry (props)"],"component":{"name":"Restaurant location","slug":"restaurant-location","props":[{"key":"name","label":"Name","type":"text","required":true,"config":{"maxLength":48},"ui":{"tab":{"id":"overview","label":"Overview","icon":"file-text"}}},{"key":"slug","label":"Slug","type":"slug","required":true,"ui":{"tab":{"id":"overview"}}},{"key":"tagline","label":"Tagline","type":"text","recommendedMax":90,"localized":true,"ui":{"tab":{"id":"overview"}}},{"key":"about","label":"About","type":"richtext","localized":true,"ui":{"tab":{"id":"overview"}}},{"key":"street","label":"Street","type":"text","required":true,"ui":{"tab":{"id":"overview"},"group":{"id":"address","label":"Address","kind":"group"}}},{"key":"city","label":"City","type":"text","required":true,"ui":{"tab":{"id":"overview"},"group":{"id":"address"}}},{"key":"region","label":"State / region","type":"text","ui":{"tab":{"id":"overview"},"group":{"id":"address"}}},{"key":"postalCode","label":"Postal code","type":"text","required":true,"ui":{"tab":{"id":"overview"},"group":{"id":"address"}}},{"key":"country","label":"Country","type":"select","helpText":"","defaultValue":"US","config":{"options":[{"label":"United States","value":"US"},{"label":"Canada","value":"CA"},{"label":"United Kingdom","value":"GB"},{"label":"Italy","value":"IT"}]},"ui":{"tab":{"id":"overview"},"group":{"id":"address"}}},{"key":"pin","label":"Map pin","type":"location","helpText":"Drop it on the entrance.","required":true,"ui":{"tab":{"id":"overview"}}},{"key":"phone","label":"Phone","type":"text","required":true,"config":{"pattern":"^\\+?[0-9 ()-]{7,20}$","patternMessage":"Use digits, spaces and + ( ) -, like +1 (541) 555-0182."},"ui":{"tab":{"id":"overview"},"group":{"id":"contact","label":"Contact","kind":"group"}}},{"key":"email","label":"Email","type":"email","ui":{"tab":{"id":"overview"},"group":{"id":"contact"}}},{"key":"services","label":"Services","type":"multiselect","helpText":"","config":{"options":[{"label":"Dine-in","value":"dine-in"},{"label":"Takeout","value":"takeout"},{"label":"Delivery","value":"delivery"},{"label":"Outdoor seating","value":"outdoor"},{"label":"Bar seating","value":"bar"},{"label":"Private dining","value":"private"}]},"ui":{"tab":{"id":"overview"}}},{"key":"priceRange","label":"Price range","type":"select","helpText":"Per person with a drink, roughly. Shown in search results.","config":{"options":[{"label":"$","value":"$"},{"label":"$$","value":"$$"},{"label":"$$$","value":"$$$"},{"label":"$$$$","value":"$$$$"}]},"ui":{"control":"segmented","tab":{"id":"overview"}}},{"key":"menus","label":"Menus","type":"multi-reference","config":{"max":6,"contentModelId":"col_menus"},"ui":{"tab":{"id":"overview"}}},{"key":"chef","label":"Head chef","type":"reference","config":{"contentModelId":"col_people"},"ui":{"tab":{"id":"overview"}}},{"key":"accessibility","label":"Accessibility","type":"longtext","ui":{"rows":2,"tab":{"id":"overview"}}},{"key":"parking","label":"Getting here","type":"longtext","ui":{"rows":2,"tab":{"id":"overview"}}},{"key":"hours","label":"Weekly hours","type":"table","helpText":"One row per day. Lunch and dinner are two shifts on the same day.","config":{"minItems":1,"maxItems":7,"fields":[{"key":"day","label":"Day","type":"select","helpText":"","required":true,"unique":true,"options":[{"label":"Monday","value":"mon"},{"label":"Tuesday","value":"tue"},{"label":"Wednesday","value":"wed"},{"label":"Thursday","value":"thu"},{"label":"Friday","value":"fri"},{"label":"Saturday","value":"sat"},{"label":"Sunday","value":"sun"}]},{"key":"isClosed","label":"Closed","type":"boolean","defaultValue":false},{"key":"shifts","label":"Open","type":"table","showIf":{"match":"all","rules":[{"field":"isClosed","op":"eq","value":"false"}]},"openMode":"inline","fields":[{"key":"label","label":"Label","type":"text","helpText":"","placeholder":"Dinner","config":{"maxLength":16}},{"key":"opens","label":"Opens","type":"text","helpText":"","required":true,"placeholder":"17:00","config":{"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","patternMessage":"Use 24-hour time, like 09:00 or 17:30."}},{"key":"closes","label":"Closes","type":"text","helpText":"","required":true,"placeholder":"22:00","config":{"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","patternMessage":"Use 24-hour time, like 09:00 or 17:30."}}],"config":{"minItems":1,"maxItems":3},"ui":{"itemLabel":"Shift","icon":"clock-hour-4","preview":{"title":"opens","subtitle":"closes"}}}]},"ui":{"itemLabel":"Day","itemName":"day","icon":"clock","preview":{"title":"day","fallback":"Untitled day"},"tab":{"id":"hours","label":"Hours","icon":"clock"}}},{"key":"specialHours","label":"Holidays & special hours","type":"table","helpText":"Overrides the weekly hours on these dates.","config":{"maxItems":30,"fields":[{"key":"date","label":"Date","type":"date","required":true},{"key":"label","label":"Label","type":"text","placeholder":"Thanksgiving","config":{"maxLength":32}},{"key":"isClosed","label":"Closed all day","type":"boolean","defaultValue":false},{"key":"opens","label":"Opens","type":"text","helpText":"","required":true,"showIf":{"match":"all","rules":[{"field":"isClosed","op":"eq","value":"false"}]},"config":{"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","patternMessage":"Use 24-hour time, like 09:00 or 17:30."}},{"key":"closes","label":"Closes","type":"text","helpText":"","required":true,"showIf":{"match":"all","rules":[{"field":"isClosed","op":"eq","value":"false"}]},"config":{"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","patternMessage":"Use 24-hour time, like 09:00 or 17:30."}}]},"ui":{"itemLabel":"Date","itemName":"special","icon":"calendar-exclamation","preview":{"title":"label","subtitle":"date"},"tab":{"id":"hours"}}},{"key":"bookingProvider","label":"Reservations","type":"select","helpText":"","defaultValue":"resy","config":{"options":[{"label":"Resy","value":"resy"},{"label":"OpenTable","value":"opentable"},{"label":"Tock","value":"tock"},{"label":"By phone","value":"phone"},{"label":"Walk-ins only","value":"none"}]},"ui":{"control":"radio","tab":{"id":"booking","label":"Booking","icon":"calendar-plus"}}},{"key":"venueId","label":"Venue ID","type":"text","helpText":"From the provider's widget code.","required":true,"showIf":{"match":"all","rules":[{"field":"bookingProvider","op":"in","value":["resy","opentable","tock"]}]},"ui":{"tab":{"id":"booking"}}},{"key":"bookButton","label":"Book button","type":"button","helpText":"Phone bookings use a tel: link.","showIf":{"match":"all","rules":[{"field":"bookingProvider","op":"in","value":["resy","opentable","tock","phone"]}]},"ui":{"tab":{"id":"booking"}}},{"key":"walkInNote","label":"Walk-in note","type":"text","placeholder":"The bar is always kept for walk-ins","config":{"maxLength":100},"ui":{"tab":{"id":"booking"}}},{"key":"hasPrivateDining","label":"Offers private dining","type":"boolean","ui":{"tab":{"id":"booking"},"group":{"id":"private","label":"Private dining","kind":"group","collapsed":true}}},{"key":"privateCapacity","label":"Seats up to","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"hasPrivateDining","op":"eq","value":"true"}]},"config":{"unit":"guests","min":2,"step":1},"ui":{"tab":{"id":"booking"},"group":{"id":"private"}}},{"key":"privateForm","label":"Enquiry form","type":"form","required":true,"showIf":{"match":"all","rules":[{"field":"hasPrivateDining","op":"eq","value":"true"}]},"ui":{"tab":{"id":"booking"},"group":{"id":"private"}}},{"key":"privateBrochure","label":"Brochure","type":"file","showIf":{"match":"all","rules":[{"field":"hasPrivateDining","op":"eq","value":"true"}]},"ui":{"tab":{"id":"booking"},"group":{"id":"private"}}},{"key":"coverImage","label":"Cover image","type":"image","required":true,"ui":{"tab":{"id":"media","label":"Media","icon":"photo"}}},{"key":"gallery","label":"Gallery","type":"array","config":{"maxItems":12,"itemType":"image"},"ui":{"layout":"gallery","itemName":"photo","itemTitle":"Photo","tab":{"id":"media"}}}],"description":"One restaurant: where, when, how to book","category":"content","icon":"building-store","preview":{"title":"name","subtitle":"city","media":"coverImage"}}},
|
|
1475
|
+
{"id":"data-table-specs","name":"Spec table","category":"Data & interactive","kind":"block","tags":["repeater","conditional","validation","a11y"],"proves":"Typed columns · rows from manual / CSV / collection · default sort · caption + row headers for screen readers","why":"Columns are DATA, not schema: an editor adds a 'Capacity' column without a developer, so the rows live in ONE `table` field (CPO D42): typed, keyed columns (Number in lb, Money in USD, Yes / no with its words), rows whose cells are keyed by column key (reorder-safe), the first column as row headers, caption + notes + footnotes, and design (cards on phones, sticky header). Source is a segmented select so typed rows, a CSV file and a collection are mutually exclusive branches. Sorting is a per-column flag; the filter box is `table.filterable`.","htmlSignals":["<table> with <thead><th scope=\"col\"> and more than 2 rows — or a CSS grid with role=\"table\" / role=\"row\"","Header cells with sort affordances: aria-sort, buttons inside <th>, ▲▼ icons","Numeric columns right-aligned or with units (in, lb, $, %) in every cell of a column","A <caption> or a heading directly above the table","Under 640px rows turn into stacked cards with label: value pairs (data-label attributes)"],"mcp":"Read the <table> with the table detector (docs/TABLES.md): <caption> (or the nearest heading) → `table.caption` (write one if missing — never empty); <thead> th → columns in order with stable `_key`s (camel-cased header text); units stripped into `unit` ('Weight (lb)' → Number, unit lb); per-column type from every body cell (numbers → Number, currency symbol → Money + code, ✓/✗/Yes/No → Yes / no, ★ or x/5 → Rating, <a> in every cell → Link); a first column of th[scope=row] (or an empty top-left header) → `headerColumn: true`; footnote marks (sup, *) → `footnotes` + the cell's `note`. More than 60 rows or a feed → `source: csv` and emit the CSV. Zebra striping and card layouts are design (`design.striped`, `design.responsive`), not fields.","pitfalls":["RESOLVED (D42): the `table` type types cells by their column (a cell may carry its own type), keys cells by column `_key`, and edits as a grid with paste from Sheets / Excel / CSV — no more drill-in per cell.","SCHEMA GAP: `defaultSort.column` must match a column `_key` of `table`; nothing checks it yet (gap 3, options from a sibling field).","Past ~60 rows, keep the data in CSV or a collection; the grid stays usable but the page gets heavy.","Do not model 'striped' or 'compact' as booleans — they are variants."],"notCarried":["ai","category","variants"],"component":{"name":"Data table","slug":"data-table","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"ui":{"tab":{"id":"content"}}},{"key":"source","label":"Rows come from","type":"select","defaultValue":"manual","config":{"options":[{"label":"Typed here","value":"manual"},{"label":"CSV file","value":"csv"},{"label":"Collection","value":"collection"}]},"ui":{"control":"segmented","tab":{"id":"data","label":"Data","icon":"database"}}},{"key":"table","label":"Table","type":"table","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"manual"}]},"config":{"mode":"data","maxColumns":8,"maxRows":60,"columnTypes":["text","number","money","boolean","rating","link","reference"],"jsonLd":["Dataset","ItemList"],"settings":{"phones":[],"sort":true,"search":true}},"ui":{"tab":{"id":"data"}}},{"key":"csvFile","label":"CSV file","type":"file","helpText":"First row = headers matching the column keys.","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"csv"}]},"ui":{"tab":{"id":"data"}}},{"key":"delimiter","label":"Separator","type":"select","defaultValue":"comma","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"csv"}]},"config":{"options":[{"label":"Comma","value":"comma"},{"label":"Semicolon","value":"semicolon"},{"label":"Tab","value":"tab"}]},"ui":{"control":"segmented","tab":{"id":"data"}}},{"key":"collection","label":"Collection","type":"reference","helpText":"One row per entry.","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"collection"}]},"config":{"targets":["collection"]},"ui":{"tab":{"id":"data"}}},{"key":"defaultSort","label":"Sort when the page loads","type":"object","config":{"fields":[{"key":"column","label":"Column key","type":"text","config":{"pattern":"^[a-z][a-zA-Z0-9]*$","patternMessage":"Use camelCase letters and numbers, like hoursPerRun."}},{"key":"direction","label":"Direction","type":"select","defaultValue":"asc","options":[{"label":"A → Z","value":"asc"},{"label":"Z → A","value":"desc"}],"ui":{"control":"segmented"}}]},"ui":{"icon":"arrows-sort","tab":{"id":"display","label":"Display"}}},{"key":"pageSize","label":"Rows per page","type":"number","helpText":"Leave empty to show every row.","config":{"min":5,"max":100,"step":5},"ui":{"tab":{"id":"display"}}}],"description":"Rows and typed columns. Sortable, searchable, readable on phones","category":"section","icon":"table","preview":{"title":"heading","subtitle":"table.caption","icon":"table"}}},
|
|
1476
|
+
{"id":"table-comparison","name":"Table","category":"Data & interactive","kind":"block","tags":["validation","a11y","seo-structured","references","commerce"],"proves":"The `table` type (D42): caption · row headers · typed columns (Number, Money, Yes / no, Rating, Reference) · row groups · per-row and per-cell types · footnotes · notes · ItemList JSON-LD","why":"The reference entry for the `table` type (CPO D42). The section is a heading + intro + ONE `table` field: columns are typed and keyed (Number in kg, Money in USD, Yes / no with its own words, Rating, a Reference column that links each row to its product), cells are keyed by column so a reorder never misaligns, rows sit in row groups, the first column is the row header. One odd value ('Heats with the fire') is a cell with its own type; the highlighted row is a flag, not a color typed into content. ItemList JSON-LD is opted in because the table IS the comparison on the page.","htmlSignals":["<table> with <caption>, <thead><th scope=col>, body <th scope=row>","Units repeated down a column (kg, min, $) and ✓/– icons with hidden words","Full-width rows (colspan) or several <tbody> acting as group headings","Footnote marks (sup, *) tied to a numbered list under the table"],"mcp":"Run the table detector (docs/brain/tables.md): caption from <caption> or the heading above (set design.hideCaption when the heading says it); header cells → columns with keys and types inferred from every body cell; first-column th → headerColumn; colspan / tbody headings → rowGroups; ✓/✗ → Yes / no; links to known entries → Reference; footnote marks → footnotes + cell.note; text under the table → notes. JSON-LD only when the table is the page's offer.","pitfalls":["Never merge body cells; repeat the value.","A blank cell in a yes/no column is 'Not included'; a blank cell anywhere else is 'No value'.","Don't retype units in cells — the column carries them."],"notCarried":["ai","category"],"component":{"name":"Table","slug":"table-block","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32}},{"key":"heading","label":"Heading","type":"text","recommendedMax":60},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":200,"ui":{"rows":2}},{"key":"table","label":"Table","type":"table","required":true,"config":{"mode":"data","jsonLd":["ItemList","Dataset","Offers"],"settings":{"sort":true,"search":true}}}],"description":"A real data table: comparisons, specs, pricing, reports","category":"section","icon":"table","preview":{"title":"heading","subtitle":"table.caption","icon":"table"}}},
|
|
1477
|
+
{"id":"chart-attrition","name":"Chart with annotations","category":"Data & interactive","kind":"block","tags":["repeater","conditional","a11y","validation"],"proves":"Chart type → series → data points (3 levels) · annotations · text alternative required · data from CSV or URL","why":"A chart is three levels of data (chart → series → points) plus presentation (axes, annotations) — so data and display are separate groups and points open inline (two primitives each) to avoid a drill per number. The written takeaway is REQUIRED because the drawing is inaccessible; the optional data table is the second fallback. Annotations reference a point by its label, not by index, so reordering points never moves a note.","htmlSignals":["<svg> with many <path>/<rect> elements and axis tick <text> nodes, or a <canvas> with a chart library global (Chart.js, Recharts, Highcharts, ECharts)","A legend list of colored swatches + series names","Inline JSON in a <script> (data-chart, __NEXT_DATA__ series) or a data-src attribute pointing at a .csv/.json","Figure + figcaption with 'Source:' text"],"mcp":"Never rasterise the chart. Recover the data first: (1) inline JSON / data attributes / library config objects; (2) the hidden accessible table many chart libraries render; (3) only as a last resort read axis ticks + bar heights and mark the entry for human review. Map each dataset to a `series` item and each label/value pair to a `point`. If there are more than 24 points per series or the data comes from a URL, use `source: csv|url` and emit the file. Write `summary` from the figcaption or by stating the trend in one sentence — it is required. Map 'Source: …' to sourceLabel/sourceLink. Library-specific styling (tooltips, gridlines, animation) stays in code; only color per series is content.","pitfalls":["SCHEMA GAP: no grid/table editor for numeric data — 2 series × 12 points is 24 inline rows. A `table` or `dataset` field type (paste from a spreadsheet) is needed.","SCHEMA GAP: `annotation.at` must equal an existing point label (cross-array reference) — the language can't express 'one of the x values in series', so it is a free string.","SCHEMA GAP: no constraint that donut charts have exactly one series with 2–5 points — validation is per field, never conditional on another field's value.","Color picker accepts any hex; the series palette should be restricted to brand chart tokens for contrast (see section-theme-tokens).","Bar charts with a non-zero `yMin` mislead; the help text warns but nothing prevents it."],"notCarried":["ai","category","variants"],"component":{"name":"Chart","slug":"chart-block","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":70,"ui":{"tab":{"id":"content"}}},{"key":"summary","label":"Takeaway","type":"longtext","helpText":"Read by screen readers instead of the chart.","required":true,"config":{"maxLength":240},"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"chartType","label":"Chart","type":"select","defaultValue":"line","config":{"options":[{"label":"Line","value":"line","icon":"chart-line","description":"Change over time"},{"label":"Bar","value":"bar","icon":"chart-bar","description":"Compare amounts"},{"label":"Area","value":"area","icon":"chart-area","description":"Totals over time"},{"label":"Donut","value":"donut","icon":"chart-donut","description":"Parts of a whole, 2–5 slices"}]},"ui":{"control":"cards","tab":{"id":"content"}}},{"key":"isStacked","label":"Stack the series","type":"boolean","showIf":{"match":"all","rules":[{"field":"chartType","op":"in","value":["bar","area"]}]},"ui":{"tab":{"id":"content"}}},{"key":"source","label":"Data comes from","type":"select","defaultValue":"manual","config":{"options":[{"label":"Typed here","value":"manual"},{"label":"CSV file","value":"csv"},{"label":"JSON URL","value":"url"}]},"ui":{"control":"segmented","tab":{"id":"data","label":"Data","icon":"database"}}},{"key":"series","label":"Series","type":"table","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"manual"}]},"config":{"minItems":1,"maxItems":5,"fields":[{"key":"name","label":"Name","type":"text","required":true,"config":{"maxLength":40}},{"key":"color","label":"Color","type":"color","helpText":"Leave empty to use the brand palette in order."},{"key":"style","label":"Line","type":"select","defaultValue":"solid","options":[{"label":"Solid","value":"solid"},{"label":"Dashed","value":"dashed"}],"ui":{"control":"segmented"}},{"key":"points","label":"Data points","type":"table","openMode":"inline","fields":[{"key":"x","label":"Label","type":"text","required":true,"placeholder":"Q1 2026","config":{"maxLength":16}},{"key":"y","label":"Value","type":"number","required":true},{"key":"label","label":"Callout","type":"text","config":{"maxLength":24}}],"config":{"minItems":2,"maxItems":24},"ui":{"itemLabel":"Point","icon":"point","preview":{"title":"x","subtitle":"y","fallback":"Untitled point"}}}]},"ui":{"itemLabel":"Series","itemName":"seriesItem","icon":"chart-dots","preview":{"title":"name","subtitle":"style","fallback":"Untitled series"},"tab":{"id":"data"}}},{"key":"csvFile","label":"CSV file","type":"file","helpText":"First column = labels, one column per series.","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"csv"}]},"ui":{"tab":{"id":"data"}}},{"key":"dataUrl","label":"JSON URL","type":"url","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"url"}]},"readOnly":"developer","lockReason":"The endpoint and its shape are set by a developer.","config":{"pattern":"^https://","patternMessage":"Use an https:// address, like https://your-site.com."},"ui":{"access":{"edit":"schema.manage"},"tab":{"id":"data"}}},{"key":"refresh","label":"Refresh","type":"select","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"url"}]},"config":{"options":[{"label":"On publish","value":"build"},{"label":"Every hour","value":"hourly"},{"label":"Every page view","value":"live"}]},"ui":{"tab":{"id":"data"}}},{"key":"axes","label":"Axes","type":"object","openMode":"drill","config":{"fields":[{"key":"xTitle","label":"Horizontal axis","type":"text","config":{"maxLength":32}},{"key":"yTitle","label":"Vertical axis","type":"text","config":{"maxLength":32}},{"key":"format","label":"Number format","type":"select","defaultValue":"number","options":[{"label":"1,234","value":"number"},{"label":"12%","value":"percent"},{"label":"$1,234","value":"currency"},{"label":"1.2k","value":"compact"}]},{"key":"yMin","label":"Start at","type":"number","helpText":"Leave empty to start at zero."},{"key":"yMax","label":"End at","type":"number"}]},"ui":{"icon":"axis-x","tab":{"id":"display","label":"Display"}}},{"key":"annotations","label":"Annotations","type":"table","config":{"maxItems":4,"fields":[{"key":"at","label":"At label","type":"text","helpText":"Must match a point label, e.g. Q3 2025.","required":true,"config":{"maxLength":16}},{"key":"label","label":"Note","type":"text","required":true,"config":{"maxLength":40}},{"key":"detail","label":"Detail","type":"longtext","config":{"maxLength":160},"ui":{"rows":2}},{"key":"tone","label":"Tone","type":"select","defaultValue":"neutral","options":[{"label":"Neutral","value":"neutral"},{"label":"Good","value":"positive"},{"label":"Bad","value":"negative"}],"ui":{"control":"segmented"}}]},"ui":{"itemLabel":"Annotation","icon":"message-circle","preview":{"title":"label","subtitle":"at","fallback":"Untitled note"},"tab":{"id":"display"}}},{"key":"showTable","label":"Show the numbers as a table","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"display"}}},{"key":"sourceLabel","label":"Source","type":"text","placeholder":"Betterwork benchmark, 1,240 companies","config":{"maxLength":80},"ui":{"tab":{"id":"display"}}},{"key":"sourceLink","label":"Source link","type":"link","ui":{"tab":{"id":"display"}}}],"description":"Line, bar, area or donut chart with a written takeaway","category":"section","icon":"chart-line","preview":{"title":"heading","subtitle":"summary","icon":"chart-line"}}},
|
|
1478
|
+
{"id":"roi-calculator","name":"ROI calculator","category":"Data & interactive","kind":"block","tags":["form","repeater","conditional","validation"],"proves":"Typed inputs (slider · number · select) → formula outputs · conditional control settings · locked formulas","why":"A calculator is a tiny program: named inputs, formulas over those names, formatted outputs. Inputs and outputs are arrays with a stable `key` so formulas survive relabelling and reordering. Control-specific settings (range for sliders/numbers, choices for select) are conditional on `control`, so an editor never sees a 'Step' for a dropdown. Formulas are developer-locked: marketers can change copy and defaults, not maths. Email capture is a reference to a real form, never a second hidden form model.","htmlSignals":["<input type=\"range\"> / <input type=\"number\"> / <select> with JS listeners and no form submit","Output elements (<output>, aria-live regions) whose text changes on input","Inline JS with arithmetic on input values (e.g. value * 0.72) or a data-formula attribute","'How we calculate' / 'Assumptions' disclosure link near the result"],"mcp":"Each interactive control → an `input`: label from <label>, key from the input's name/id camel-cased, control from the element type (range → slider), min/max/step/value from attributes, <option>s → choices with numeric values. Read the bundled JS for the arithmetic: each computed output → an `output` with its formula rewritten in terms of input keys (constants inline, e.g. 0.72), format from the rendered text ($ → currency, % → percent). If the formula calls functions, lookup tables or tiered pricing you can't express as arithmetic, keep the calculator as a code component and expose ONLY copy fields — flag it, never approximate a formula. Assumptions text → richtext; a submit/email step → `allowEmailResults` + reference to a created form.","pitfalls":["SCHEMA GAP: no formula/expression field type. `code` stores a string; nothing parses it, checks that it only references existing input/output keys, or detects cycles (costSaved → perEmployee → costSaved).","SCHEMA GAP: `unique` on input/output keys is not enforced, and keys must be unique ACROSS two arrays (inputs and outputs share one namespace) — no cross-array uniqueness exists.","SCHEMA GAP: cross-field numeric rules (min < defaultValue < max, step divides the range) can't be expressed; each number validates alone.","SCHEMA GAP: `readOnly: \"developer\"` is the only field-level permission — no 'finance can edit formulas' role scope.","A select's choices need numeric values; a text-only dropdown (\"Small team\") must be given a number or it breaks the maths."],"notCarried":["ai","category","variants"],"component":{"name":"ROI calculator","slug":"roi-calculator","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":180,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"inputs","label":"Inputs","type":"table","config":{"minItems":1,"maxItems":6,"fields":[{"key":"label","label":"Question","type":"text","required":true,"placeholder":"How many people do you pay?","config":{"maxLength":60}},{"key":"key","label":"Key","type":"text","helpText":"The name formulas use, e.g. employees.","required":true,"unique":true,"config":{"pattern":"^[a-z][a-zA-Z0-9]*$","patternMessage":"Use camelCase letters and numbers, like hoursPerRun."}},{"key":"control","label":"Control","type":"select","defaultValue":"slider","options":[{"label":"Slider","value":"slider"},{"label":"Number","value":"number"},{"label":"Choice","value":"select"}],"ui":{"control":"segmented"}},{"key":"min","label":"Minimum","type":"number","showIf":{"match":"all","rules":[{"field":"control","op":"in","value":["slider","number"]}]}},{"key":"max","label":"Maximum","type":"number","showIf":{"match":"all","rules":[{"field":"control","op":"in","value":["slider","number"]}]}},{"key":"step","label":"Step","type":"number","showIf":{"match":"all","rules":[{"field":"control","op":"in","value":["slider","number"]}]}},{"key":"unit","label":"Unit","type":"text","showIf":{"match":"all","rules":[{"field":"control","op":"in","value":["slider","number"]}]},"placeholder":"people","config":{"maxLength":10}},{"key":"choices","label":"Choices","type":"table","showIf":{"match":"all","rules":[{"field":"control","op":"eq","value":"select"}]},"openMode":"inline","fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":32}},{"key":"value","label":"Value","type":"number","required":true}],"config":{"minItems":2,"maxItems":6},"ui":{"itemLabel":"Choice","icon":"circle-dot","preview":{"title":"label","subtitle":"value","fallback":"Untitled choice"}}},{"key":"defaultValue","label":"Starts at","type":"number","required":true},{"key":"help","label":"Help","type":"text","config":{"maxLength":90}}]},"ui":{"itemLabel":"Input","icon":"adjustments-horizontal","preview":{"title":"label","subtitle":"key","fallback":"Untitled input"},"tab":{"id":"logic","label":"Inputs & formulas","icon":"math-function"}}},{"key":"outputs","label":"Results","type":"table","config":{"minItems":1,"maxItems":4,"fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":48}},{"key":"key","label":"Key","type":"text","helpText":"Later results can use it, e.g. costSaved.","required":true,"unique":true,"config":{"pattern":"^[a-z][a-zA-Z0-9]*$","patternMessage":"Use camelCase letters and numbers, like hoursPerRun."}},{"key":"formula","label":"Formula","type":"code","helpText":"Arithmetic on input and result keys, e.g. runs * hoursPerRun * 0.72.","required":true,"readOnly":"developer","lockReason":"Formulas are checked by finance before they ship.","ui":{"access":{"edit":"schema.manage"}}},{"key":"format","label":"Format","type":"select","defaultValue":"number","options":[{"label":"1,234","value":"number"},{"label":"$","value":"currency"},{"label":"%","value":"percent"},{"label":"Hours","value":"hours"}],"ui":{"control":"segmented"}},{"key":"decimals","label":"Decimals","type":"number","defaultValue":0,"config":{"min":0,"max":2,"step":1}},{"key":"isHighlighted","label":"Headline result","type":"boolean"}]},"ui":{"itemLabel":"Result","icon":"equal","preview":{"title":"label","subtitle":"formula","fallback":"Untitled result"},"tab":{"id":"logic"}}},{"key":"assumptions","label":"How we calculate","type":"richtext","helpText":"Shown behind a \"How we calculate\" link.","ui":{"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"disclaimer","label":"Small print","type":"longtext","config":{"maxLength":240},"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"allowEmailResults","label":"Offer to email the results","type":"boolean","helpText":"Submissions arrive in the chosen form.","ui":{"tab":{"id":"settings","label":"Settings"}}},{"key":"form","label":"Send to form","type":"form","required":true,"showIf":{"match":"all","rules":[{"field":"allowEmailResults","op":"eq","value":"true"}]},"ui":{"tab":{"id":"settings"}}}],"description":"Inputs on the left, live results on the right","category":"section","icon":"calculator","preview":{"title":"heading","subtitle":"intro","icon":"calculator"}}},
|
|
1479
|
+
{"id":"product-configurator","name":"Product configurator","category":"Data & interactive","kind":"block","tags":["commerce","deep-nesting","repeater","conditional","references","validation"],"proves":"Steps → options → availability rules (4 levels) · price deltas · single vs multi pick · Shopify variant references","why":"Steps → options is a two-level repeater; each option carries its own price delta, image and Shopify product so the cart can be built without a lookup table. Dependencies ('griddle needs the XL grate') live ON the dependent option as an availability object — four levels deep (Configurator › Step › Option › Availability) — referencing another pick by 'stepKey.optionKey' so rules survive reordering. Inventory and base price come from Shopify (reference), never retyped.","htmlSignals":["Numbered steps (Step 1 of 3) with radio groups or toggle cards per step","Option labels with '+$60' / '−$10' price adjustments","A running total / sticky summary that updates on change","Options disabled with notes like 'Requires XL' or 'Sold out' (aria-disabled, data-requires)","Shopify variant ids in data attributes or a cart/add.js form"],"mcp":"Each step container (fieldset/legend or numbered heading) → a `step`; radio groups → selection single, checkbox groups → multiple + maxPicks from any limit text. Each option → label, detail, image (the swatch color if it's a color chip), priceDelta parsed from '+$60' (signed number, no currency symbol), variant from data-variant-id/gid. Detect dependencies from disabled-state logic or text ('Fits the XL grate only') and write `availability.rule` as stepKey.optionKey using the keys you generated — never by index. If rules involve more than one other pick (A and B, A or B), or quantities, keep the logic in code and only model the copy, and add a pitfall note. Depth check: if options have their own sub-options (a third level of choice), split each step into its own block/component rather than nesting a fifth level.","pitfalls":["SCHEMA GAP: conditions can only read SIBLING fields — an option's `swatch` should show only when its PARENT step's `display` is swatches, which can't be expressed, so the field is always visible.","SCHEMA GAP: availability.rule is a string pattern, not a reference to a real step/option; renaming a key breaks rules silently. Needs an same-entry reference type ('pick a sibling item').","SCHEMA GAP: no boolean logic in rules (requires A AND B, excludes any of …) and no cross-step validation (two defaults in a single-pick step, `maxPicks` < number of defaults).","SCHEMA GAP: option keys need to be unique within their step only (scoped uniqueness) — `unique` is neither scoped nor enforced.","Price deltas drift from Shopify variant prices; prefer reading the variant price and keeping priceDelta only for non-Shopify add-ons."],"notCarried":["ai","category","shopify reference","variants"],"component":{"name":"Product configurator","slug":"product-configurator","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":50,"ui":{"tab":{"id":"content","label":"Content","order":0}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":160,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"image","label":"Image","type":"image","required":true,"ui":{"tab":{"id":"content"}}},{"key":"baseProduct","label":"Base product","type":"text","helpText":"Its Shopify price is the starting total.","required":true,"readOnly":true,"lockReason":"A Shopify product id. No Shopify integration backs a reference in BetterCMS yet, so the id is kept as it is.","ui":{"tab":{"id":"checkout","label":"Checkout","order":2}}},{"key":"steps","label":"Steps","type":"table","config":{"minItems":1,"maxItems":6,"fields":[{"key":"title","label":"Step title","type":"text","required":true,"placeholder":"Choose your grate","config":{"maxLength":40}},{"key":"key","label":"Key","type":"text","helpText":"Used by rules in other steps, e.g. grate.","required":true,"unique":true,"config":{"pattern":"^[a-z][a-zA-Z0-9]*$","patternMessage":"Use camelCase letters and numbers, like hoursPerRun."}},{"key":"help","label":"Help","type":"text","config":{"maxLength":90}},{"key":"selection","label":"Customers pick","type":"select","defaultValue":"single","options":[{"label":"One","value":"single"},{"label":"Several","value":"multiple"}],"ui":{"control":"segmented"}},{"key":"isRequired","label":"Required","type":"boolean","helpText":"Checkout stays disabled until this step has a pick.","defaultValue":true},{"key":"maxPicks","label":"At most","type":"number","showIf":{"match":"all","rules":[{"field":"selection","op":"eq","value":"multiple"}]},"config":{"min":2,"max":8,"step":1}},{"key":"display","label":"Show options as","type":"select","defaultValue":"cards","options":[{"label":"Cards","value":"cards"},{"label":"Swatches","value":"swatches"},{"label":"List","value":"list"}],"ui":{"control":"segmented"}},{"key":"options","label":"Options","type":"table","fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":40}},{"key":"key","label":"Key","type":"text","helpText":"Unique within the step, e.g. xl.","required":true,"config":{"pattern":"^[a-z][a-zA-Z0-9]*$","patternMessage":"Use camelCase letters and numbers, like hoursPerRun."}},{"key":"detail","label":"Detail","type":"text","config":{"maxLength":80}},{"key":"image","label":"Image","type":"image"},{"key":"swatch","label":"Swatch","type":"color"},{"key":"priceDelta","label":"Price change","type":"number","helpText":"Added to the total. Use a negative number for a discount.","config":{"unit":"USD","step":1}},{"key":"variant","label":"Shopify product","type":"text","helpText":"Added to the cart with the base product.","readOnly":true,"lockReason":"A Shopify product id. No Shopify integration backs a reference in BetterCMS yet, so the id is kept as it is."},{"key":"isDefault","label":"Picked by default","type":"boolean"},{"key":"availability","label":"Availability","type":"object","openMode":"drill","fields":[{"key":"mode","label":"Available","type":"select","defaultValue":"always","options":[{"label":"Always","value":"always"},{"label":"Only with another pick","value":"requires","description":"e.g. the griddle needs the XL grate"},{"label":"Not with another pick","value":"excludes"}],"ui":{"control":"radio"}},{"key":"rule","label":"Other pick","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"mode","op":"in","value":["requires","excludes"]}]},"config":{"pattern":"^[a-z][a-zA-Z0-9]*\\.[a-z][a-zA-Z0-9]*$","patternMessage":"Use the step key, a dot, then the option key, like grate.xl."}},{"key":"unavailableNote","label":"Note when unavailable","type":"text","showIf":{"match":"all","rules":[{"field":"mode","op":"in","value":["requires","excludes"]}]},"config":{"maxLength":60}},{"key":"isSoldOut","label":"Sold out","type":"boolean"}],"ui":{"icon":"git-branch"}}],"config":{"minItems":2,"maxItems":8},"ui":{"itemLabel":"Option","icon":"circle-check","preview":{"title":"label","subtitle":"priceDelta","media":"image","fallback":"Untitled option"}}}]},"ui":{"itemLabel":"Step","icon":"list-numbers","preview":{"title":"title","subtitle":"selection","fallback":"Untitled step"},"tab":{"id":"steps","label":"Steps","icon":"list-numbers","order":1}}},{"key":"showTotal","label":"Show running total","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"checkout"}}},{"key":"button","label":"Add to cart button","type":"button","ui":{"tab":{"id":"checkout"}}},{"key":"shippingNote","label":"Under the button","type":"text","placeholder":"Free shipping over $150","config":{"maxLength":80},"ui":{"tab":{"id":"checkout"}}}],"description":"Build a kit step by step with a running total","category":"section","icon":"adjustments","preview":{"title":"heading","media":"image"}}},
|
|
1480
|
+
{"id":"stockist-map","name":"Map with markers","category":"Data & interactive","kind":"block","tags":["repeater","conditional","embed","a11y","references","seo-structured"],"proves":"Categories → markers → popup → button · geopoints · manual or collection source · list fallback for keyboard users","why":"Categories are an array (editors add 'Pop-ups' without a developer) and drive both filter chips and pin styling; each marker stores a geopoint, a readable address (the list fallback reads it, not the map) and a popup object that drills in to a button — Map › Place › Popup › Button. A collection source exists because stockist lists outgrow a block fast. Provider is developer-locked because it depends on an API key in the environment; consent is a content decision.","htmlSignals":["A Leaflet/MapLibre/Mapbox/Google Maps container (div.leaflet-container, .mapboxgl-map, iframe to maps.google.com/embed)","Marker data in JSON (data-markers, window.locations) or repeated <li> with address + lat/lng data attributes","Filter chips above the map toggling categories","Popups with an image, a line of text and 'Directions' links","LocalBusiness / Store JSON-LD per location"],"mcp":"Read marker data from the JSON the map is initialised with (or the visible list); each → `marker` with geopoint {lat,lng,label}. Derive `categories` from distinct marker types/filters; set each marker's `category` to the generated key. Popup HTML → popup.image/body/button. If there are more than 50 places or the list is a CMS feed, create a Locations collection and set `source: collection` — each location becomes an entry (it has its own page, hours and JSON-LD). Never extract the provider API key; set `provider` only. A static map image with no interactivity is NOT this entry — model it as an image.","pitfalls":["SCHEMA GAP: a marker's category should be a select whose options come from the sibling `categories` array (dynamic options); it is a patterned string with no integrity check.","SCHEMA GAP: opening hours want a structured type (day-of-week ranges, exceptions/holidays) to emit LocalBusiness openingHoursSpecification; strings can't be validated against real times.","SCHEMA GAP: no address type (street, locality, region, postcode, country) — needed for JSON-LD PostalAddress and geocoding; a multi-line text loses structure.","Turning off `showList` leaves keyboard and screen-reader users with no way to reach the places."],"notCarried":["ai","category","variants"],"component":{"name":"Map","slug":"marker-map","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":50,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":160,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"categories","label":"Categories","type":"table","config":{"maxItems":6,"fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":24}},{"key":"key","label":"Key","type":"text","required":true,"unique":true,"config":{"pattern":"^[a-z][a-zA-Z0-9]*$","patternMessage":"Use camelCase letters and numbers, like hoursPerRun."}},{"key":"color","label":"Pin color","type":"color"},{"key":"icon","label":"Pin icon","type":"icon"}]},"ui":{"itemLabel":"Category","icon":"tag","preview":{"title":"label","subtitle":"key","fallback":"Untitled category"},"tab":{"id":"places","label":"Places","icon":"map-pins"}}},{"key":"source","label":"Places come from","type":"select","defaultValue":"manual","config":{"options":[{"label":"Added here","value":"manual"},{"label":"Locations collection","value":"collection"}]},"ui":{"control":"segmented","tab":{"id":"places"}}},{"key":"markers","label":"Places","type":"table","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"manual"}]},"config":{"minItems":1,"maxItems":50,"fields":[{"key":"name","label":"Name","type":"text","required":true,"config":{"maxLength":60}},{"key":"category","label":"Category key","type":"text","helpText":"One of the category keys above, e.g. retailer.","required":true,"config":{"pattern":"^[a-z][a-zA-Z0-9]*$","patternMessage":"Use camelCase letters and numbers, like hoursPerRun."}},{"key":"location","label":"Pin","type":"location","required":true},{"key":"address","label":"Address","type":"longtext","required":true,"config":{"maxLength":160},"ui":{"rows":2}},{"key":"phone","label":"Phone","type":"text","config":{"pattern":"^\\+?[0-9 ()-]{7,20}$","patternMessage":"Use digits, spaces and an optional +, like +1 541 555 0182."}},{"key":"hours","label":"Opening hours","type":"table","openMode":"inline","fields":[{"key":"days","label":"Days","type":"text","required":true,"placeholder":"Mon–Fri","config":{"maxLength":20}},{"key":"times","label":"Hours","type":"text","required":true,"config":{"pattern":"^(Closed|\\d{1,2}:\\d{2}–\\d{1,2}:\\d{2})$","patternMessage":"Use opening hours, like 09:00–18:00 or Closed."}}],"config":{"maxItems":7},"ui":{"itemLabel":"Line","itemName":"hoursLine","icon":"clock","preview":{"title":"days","subtitle":"times","fallback":"Untitled line"}}},{"key":"popup","label":"Popup","type":"object","openMode":"drill","fields":[{"key":"image","label":"Image","type":"image"},{"key":"body","label":"Body","type":"longtext","recommendedMax":120,"ui":{"rows":2}},{"key":"button","label":"Button","type":"button"}],"ui":{"icon":"message-2"}}]},"ui":{"itemLabel":"Place","icon":"map-pin","preview":{"title":"name","subtitle":"address","fallback":"Untitled place"},"tab":{"id":"places"}}},{"key":"collection","label":"Collection","type":"reference","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"collection"}]},"config":{"targets":["collection"]},"ui":{"tab":{"id":"places"}}},{"key":"provider","label":"Map provider","type":"select","readOnly":"developer","lockReason":"The provider key is set in the site's environment.","config":{"options":[{"label":"MapLibre","value":"maplibre"},{"label":"Mapbox","value":"mapbox"},{"label":"Google Maps","value":"google"}]},"ui":{"access":{"edit":"schema.manage"},"tab":{"id":"map","label":"Map"}}},{"key":"style","label":"Map style","type":"select","config":{"options":[{"label":"Outdoors","value":"outdoors"},{"label":"Light","value":"light"},{"label":"Satellite","value":"satellite"}]},"ui":{"control":"segmented","tab":{"id":"map"}}},{"key":"center","label":"Start centered on","type":"location","helpText":"Leave empty to fit every pin.","ui":{"tab":{"id":"map"}}},{"key":"zoom","label":"Start zoom","type":"number","config":{"min":2,"max":16,"step":1},"ui":{"control":"slider","tab":{"id":"map"}}},{"key":"useClustering","label":"Group nearby pins","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"map"}}},{"key":"showList","label":"List beside the map","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"map"}}},{"key":"showNearMe","label":"Show Near me button","type":"boolean","ui":{"tab":{"id":"map"}}},{"key":"useConsentGate","label":"Ask before loading the map","type":"boolean","helpText":"Needed in the EU for Google Maps.","ui":{"tab":{"id":"map"}}}],"description":"Pins on a map with popups, filters and a list beside it","category":"section","icon":"map-pin","preview":{"title":"heading","subtitle":"intro","icon":"map-pin"}}},
|
|
1481
|
+
{"id":"product-compare-slider","name":"Product comparison slider","category":"Data & interactive","kind":"block","tags":["references","repeater","conditional","validation","commerce","responsive-layout"],"proves":"References to products · attributes pulled from referenced fields or typed per product · swipe on phones","why":"Comparison values belong to the PRODUCTS, not the section — so rows default to 'pull this field from each referenced product' and only rows that aren't product data ('Best for') are typed here, keyed by product reference. That keeps prices and weights in one place. Products are an array of references with min 2 / max 4 and uniqueness; the phone behavior is an explicit choice because a 4-column table is unreadable at 390px.","htmlSignals":["A table or CSS grid whose column headers are product cards (image, name, price, button)","Row labels for specs (Weight, Capacity, Warranty) repeated across products","Horizontal scroll-snap on mobile (overflow-x: auto; scroll-snap-type: x) or 'swipe to compare' hints","Ticks/crosses or 'Best value' highlighting in cells"],"mcp":"Resolve each column header to an existing product entry (match by URL/handle/SKU) and add it to `products`; if a product doesn't exist, create it first — never type product names into the section. For each row: if the value matches a field on the product model (weight, price…), emit `source: field` with that field; otherwise `source: custom` with one value per product reference. Ticks → 'Yes'/'No' values or a boolean product field. Scroll-snap / swipe UI → `mobile: swipe`. If the page compares more than 4 products or more than 14 rows, it's a spec database: build a Products-table entry (data-table with collection source) instead.","pitfalls":["SCHEMA GAP: `field` options should be generated from the referenced collection's fields (dynamic options); here they're a hard-coded list that drifts when the Products model changes.","SCHEMA GAP: `recommended` must be one of `products` (constrained reference) — not expressible.","SCHEMA GAP: array `unique: true` (no duplicate product) is declared but not enforced by the validator.","SCHEMA GAP: custom `values` should have exactly one entry per product (a matrix keyed by another array); adding a fourth product leaves rows with three values and nothing warns."],"notCarried":["ai","category","variants"],"component":{"name":"Comparison slider","slug":"comparison-slider","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":50,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"intro","label":"Intro","type":"longtext","ui":{"rows":2,"tab":{"id":"content"}}},{"key":"products","label":"Products","type":"multi-reference","unique":true,"config":{"min":2,"max":4,"contentModelId":"col_products"},"ui":{"itemLabel":"Product","tab":{"id":"content"}}},{"key":"recommended","label":"Recommended","type":"reference","helpText":"Must be one of the products above.","config":{"contentModelId":"col_products"},"ui":{"tab":{"id":"content"}}},{"key":"attributes","label":"Rows","type":"table","config":{"minItems":1,"maxItems":14,"fields":[{"key":"source","label":"Values come from","type":"select","defaultValue":"field","options":[{"label":"Product field","value":"field"},{"label":"Typed here","value":"custom"}],"ui":{"control":"segmented"}},{"key":"field","label":"Product field","type":"select","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"field"}]},"options":[{"label":"Capacity","value":"capacity"},{"label":"Weight","value":"weight"},{"label":"Material","value":"material"},{"label":"Heat rating","value":"heatRating"},{"label":"Induction","value":"induction"},{"label":"Packed size","value":"packedSize"},{"label":"Warranty","value":"warranty"},{"label":"Price","value":"price"}]},{"key":"label","label":"Row label","type":"text","helpText":"Leave empty to use the field's name.","config":{"maxLength":32}},{"key":"tooltip","label":"Explainer","type":"text","config":{"maxLength":90}},{"key":"values","label":"Values","type":"table","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"custom"}]},"openMode":"inline","fields":[{"key":"product","label":"Product","type":"reference","config":{"contentModelId":"col_products"}},{"key":"value","label":"Value","type":"text","required":true,"config":{"maxLength":40}}],"config":{"maxItems":4},"ui":{"itemLabel":"Value","itemName":"cellValue","icon":"square","preview":{"title":"value","fallback":"Untitled value"}}},{"key":"showDifferences","label":"Highlight differences","type":"boolean"}]},"ui":{"itemLabel":"Row","icon":"list-details","preview":{"title":"label","subtitle":"field","fallback":"Untitled row"},"tab":{"id":"rows","label":"Rows","icon":"list-details"}}},{"key":"mobile","label":"On phones","type":"select","defaultValue":"swipe","config":{"options":[{"label":"Swipe","value":"swipe","description":"One product at a time, row labels stay put"},{"label":"Pick two","value":"pair","description":"Two dropdowns choose which pair to compare"},{"label":"Stack","value":"stack","description":"Each product as its own card"}]},"ui":{"control":"radio","tab":{"id":"display","label":"Display"}}},{"key":"isHeaderSticky","label":"Keep the header row in view","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"display"}}},{"key":"showAddToCart","label":"Show add to cart buttons","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"display"}}}],"description":"Two to four products side by side, swipeable on phones","category":"section","icon":"arrows-left-right","preview":{"title":"heading","subtitle":"intro"}}},
|
|
1482
|
+
{"id":"countdown-live-counter","name":"Countdown and live counter","category":"Data & interactive","kind":"block","tags":["conditional","validation","localized","a11y"],"proves":"Datetime + time zone · what happens after zero (conditional) · live counter from a static rate or an API · aria-live","why":"The timer is a datetime plus an explicit time zone (a launch is 9am Pacific for everyone), and the zero moment is a real content state: the after-launch heading and button are conditional fields so the section never shows 00:00:00 for a week. The live counter is a drill-in object with its own nested conditionals (enabled → source → rate fields or endpoint), which is the pattern for any self-contained sub-feature. Unit labels are localized strings because 'hrs' is copy.","htmlSignals":["Digits grouped as dd:hh:mm:ss updated by setInterval, often with data-countdown / data-end attributes holding an ISO date","Labels like 'days hrs min sec' under the digits","'X people waiting' / 'Y sold' numbers polling an endpoint (fetch in a loop)","Launch banner copy: 'Drops Friday', 'Ends in'"],"mcp":"Read the target from data attributes or the JS (new Date('…')); if it has no offset, ask for or default the brand's zone and set `timezone` — never store a local time without one. Units shown → `units`; their labels → `labels`. Find what the script does at zero (hide/replace text) and map to `afterEnd`, extracting the replacement copy. A polling number → counter.source api (copy the endpoint, developer-locked); a number computed from a start + rate → source rate. If the counter is fabricated (random increments with no data), do not migrate it — flag it.","pitfalls":["SCHEMA GAP: datetime has no time zone component; the zone is a separate select and nothing forces them to be read together (and DST is the renderer's job).","SCHEMA GAP: conditions don't chain — `startValue` is shown when source = rate even if the parent `showLiveCounter` toggle is off, because a field can only test one sibling. Needs `hidden` with AND/OR or nested condition groups.","SCHEMA GAP: no 'scheduled content' primitive — after-launch copy should be a scheduled change (release) rather than a second set of fields.","Countdowns that reset per visitor are dark patterns (and illegal under EU UCPD guidance); the schema has no per-visitor mode on purpose."],"notCarried":["ai","category","variants"],"component":{"name":"Countdown","slug":"countdown","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":50,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Body","type":"longtext","localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"target","label":"Counts down to","type":"datetime","required":true,"ui":{"tab":{"id":"timer","label":"Timer","icon":"clock"}}},{"key":"timezone","label":"Time zone","type":"select","helpText":"The launch happens at this zone's time for everyone.","defaultValue":"America/Los_Angeles","config":{"options":[{"label":"Pacific (Bend)","value":"America/Los_Angeles"},{"label":"Eastern","value":"America/New_York"},{"label":"London","value":"Europe/London"},{"label":"UTC","value":"UTC"}]},"ui":{"tab":{"id":"timer"}}},{"key":"units","label":"Show","type":"multiselect","defaultValue":["days","hours","minutes","seconds"],"config":{"options":[{"label":"Days","value":"days"},{"label":"Hours","value":"hours"},{"label":"Minutes","value":"minutes"},{"label":"Seconds","value":"seconds"}]},"ui":{"tab":{"id":"timer"}}},{"key":"labels","label":"Unit labels","type":"object","openMode":"drill","config":{"fields":[{"key":"days","label":"Days","type":"text","localized":true,"placeholder":"days","config":{"maxLength":12}},{"key":"hours","label":"Hours","type":"text","localized":true,"placeholder":"hrs","config":{"maxLength":12}},{"key":"minutes","label":"Minutes","type":"text","localized":true,"placeholder":"min","config":{"maxLength":12}},{"key":"seconds","label":"Seconds","type":"text","localized":true,"placeholder":"sec","config":{"maxLength":12}}]},"ui":{"icon":"language","tab":{"id":"timer"}}},{"key":"afterEnd","label":"When it reaches zero","type":"select","defaultValue":"message","config":{"options":[{"label":"Hide the block","value":"hide"},{"label":"Show a message","value":"message","description":"Swap the timer for new copy and a button"},{"label":"Keep it at zero","value":"keep"}]},"ui":{"control":"radio","tab":{"id":"timer"}}},{"key":"endedHeading","label":"Heading after launch","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"afterEnd","op":"eq","value":"message"}]},"localized":true,"config":{"maxLength":60},"ui":{"tab":{"id":"timer"}}},{"key":"endedButton","label":"Button after launch","type":"button","showIf":{"match":"all","rules":[{"field":"afterEnd","op":"eq","value":"message"}]},"ui":{"tab":{"id":"timer"}}},{"key":"counter","label":"Live counter","type":"object","openMode":"drill","config":{"fields":[{"key":"showLiveCounter","label":"Show a live counter","type":"boolean"},{"key":"label","label":"Label","type":"text","showIf":{"match":"all","rules":[{"field":"showLiveCounter","op":"eq","value":"true"}]},"localized":true,"config":{"maxLength":40}},{"key":"source","label":"Number comes from","type":"select","defaultValue":"rate","showIf":{"match":"all","rules":[{"field":"showLiveCounter","op":"eq","value":"true"}]},"options":[{"label":"Start + rate","value":"rate"},{"label":"Live API","value":"api"}],"ui":{"control":"segmented"}},{"key":"startValue","label":"Starts at","type":"number","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"rate"}]}},{"key":"startedAt","label":"Counted from","type":"datetime","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"rate"}]}},{"key":"perHour","label":"Adds per hour","type":"number","helpText":"Only a real average. Fake urgency breaks EU law.","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"rate"}]},"config":{"min":0,"step":1}},{"key":"endpoint","label":"Endpoint","type":"url","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"api"}]},"readOnly":"developer","lockReason":"Set by a developer. It must return { \"value\": number }.","config":{"pattern":"^https://","patternMessage":"Use an https:// address, like https://your-site.com."},"ui":{"access":{"view":"schema.view","edit":"schema.manage"}}},{"key":"refreshSeconds","label":"Refresh every","type":"number","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"api"}]},"config":{"min":10,"max":600,"step":5,"unit":"s"}},{"key":"format","label":"Format","type":"select","defaultValue":"full","showIf":{"match":"all","rules":[{"field":"showLiveCounter","op":"eq","value":"true"}]},"options":[{"label":"12,431","value":"full"},{"label":"12.4k","value":"compact"}],"ui":{"control":"segmented"}}]},"ui":{"icon":"activity","tab":{"id":"counter","label":"Live counter","icon":"activity"}}},{"key":"announce","label":"Screen reader updates","type":"select","defaultValue":"off","advanced":true,"config":{"options":[{"label":"Off","value":"off","description":"Recommended: a ticking timer announced every second is noise"},{"label":"Every minute","value":"minute"}]},"ui":{"tab":{"id":"timer"}}}],"description":"Counts down to a launch, then swaps to a message or button","category":"section","icon":"hourglass","preview":{"title":"heading","subtitle":"target"}}},
|
|
1483
|
+
{"id":"reader-poll","name":"Poll","category":"Data & interactive","kind":"block","tags":["form","conditional","repeater","validation","references"],"proves":"Question type switches options vs scale vs open answer · results visibility · close date · one vote per reader","why":"The answer type drives everything below it, so options, scale and the open-answer settings are mutually exclusive conditional branches of one `type` select. Rules (results visibility, close date, eligibility) are a separate tab because they change behavior, not copy. Votes are SUBMISSIONS — runtime data in Forms, never stored on the block — so the schema has no results fields at all.","htmlSignals":["A <form> with one question and radio/checkbox inputs, submitting via fetch, often with 'Vote' as the button","Results rendered as horizontal percentage bars after submit","'Poll closes …' text or a disabled state with 'Final results'","Third-party poll scripts (Crowdsignal, Typeform embed) — map those to the embed entry instead"],"mcp":"Question text → `question`; radio inputs → type single, checkboxes → multiple (+ maxPicks if a limit is stated), a range/star/NPS row → scale with min/max and end labels, textarea → open. Each option label (+ image) → `pollOption`. Never import vote counts. If the page has several questions in sequence, it's a SURVEY: create a form (Forms) with those fields and place a form block — do not nest questions in this poll block.","pitfalls":["SCHEMA GAP: cross-field rules (scale.min < scale.max, maxPicks ≤ number of options, closesAt in the future) can't be written as validation.","SCHEMA GAP: 'images on every option or none' is a whole-array rule; validation is per item.","SCHEMA GAP: no read-only, platform-computed fields — editors can't see live tallies inside the schema form; results live in another surface.","Multiple-question surveys modelled as nested arrays in a block become an unversioned form builder — use Forms."],"notCarried":["ai","category","variants"],"component":{"name":"Poll","slug":"poll","props":[{"key":"question","label":"Question","type":"text","required":true,"config":{"maxLength":140},"ui":{"tab":{"id":"content","label":"Question"}}},{"key":"context","label":"Context","type":"longtext","recommendedMax":200,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"type","label":"Answer type","type":"select","defaultValue":"single","config":{"options":[{"label":"Pick one","value":"single","icon":"circle-dot"},{"label":"Pick several","value":"multiple","icon":"checkbox"},{"label":"Scale","value":"scale","icon":"adjustments-horizontal","description":"1 to 5, 0 to 10"},{"label":"Written answer","value":"open","icon":"message"}]},"ui":{"control":"cards","tab":{"id":"content"}}},{"key":"options","label":"Options","type":"table","showIf":{"match":"all","rules":[{"field":"type","op":"in","value":["single","multiple"]}]},"config":{"minItems":2,"maxItems":8,"fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":60}},{"key":"image","label":"Image","type":"image"}]},"ui":{"itemLabel":"Option","itemName":"pollOption","icon":"circle-dot","preview":{"title":"label","media":"image","fallback":"Untitled option"},"tab":{"id":"content"}}},{"key":"maxPicks","label":"At most","type":"number","showIf":{"match":"all","rules":[{"field":"type","op":"eq","value":"multiple"}]},"config":{"min":2,"max":8,"step":1},"ui":{"tab":{"id":"content"}}},{"key":"allowOther","label":"\"Other\" with a text box","type":"boolean","showIf":{"match":"all","rules":[{"field":"type","op":"in","value":["single","multiple"]}]},"ui":{"tab":{"id":"content"}}},{"key":"scale","label":"Scale","type":"object","showIf":{"match":"all","rules":[{"field":"type","op":"eq","value":"scale"}]},"openMode":"drill","config":{"fields":[{"key":"min","label":"From","type":"number","defaultValue":1,"config":{"min":0,"max":1,"step":1}},{"key":"max","label":"To","type":"number","defaultValue":5,"config":{"min":3,"max":10,"step":1}},{"key":"minLabel","label":"Low label","type":"text","placeholder":"Not at all","config":{"maxLength":24}},{"key":"maxLabel","label":"High label","type":"text","placeholder":"Every day","config":{"maxLength":24}}]},"ui":{"icon":"adjustments-horizontal","tab":{"id":"content"}}},{"key":"placeholder","label":"Answer placeholder","type":"text","showIf":{"match":"all","rules":[{"field":"type","op":"eq","value":"open"}]},"config":{"maxLength":60},"ui":{"tab":{"id":"content"}}},{"key":"useModeration","label":"Review answers before showing them","type":"boolean","defaultValue":true,"showIf":{"match":"all","rules":[{"field":"type","op":"eq","value":"open"}]},"ui":{"tab":{"id":"content"}}},{"key":"showResults","label":"Show results","type":"select","defaultValue":"afterVote","config":{"options":[{"label":"After someone votes","value":"afterVote"},{"label":"When the poll closes","value":"afterClose"},{"label":"Never","value":"never","description":"Results stay in Forms › Submissions"}]},"ui":{"control":"radio","tab":{"id":"rules","label":"Rules"}}},{"key":"closesAt","label":"Closes","type":"datetime","helpText":"Leave empty to keep it open.","ui":{"tab":{"id":"rules"}}},{"key":"voters","label":"Who can vote","type":"select","defaultValue":"anyone","config":{"options":[{"label":"Anyone","value":"anyone"},{"label":"Signed in","value":"members"},{"label":"Subscribers","value":"subscribers"}]},"ui":{"control":"segmented","tab":{"id":"rules"}}},{"key":"useOneVote","label":"One vote per reader","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"rules"}}},{"key":"thankYou","label":"Thank-you line","type":"text","config":{"maxLength":100},"ui":{"tab":{"id":"rules"}}},{"key":"relatedStory","label":"Results story","type":"reference","helpText":"Linked under the results once it's published.","config":{"contentModelId":"col_journal"},"ui":{"tab":{"id":"rules"}}}],"description":"One question with live results","category":"section","icon":"chart-bar-popular","preview":{"title":"question","subtitle":"type","icon":"chart-bar-popular"}}},
|
|
1484
|
+
{"id":"safe-embed","name":"Embed (iframe, HTML, script)","category":"Data & interactive","kind":"block","tags":["embed","conditional","a11y","validation"],"proves":"Kind switches URL vs code · sandbox + permissions developer-locked · consent placeholder · required iframe title","why":"Embeds are the only block that runs someone else's code, so security choices (sandbox, permissions, raw HTML, script URLs) are a developer-locked tab and content choices (title, size, caption, fallback) stay editable. The kind select makes URL vs code branches exclusive. Consent is a drill-in object with its own conditionals: a block can stay compliant per embed instead of relying on a site-wide script blocker. The iframe title is required — without it the frame is unlabelled for screen readers.","htmlSignals":["<iframe> with src on a third-party domain (stackblitz, codepen, figma, calendly, typeform, airtable, google maps)","<script src> to a widget host next to a placeholder <div id=…> (Calendly, HubSpot forms, Intercom)","<blockquote class=\"twitter-tweet|instagram-media\"> + a platform script (oEmbed providers)","Consent wrappers: data-cookieconsent, data-category, class=\"cmplz-placeholder\", 'Load content' buttons"],"mcp":"<iframe> → kind iframe: copy src (force https), title (if missing, write one from the provider + context — required), width/height → size (aspect if responsive wrapper padding-top %, else fixed px), sandbox and allow attributes → sandbox/permissions verbatim. Known oEmbed URLs (YouTube, Vimeo, Figma, CodePen, Tweets) → kind oembed with the canonical URL; video specifically → prefer the video block. Script widgets → kind script with the src; the inline config goes into `html` only if it's static. Never migrate tracking pixels, analytics tags or chat widgets as embeds — they belong to site settings (scripts). If a consent wrapper exists, map its category and text to `consent`.","pitfalls":["SCHEMA GAP: no host allowlist validation — `pattern` checks https only; a real policy (allowed domains per site, CSP frame-src sync) has no schema expression.","SCHEMA GAP: `readOnly: \"developer\"` locks whole fields; there's no 'marketers can REMOVE a sandbox flag but not ADD one' (monotonic permissions).","SCHEMA GAP: the code field has no language setting in the schema (`html` vs `js`), so the editor guesses; see code-snippet-tabs.","oEmbed providers change markup; store the URL, never the provider's HTML snapshot.","allow-scripts + allow-same-origin on a same-origin URL removes the sandbox entirely — the help warns, nothing blocks it."],"notCarried":["category","variants"],"component":{"name":"Embed","slug":"embed","props":[{"key":"title","label":"Title","type":"text","helpText":"Not shown. Read by screen readers.","required":true,"config":{"maxLength":80},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"kind","label":"Type","type":"select","defaultValue":"iframe","config":{"options":[{"label":"Page in a frame","value":"iframe","icon":"app-window","description":"Paste the embed URL"},{"label":"Link preview","value":"oembed","icon":"link","description":"YouTube, Figma, CodePen…"},{"label":"HTML","value":"html","icon":"code","description":"Snippet from a provider"},{"label":"Script","value":"script","icon":"file-code","description":"A widget loaded from a URL"}]},"ui":{"control":"cards","tab":{"id":"content"}}},{"key":"url","label":"Embed URL","type":"url","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"in","value":["iframe","oembed"]}]},"config":{"pattern":"^https://","patternMessage":"Use an https:// address, like https://your-site.com/embed."},"ui":{"tab":{"id":"content"}}},{"key":"html","label":"HTML","type":"code","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"html"}]},"readOnly":"developer","lockReason":"Raw HTML runs on your site. A developer reviews it.","ui":{"access":{"edit":"schema.manage"},"tab":{"id":"content"}}},{"key":"scriptSrc","label":"Script URL","type":"url","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"script"}]},"readOnly":"developer","lockReason":"Scripts run with full access to the page. A developer reviews them.","config":{"pattern":"^https://","patternMessage":"Use an https:// script, like https://your-site.com/widget.js."},"ui":{"access":{"edit":"schema.manage"},"tab":{"id":"content"}}},{"key":"size","label":"Size","type":"object","config":{"fields":[{"key":"ratio","label":"Shape","type":"select","defaultValue":"16:9","options":[{"label":"16:9","value":"16:9"},{"label":"4:3","value":"4:3"},{"label":"1:1","value":"1:1"},{"label":"Fixed","value":"fixed"}],"ui":{"control":"segmented"}},{"key":"height","label":"Height","type":"number","showIf":{"match":"all","rules":[{"field":"ratio","op":"eq","value":"fixed"}]},"config":{"unit":"px","min":120,"max":1200,"step":10}}]},"ui":{"icon":"aspect-ratio","tab":{"id":"content"}}},{"key":"caption","label":"Caption","type":"text","config":{"maxLength":140},"ui":{"tab":{"id":"content"}}},{"key":"fallback","label":"Fallback text","type":"longtext","helpText":"Shown if the embed can't load or consent is declined.","ui":{"rows":2,"tab":{"id":"content"}}},{"key":"sandbox","label":"Allow inside the frame","type":"multiselect","defaultValue":["allow-scripts","allow-same-origin"],"showIf":{"match":"all","rules":[{"field":"kind","op":"in","value":["iframe","oembed"]}]},"readOnly":"developer","lockReason":"Loosening the sandbox is a security decision.","config":{"options":[{"label":"Scripts","value":"allow-scripts"},{"label":"Same origin (cookies, storage)","value":"allow-same-origin"},{"label":"Forms","value":"allow-forms"},{"label":"Pop-ups","value":"allow-popups"},{"label":"Pop-ups without the sandbox","value":"allow-popups-to-escape-sandbox"},{"label":"Downloads","value":"allow-downloads"},{"label":"Presentation","value":"allow-presentation"}]},"ui":{"access":{"view":"schema.view","edit":"schema.manage"},"tab":{"id":"security","label":"Security","icon":"shield-lock"}}},{"key":"permissions","label":"Browser features","type":"multiselect","showIf":{"match":"all","rules":[{"field":"kind","op":"in","value":["iframe","oembed"]}]},"config":{"options":[{"label":"Full screen","value":"fullscreen"},{"label":"Copy to clipboard","value":"clipboard-write"},{"label":"Autoplay","value":"autoplay"},{"label":"Camera","value":"camera"},{"label":"Microphone","value":"microphone"},{"label":"Payment","value":"payment"}]},"ui":{"access":{"view":"schema.view","edit":"schema.manage"},"tab":{"id":"security"}}},{"key":"useLazyLoad","label":"Load when scrolled near","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"security"}}},{"key":"consent","label":"Consent","type":"object","openMode":"drill","config":{"fields":[{"key":"isRequired","label":"Ask first","type":"boolean","defaultValue":true},{"key":"category","label":"Cookie category","type":"select","defaultValue":"functional","showIf":{"match":"all","rules":[{"field":"isRequired","op":"eq","value":"true"}]},"options":[{"label":"Functional","value":"functional"},{"label":"Analytics","value":"analytics"},{"label":"Marketing","value":"marketing"}]},{"key":"provider","label":"Provider name","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"isRequired","op":"eq","value":"true"}]},"placeholder":"StackBlitz","config":{"maxLength":40}},{"key":"message","label":"Placeholder text","type":"longtext","showIf":{"match":"all","rules":[{"field":"isRequired","op":"eq","value":"true"}]},"config":{"maxLength":200},"ui":{"rows":2}},{"key":"privacyLink","label":"Provider's privacy policy","type":"link","showIf":{"match":"all","rules":[{"field":"isRequired","op":"eq","value":"true"}]}},{"key":"previewImage","label":"Placeholder image","type":"image","showIf":{"match":"all","rules":[{"field":"isRequired","op":"eq","value":"true"}]}}]},"ui":{"icon":"cookie","tab":{"id":"privacy","label":"Privacy","icon":"cookie"}}}],"description":"Third-party content in a safe frame, with a consent placeholder","category":"section","icon":"frame","preview":{"title":"title","subtitle":"kind","icon":"code"}}},
|
|
1485
|
+
{"id":"code-snippet-tabs","name":"Code snippet with tabs","category":"Data & interactive","kind":"block","tags":["repeater","validation","conditional","embed"],"proves":"Tabs per language/package manager · line highlights by pattern · synced tab groups · optional runnable playground","why":"Each tab is one file in one language, so language and filename sit with the code; highlights are a validated range string because that's how authors think about lines. Package-manager tabs across a docs page should switch together — a sync group key, not a global setting. The playground is a conditional drill-in object: most snippets aren't runnable and shouldn't show its settings.","htmlSignals":["<pre><code class=\"language-ts\"> blocks (Prism, Shiki, highlight.js markup: span.token, data-line)","A tablist (role=\"tablist\") switching between <pre> panels labeled npm / pnpm / yarn / bun","Copy buttons (navigator.clipboard) and filename headers above the code","'Open in StackBlitz/CodeSandbox' buttons or forms posting to those hosts"],"mcp":"Each tab panel → `codeTab`: label from the tab button, language from the class (language-xxx / data-language), filename from the header, code from the TEXT content (strip highlighting spans, keep whitespace exactly, decode entities). Highlighted lines (data-line, .highlighted) → `highlight` as ranges. Tabs labeled with package managers → `syncGroup: package-manager`. A single <pre> with no tabs is still this block with one tab — unless it's inside a long-form article, where it belongs in the rich-text body as a code block.","pitfalls":["SCHEMA GAP: the `code` field type has no `language` (or filename / highlightedLines) option in the schema; the control lets the user pick HTML/CSS/JS/Liquid only. Languages like Shell, TS, Python, Go need a schema-level `code: { language, languages[] }` setting.","SCHEMA GAP: tab labels should be unique within a snippet; `unique` isn't enforced.","Highlight ranges aren't checked against the code's line count.","Never store highlighted HTML (token spans) — store plain text and highlight at render."],"notCarried":["category","variants"],"component":{"name":"Code snippet","slug":"code-snippet","props":[{"key":"heading","label":"Heading","type":"text","recommendedMax":60,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"intro","label":"Intro","type":"longtext","ui":{"rows":2,"tab":{"id":"content"}}},{"key":"tabs","label":"Tabs","type":"table","config":{"minItems":1,"maxItems":6,"fields":[{"key":"label","label":"Tab label","type":"text","required":true,"placeholder":"npm","config":{"maxLength":16}},{"key":"language","label":"Language","type":"select","required":true,"options":[{"label":"Shell","value":"bash"},{"label":"TypeScript","value":"ts"},{"label":"JavaScript","value":"js"},{"label":"Python","value":"python"},{"label":"Go","value":"go"},{"label":"JSON","value":"json"},{"label":"HTML","value":"html"},{"label":"CSS","value":"css"}]},{"key":"filename","label":"File name","type":"text","config":{"maxLength":48,"pattern":"^[\\w./-]+$","patternMessage":"Use letters, numbers, dots, slashes and hyphens, like src/index.ts."}},{"key":"code","label":"Code","type":"code","required":true},{"key":"highlight","label":"Highlight lines","type":"text","placeholder":"3, 7-9","config":{"pattern":"^\\d+(-\\d+)?(,\\s*\\d+(-\\d+)?)*$","patternMessage":"Use line numbers and ranges, like 1, 4-6."}}]},"ui":{"itemLabel":"Tab","itemName":"codeTab","icon":"file-code","preview":{"title":"label","subtitle":"filename","fallback":"Untitled tab"},"tab":{"id":"content"}}},{"key":"syncGroup","label":"Sync group","type":"text","helpText":"Snippets with the same group switch tabs together.","advanced":true,"config":{"pattern":"^[a-z0-9-]+$","patternMessage":"Use lowercase letters, numbers and hyphens, like camp-kitchen."},"ui":{"tab":{"id":"behavior","label":"Behavior"}}},{"key":"showLineNumbers","label":"Show line numbers","type":"boolean","ui":{"tab":{"id":"behavior"}}},{"key":"showCopyButton","label":"Show copy button","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"behavior"}}},{"key":"useLineWrap","label":"Wrap long lines","type":"boolean","ui":{"tab":{"id":"behavior"}}},{"key":"isRunnable","label":"Open in a playground","type":"boolean","ui":{"tab":{"id":"behavior"}}},{"key":"playground","label":"Playground","type":"object","showIf":{"match":"all","rules":[{"field":"isRunnable","op":"eq","value":"true"}]},"openMode":"drill","config":{"fields":[{"key":"provider","label":"Provider","type":"select","defaultValue":"stackblitz","options":[{"label":"StackBlitz","value":"stackblitz"},{"label":"CodeSandbox","value":"codesandbox"}],"ui":{"control":"segmented"}},{"key":"template","label":"Template","type":"select","defaultValue":"node","options":[{"label":"Node","value":"node"},{"label":"Vite + React","value":"vite-react"},{"label":"Next.js","value":"next"}]},{"key":"openFile","label":"Open file","type":"text","config":{"pattern":"^[\\w./-]+$","patternMessage":"Use a file path, like src/index.ts."}},{"key":"dependencies","label":"Dependencies","type":"array","helpText":"Package names to install, e.g. @northwind/sdk.","config":{"itemType":"text"},"ui":{"itemLabel":"Package","itemTitle":"Package"}}]},"ui":{"icon":"player-play","tab":{"id":"behavior"}}}],"description":"One snippet in several languages, with copy and run","category":"section","icon":"terminal-2","preview":{"title":"heading","subtitle":"intro","icon":"terminal-2"}}},
|
|
1486
|
+
{"id":"page-builder-six-deep","name":"Page builder, six levels deep","category":"Advanced patterns","kind":"block","tags":["deep-nesting","slots","repeater","variants","media-heavy"],"proves":"Guide › Tab › Columns › Column › Accordion › Item › Card › Button — slots inside arrays inside slots · breadcrumb folding + Pathway","why":"Real page builders alternate between ARRAYS (items the section owns: tabs, columns, accordion items) and SLOTS (any block from the library: columns, accordion, text, image). Each slot is a seam where the content stops belonging to this section and becomes a reusable block with its own schema and variants; each array is where content is specific to its parent. The card is an object (always one per item), the button is the one-unit button screen. Depth is legal here because every level is a different KIND of thing with its own preview row — drill-in keeps each frame small.","htmlSignals":["role=\"tablist\" with role=\"tabpanel\" siblings, each panel holding a CSS grid/flex row of columns","<details>/<summary> or buttons with aria-expanded inside a column","Expanded panels containing a card (image + heading + paragraph + link/button)","Repeated structural wrappers (.row > .col > .accordion > .item > .card) 5+ levels deep"],"mcp":"Measure depth by counting CONTENT boundaries, not DOM nodes: each element that repeats siblings of the same shape (tabs, columns, items) is an array level; each element whose inner structure differs from its siblings (a column holding an accordion next to a column holding an image) is a slot level. Wrappers that only add spacing/grid are design, not levels. Model arrays for same-shaped siblings and slots where siblings differ. Rules: (1) up to 6 content levels → one section, as here; (2) if any slot child is used on more than one page, extract it as a block (D17) and place it as a linked instance; (3) if depth would exceed 6, or a level has more than ~10 children, split the deepest repeated subtree into its own block (e.g. the accordion becomes a standalone FAQ block referenced from the column); (4) never make a column an array item AND a slot at the same time. Paths: emit _key on every array item and slot child so drill-in paths stay stable across reorders.","pitfalls":["SCHEMA GAP: a TypeSchema can't declare its own nested block types — slot `allow` names blocks from a global registry, so a converted section that introduces Columns/Accordion must also register them (handled here via the new optional KitEntry.blocks; production needs a schema-level `blocks` or a registry write).","SCHEMA GAP: no per-slot depth limit — nothing stops an author nesting Columns inside Columns inside an Accordion card forever. Needs `maxDepth` or 'cannot contain itself' on slot fields.","SCHEMA GAP: fieldsets and groups exist only at the TypeSchema root; array item objects (Tab, Column, Item) can't group their own fields.","Breadcrumb at 7 drills (Guide / Seasoning / Steps and kit / Steps / Season over fire… / Heat it upside down / Card / Button) must fold into the Pathway chip on a 380px inspector — this entry is the test case.","Accordion item cards inside a 1/3 column are cramped; that's a variant decision, not a field."],"notCarried":["ai","category","variants"],"component":{"name":"Guide","slug":"guide-section","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"ui":{"tab":{"id":"content"}}},{"key":"intro","label":"Intro","type":"longtext","recommendedMax":180,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"tabs","label":"Tabs","type":"table","config":{"minItems":2,"maxItems":6,"fields":[{"key":"label","label":"Tab label","type":"text","required":true,"config":{"maxLength":24}},{"key":"icon","label":"Icon","type":"icon"},{"key":"content","label":"Content","type":"slot","config":{"multiple":true,"minItems":1,"maxItems":6,"allow":["layoutColumns","accordionBlock","textBlock","imageBlock","videoBlock","ctaCard"]}}]},"ui":{"itemLabel":"Tab","itemName":"guideTab","icon":"layout-list","preview":{"title":"label","fallback":"Untitled tab"},"tab":{"id":"content"}}},{"key":"theme","label":"Theme","type":"select","defaultValue":"light","config":{"options":[{"label":"Light","value":"light"},{"label":"Warm","value":"warm"},{"label":"Dark","value":"dark"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design"}}},{"key":"anchor","label":"Anchor","type":"text","advanced":true,"config":{"pattern":"^[a-z][a-z0-9-]*$","patternMessage":"Use lowercase letters, numbers and hyphens, starting with a letter, like grate-xl."},"ui":{"tab":{"id":"design"}}}],"description":"Tabbed guide: each tab holds columns, accordions and cards","category":"content","icon":"book-2","preview":{"title":"heading","subtitle":"eyebrow"}}},
|
|
1487
|
+
{"id":"polymorphic-modules","name":"Any-block module list","category":"Advanced patterns","kind":"entry","tags":["polymorphic-array","media-heavy","references","rich-text","repeater"],"proves":"One array mixing 8 item types (_type per item) · type picker on add · references and media inside items","why":"A feature story is a sequence where ORDER is content and each item is a different shape. A polymorphic array (`of` with 8 object types, each item stamped `_type`) keeps every module typed and validated while letting the author interleave freely. These module types are local to the Feature model — they aren't reusable site blocks, so they're inline `of` types rather than a slot of library blocks. Rule of thumb: inline types when the shapes belong to one model; a slot when the blocks are shared across pages and need variants.","htmlSignals":["<article> whose direct children alternate between prose <div>s, <figure>s, <blockquote>s, galleries and asides","Repeated wrapper classes with a type suffix (.module--quote, .module--gallery, data-module=\"stat\")","Full-bleed figures breaking out of the text column (negative margins, width: 100vw)","Inline 'Read next' cards linking to another story mid-article"],"mcp":"Walk the article's top-level children in order. Classify each into one module type by its dominant element (figure+img → imageModule, multiple imgs → galleryModule, blockquote → quoteModule, iframe/video → videoModule, large numeral + label → statModule, aside/callout → calloutModule, internal story link card → relatedModule, otherwise textModule). Merge consecutive prose paragraphs into ONE textModule (never one module per <p>). Unknown shapes → textModule with the HTML preserved and a review flag. If more than 40 modules, or most modules are prose with occasional media, use a rich-text body with embedded blocks instead (see rich-text-embedded-components).","pitfalls":["SCHEMA GAP: no per-type count limits inside a polymorphic array (e.g. at most one relatedModule, galleries never adjacent) — `validation` applies to the whole array.","SCHEMA GAP: no way to share module types between models without copying FieldSchema objects; a named, reusable object type registry (Sanity's defineType for objects) is missing for non-block types.","Changing an item's type (quote → callout) isn't a schema operation; the editor must delete and re-add, losing the _key.","A textModule per paragraph is the most common bad conversion — it explodes the list and breaks reading flow."],"notCarried":["ai","category","kind:entry (props)"],"component":{"name":"Feature","slug":"feature-story","props":[{"key":"title","label":"Title","type":"text","required":true,"recommendedMax":80,"config":{"maxLength":100},"ui":{"tab":{"id":"story","label":"Story"}}},{"key":"dek","label":"Dek","type":"longtext","recommendedMax":180,"ui":{"rows":2,"tab":{"id":"story"}}},{"key":"coverImage","label":"Cover image","type":"image","required":true,"ui":{"tab":{"id":"story"}}},{"key":"authors","label":"Authors","type":"multi-reference","config":{"min":1,"max":3,"contentModelId":"col_authors"},"ui":{"itemName":"writer","itemTitle":"Writer","tab":{"id":"story"}}},{"key":"modules","label":"Modules","type":"table","config":{"mode":"types","minItems":1,"maxItems":40,"itemTypes":[{"name":"textModule","label":"Text","fields":[{"key":"heading","label":"Heading","type":"text","recommendedMax":70},{"key":"body","label":"Body","type":"richtext","required":true}],"icon":"typography","preview":{"title":"heading","subtitle":"body","fallback":"Untitled text"}},{"name":"imageModule","label":"Image","fields":[{"key":"image","label":"Image","type":"image","required":true},{"key":"caption","label":"Caption","type":"text","config":{"maxLength":160}},{"key":"credit","label":"Credit","type":"text","config":{"maxLength":60}},{"key":"width","label":"Width","type":"select","defaultValue":"column","options":[{"label":"Text column","value":"column"},{"label":"Wide","value":"wide"},{"label":"Full bleed","value":"full"}],"ui":{"control":"segmented"}}],"icon":"photo","preview":{"title":"caption","media":"image","fallback":"Untitled image"}},{"name":"quoteModule","label":"Pull quote","fields":[{"key":"quote","label":"Quote","type":"longtext","required":true,"recommendedMax":160,"ui":{"rows":3}},{"key":"attribution","label":"Who said it","type":"text","config":{"maxLength":60}},{"key":"role","label":"Role","type":"text","config":{"maxLength":60}}],"icon":"quote","preview":{"title":"quote","subtitle":"attribution","fallback":"Untitled quote"}},{"name":"statModule","label":"Big number","fields":[{"key":"value","label":"Number","type":"text","required":true,"config":{"maxLength":12}},{"key":"label","label":"What it counts","type":"text","required":true,"config":{"maxLength":80}},{"key":"source","label":"Source","type":"link"}],"icon":"chart-bar","preview":{"title":"value","subtitle":"label","fallback":"Untitled number"}},{"name":"videoModule","label":"Video","fields":[{"key":"title","label":"Title","type":"text","required":true,"config":{"maxLength":80}},{"key":"video","label":"Video","type":"video","required":true},{"key":"poster","label":"Poster","type":"image"},{"key":"transcript","label":"Transcript","type":"richtext","helpText":"Required for interviews."}],"icon":"player-play","preview":{"title":"title","media":"poster","fallback":"Untitled video"}},{"name":"galleryModule","label":"Gallery","fields":[{"key":"images","label":"Images","type":"array","config":{"minItems":2,"maxItems":12,"itemType":"image"},"ui":{"layout":"gallery","itemName":"photo","itemTitle":"Photo"}},{"key":"layout","label":"Layout","type":"select","defaultValue":"grid","options":[{"label":"Grid","value":"grid"},{"label":"Carousel","value":"carousel"}],"ui":{"control":"segmented"}},{"key":"caption","label":"Caption","type":"text","config":{"maxLength":160}}],"icon":"layout-grid","preview":{"title":"caption","fallback":"Untitled gallery"}},{"name":"calloutModule","label":"Callout","fields":[{"key":"tone","label":"Tone","type":"select","defaultValue":"note","options":[{"label":"Note","value":"note"},{"label":"Correction","value":"correction"},{"label":"Context","value":"context"}],"ui":{"control":"segmented"}},{"key":"title","label":"Title","type":"text","config":{"maxLength":60}},{"key":"body","label":"Body","type":"longtext","required":true,"config":{"maxLength":400},"ui":{"rows":3}}],"icon":"info-square-rounded","preview":{"title":"title","subtitle":"tone","fallback":"Untitled callout"}},{"name":"relatedModule","label":"Related story","fields":[{"key":"story","label":"Story","type":"reference","required":true,"config":{"contentModelId":"col_journal"}},{"key":"label","label":"Label","type":"text","placeholder":"Read next","config":{"maxLength":24}}],"icon":"news","preview":{"title":"label","fallback":"Untitled related story"}}]},"ui":{"itemLabel":"Module","tab":{"id":"modules","label":"Modules","icon":"stack-2"}}}],"description":"A long read assembled from modules","category":"content","icon":"news","preview":{"title":"title","subtitle":"dek","media":"coverImage"}}},
|
|
1488
|
+
{"id":"conditional-source-matrix","name":"Conditional source matrix","category":"Advanced patterns","kind":"block","tags":["conditional","references","dynamic-query","commerce","validation"],"proves":"Manual / collection / query / external branches · nested conditionals two levels down · empty-state fallback","why":"Four sources are four different data contracts, so they're exclusive branches of one select — and the bigger branches are drill-in OBJECTS so each can have its own conditionals (query.price → min/max, query.sort → metafield, external.provider → intent/seed vs endpoint/mapping). Nesting the conditionals inside objects is how a sibling-only condition system expresses a second level. Inactive branches keep their values (the query is still there if you switch back), and the renderer reads only the active branch.","htmlSignals":["Product cards rendered client-side from a fetch (Shopify recommendations API /recommendations/products.json, Algolia, Klaviyo)","data-collection / data-handle / data-tags / data-limit attributes on the section wrapper","'You might also like' / 'Goes well with' headings with a product carousel","Server-rendered lists with sort/filter query params in the 'View all' link"],"mcp":"Decide the source from how the products got there, not how they look: hard-coded product links → manual (resolve each to a Shopify reference); a collection URL or data-handle → collection; filter params (tag, price, sort) → query; a call to a recommendations endpoint → external. Fill only the active branch; leave others empty. Map the 'View all' link to cta. When the HTML is a snapshot of a dynamic list, NEVER freeze it into a manual list — that silently stops it updating; flag it if you can't tell.","pitfalls":["SCHEMA GAP: conditions test one sibling for equality — no AND/OR, no comparison (>), no reading a parent or the whole entry (e.g. 'show seed only when this block is on a product page'). Nested objects are a workaround, not a model.","SCHEMA GAP: values in hidden branches are kept and published; there's no 'clear when hidden' option, so stale manual product ids ship in the payload of an external-source block.","SCHEMA GAP: required-if — `maxPrice` is required only when visible; that works because hidden fields skip validation, but 'required when price = between AND minPrice is set' can't be written.","minPrice < maxPrice can't be validated (cross-field)."],"notCarried":["ai","category","shopify reference","variants"],"component":{"name":"Product row","slug":"product-feed","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":50,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"viewAllButton","label":"View all button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"source","label":"Products come from","type":"select","defaultValue":"manual","config":{"options":[{"label":"Picked by hand","value":"manual","icon":"hand-finger"},{"label":"A collection","value":"collection","icon":"folder"},{"label":"Rules","value":"query","icon":"filter","description":"Tags, price, stock"},{"label":"Recommendations","value":"external","icon":"sparkles","description":"Shopify or a custom API"}]},"ui":{"control":"cards","tab":{"id":"source","label":"Products","icon":"filter"}}},{"key":"products","label":"Products","type":"array","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"manual"}]},"unique":true,"readOnly":true,"lockReason":"A Shopify product id. No Shopify integration backs a reference in BetterCMS yet, so the id is kept as it is.","config":{"minItems":1,"maxItems":12,"itemType":"text"},"ui":{"tab":{"id":"source"}}},{"key":"collectionHandle","label":"Shopify collection handle","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"collection"}]},"config":{"pattern":"^[a-z0-9-]+$","patternMessage":"Use the handle from the collection's URL, like camp-kitchen."},"ui":{"tab":{"id":"source"}}},{"key":"query","label":"Rules","type":"object","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"query"}]},"openMode":"drill","config":{"fields":[{"key":"tags","label":"Has any of these tags","type":"tags"},{"key":"price","label":"Price","type":"select","defaultValue":"any","options":[{"label":"Any","value":"any"},{"label":"Under","value":"under"},{"label":"Between","value":"between"}],"ui":{"control":"segmented"}},{"key":"minPrice","label":"From","type":"number","showIf":{"match":"all","rules":[{"field":"price","op":"eq","value":"between"}]},"config":{"unit":"USD","min":0}},{"key":"maxPrice","label":"Up to","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"price","op":"in","value":["under","between"]}]},"config":{"unit":"USD","min":0}},{"key":"showInStockOnly","label":"In stock only","type":"boolean","defaultValue":true},{"key":"sort","label":"Sort by","type":"select","defaultValue":"bestselling","options":[{"label":"Bestselling","value":"bestselling"},{"label":"Newest","value":"newest"},{"label":"Price, low to high","value":"priceAsc"},{"label":"A metafield","value":"metafield","description":"e.g. custom.rank"}]},{"key":"metafield","label":"Metafield","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"sort","op":"eq","value":"metafield"}]},"config":{"pattern":"^[a-z_]+\\.[a-z_]+$","patternMessage":"Use namespace.key, like custom.rank."}},{"key":"limit","label":"How many","type":"number","defaultValue":8,"config":{"min":1,"max":24,"step":1}}]},"ui":{"icon":"filter","tab":{"id":"source"}}},{"key":"external","label":"Recommendations","type":"object","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"external"}]},"openMode":"drill","config":{"fields":[{"key":"provider","label":"Provider","type":"select","defaultValue":"shopify","options":[{"label":"Shopify","value":"shopify"},{"label":"Custom API","value":"custom"}],"ui":{"control":"segmented"}},{"key":"intent","label":"Recommend","type":"select","defaultValue":"related","showIf":{"match":"all","rules":[{"field":"provider","op":"eq","value":"shopify"}]},"options":[{"label":"Similar products","value":"related"},{"label":"Goes well with","value":"complementary"}]},{"key":"seed","label":"Based on","type":"text","helpText":"Leave empty on product pages to use the page's product.","showIf":{"match":"all","rules":[{"field":"provider","op":"eq","value":"shopify"}]},"readOnly":true,"lockReason":"A Shopify product id. No Shopify integration backs a reference in BetterCMS yet, so the id is kept as it is."},{"key":"endpoint","label":"Endpoint","type":"url","required":true,"showIf":{"match":"all","rules":[{"field":"provider","op":"eq","value":"custom"}]},"config":{"pattern":"^https://","patternMessage":"Use an https:// address, like https://your-site.com."},"ui":{"access":{"view":"schema.view","edit":"schema.manage"}}},{"key":"mapping","label":"Response mapping","type":"object","helpText":"Where each product field lives in the API's JSON.","showIf":{"match":"all","rules":[{"field":"provider","op":"eq","value":"custom"}]},"openMode":"drill","fields":[{"key":"list","label":"List path","type":"text","placeholder":"data.items"},{"key":"id","label":"Shopify id path","type":"text","placeholder":"product_gid"},{"key":"reason","label":"Reason path","type":"text","placeholder":"explanation"}],"ui":{"icon":"arrows-exchange","access":{"view":"schema.view","edit":"schema.manage"}}},{"key":"limit","label":"How many","type":"number","defaultValue":4,"config":{"min":1,"max":12,"step":1}}]},"ui":{"icon":"sparkles","tab":{"id":"source"}}},{"key":"whenEmpty","label":"If nothing matches","type":"select","defaultValue":"bestsellers","config":{"options":[{"label":"Hide the section","value":"hide"},{"label":"Show bestsellers instead","value":"bestsellers"},{"label":"Show a message","value":"message"}]},"ui":{"control":"radio","tab":{"id":"source"}}},{"key":"emptyMessage","label":"Message","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"whenEmpty","op":"eq","value":"message"}]},"config":{"maxLength":90},"ui":{"tab":{"id":"source"}}}],"description":"Products picked by hand, from a collection, by rules or from a recommendations API","category":"conversion","icon":"shopping-cart","preview":{"title":"heading","subtitle":"source"}}},
|
|
1489
|
+
{"id":"global-announcement-overrides","name":"Global block with per-page overrides","category":"Advanced patterns","kind":"global","tags":["global-synced","conditional","validation","variants"],"proves":"Synced fields from a global · locked vs overridable fields · instance-only settings · override map in the value","why":"A global block has three kinds of field and the schema shows them as three groups: synced fields a page MAY override (message, link, tone — marked `synced`), fields owned by the global that pages can't touch (offer terms, schedule — developer-locked with a reason), and instance-only settings that never exist on the global (hide on this page). The value carries `_ref` (the global's id) and `_overrides` (only the fields this page changed), matching D28's instance = reference + overrides, so editing the global still reaches every page except the overridden field. `published` here is the global's own value, which is what the overridden fields differ from.","htmlSignals":["A thin full-width bar above the header, identical on every page of the crawl except a few","Dismiss (×) button with a cookie/localStorage key (announcement-dismissed)","Small-print text ('Ends Oct 12', 'US only') next to the message","Pages where the bar's text differs from the site-wide version (candidate overrides)"],"mcp":"Crawl first, then decide: an element repeated with identical content on ≥80% of pages is a GLOBAL — create it once (kind: global) from the most common version. Pages whose copy differs get an instance with `_overrides` containing ONLY the differing fields; never create a second global for a variant. Offer terms and dates that never differ → global-owned (locked). If a page lacks the bar, set visibility: hide on that page rather than omitting the instance silently. If more than half the fields differ on more than a third of pages, it isn't one global — model a regular block instead.","pitfalls":["SCHEMA GAP: no per-field `overridable` flag. `synced` marks inheritance and `readOnly: \"developer\"` locks, but 'locked in instances, editable in the global' is a different permission from 'developer only' — the language conflates them.","SCHEMA GAP: the override map (`_overrides`) and instance reference (`_ref`) are value conventions, not schema — nothing validates that an override key is an overridable field.","SCHEMA GAP: instance-only fields (visibility, hideReason) live in the same schema as the global's fields; there's no way to say 'this field exists only on instances'.","Overriding `tone` on one page makes the bar look broken next to the header; consider locking design fields too."],"notCarried":["ai","category","kind:global","variants"],"component":{"name":"Announcement bar","slug":"announcement-bar","props":[{"key":"message","label":"Message","type":"text","required":true,"recommendedMax":70,"localized":true,"config":{"maxLength":90},"ui":{"synced":{"from":"Announcement bar"},"tab":{"id":"content","label":"Content"}}},{"key":"link","label":"Link","type":"link","ui":{"synced":{"from":"Announcement bar"},"tab":{"id":"content"}}},{"key":"tone","label":"Tone","type":"select","defaultValue":"brand","config":{"options":[{"label":"Brand","value":"brand"},{"label":"Dark","value":"dark"},{"label":"Warning","value":"warning"}]},"ui":{"control":"segmented","synced":{"from":"Announcement bar"},"tab":{"id":"content"}}},{"key":"legal","label":"Offer terms","type":"text","readOnly":"developer","lockReason":"Set in the global block. Pages can't override offer terms.","config":{"maxLength":120},"ui":{"access":{"edit":"schema.manage"},"tab":{"id":"global","label":"Set in the global","icon":"world"}}},{"key":"startsAt","label":"Show from","type":"datetime","readOnly":"developer","lockReason":"The schedule is the same on every page.","ui":{"access":{"edit":"schema.manage"},"tab":{"id":"global"}}},{"key":"endsAt","label":"Hide after","type":"datetime","readOnly":"developer","lockReason":"The schedule is the same on every page.","ui":{"access":{"edit":"schema.manage"},"tab":{"id":"global"}}},{"key":"isDismissible","label":"Visitors can close it","type":"boolean","helpText":"Remembers the close for 7 days.","readOnly":"developer","lockReason":"Set in the global block.","ui":{"access":{"edit":"schema.manage"},"tab":{"id":"global"}}},{"key":"visibility","label":"On this page","type":"select","helpText":"This page only. The global keeps showing elsewhere.","defaultValue":"show","config":{"options":[{"label":"Show","value":"show"},{"label":"Hide on this page","value":"hide"}]},"ui":{"control":"radio","tab":{"id":"page","label":"This page","icon":"file"}}},{"key":"hideReason","label":"Why it's hidden","type":"text","helpText":"Only editors see this.","required":true,"showIf":{"match":"all","rules":[{"field":"visibility","op":"eq","value":"hide"}]},"config":{"maxLength":80},"ui":{"tab":{"id":"page"}}}],"description":"One message across the site. Pages may override the wording, never the legal line or schedule","category":"navbar","icon":"speakerphone","preview":{"title":"message","subtitle":"tone"}}},
|
|
1490
|
+
{"id":"localized-section","name":"Localized section with RTL","category":"Advanced patterns","kind":"block","tags":["localized","repeater","conditional","a11y","validation"],"proves":"Localized fields · per-locale and per-field translation status · fallback chain · Arabic right-to-left sample","why":"Two localization models meet here. Field-level (`localized: true` on heading/body/cta) is how the editor shows a locale badge and switches values per locale — it's the right primitive. But the language has nowhere to store status, direction or fallback per locale, so the closest honest model is an internationalized array (one item per locale, Sanity's @sanity/internationalized-array pattern) carrying the translated copy plus status, direction and a per-field status list. Shared things (the image file) stay single; its alt text is copy, so it's translated.","htmlSignals":["<html lang> / hreflang alternates across crawled URLs (/pt/, /ar/) with the same section structure","dir=\"rtl\" on <html> or on the section; mirrored layouts (padding-inline-start)","A language switcher in the header","Identical structure with different text across locale paths"],"mcp":"Crawl every hreflang alternate. Match the same section across locales by position + structure, never by text. The default locale's copy fills the localized fields; every other locale becomes a `translation` item with its copy, `dir` from the dir attribute (rtl for ar/he/fa/ur), status 'approved' for human-published pages (or 'machine' if the site marks machine translation). Missing locales get a `missing` item so the gap is visible. Never translate during conversion. Keep the image shared unless a locale uses a different photo — then the image becomes localized too and you note it.","pitfalls":["SCHEMA GAP: `localized: true` is a flag only — no value shape for per-locale values, no translation status, no per-locale direction, and no fallback chain in the schema. The internationalized array here duplicates the localized fields by hand.","SCHEMA GAP: the locale list is a hard-coded select; it should come from site settings (dynamic options).","SCHEMA GAP: `unique: true` on `locale` is declared but not enforced — two pt-PT items can exist.","SCHEMA GAP: no `dir`/`lang` awareness in text fields — the editor won't render the Arabic copy right-to-left while typing.","Recommended lengths are per English; German runs ~30% longer and Arabic ~25% shorter, so a single recommendedMax misfires per locale."],"notCarried":["ai","category","variants"],"component":{"name":"Studio intro","slug":"studio-intro","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"localized":true,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"body","label":"Body","type":"longtext","recommendedMax":280,"localized":true,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","localized":true,"ui":{"tab":{"id":"content"}}},{"key":"image","label":"Image","type":"image","helpText":"Shared by every language. Alt text is translated with the copy.","ui":{"tab":{"id":"content"}}},{"key":"baseLocale","label":"Written in","type":"select","defaultValue":"en","readOnly":"developer","lockReason":"The site's default locale.","config":{"options":[{"label":"English","value":"en"}]},"ui":{"access":{"edit":"schema.manage"},"tab":{"id":"translations","label":"Translations","icon":"language"}}},{"key":"fallback","label":"When a translation is missing","type":"select","defaultValue":"chain","config":{"options":[{"label":"Show English","value":"base"},{"label":"Try a related locale first","value":"chain","description":"e.g. pt-BR before English for pt-PT"},{"label":"Hide the section","value":"hide"}]},"ui":{"control":"radio","tab":{"id":"translations"}}},{"key":"translations","label":"Locales","type":"table","config":{"maxItems":12,"fields":[{"key":"locale","label":"Locale","type":"select","required":true,"unique":true,"options":[{"label":"Portuguese (Portugal)","value":"pt-PT"},{"label":"Portuguese (Brazil)","value":"pt-BR"},{"label":"Arabic (UAE)","value":"ar-AE"},{"label":"French","value":"fr-FR"},{"label":"German","value":"de-DE"}]},{"key":"status","label":"Status","type":"select","defaultValue":"missing","options":[{"label":"Missing","value":"missing"},{"label":"Machine","value":"machine"},{"label":"In review","value":"review"},{"label":"Approved","value":"approved"}],"ui":{"control":"segmented"}},{"key":"dir","label":"Direction","type":"select","helpText":"Auto reads it from the language.","defaultValue":"auto","options":[{"label":"Auto","value":"auto"},{"label":"Left to right","value":"ltr"},{"label":"Right to left","value":"rtl"}],"ui":{"control":"segmented"}},{"key":"fallbackTo","label":"Fall back to","type":"text","helpText":"Used when the section's fallback is \"related locale\".","config":{"pattern":"^[a-z]{2}(-[A-Z]{2})?$","patternMessage":"Use a locale code, like pt-BR or en."}},{"key":"heading","label":"Heading","type":"text","recommendedMax":60},{"key":"body","label":"Body","type":"longtext","recommendedMax":280,"ui":{"rows":3}},{"key":"buttonLabel","label":"Button label","type":"text","config":{"maxLength":32}},{"key":"imageAlt","label":"Image alt text","type":"text","config":{"maxLength":140}},{"key":"fields","label":"Field status","type":"table","openMode":"inline","fields":[{"key":"field","label":"Field","type":"select","options":[{"label":"Heading","value":"heading"},{"label":"Body","value":"body"},{"label":"Button label","value":"ctaLabel"},{"label":"Alt text","value":"imageAlt"}]},{"key":"status","label":"Status","type":"select","options":[{"label":"Missing","value":"missing"},{"label":"Machine","value":"machine"},{"label":"In review","value":"review"},{"label":"Approved","value":"approved"}]},{"key":"by","label":"By","type":"text","config":{"maxLength":40}}],"ui":{"itemLabel":"Field","itemName":"fieldStatus","icon":"check","preview":{"title":"field","subtitle":"status","fallback":"Untitled field"}}}]},"ui":{"itemLabel":"Locale","icon":"language","preview":{"title":"locale","subtitle":"status","fallback":"Untitled locale"},"tab":{"id":"translations"}}}],"description":"Statement, body and a button, translated per locale","category":"content","icon":"language","preview":{"title":"heading","subtitle":"body","media":"image"}}},
|
|
1491
|
+
{"id":"personalized-hero","name":"Personalized hero with A/B test","category":"Advanced patterns","kind":"block","tags":["personalization","deep-nesting","conditional","repeater","validation"],"proves":"Default fallback · audience segments with rules · priority · experiment variants with split weights · 5 levels deep","why":"Personalization is three layers with a strict precedence: audiences (first match wins), then the experiment (for everyone unmatched), then the default — which is required and complete so the page is never blank. Audience and variant content are partial objects that inherit empty fields from the default, so editors only write what changes. The deepest path (Hero › A/B test › Version › Content › Button) is five frames.","htmlSignals":["Multiple hidden copies of the hero (display:none, data-variant, data-audience attributes) or a flicker-guard style (async-hide, .optimize-loading)","Experimentation scripts: Optimizely, VWO, Google Optimize remnants, Statsig, LaunchDarkly, Mutiny, Intellimize","Server responses differing by query string (utm_campaign) or Vary headers","Clearbit/6sense reveal calls before the hero renders"],"mcp":"A static crawl only sees ONE variant — never invent audiences. Use what the page reveals: variant markup in the DOM, experiment configs inlined by the testing tool, or utm-specific versions you were given URLs for. The visible version becomes `default` (complete). Each discovered audience → `segment` with rules translated from the tool's targeting (unknown targeting → a rule with attribute 'utmCampaign' and a review flag). Experiment configs → `experiment.variants` with their weights; the original is the control. If the testing tool rewrites more than this hero (multi-section tests), don't model it per block — flag it for a page-level experiment.","pitfalls":["SCHEMA GAP: no cross-item validation — version weights must sum to 100 and exactly one version should be the control; neither can be expressed.","SCHEMA GAP: no audience/segment collection to reference; rules are re-typed in every block that personalizes (a `reference: { to: ['audience'] }` target is needed).","SCHEMA GAP: partial content objects need 'inherits from default' semantics in the schema (show the default as a placeholder, mark overridden fields); today it's a description.","SCHEMA GAP: a variant's hidden `content` is still required-validated by its own rules if `isControl` flips — conditions hide but can't relax `required` declaratively.","Personalized copy must not change the H1 for SEO purposes; the native page title stays the default's."],"notCarried":["ai","category","variants"],"component":{"name":"Personalized hero","slug":"personalized-hero","props":[{"key":"default","label":"Default","type":"object","helpText":"Shown when no audience matches.","openMode":"drill","config":{"fields":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60},{"key":"body","label":"Body","type":"longtext","recommendedMax":160,"ui":{"rows":2}},{"key":"primaryButton","label":"Primary button","type":"button","required":true},{"key":"image","label":"Image","type":"image","required":true}]},"ui":{"icon":"user","tab":{"id":"content","label":"Everyone"}}},{"key":"segments","label":"Audiences","type":"table","helpText":"Checked top to bottom. The first match wins.","config":{"maxItems":6,"fields":[{"key":"name","label":"Name","type":"text","required":true,"config":{"maxLength":40}},{"key":"match","label":"Match","type":"select","defaultValue":"all","options":[{"label":"All rules","value":"all"},{"label":"Any rule","value":"any"}],"ui":{"control":"segmented"}},{"key":"rules","label":"Rules","type":"table","fields":[{"key":"attribute","label":"When","type":"select","required":true,"options":[{"label":"Industry (from IP)","value":"industry"},{"label":"Company size (from IP)","value":"companySize"},{"label":"Country","value":"country"},{"label":"UTM campaign","value":"utmCampaign"},{"label":"Returning visitor","value":"returning"},{"label":"CRM lifecycle stage","value":"lifecycle"}]},{"key":"operator","label":"Condition","type":"select","defaultValue":"is","options":[{"label":"Is","value":"is"},{"label":"Is not","value":"isNot"},{"label":"Contains","value":"contains"}],"ui":{"control":"segmented"}},{"key":"value","label":"Value","type":"text","required":true,"config":{"maxLength":60}}],"config":{"minItems":1,"maxItems":5},"ui":{"itemLabel":"Rule","icon":"filter","preview":{"title":"attribute","subtitle":"value","fallback":"Untitled rule"}}},{"key":"content","label":"What they see","type":"object","helpText":"Empty fields fall back to the default.","openMode":"drill","fields":[{"key":"heading","label":"Heading","type":"text","recommendedMax":60},{"key":"body","label":"Body","type":"longtext","recommendedMax":160,"ui":{"rows":2}},{"key":"primaryButton","label":"Primary button","type":"button"},{"key":"image","label":"Image","type":"image"}],"ui":{"icon":"layout"}}]},"ui":{"itemLabel":"Audience","icon":"users","preview":{"title":"name","subtitle":"match","fallback":"Untitled audience"},"tab":{"id":"audiences","label":"Audiences","icon":"users"}}},{"key":"experiment","label":"A/B test","type":"object","helpText":"Splits visitors who match no audience between versions.","openMode":"drill","config":{"fields":[{"key":"isRunning","label":"Run a test","type":"boolean"},{"key":"name","label":"Test name","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"isRunning","op":"eq","value":"true"}]},"config":{"maxLength":60}},{"key":"goal","label":"Winning means more","type":"select","defaultValue":"demo","showIf":{"match":"all","rules":[{"field":"isRunning","op":"eq","value":"true"}]},"options":[{"label":"Demos booked","value":"demo"},{"label":"Trials started","value":"signup"},{"label":"Button clicks","value":"click"}]},{"key":"variants","label":"Versions","type":"table","helpText":"Weights must add up to 100%.","showIf":{"match":"all","rules":[{"field":"isRunning","op":"eq","value":"true"}]},"fields":[{"key":"name","label":"Name","type":"text","required":true,"placeholder":"B: outcome headline","config":{"maxLength":24}},{"key":"weight","label":"Traffic","type":"number","required":true,"config":{"min":0,"max":100,"step":5,"unit":"%"},"ui":{"control":"slider"}},{"key":"isControl","label":"This is the control","type":"boolean","helpText":"Uses the default content unchanged.","defaultValue":false},{"key":"content","label":"Content","type":"object","showIf":{"match":"all","rules":[{"field":"isControl","op":"eq","value":"false"}]},"openMode":"drill","fields":[{"key":"heading","label":"Heading","type":"text","recommendedMax":60},{"key":"body","label":"Body","type":"longtext","ui":{"rows":2}},{"key":"primaryButton","label":"Primary button","type":"button"}],"ui":{"icon":"layout"}}],"config":{"minItems":2,"maxItems":4},"ui":{"itemLabel":"Version","itemName":"testVariant","icon":"flask","preview":{"title":"name","subtitle":"weight","fallback":"Untitled version"}}},{"key":"endsAt","label":"Stop on","type":"date","showIf":{"match":"all","rules":[{"field":"isRunning","op":"eq","value":"true"}]}}]},"ui":{"icon":"flask","tab":{"id":"experiment","label":"A/B test","icon":"flask"}}}],"description":"One hero, different words for different visitors","category":"hero","icon":"users-group","preview":{"title":"default.heading","subtitle":"experiment.name"}}},
|
|
1492
|
+
{"id":"responsive-layout-settings","name":"Responsive layout settings","category":"Advanced patterns","kind":"block","tags":["responsive-layout","repeater","conditional","variants"],"proves":"Columns, gap and spacing per breakpoint · inherit from the larger screen · per-item visibility and mobile order","why":"Layout is design, so it lives in its own Layout tab, split by breakpoint because the decisions differ per screen (4 columns → 2 → swipe). Tablet inherits desktop until switched off — editors only touch what changes. Per-item visibility and mobile order sit on the item because they're about THAT card. Arrangement on phones is a choice of patterns (cards select), not a column count, because 'swipe' isn't a number of columns.","htmlSignals":["Media-query grid changes: grid-template-columns: repeat(4, …) at ≥1280px, repeat(2, …) at ≥768px, one column below","Responsive utility classes: md:grid-cols-2 lg:grid-cols-4, hidden md:block, order-first sm:order-none","Scroll-snap carousels that only apply under a breakpoint","Different padding utilities per breakpoint (py-12 lg:py-24)"],"mcp":"Read the computed layout at three widths (1440, 834, 390) rather than parsing CSS. Desktop values → `desktop`; if tablet matches desktop, set `tablet.useDesktopLayout: true` and leave the rest empty; otherwise fill only what differs. Phone: one column → stack, horizontal overflow with snap → carousel (+ showPeek if the next card is partially visible), two columns → grid. Map spacing to tokens (S/M/L/XL) by nearest value — never store raw pixels. Items hidden at a width → remove that breakpoint from `showOn`; reordered items → `mobileOrder`.","pitfalls":["SCHEMA GAP: no responsive value type (`responsive<T>`: one field with a value per breakpoint and inheritance). Modelling it as three objects triplicates fields and the inherit toggle is a hand-made convention.","SCHEMA GAP: breakpoints are hard-coded in descriptions (768/1280); they should come from the site's design tokens.","SCHEMA GAP: `mobileOrder` should be unique among items and ≤ item count — cross-item validation isn't available.","Hiding content on phones hides it from mobile-first indexing too; prefer reordering over hiding."],"notCarried":["category","variants"],"component":{"name":"Services grid","slug":"services-grid","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":50,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"intro","label":"Intro","type":"longtext","ui":{"rows":2,"tab":{"id":"content"}}},{"key":"items","label":"Services","type":"table","config":{"minItems":2,"maxItems":9,"fields":[{"key":"title","label":"Title","type":"text","required":true,"config":{"maxLength":40}},{"key":"body","label":"Body","type":"longtext","recommendedMax":140,"ui":{"rows":2}},{"key":"image","label":"Image","type":"image"},{"key":"link","label":"Link","type":"link"},{"key":"showOn","label":"Show on","type":"multiselect","defaultValue":["desktop","tablet","mobile"],"options":[{"label":"Desktop","value":"desktop"},{"label":"Tablet","value":"tablet"},{"label":"Phone","value":"mobile"}]},{"key":"mobileOrder","label":"Order on phones","type":"number","helpText":"Leave empty to keep the desktop order.","config":{"min":1,"max":9,"step":1}}]},"ui":{"itemLabel":"Service","itemName":"service","icon":"briefcase","preview":{"title":"title","subtitle":"body","media":"image","fallback":"Untitled service"},"tab":{"id":"content"}}},{"key":"desktop","label":"Desktop","type":"object","helpText":"1280px and wider.","openMode":"drill","config":{"fields":[{"key":"columns","label":"Columns","type":"number","defaultValue":3,"config":{"min":1,"max":4,"step":1},"ui":{"control":"slider"}},{"key":"gap","label":"Gap","type":"select","defaultValue":"lg","options":[{"label":"S","value":"sm"},{"label":"M","value":"md"},{"label":"L","value":"lg"}],"ui":{"control":"segmented"}},{"key":"paddingY","label":"Space above and below","type":"select","defaultValue":"xl","options":[{"label":"M","value":"md"},{"label":"L","value":"lg"},{"label":"XL","value":"xl"}],"ui":{"control":"segmented"}},{"key":"alignment","label":"Alignment","type":"select","defaultValue":"left","options":[{"label":"Left","value":"left"},{"label":"Center","value":"center"}],"ui":{"control":"segmented"}}]},"ui":{"icon":"device-desktop","tab":{"id":"layout","label":"Layout","icon":"devices"}}},{"key":"tablet","label":"Tablet","type":"object","helpText":"768 to 1279px.","openMode":"drill","config":{"fields":[{"key":"useDesktopLayout","label":"Same as desktop","type":"boolean","defaultValue":true},{"key":"columns","label":"Columns","type":"number","showIf":{"match":"all","rules":[{"field":"useDesktopLayout","op":"eq","value":"false"}]},"config":{"min":1,"max":3,"step":1},"ui":{"control":"slider"}},{"key":"gap","label":"Gap","type":"select","showIf":{"match":"all","rules":[{"field":"useDesktopLayout","op":"eq","value":"false"}]},"options":[{"label":"S","value":"sm"},{"label":"M","value":"md"},{"label":"L","value":"lg"}],"ui":{"control":"segmented"}},{"key":"paddingY","label":"Space above and below","type":"select","showIf":{"match":"all","rules":[{"field":"useDesktopLayout","op":"eq","value":"false"}]},"options":[{"label":"M","value":"md"},{"label":"L","value":"lg"},{"label":"XL","value":"xl"}],"ui":{"control":"segmented"}}]},"ui":{"icon":"device-tablet","tab":{"id":"layout"}}},{"key":"mobile","label":"Phone","type":"object","helpText":"Under 768px.","openMode":"drill","config":{"fields":[{"key":"arrangement","label":"Arrange as","type":"select","defaultValue":"stack","options":[{"label":"Stack","value":"stack","icon":"layout-rows","description":"One card per row"},{"label":"Swipe","value":"carousel","icon":"carousel-horizontal","description":"Cards scroll sideways"},{"label":"Two columns","value":"grid","icon":"layout-grid","description":"Small cards, short text only"}],"ui":{"control":"cards"}},{"key":"showPeek","label":"Show the next card's edge","type":"boolean","defaultValue":true,"showIf":{"match":"all","rules":[{"field":"arrangement","op":"eq","value":"carousel"}]}},{"key":"paddingY","label":"Space above and below","type":"select","defaultValue":"md","options":[{"label":"S","value":"sm"},{"label":"M","value":"md"},{"label":"L","value":"lg"}],"ui":{"control":"segmented"}},{"key":"hideImages","label":"Hide card images","type":"boolean"}]},"ui":{"icon":"device-mobile","tab":{"id":"layout"}}}],"description":"Services as cards, laid out separately for desktop, tablet and phone","category":"content","icon":"apps","preview":{"title":"heading","subtitle":"intro"}}},
|
|
1493
|
+
{"id":"grid-cell-layout","name":"Grid with per-cell layout","category":"Advanced patterns","kind":"block","tags":["slots","variants","media-heavy","responsive-layout","commerce"],"proves":"Slot cells with column span, row span, surface and alignment settings · mixed block types per cell","why":"Cells are a SLOT (any library block) because a gift grid mixes products, photos, quotes and offers; the cell's geometry (spans, surface, alignment, padding) belongs to the PLACEMENT, not to the block — the same product card could be 1×1 here and 2×2 elsewhere — so it lives in `slotSettings` and is stored under `_settings`. Numeric spans (sliders) replace bento's preset sizes when the design needs arbitrary rectangles.","htmlSignals":["CSS grid with per-child grid-column: span N / grid-row: span N (or col-span-2 row-span-2 utilities)","Children of different kinds (product card, figure, blockquote, CTA) in one grid","Per-cell background classes (bg-dark, bg-brand) and place-self/align-self alignment","grid-auto-flow: dense"],"mcp":"Each grid child → one slot child: classify its content into a library block (_type) and copy the content fields; read its computed grid-column/grid-row span → colSpan/rowSpan; background → nearest surface token; align-self/justify-self → alignY/alignX; padding → token. Children hidden under 768px → hideOnMobile. If two children are the same kind with the same geometry in a regular rhythm, it's a plain card grid (array), not this layout grid. If spans fall into exactly 4 presets, prefer the Bento block.","pitfalls":["SCHEMA GAP: no cross-cell validation that spans fit the column count (a 3-wide cell in a 4-column grid after a 2-wide one wraps unexpectedly); `colSpan` max can't depend on the parent's variant.","SCHEMA GAP: slotSettings can't be conditional on the child's block type (e.g. padding 'none' only for images) and can't vary per breakpoint.","Dense packing changes the visual order but not the DOM order — screen readers follow the DOM."],"notCarried":["ai","category","variants"],"component":{"name":"Layout grid","slug":"layout-grid","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","recommendedMax":56,"ui":{"tab":{"id":"content"}}},{"key":"cells","label":"Cells","type":"slot","config":{"multiple":true,"minItems":2,"maxItems":12,"allow":["imageBlock","productCard","quoteBlock","textBlock","ctaCard","statBlock","videoBlock","featureBlock"],"slotSettings":[{"key":"colSpan","label":"Width","type":"number","helpText":"In columns, on desktop.","defaultValue":1,"config":{"min":1,"max":4,"step":1},"ui":{"control":"slider"}},{"key":"rowSpan","label":"Height","type":"number","helpText":"In rows, on desktop.","defaultValue":1,"config":{"min":1,"max":3,"step":1},"ui":{"control":"slider"}},{"key":"surface","label":"Surface","type":"select","defaultValue":"light","options":[{"label":"Light","value":"light"},{"label":"Warm","value":"warm"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}],"ui":{"control":"segmented"}},{"key":"alignX","label":"Horizontal","type":"select","defaultValue":"start","options":[{"label":"Start","value":"start"},{"label":"Center","value":"center"},{"label":"End","value":"end"}],"ui":{"control":"segmented"}},{"key":"alignY","label":"Vertical","type":"select","defaultValue":"end","options":[{"label":"Top","value":"start"},{"label":"Middle","value":"center"},{"label":"Bottom","value":"end"}],"ui":{"control":"segmented"}},{"key":"padding","label":"Padding","type":"select","defaultValue":"md","options":[{"label":"None","value":"none"},{"label":"M","value":"md"},{"label":"L","value":"lg"}],"ui":{"control":"segmented"}},{"key":"hideOnMobile","label":"Hide on phones","type":"boolean"}]},"ui":{"tab":{"id":"content"}}},{"key":"rowHeight","label":"Row height","type":"select","defaultValue":"md","config":{"options":[{"label":"220px","value":"sm"},{"label":"300px","value":"md"},{"label":"380px","value":"lg"}]},"ui":{"control":"segmented","tab":{"id":"grid","label":"Grid"}}},{"key":"gap","label":"Gap","type":"select","defaultValue":"md","config":{"options":[{"label":"S","value":"sm"},{"label":"M","value":"md"},{"label":"L","value":"lg"}]},"ui":{"control":"segmented","tab":{"id":"grid"}}},{"key":"useDenseFill","label":"Fill gaps","type":"boolean","helpText":"Changes the visual order, not the reading order.","ui":{"tab":{"id":"grid"}}}],"description":"A grid where every cell sets its own size, surface and alignment","category":"content","icon":"layout-board-split","preview":{"title":"heading","subtitle":"eyebrow"}}},
|
|
1494
|
+
{"id":"references-web","name":"References web","category":"Advanced patterns","kind":"block","tags":["references","dynamic-query","conditional","repeater","validation"],"proves":"Products, authors, recipes and pages referenced manually, by filter, or pinned + auto-filled · reverse 'used in' as a gap","why":"Everything here is a pointer, so nothing is retyped: products from Shopify, writers from Authors, the recipe from Recipes, a static page for the guide. 'Read next' has three honest modes — picked, automatic, or pinned + automatic — and the automatic part is a drill-in object of filter references (topics), so editors curate the top slot and let the query fill the rest. The section is the canonical example of reference fan-out: one block touches five collections.","htmlSignals":["Product cards linking to /products/* under an article","An author bio card (photo, name, role, link to /authors/*)","'Read next' / 'More from' lists where the first item is constant across stories and the rest vary (pinned + auto)","Links to a guide page with a consistent label"],"mcp":"Resolve EVERY link to an existing page or CMS item before creating anything: product URLs → Shopify product references; author links → Authors entries (create missing authors as entries, never inline names); /recipes/* → Recipes entries; static pages → page references. For 'Read next': compare across several crawled stories — items that repeat are pinned, items that vary and share a topic → auto with those topics. If you see the same footer on every story with identical items, it's a global block, not this. Unresolvable links stay as links in a text field with a review flag.","pitfalls":["SCHEMA GAP: no reverse references / 'used in' — the platform knows where a product or author is referenced, but a schema can't declare a read-only computed backlinks field (Sanity shows incoming references natively).","SCHEMA GAP: reference filters (`reference.filter`) exist in types.ts but aren't used by the picker, so 'only published stories in the Recipes topic' can't constrain `pinned`.","SCHEMA GAP: `excludeCurrent` and 'pinned count toward total' are semantics of the query that the schema can't describe — they're renderer conventions.","SCHEMA GAP: `unique` on reference arrays isn't enforced; the same story can be pinned twice.","Referencing pages by id survives slug changes; storing hrefs doesn't."],"notCarried":["category","shopify reference"],"component":{"name":"Story footer","slug":"story-footer","props":[{"key":"heading","label":"Heading","type":"text","recommendedMax":40,"placeholder":"Cook along","ui":{"tab":{"id":"shop","label":"Shop","icon":"shopping-bag"}}},{"key":"products","label":"Products in this story","type":"array","unique":true,"readOnly":true,"lockReason":"A Shopify product id. No Shopify integration backs a reference in BetterCMS yet, so the id is kept as it is.","config":{"maxItems":4,"itemType":"text"},"ui":{"itemTitle":"Product","tab":{"id":"shop"}}},{"key":"recipe","label":"Recipe","type":"reference","config":{"contentModelId":"col_recipes"},"ui":{"tab":{"id":"shop"}}},{"key":"authors","label":"Writers","type":"multi-reference","config":{"max":2,"contentModelId":"col_authors"},"ui":{"tab":{"id":"people","label":"People","icon":"user"}}},{"key":"guidePage","label":"Product page","type":"reference","config":{"targets":["page"]},"ui":{"tab":{"id":"people"}}},{"key":"mode","label":"Stories come from","type":"select","defaultValue":"mixed","config":{"options":[{"label":"Picked","value":"manual"},{"label":"Automatic","value":"auto"},{"label":"Pinned + automatic","value":"mixed"}]},"ui":{"control":"segmented","tab":{"id":"next","label":"Read next","icon":"news"}}},{"key":"pinned","label":"Pinned stories","type":"multi-reference","showIf":{"match":"all","rules":[{"field":"mode","op":"in","value":["manual","mixed"]}]},"unique":true,"config":{"min":1,"max":3,"contentModelId":"col_journal"},"ui":{"itemName":"story","itemTitle":"Story","tab":{"id":"next"}}},{"key":"auto","label":"Fill the rest with","type":"object","showIf":{"match":"all","rules":[{"field":"mode","op":"in","value":["auto","mixed"]}]},"openMode":"drill","config":{"fields":[{"key":"topics","label":"Topics","type":"multi-reference","config":{"max":3,"contentModelId":"col_topics"}},{"key":"useSameAuthor","label":"Prefer the same writer","type":"boolean"},{"key":"excludeCurrent","label":"Leave out this page","type":"boolean","defaultValue":true},{"key":"total","label":"Total stories","type":"number","helpText":"Pinned stories count toward the total.","defaultValue":3,"config":{"min":1,"max":6,"step":1}}]},"ui":{"icon":"filter","tab":{"id":"next"}}}],"description":"Shop the story, meet the writer, read next","category":"content","icon":"link","preview":{"title":"heading"}}},
|
|
1495
|
+
{"id":"changelog-nested-validation","name":"Changelog with nested validation","category":"Advanced patterns","kind":"block","tags":["validation","repeater","conditional","rich-text"],"proves":"Releases → changes → packages · semver and issue-key patterns · min/max at every level · required-if breaking","why":"A changelog is the purest nested repeater: releases → changes → packages, each level with its own limits and formats. Validation sits where the knowledge is: semver on versions, the tracker key on issues, package names restricted to the org scope. 'Migration notes required when breaking' is the required-if pattern: a conditional field (shown when breaking) with `required`, which only validates while visible. Packages are three short strings, so they open inline.","htmlSignals":["Headings with version numbers (## 5.0.0 — 2026-09-30) followed by grouped lists (Added / Changed / Fixed)","Keep a Changelog structure, 'BREAKING' labels, links to issue trackers (NW-1042, #1234)","Inline code and fenced code blocks in migration sections","An RSS/Atom link for releases"],"mcp":"Each version heading → `release` (version from the heading text, date from the heading or <time>). Group headings (Added/Fixed…) set `kind` for the list items under them; each list item → `change` with its summary (first sentence, ≤120 chars), issue keys extracted by pattern, 'BREAKING'/'⚠' → breaking: true with the following paragraph(s)/code as migration (keep code blocks). Package bumps mentioned as 'pkg@x → y' → packages. Split rule: more than ~30 releases, or releases with their own URLs, → model Release as a COLLECTION (one entry per version, sorted by date) and place a dynamic-query block; never keep years of history in one block.","pitfalls":["SCHEMA GAP: `unique` on version is declared but not enforced; duplicates and ordering (newest first, versions descending) can't be validated.","SCHEMA GAP: required-if only works by hiding the field; 'required when breaking, but still visible and optional otherwise' can't be expressed. Needs `validation.requiredWhen: Condition`.","SCHEMA GAP: `min` on a string means characters, on an array items, on a number value — one overloaded key; no word-count or line-count limits.","SCHEMA GAP: patterns validate strings only; a `date` can't be constrained (not in the future, after the previous release).","Inline packages with three fields each get cramped; the inline openMode threshold (≤3 primitives) is exactly at its limit here."],"notCarried":["category","variants"],"component":{"name":"Release notes","slug":"release-notes","props":[{"key":"heading","label":"Heading","type":"text","required":true,"config":{"maxLength":60}},{"key":"intro","label":"Intro","type":"longtext","ui":{"rows":2}},{"key":"releases","label":"Releases","type":"table","config":{"minItems":1,"maxItems":30,"fields":[{"key":"version","label":"Version","type":"text","required":true,"unique":true,"config":{"pattern":"^\\d+\\.\\d+\\.\\d+(-(alpha|beta|rc)\\.\\d+)?$","patternMessage":"Use semantic versioning, like 4.2.0 or 5.0.0-beta.1."}},{"key":"date","label":"Released","type":"date","required":true},{"key":"channel","label":"Channel","type":"select","defaultValue":"stable","options":[{"label":"Stable","value":"stable"},{"label":"Beta","value":"beta"},{"label":"LTS","value":"lts"}],"ui":{"control":"segmented"}},{"key":"summary","label":"Summary","type":"longtext","config":{"maxLength":240},"ui":{"rows":2}},{"key":"changes","label":"Changes","type":"table","fields":[{"key":"kind","label":"Kind","type":"select","required":true,"options":[{"label":"Added","value":"added"},{"label":"Changed","value":"changed"},{"label":"Fixed","value":"fixed"},{"label":"Deprecated","value":"deprecated"},{"label":"Removed","value":"removed"},{"label":"Security","value":"security"}]},{"key":"summary","label":"Summary","type":"text","required":true,"config":{"minLength":10,"maxLength":120}},{"key":"issue","label":"Issue","type":"text","config":{"pattern":"^NW-\\d{1,6}$","patternMessage":"Use the tracker key, like NW-1042."}},{"key":"isBreaking","label":"Breaking change","type":"boolean","helpText":"Requires migration notes."},{"key":"migration","label":"Migration notes","type":"richtext","required":true,"showIf":{"match":"all","rules":[{"field":"isBreaking","op":"eq","value":"true"}]}},{"key":"packages","label":"Packages","type":"table","openMode":"inline","fields":[{"key":"name","label":"Package","type":"text","required":true,"config":{"pattern":"^@northwind/[a-z0-9-]+$","patternMessage":"Use a Northwind package name, like @northwind/sdk."}},{"key":"from","label":"From","type":"text","config":{"pattern":"^\\d+\\.\\d+\\.\\d+$","patternMessage":"Use semantic versioning, like 4.1.3."}},{"key":"to","label":"To","type":"text","required":true,"config":{"pattern":"^\\d+\\.\\d+\\.\\d+(-[a-z]+\\.\\d+)?$","patternMessage":"Use semantic versioning, like 4.2.0."}}],"config":{"maxItems":6},"ui":{"itemLabel":"Package","icon":"package","preview":{"title":"name","subtitle":"to","fallback":"Untitled package"}}}],"config":{"minItems":1,"maxItems":20},"ui":{"itemLabel":"Change","icon":"point","preview":{"title":"summary","subtitle":"kind","fallback":"Untitled change"}}}]},"ui":{"itemLabel":"Release","icon":"tag","preview":{"title":"version","subtitle":"date","fallback":"Untitled release"}}},{"key":"hasRssFeed","label":"Publish an RSS feed","type":"boolean","helpText":"Publishes /changelog.xml.","defaultValue":true}],"description":"Releases with typed changes and migration notes","category":"content","icon":"notes","preview":{"title":"heading","subtitle":"intro"}}},
|
|
1496
|
+
{"id":"rich-text-embedded-components","name":"Rich text with components and annotations","category":"Advanced patterns","kind":"entry","tags":["rich-text","references","embed","a11y","media-heavy"],"proves":"Body with linked component instances (D28), inline entry links, footnote and glossary annotations, image and table islands","why":"Prose is the content; components, tables and images are ISLANDS inside it, stored as the D28 `data-rt` markup (component = library ref + id + overrides, so editing the original Product card still reaches this story except the overridden badge). Inline product links carry the entry id, not just the href, so they survive slug changes. Footnotes and terms terms are annotations: a mark in the text (`data-footnote`, `data-term`) pointing at a keyed item in a sibling array, so the note text stays editable and numbered in reading order.","htmlSignals":["<article> prose with embedded non-prose elements: product cards, newsletter sign-ups, tables, figures","<sup><a href=\"#fn1\"> footnote markers and an <ol class=\"footnotes\"> at the end","<abbr title> / <dfn> / tooltip spans with definitions","Internal links to /products/* or /recipes/* inside paragraphs"],"mcp":"Convert prose to the richtext HTML subset (p, h2–h4, lists, blockquote, strong/em, a, code). For every embedded non-prose element: if it matches a library block (product card, CTA) emit a `data-rt=\"component\"` island referencing it (overrides = only fields that differ from the library version); tables/images/callouts use their `data-rt` islands. Internal links → add data-link-type=\"entry\" + data-entry-id after resolving the target. Footnote markers → `<sup data-footnote=\"key\">` and move each footnote's text into `footnotes` with the same key. Definitions → `terms` + `data-term`. If an island is used in more than one article, it must be a library component, not a copied HTML snippet.","pitfalls":["SCHEMA GAP: the richtext field can't declare WHAT it allows — styles (h2–h4 only), marks, annotation types (footnote, term, internal link) or the component allowlist. Sanity's `of` on block fields does this; ours accepts any island.","SCHEMA GAP: annotations are attributes in HTML; nothing validates that `data-footnote` keys exist in `footnotes` (or that every footnote is used). Needs typed annotation marks pointing at items.","SCHEMA GAP: inline references (entry links) are link-picker values serialised into the HTML, so the platform's reference graph can't see them without parsing the body.","Footnote numbers in the text are render-time; storing '1', '2' in the markup goes stale when paragraphs move (shown here only for readable fallback)."],"notCarried":["ai","category","kind:entry (props)"],"component":{"name":"Recipe story","slug":"recipe-story","props":[{"key":"title","label":"Title","type":"text","required":true,"recommendedMax":70,"config":{"maxLength":90},"ui":{"tab":{"id":"story","label":"Story"}}},{"key":"dek","label":"Dek","type":"longtext","recommendedMax":160,"ui":{"rows":2,"tab":{"id":"story"}}},{"key":"coverImage","label":"Cover image","type":"image","required":true,"ui":{"tab":{"id":"story"}}},{"key":"body","label":"Body","type":"richtext","helpText":"Type / for components, images and tables.","required":true,"helpMore":"Components are linked copies: edit the original in Content › Blocks and every story updates, except the fields you override here.","ui":{"tab":{"id":"story"}}},{"key":"footnotes","label":"Footnotes","type":"table","helpText":"Numbered in the order they appear in the body.","openMode":"inline","config":{"fields":[{"key":"key","label":"Key","type":"text","required":true,"unique":true,"config":{"pattern":"^fn-[a-z0-9-]+$","patternMessage":"Use lowercase letters, numbers and hyphens, like fn-saffron."}},{"key":"text","label":"Note","type":"longtext","required":true,"config":{"maxLength":300},"ui":{"rows":2}},{"key":"source","label":"Source","type":"link"}]},"ui":{"itemLabel":"Footnote","icon":"superscript","preview":{"title":"text","subtitle":"key","fallback":"Untitled footnote"},"tab":{"id":"notes","label":"Notes & terms","icon":"notes"}}},{"key":"terms","label":"Terms","type":"table","openMode":"inline","config":{"fields":[{"key":"word","label":"Word","type":"text","required":true,"config":{"maxLength":32}},{"key":"definition","label":"Definition","type":"text","required":true,"config":{"maxLength":140}}]},"ui":{"itemLabel":"Term","icon":"vocabulary","preview":{"title":"word","subtitle":"definition","fallback":"Untitled term"},"tab":{"id":"notes"}}}],"description":"A Journal story with a recipe, product cards and footnotes inside the text","category":"content","icon":"chef-hat","preview":{"title":"title","subtitle":"dek","media":"coverImage"}}},
|
|
1497
|
+
{"id":"dynamic-query-listing","name":"Dynamic query listing","category":"Advanced patterns","kind":"block","tags":["dynamic-query","conditional","repeater","references","validation"],"proves":"Collection + filter rows (field · operator · value) + sort + limit + pagination + pinned + empty state","why":"A listing is a QUERY, not a list of items: the block stores the question (collection, filter rows, match all/any, sort rules, limit) and the platform answers it at build/request time, so the section never goes stale. Each filter row is field · operator · value with the value input switching by operator (single value, list, day count, or none for 'is filled in'). Pinned references and the empty state cover the two editorial needs a query can't: 'this one first' and 'what if nothing matches'. Numbered pagination exists because 'load more' isn't crawlable.","htmlSignals":["A card grid whose items share a template and link into one URL pattern (/journal/*, /topics/*)","Pagination (?page=2, rel=next), 'Load more' buttons fetching JSON","The same section showing different items on different topic/category pages (filter = current page)","Empty-state copy ('No stories yet') in the markup or on sparse pages"],"mcp":"Never convert a feed into static items. Infer the query by comparing the listing across pages and over time: shared topic → filter in; dates all recent → withinDays; order → sort rules; count → limit; pagination UI → pagination. Items that appear in a fixed first position across crawls → pinned. If the source collection doesn't exist yet, create it and import the items as entries FIRST, then emit this block pointing at it. If the listing is hand-curated (no shared criteria, rarely changes) use a references array instead.","pitfalls":["SCHEMA GAP: a filter's `field` (and a sort rule's) should be a select populated from the chosen collection's fields, and the value input should take that field's TYPE (reference picker for topics, date for publishedAt). The schema can't derive options or types from another field's value.","SCHEMA GAP: `pinned` references are hard-wired to col_journal; they should target whatever `collection` is chosen (reference targets from a sibling value).","SCHEMA GAP: no live query preview/count in the schema (e.g. '7 entries match') and no validation that the query returns anything.","SCHEMA GAP: AND/OR groups beyond one level (all of (A, any of (B, C))) can't be modelled without recursive object types — the language has no recursion."],"notCarried":["category","variants"],"component":{"name":"Query listing","slug":"query-listing","props":[{"key":"heading","label":"Heading","type":"text","recommendedMax":50,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"viewAllButton","label":"View all button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"collection","label":"Collection","type":"reference","required":true,"config":{"targets":["collection"]},"ui":{"tab":{"id":"query","label":"Query","icon":"filter"}}},{"key":"match","label":"Show entries matching","type":"select","defaultValue":"all","config":{"options":[{"label":"All filters","value":"all"},{"label":"Any filter","value":"any"}]},"ui":{"control":"segmented","tab":{"id":"query"}}},{"key":"filters","label":"Filters","type":"table","config":{"maxItems":6,"fields":[{"key":"field","label":"Field","type":"text","required":true,"config":{"pattern":"^[a-zA-Z][a-zA-Z0-9.]*$","patternMessage":"Use a field id from the collection, like topics or author.slug."}},{"key":"operator","label":"Condition","type":"select","required":true,"defaultValue":"eq","options":[{"label":"Is","value":"eq"},{"label":"Is not","value":"neq"},{"label":"Is any of","value":"in"},{"label":"Contains","value":"contains"},{"label":"After or more than","value":"gt"},{"label":"Before or less than","value":"lt"},{"label":"In the last N days","value":"withinDays"},{"label":"Is filled in","value":"exists"}]},{"key":"value","label":"Value","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"operator","op":"in","value":["eq","neq","contains","gt","lt"]}]},"config":{"maxLength":80}},{"key":"values","label":"Values","type":"tags","showIf":{"match":"all","rules":[{"field":"operator","op":"eq","value":"in"}]},"config":{"min":1,"max":10}},{"key":"days","label":"Days","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"operator","op":"eq","value":"withinDays"}]},"config":{"min":1,"max":365,"step":1}},{"key":"useCurrentPage","label":"Match this page","type":"boolean","showIf":{"match":"all","rules":[{"field":"operator","op":"in","value":["eq","in"]}]}}]},"ui":{"itemLabel":"Filter","icon":"filter","preview":{"title":"field","subtitle":"value","fallback":"Untitled filter"},"tab":{"id":"query"}}},{"key":"sort","label":"Sort","type":"table","openMode":"inline","config":{"minItems":1,"maxItems":3,"fields":[{"key":"field","label":"Field","type":"text","required":true,"placeholder":"publishedAt"},{"key":"direction","label":"Direction","type":"select","defaultValue":"desc","options":[{"label":"Newest / high","value":"desc"},{"label":"Oldest / low","value":"asc"}],"ui":{"control":"segmented"}}]},"ui":{"itemLabel":"Sort rule","itemName":"sortRule","icon":"arrows-sort","preview":{"title":"field","subtitle":"direction","fallback":"Untitled sort"},"tab":{"id":"query"}}},{"key":"pinned","label":"Pinned","type":"multi-reference","unique":true,"config":{"max":3,"contentModelId":"col_journal"},"ui":{"itemName":"entry","itemTitle":"Entry","tab":{"id":"query"}}},{"key":"excludeCurrent","label":"Leave out this page","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"query"}}},{"key":"limit","label":"How many","type":"number","required":true,"defaultValue":6,"config":{"min":1,"max":24,"step":1,"unit":"entries"},"ui":{"tab":{"id":"display","label":"Display"}}},{"key":"pagination","label":"More entries","type":"select","defaultValue":"loadMore","config":{"options":[{"label":"None","value":"none","description":"The first batch, plus the View all button"},{"label":"Load more button","value":"loadMore"},{"label":"Numbered pages","value":"numbered","description":"Crawlable ?page=2 links"}]},"ui":{"control":"radio","tab":{"id":"display"}}},{"key":"cardFields","label":"On each card","type":"multiselect","defaultValue":["image","excerpt","date"],"config":{"options":[{"label":"Image","value":"image"},{"label":"Excerpt","value":"excerpt"},{"label":"Author","value":"author"},{"label":"Date","value":"date"},{"label":"Reading time","value":"readingTime"},{"label":"Topic","value":"topic"}]},"ui":{"tab":{"id":"display"}}},{"key":"empty","label":"When nothing matches","type":"object","openMode":"drill","config":{"fields":[{"key":"mode","label":"Show","type":"select","defaultValue":"message","options":[{"label":"Nothing","value":"hide"},{"label":"A message","value":"message"}],"ui":{"control":"segmented"}},{"key":"heading","label":"Heading","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"mode","op":"eq","value":"message"}]},"config":{"maxLength":60}},{"key":"body","label":"Body","type":"longtext","showIf":{"match":"all","rules":[{"field":"mode","op":"eq","value":"message"}]},"ui":{"rows":2}},{"key":"button","label":"Button","type":"button","showIf":{"match":"all","rules":[{"field":"mode","op":"eq","value":"message"}]}}]},"ui":{"icon":"mood-empty","tab":{"id":"display"}}}],"description":"Entries from a collection, filtered, sorted and paged","category":"content","icon":"database-search","preview":{"title":"heading","subtitle":"collection"}}},
|
|
1498
|
+
{"id":"overlays-with-triggers","name":"Modal, drawer and popover","category":"Advanced patterns","kind":"block","tags":["slots","conditional","a11y","form","repeater","references"],"proves":"Overlays with their own slot content, opened by triggers on other blocks (click, delay, scroll, exit) · focus and dismiss rules","why":"Overlays are page-level content that isn't in the flow: they're modelled as one block holding an array of overlays, each with its own slot of library blocks (so a modal can reuse a quote or a CTA card) plus an optional form reference. Triggers on OTHER blocks are ordinary buttons linking to #id — the overlay owns the id, the button owns nothing special — which keeps the button field type unchanged. Automatic triggers and frequency caps are opt-in and live on the overlay. Size/side are conditional on kind; popovers have no backdrop setting.","htmlSignals":["<dialog> elements or role=\"dialog\" aria-modal=\"true\" containers hidden by default","Buttons with aria-haspopup=\"dialog\", aria-controls, data-modal-target or href=\"#id\" pointing at those containers","Exit-intent / scroll-depth / timer scripts (mouseleave on document, IntersectionObserver at 70%)","Off-canvas drawers (transform: translateX(100%), .drawer, .offcanvas)"],"mcp":"Find every hidden dialog/drawer/popover container and its trigger(s). Each → an `overlay` with id from the container id (kebab-case), kind from role/position, content converted to library blocks in the slot, forms → reference a created form. Rewrite each trigger button's link to `{ type: 'section', href: '#id' }`. Timer/scroll/exit scripts → `triggers` with their thresholds; localStorage/cookie caps → frequency. Cookie banners and age gates are NOT overlays — they belong to site settings. If an overlay appears on many pages with the same content, make the overlay set a global block.","pitfalls":["SCHEMA GAP: no link type 'open overlay' and no intra-page reference — triggers are '#id' strings, so renaming an overlay id silently breaks every button that opens it.","SCHEMA GAP: `unique` on overlay ids isn't enforced, and ids must also not collide with section anchors on the same page (page-wide uniqueness).","SCHEMA GAP: popovers need an anchor element; positioning relative to a specific button can't be expressed beyond the #id link.","Exit-intent doesn't exist on touch devices; the label says '(desktop)' because the schema can't scope a trigger per device.","Focus trap, Esc to close and focus return are renderer requirements, not fields — never make them toggles."],"notCarried":["category"],"component":{"name":"Overlays","slug":"overlays","props":[{"key":"label","label":"Name","type":"text","helpText":"Only editors see this.","config":{"maxLength":40}},{"key":"items","label":"Overlays","type":"table","config":{"maxItems":6,"fields":[{"key":"title","label":"Title","type":"text","required":true,"config":{"maxLength":60}},{"key":"id","label":"Opens from","type":"text","helpText":"Link any button to #id to open it, e.g. #book-demo.","required":true,"unique":true,"config":{"pattern":"^[a-z][a-z0-9-]*$","patternMessage":"Use lowercase letters, numbers and hyphens, starting with a letter, like grate-xl."}},{"key":"kind","label":"Type","type":"select","defaultValue":"modal","options":[{"label":"Modal","value":"modal","icon":"app-window","description":"Centered, page dimmed"},{"label":"Drawer","value":"drawer","icon":"layout-sidebar-right","description":"Slides in from the side"},{"label":"Popover","value":"popover","icon":"message-2","description":"Small, next to the button"}],"ui":{"control":"cards"}},{"key":"side","label":"Side","type":"select","defaultValue":"right","showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"drawer"}]},"options":[{"label":"Right","value":"right"},{"label":"Left","value":"left"},{"label":"Bottom","value":"bottom"}],"ui":{"control":"segmented"}},{"key":"size","label":"Size","type":"select","defaultValue":"md","showIf":{"match":"all","rules":[{"field":"kind","op":"in","value":["modal","drawer"]}]},"options":[{"label":"S","value":"sm"},{"label":"M","value":"md"},{"label":"L","value":"lg"}],"ui":{"control":"segmented"}},{"key":"content","label":"Content","type":"slot","config":{"multiple":true,"minItems":1,"maxItems":4,"allow":["textBlock","imageBlock","videoBlock","ctaCard","quoteBlock","featureBlock"]}},{"key":"form","label":"Form","type":"reference","config":{"targets":["form"]}},{"key":"triggers","label":"Also opens","type":"table","openMode":"inline","fields":[{"key":"type","label":"When","type":"select","options":[{"label":"After a delay","value":"delay"},{"label":"After scrolling","value":"scroll"},{"label":"On leaving (desktop)","value":"exit"}]},{"key":"seconds","label":"Seconds","type":"number","showIf":{"match":"all","rules":[{"field":"type","op":"eq","value":"delay"}]},"config":{"min":5,"max":120,"step":5}},{"key":"percent","label":"Scrolled","type":"number","showIf":{"match":"all","rules":[{"field":"type","op":"eq","value":"scroll"}]},"config":{"min":25,"max":90,"step":5,"unit":"%"}}],"config":{"maxItems":3},"ui":{"itemLabel":"Trigger","icon":"bolt","preview":{"title":"type","fallback":"Untitled trigger"}}},{"key":"frequency","label":"Show automatic triggers","type":"select","defaultValue":"once","options":[{"label":"Once per visit","value":"session"},{"label":"Once per visitor","value":"once"},{"label":"Once a week","value":"week"}]},{"key":"allowBackdropClose","label":"Close when clicking outside","type":"boolean","defaultValue":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"in","value":["modal","drawer"]}]}}]},"ui":{"itemLabel":"Overlay","itemName":"overlay","icon":"app-window","preview":{"title":"title","subtitle":"kind","fallback":"Untitled overlay"}}}],"description":"Modals, drawers and popovers for this page, opened by buttons elsewhere","category":"conversion","icon":"app-window","preview":{"title":"label"}}},
|
|
1499
|
+
{"id":"scrollytelling","name":"Sticky scroll story","category":"Advanced patterns","kind":"block","tags":["media-heavy","repeater","conditional","a11y","variants"],"proves":"Steps synced to scroll with media swaps (image / video / stat) · trigger offset · reduced-motion and phone fallbacks","why":"Scrollytelling pairs each step's text with what the sticky panel shows, so media is a drill-in object ON the step (not a parallel array that can drift out of sync). The media kind is a conditional switch (image / video + poster / big number / keep previous). Motion settings are a separate tab, and both fallbacks — reduced motion and phones — are required choices, because a sticky scroll story is unusable for many people without them.","htmlSignals":["position: sticky media container beside a column of tall text blocks (min-height: 80–100vh each)","Scrollama / GSAP ScrollTrigger / IntersectionObserver code toggling an active step class","Media elements with data-step attributes swapped via opacity","prefers-reduced-motion media queries around the transitions"],"mcp":"Each text step → `scrollStep`; find the media shown while it's active (data-step mapping, or the media element index) and put it in that step's `media` — never in a separate list. Steps that don't change the media → kind 'same'. Read the trigger offset from the library config (ScrollTrigger start: 'top center' → 50). If there's no reduced-motion handling in the source, default reducedMotion: stack and note it. Over 8 steps → split into two stories; each scroll story should make one point.","pitfalls":["SCHEMA GAP: 'keep previous' media depends on the previous array item; the schema can't validate that the first step doesn't use it (position-dependent rules).","SCHEMA GAP: no animation/timeline type — transitions are a select of presets; anything choreographed (scrubbed video, SVG path drawing) stays in code with copy fields only.","Videos in sticky panels must be muted and short; there's no duration validation on the video type.","Preview `media: \"media.image\"` is empty for stat/video steps — preview can't fall back across paths."],"notCarried":["ai","category","variants"],"component":{"name":"Scroll story","slug":"scroll-story","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":60,"ui":{"tab":{"id":"content"}}},{"key":"intro","label":"Intro","type":"longtext","ui":{"rows":2,"tab":{"id":"content"}}},{"key":"steps","label":"Steps","type":"table","config":{"minItems":2,"maxItems":8,"fields":[{"key":"heading","label":"Heading","type":"text","required":true,"config":{"maxLength":60}},{"key":"body","label":"Body","type":"longtext","recommendedMax":240,"ui":{"rows":3}},{"key":"media","label":"Media","type":"object","openMode":"drill","fields":[{"key":"kind","label":"Show","type":"select","defaultValue":"image","options":[{"label":"Image","value":"image"},{"label":"Video","value":"video"},{"label":"Big number","value":"stat"},{"label":"Keep previous","value":"same"}],"ui":{"control":"segmented"}},{"key":"image","label":"Image","type":"image","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"image"}]}},{"key":"video","label":"Video","type":"video","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"video"}]}},{"key":"poster","label":"Poster","type":"image","showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"video"}]}},{"key":"stat","label":"Number","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"stat"}]},"config":{"maxLength":10}},{"key":"statLabel","label":"Number label","type":"text","showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"stat"}]},"config":{"maxLength":40}},{"key":"transition","label":"Change with","type":"select","defaultValue":"fade","options":[{"label":"Fade","value":"fade"},{"label":"Slide","value":"slide"},{"label":"Zoom","value":"zoom"}],"ui":{"control":"segmented"}}],"ui":{"icon":"photo"}}]},"ui":{"itemLabel":"Step","itemName":"scrollStep","icon":"list-numbers","preview":{"title":"heading","subtitle":"body","media":"media.image","fallback":"Untitled step"},"tab":{"id":"content"}}},{"key":"triggerAt","label":"Switch when the step reaches","type":"number","defaultValue":50,"config":{"min":20,"max":80,"step":5,"unit":"% of screen"},"ui":{"control":"slider","tab":{"id":"motion","label":"Motion","icon":"player-track-next"}}},{"key":"progress","label":"Progress","type":"select","defaultValue":"dots","config":{"options":[{"label":"None","value":"none"},{"label":"Dots","value":"dots"},{"label":"Bar","value":"bar"}]},"ui":{"control":"segmented","tab":{"id":"motion"}}},{"key":"reducedMotion","label":"When motion is reduced","type":"select","defaultValue":"stack","config":{"options":[{"label":"Stack each step with its media","value":"stack","description":"No sticky panel, no transitions"},{"label":"Keep sticky, switch without animation","value":"cut"}]},"ui":{"control":"radio","tab":{"id":"motion"}}},{"key":"phone","label":"On phones","type":"select","defaultValue":"stack","config":{"options":[{"label":"Stack steps with their media","value":"stack"},{"label":"Sticky media on top, text scrolls under","value":"sticky"}]},"ui":{"control":"radio","tab":{"id":"motion"}}}],"description":"Text scrolls past while the media beside it stays put and changes","category":"content","icon":"arrows-down","preview":{"title":"heading","subtitle":"intro"}}},
|
|
1500
|
+
{"id":"a11y-critical-media-form","name":"Accessibility-critical media and form","category":"Advanced patterns","kind":"block","tags":["a11y","media-heavy","form","validation","conditional"],"proves":"Captions per language, transcript, audio-described version · decorative vs informative images · labels, hints and error messages","why":"Accessibility needs are content, so they're fields with rules, not afterthoughts: caption tracks are an array (one per language and kind) with at least one required; the transcript is required rich text; the audio-described cut is its own video. Images declare their ROLE because alt text depends on it — informative needs alt, complex needs alt plus a long description (conditional + required), decorative must have EMPTY alt. Form fields carry a visible label, autocomplete purpose, a hint and a fix-it error message (required only when the field is required).","htmlSignals":["<video> with <track kind=\"captions|subtitles|descriptions\" srclang label>","Transcript sections (details/summary 'Transcript', or a heading 'Transcript' after a video)","<img alt=\"\"> / role=\"presentation\" (decorative) vs descriptive alt; aria-describedby / longdesc links for charts","<label for>, aria-describedby hints, aria-invalid + error text, autocomplete attributes on inputs"],"mcp":"Copy every <track> into `tracks` (kind, srclang → language, label, src → file). Never auto-generate captions during conversion — if none exist, leave tracks empty so validation flags it. A transcript on the page → `transcript`; if absent, flag (it's required). For each image: alt=\"\" or role=presentation → role decorative (keep alt empty); an image of a chart/diagram → complex and pull the long description from aria-describedby/longdesc/adjacent text; otherwise informative with the existing alt (rewrite 'image of…' phrasing). For forms: labels from <label> (if only placeholders exist, convert placeholder → label and flag), autocomplete from attributes, error messages from the validation script or aria-errormessage.","pitfalls":["SCHEMA GAP: alt text lives INSIDE the image value; conditions and validation can't reach a sub-key, so 'alt required unless decorative' and 'alt must be empty when decorative' can't be expressed. The image type needs a built-in `decorative` flag and an alt rule. Proof: the kit validator flags this entry's own decorative image (figures.1) with \"No alt text\" — a false positive the schema can't prevent.","SCHEMA GAP: no file-type constraint on `file` (accept: .vtt) and no way to validate a caption track's language matches the video's.","SCHEMA GAP: 'at least one track of kind captions' is a rule about item VALUES in an array; only item counts can be validated.","SCHEMA GAP: the sign-up form is modelled inline because forms are a separate system; ideally the block only references the form and the form owns labels/errors.","Autoplay over 5 seconds without a pause control fails WCAG 2.2.2; the help warns, nothing blocks it."],"notCarried":["category"],"component":{"name":"Video explainer","slug":"explainer","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":70,"ui":{"tab":{"id":"video","label":"Video","icon":"player-play"}}},{"key":"video","label":"Video","type":"object","openMode":"drill","config":{"fields":[{"key":"file","label":"Video","type":"video","required":true},{"key":"title","label":"Accessible name","type":"text","helpText":"Not shown. Read by screen readers.","required":true,"config":{"maxLength":100}},{"key":"poster","label":"Poster","type":"image"},{"key":"tracks","label":"Captions and subtitles","type":"table","helpText":".vtt files. At least captions in the video's own language.","fields":[{"key":"kind","label":"Kind","type":"select","defaultValue":"captions","options":[{"label":"Captions","value":"captions","description":"Dialogue plus sounds, for deaf viewers"},{"label":"Subtitles","value":"subtitles","description":"Dialogue translated"},{"label":"Descriptions","value":"descriptions","description":"Text descriptions of what's on screen"}],"ui":{"control":"segmented"}},{"key":"language","label":"Language","type":"select","required":true,"options":[{"label":"English","value":"en"},{"label":"Spanish","value":"es"},{"label":"Portuguese","value":"pt"},{"label":"French","value":"fr"}]},{"key":"label","label":"Menu label","type":"text","required":true,"placeholder":"English (CC)","config":{"maxLength":32}},{"key":"file","label":"WebVTT file","type":"file","required":true},{"key":"isOnByDefault","label":"On by default","type":"boolean"}],"config":{"minItems":1,"maxItems":8},"ui":{"itemLabel":"Track","icon":"badge-cc","preview":{"title":"label","subtitle":"kind","fallback":"Untitled track"}}},{"key":"transcript","label":"Transcript","type":"richtext","required":true},{"key":"described","label":"Audio-described version","type":"video"},{"key":"autoplay","label":"Play automatically","type":"boolean"}]},"ui":{"icon":"player-play","tab":{"id":"video"}}},{"key":"figures","label":"Images","type":"table","config":{"maxItems":6,"fields":[{"key":"image","label":"Image","type":"image","helpText":"Leave alt text empty only if the image is decorative.","required":true},{"key":"role","label":"This image","type":"select","defaultValue":"informative","options":[{"label":"Carries information","value":"informative","description":"Needs alt text"},{"label":"Is a chart or diagram","value":"complex","description":"Needs alt text and a long description"},{"label":"Is decoration only","value":"decorative","description":"Hidden from screen readers"}],"ui":{"control":"radio"}},{"key":"longDescription","label":"Long description","type":"longtext","helpText":"Linked under the image.","required":true,"showIf":{"match":"all","rules":[{"field":"role","op":"eq","value":"complex"}]},"config":{"maxLength":1200},"ui":{"rows":4}},{"key":"caption","label":"Caption","type":"text","config":{"maxLength":160}}]},"ui":{"itemLabel":"Image","icon":"photo","preview":{"title":"caption","subtitle":"role","media":"image","fallback":"Untitled image"},"tab":{"id":"images","label":"Images","icon":"photo"}}},{"key":"signup","label":"Sign-up form","type":"object","openMode":"drill","config":{"fields":[{"key":"heading","label":"Heading","type":"text","required":true,"config":{"maxLength":60}},{"key":"fields","label":"Fields","type":"table","fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":40}},{"key":"name","label":"Field name","type":"text","required":true,"config":{"pattern":"^[a-z][a-zA-Z0-9]*$","patternMessage":"Use a field key, like email or firstName."}},{"key":"type","label":"Type","type":"select","defaultValue":"text","options":[{"label":"Text","value":"text"},{"label":"Email","value":"email"},{"label":"Dropdown","value":"select"},{"label":"Checkbox","value":"checkbox"}]},{"key":"autocomplete","label":"Autofill as","type":"select","helpText":"Lets browsers fill it in. Required for personal details.","options":[{"label":"Nothing","value":"off"},{"label":"Email","value":"email"},{"label":"First name","value":"given-name"},{"label":"Last name","value":"family-name"},{"label":"Postcode","value":"postal-code"}]},{"key":"hint","label":"Hint","type":"text","helpText":"Read out with the label.","config":{"maxLength":90}},{"key":"isRequired","label":"Required","type":"boolean"},{"key":"errorMessage","label":"Error message","type":"text","required":true,"showIf":{"match":"all","rules":[{"field":"isRequired","op":"eq","value":"true"}]},"config":{"maxLength":90}}],"config":{"minItems":1,"maxItems":5},"ui":{"itemLabel":"Field","itemName":"formField","icon":"forms","preview":{"title":"label","subtitle":"type","fallback":"Untitled field"}}},{"key":"submitButton","label":"Submit button","type":"button"},{"key":"privacy","label":"Privacy note","type":"text","config":{"maxLength":140}},{"key":"form","label":"Send to form","type":"reference","required":true,"config":{"targets":["form"]}}]},"ui":{"icon":"forms","tab":{"id":"form","label":"Sign-up","icon":"forms"}}}],"description":"A video with captions and a transcript, supporting images, and a sign-up form","category":"content","icon":"player-play","preview":{"title":"heading","media":"video.poster"}}},
|
|
1501
|
+
{"id":"section-theme-tokens","name":"Section theme tokens","category":"Advanced patterns","kind":"block","tags":["variants","conditional","a11y","validation","media-heavy"],"proves":"Dark section inside a light page · surface + accent override from brand tokens · spacing tokens · all in the Design group","why":"Everything about how the section LOOKS sits in the Design tab and is expressed as tokens (scheme, surface, accent, spacing, width) — never raw hex or pixels — so the brand kit stays the single source and dark sections flip every color consistently. Conditionals keep the tab calm: the photo and overlay appear only for the Photo surface, the accent choice only when overriding. Content (quote, name, photo, button) is untouched by any design choice, so switching variant or scheme never loses copy.","htmlSignals":["A section with its own data-theme=\"dark\" / .dark / .theme-inverse class inside a light page","Background image with a semi-transparent overlay layer (::before with rgba/opacity)","CSS custom properties redefined on the section (--accent, --surface) or utility classes like bg-neutral-900 text-white","Section-specific padding utilities (py-24) differing from neighbours"],"mcp":"Map computed styles to the nearest TOKEN, never copy values: background luminance < 0.2 → scheme dark (+ surface inverse unless it's a photo); a background-image with an overlay → surface image + overlay % from the overlay's alpha; accent = the button color matched to the brand kit's accents (if no match within ΔE 5, keep the default and flag); padding → nearest spacing token; max-width → width token. If the source uses a color the brand kit doesn't have, propose adding it to the brand kit rather than storing a hex in the block.","pitfalls":["SCHEMA GAP: no token-reference type — `accent` is a hard-coded select that must be kept in sync with the brand kit by hand; it should be a dynamic option list from the brand kit (and `color` fields should be restrictable to tokens).","SCHEMA GAP: no contrast validation between surface/overlay and text color (WCAG 1.4.3); 'overlay below 35% on a bright photo' can't be flagged.","SCHEMA GAP: no responsive values for spacing (see responsive-layout-settings).","SCHEMA GAP: design fields are repeated per block; a shared, named object type (sectionTheme) can't be referenced by name, only copied."],"notCarried":["category","variants"],"component":{"name":"Quote band","slug":"quote-band","props":[{"key":"quote","label":"Quote","type":"longtext","required":true,"recommendedMax":200,"ui":{"rows":3,"tab":{"id":"content","label":"Content"}}},{"key":"author","label":"Name","type":"text","required":true,"config":{"maxLength":48},"ui":{"tab":{"id":"content"}}},{"key":"role","label":"Role","type":"text","config":{"maxLength":60},"ui":{"tab":{"id":"content"}}},{"key":"photo","label":"Photo","type":"image","ui":{"tab":{"id":"content"}}},{"key":"button","label":"Button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"scheme","label":"Color scheme","type":"select","defaultValue":"inherit","config":{"options":[{"label":"Page","value":"inherit"},{"label":"Light","value":"light"},{"label":"Dark","value":"dark"}]},"ui":{"control":"segmented","tab":{"id":"design","label":"Design","icon":"palette"}}},{"key":"surface","label":"Background","type":"select","defaultValue":"default","config":{"options":[{"label":"Default","value":"default","description":"Page background"},{"label":"Raised","value":"raised","description":"One step lighter"},{"label":"Inverse","value":"inverse","description":"Charcoal in light mode"},{"label":"Brand","value":"brand","description":"Ember orange"},{"label":"Photo","value":"image","description":"With an overlay"}]},"ui":{"control":"cards","tab":{"id":"design"}}},{"key":"background","label":"Background photo","type":"image","required":true,"showIf":{"match":"all","rules":[{"field":"surface","op":"eq","value":"image"}]},"ui":{"tab":{"id":"design"}}},{"key":"overlay","label":"Overlay","type":"number","defaultValue":45,"showIf":{"match":"all","rules":[{"field":"surface","op":"eq","value":"image"}]},"config":{"min":0,"max":90,"step":5,"unit":"%"},"ui":{"control":"slider","tab":{"id":"design"}}},{"key":"useCustomAccent","label":"Use a different accent color","type":"boolean","ui":{"tab":{"id":"design"}}},{"key":"accent","label":"Accent","type":"select","defaultValue":"ember","showIf":{"match":"all","rules":[{"field":"useCustomAccent","op":"eq","value":"true"}]},"config":{"options":[{"label":"Ember","value":"ember"},{"label":"Moss","value":"moss"},{"label":"Sand","value":"sand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"spacing","label":"Spacing","type":"object","config":{"fields":[{"key":"top","label":"Top","type":"select","defaultValue":"lg","options":[{"label":"None","value":"none"},{"label":"M","value":"md"},{"label":"L","value":"lg"},{"label":"XL","value":"xl"}],"ui":{"control":"segmented"}},{"key":"bottom","label":"Bottom","type":"select","defaultValue":"lg","options":[{"label":"None","value":"none"},{"label":"M","value":"md"},{"label":"L","value":"lg"},{"label":"XL","value":"xl"}],"ui":{"control":"segmented"}}]},"ui":{"icon":"spacing-vertical","tab":{"id":"design"}}},{"key":"width","label":"Content width","type":"select","defaultValue":"narrow","config":{"options":[{"label":"Narrow","value":"narrow"},{"label":"Default","value":"default"},{"label":"Wide","value":"wide"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"isFullBleed","label":"Edge to edge","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"design"}}}],"description":"A full-width quote with its own color scheme","category":"social-proof","icon":"quote","preview":{"title":"author","subtitle":"quote","media":"photo"}}},
|
|
1502
|
+
{"id":"core-faq","name":"FAQ","category":"Core examples","kind":"block","tags":["repeater","conditional","validation","dynamic-query"],"proves":"Small items still drill in · conditional source · developer lock · presence in a row","why":"Questions are same-shaped rows, so `items` is one list whose row holds a plain-string question and a rich-text answer (bold, links and lists live in answers). `source` switches between questions written here and references into a FAQs collection; each list shows only for its own choice, so the two never disagree. Search, rich results, columns and the anchor are Settings, not copy, and the anchor is locked to people who manage the schema.","htmlSignals":["Repeated <details> with a <summary> holding the question (often an <h3> plus a chevron <svg>) and a body <div> holding the answer","The first <details> carries `open`","A hand-written <script type=\"application/ld+json\"> with \"@type\":\"FAQPage\" beside the band","A closing line with a support link under the list (\"Still stuck? Talk to support\")"],"mcp":"create_component with sectionType 'FAQ'; each prop needs a `target` into your own blockJson (a root `section` with `style`). eyebrow/heading/intro → text props on heading/text blocks. items → ONE `table` prop: config.fields question (text, required) + answer (richtext, required), plus category (select) and open (boolean); ui { preview: { title: 'question' }, itemLabel: 'Question', icon: 'help-circle' }. source → a select with ui.control 'segmented'; give items and collection a `showIf` on source ('inline' / 'collection'), placed after source. collection → multi-reference with config.contentModelId of the FAQs model. showSearch/richResults/columns/anchor → ui.tab 'Settings'. Accordion and Two columns are two components sharing sectionType 'FAQ' with the same prop keys.","pitfalls":["richResults does nothing by itself: FAQPage markup comes from update_page schemaType 'FAQPage', and the builder finds the first list whose rows have keys containing `question` and `answer`. Delete the hand-written JSON-LD.","`open` on the first item is behaviour in the worked example and stays in the template; keep the per-item boolean only if the template reads it.","Bind the question on the <h3>, not the <summary>, or a save erases the chevron."],"notCarried":["ai","variants"],"component":{"name":"FAQ","slug":"faq","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","placeholder":"Support","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"intro","label":"Intro","type":"longtext","ui":{"rows":2,"tab":{"id":"content"}}},{"key":"source","label":"Questions come from","type":"select","defaultValue":"inline","config":{"options":[{"label":"Written here","value":"inline"},{"label":"FAQs collection","value":"collection"}]},"ui":{"control":"segmented","tab":{"id":"content"}}},{"key":"items","label":"Questions","type":"table","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"inline"}]},"config":{"minItems":1,"maxItems":12,"fields":[{"key":"question","label":"Question","type":"text","required":true,"config":{"maxLength":120}},{"key":"answer","label":"Answer","type":"richtext","required":true},{"key":"category","label":"Category","type":"select","options":[{"label":"Orders","value":"orders"},{"label":"Shipping","value":"shipping"},{"label":"Care","value":"care"},{"label":"Warranty","value":"warranty"}]},{"key":"open","label":"Open by default","type":"boolean"}]},"ui":{"itemLabel":"Question","itemName":"faqItem","icon":"help-circle","preview":{"title":"question","subtitle":"answer","fallback":"Untitled question"},"tab":{"id":"content"}}},{"key":"collection","label":"FAQs to show","type":"multi-reference","showIf":{"match":"all","rules":[{"field":"source","op":"eq","value":"collection"}]},"config":{"max":8,"contentModelId":"col_faq"},"ui":{"itemName":"faqRef","itemTitle":"FAQ","tab":{"id":"content"}}},{"key":"showSearch","label":"Show search","type":"boolean","ui":{"tab":{"id":"settings","label":"Settings"}}},{"key":"richResults","label":"FAQ rich results","type":"boolean","defaultValue":true,"ui":{"tab":{"id":"settings"}}},{"key":"columns","label":"Columns on desktop","type":"number","config":{"scale":{"steps":{"min":1,"max":3,"step":1}}},"ui":{"tab":{"id":"settings"}}},{"key":"anchor","label":"Anchor","type":"text","helpText":"For #links to this block.","advanced":true,"ui":{"access":{"edit":"schema.manage"},"tab":{"id":"settings"}}}],"description":"Questions and answers, written here or from the FAQs collection","category":"content","icon":"help-circle","preview":{"title":"heading"}}},
|
|
1503
|
+
{"id":"core-megaMenu","name":"Mega menu","category":"Core examples","kind":"global","tags":["deep-nesting","repeater","global-synced","validation"],"proves":"Five levels deep · breadcrumb truncation · global block (synced) · link + button units","why":"The navigation is a GLOBAL block: one copy synced across every page, edited once. Its menu is five levels deep (Navigation › menu item › column › link › badge) to prove drill-in and breadcrumb truncation. A menu item `kind` decides whether it is a plain link or opens a mega panel of columns plus a featured card, so a link row never carries panel data. Sticky, transparent and the mobile style sit in a Behaviour tab; the logo and its screen-reader text share a Brand fieldset.","htmlSignals":["A <header> on every page holding <nav aria-label=\"Main\"> with a logo <a><img></a> and a <ul> of menu items","A <button aria-expanded aria-controls> that opens a hidden panel, beside plain <a> items that just link","Panel columns: a title <p> over a <ul> of links, each link a label <span>, an optional description <span> and an optional badge (\"New\")","Sign-in link and a primary button at the end of the bar"],"mcp":"Site chrome is the Layout, never a page section: get_layout, then update_layout scope 'global' with `add-field` commands on the reserved `navigation` Section (ifMatch from get_layout), set-field-value for the rows, publish_layout. Read the Section's existing logo, link and button fields first and add only what is missing. If the header is a component placed with `add-component`, each prop needs a `target` into your own blockJson: items → a `table` prop (ui { itemLabel: 'Menu item', preview: { title: 'label' } }) whose rows hold label, link (a `link` value), kind (select) and ONE nested `table` of links with `column` as a text sub-field; cta → a `button` prop on a button block's `props`. logo/logoText → ui.group 'brand'; sticky/transparent/mobile → ui.tab 'Behaviour'.","pitfalls":["Two list levels is the cap for a component's props and the depth chrome is proven to bind at: store menu → links and put the column title on each link row. The kit's third level (columns) and the badge object do not fit; flatten the badge to a scalar on the link row.","A nav placed on a page as a section renders nowhere, and a page that draws its own header gets no Layout copy on top: remove page-level copies.","The worked example keeps `sticky`, colours and height in code; expose the Behaviour props only if the template reads them."],"notCarried":["ai","category","kind:global","variants"],"component":{"name":"Navigation","slug":"navigation","props":[{"key":"logo","label":"Logo","type":"image","ui":{"tab":{"id":"content","label":"Content"},"group":{"id":"brand","label":"Brand","kind":"group","columns":1}}},{"key":"logoText","label":"Logo text","type":"text","helpText":"Read by screen readers. Shown if the logo can't load.","ui":{"tab":{"id":"content"},"group":{"id":"brand"}}},{"key":"items","label":"Menu items","type":"table","config":{"minItems":1,"maxItems":7,"fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":20}},{"key":"kind","label":"Opens","type":"select","defaultValue":"link","options":[{"label":"A page","value":"link"},{"label":"Mega menu","value":"mega"}],"ui":{"control":"segmented"}},{"key":"link","label":"Links to","type":"link","showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"link"}]}},{"key":"columns","label":"Columns","type":"table","showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"mega"}]},"fields":[{"key":"title","label":"Column title","type":"text","required":true,"config":{"maxLength":24}},{"key":"links","label":"Links","type":"table","fields":[{"key":"label","label":"Label","type":"text","required":true,"config":{"maxLength":32}},{"key":"link","label":"Links to","type":"link","required":true},{"key":"description","label":"Description","type":"text","recommendedMax":60},{"key":"icon","label":"Icon","type":"icon"},{"key":"badge","label":"Badge","type":"object","openMode":"drill","fields":[{"key":"label","label":"Label","type":"text","placeholder":"New","config":{"maxLength":14}},{"key":"color","label":"Colour","type":"color"},{"key":"startsAt","label":"Show from","type":"datetime"},{"key":"endsAt","label":"Hide after","type":"datetime"}],"ui":{"icon":"badge"}}],"config":{"maxItems":8},"ui":{"itemLabel":"Link","itemName":"subLink","icon":"link","preview":{"title":"label","subtitle":"description","fallback":"Untitled link"}}}],"config":{"maxItems":4},"ui":{"itemLabel":"Column","icon":"columns-3","preview":{"title":"title","fallback":"Untitled column"}}},{"key":"featured","label":"Featured card","type":"object","showIf":{"match":"all","rules":[{"field":"kind","op":"eq","value":"mega"}]},"openMode":"drill","fields":[{"key":"image","label":"Image","type":"image","required":true},{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":24}},{"key":"title","label":"Title","type":"text","required":true,"recommendedMax":40},{"key":"body","label":"Body","type":"longtext","recommendedMax":110,"ui":{"rows":2}},{"key":"cta","label":"Button","type":"button"}],"ui":{"icon":"layout-cards"}}]},"ui":{"itemLabel":"Menu item","itemName":"menuItem","icon":"menu","preview":{"title":"label","subtitle":"kind","fallback":"Untitled item"},"tab":{"id":"content"}}},{"key":"cta","label":"Main button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"sticky","label":"Stick to the top","type":"boolean","ui":{"tab":{"id":"behaviour","label":"Behaviour"}}},{"key":"transparent","label":"Transparent over heroes","type":"boolean","ui":{"tab":{"id":"behaviour"}}},{"key":"mobile","label":"Mobile menu","type":"select","config":{"options":[{"label":"Drawer","value":"drawer","description":"Slides in from the right"},{"label":"Full screen","value":"fullscreen","description":"Covers the page, big type"}]},"ui":{"control":"radio","tab":{"id":"behaviour"}}}],"description":"Logo, menu, mega panels and the main button. Synced across every page.","category":"navbar","icon":"layout-navbar"}},
|
|
1504
|
+
{"id":"core-tabs","name":"Tabs","category":"Core examples","kind":"block","tags":["slots","polymorphic-array","repeater","variants"],"proves":"Array of tabs, each a slot of blocks · block picker · drilling into a nested block","why":"Tabs are a list (2–6), and each tab is a label, an optional icon and a SLOT of blocks, so a panel can hold text, images, features, stats, quotes, CTA cards, product cards or video in any order. The allow list keeps panels to small content blocks. Which tab opens first and whether the URL remembers it are settings; underline, pills and side tabs are layout variants.","htmlSignals":["A role=\"tablist\" of <button role=\"tab\" aria-controls aria-selected>, often each with an icon <i>","One role=\"tabpanel\" per tab, all but the first `hidden`","Panels that hold their own rows of cards, images and stats rather than one paragraph"],"mcp":"A band one page owns can stay a `section` block holding a `tabs` block {tabs:[{id,label,children}]} (worked example 8). As a component: create_component with a sectionType naming the family (e.g. 'Tabs'); each prop needs a `target` into your own blockJson. tabs → ONE `table` prop (ui { itemLabel: 'Tab', preview: { title: 'label' }, icon: 'layout-list' }) with config.fields label (text, required), icon (`icon`) and content: a `slot` sub-field with config.multiple, maxItems 6 and config.componentIds; point the table at your tabs block the way §14's slider table targets props.slides. heading → text prop; defaultTab → number with config.min 1, max 6. Underline, Pills and Side tabs are three components sharing sectionType 'Tabs' with the same prop keys.","pitfalls":["The kit's slot allow list names kit blocks, not components: config.componentIds must list published components of this project.","Slot children are `{ componentId, overrides? }` with no `_variant`: a variant is its own component.","Give every block, tab and instance a stable `id`; changing one orphans the edits made against it."],"notCarried":["ai","category","variants"],"component":{"name":"Tabs","slug":"tabs","props":[{"key":"heading","label":"Heading","type":"text","recommendedMax":56},{"key":"tabs","label":"Tabs","type":"table","config":{"minItems":2,"maxItems":6,"fields":[{"key":"label","label":"Tab label","type":"text","required":true,"config":{"maxLength":24}},{"key":"icon","label":"Icon","type":"icon"},{"key":"content","label":"Content","type":"slot","config":{"multiple":true,"maxItems":6,"allow":["textBlock","imageBlock","featureBlock","statBlock","quoteBlock","ctaCard","productCard","videoBlock"]}}]},"ui":{"itemLabel":"Tab","icon":"layout-list","preview":{"title":"label","fallback":"Untitled tab"}}},{"key":"defaultTab","label":"Open first","type":"number","config":{"min":1,"max":6,"step":1}},{"key":"syncUrl","label":"Remember tab in the URL","type":"boolean","advanced":true}],"description":"Tabbed panels, each holding its own blocks","category":"section","icon":"layout-list","preview":{"title":"heading"}}},
|
|
1505
|
+
{"id":"core-pricing","name":"Pricing","category":"Core examples","kind":"block","tags":["repeater","conditional","commerce","variants","validation"],"proves":"Tiers → features · conditional Contact sales when price is custom · inline small items","why":"Tiers are same-shaped rows (1–4). A tier's priceType decides what shows: a fixed tier shows the amount and billing interval, a custom tier swaps in a Contact sales button, so a tier never shows a price and 'Contact sales' at once. Features are tiny rows (label, included, tooltip) that open inline. The comparison table, currency, the monthly/yearly switch and its discount are Display settings; Three tiers, Two tiers and Comparison table are variants.","htmlSignals":["A grid of plan <article>s, each with an <h3> name, a short audience line, a price, a <ul class=\"checks\"> and one CTA <a>","One card flagged with a ribbon or modifier class (\"Most popular\")","A price <span> with data-monthly / data-yearly next to a role=\"radiogroup\" billing toggle (\"Save 20%\")","A price in words (\"Let's talk\") linking to contact sales instead of an amount","A <table class=\"compare\"> with one column per plan and check / cross icons labelled by aria-label"],"mcp":"create_component with sectionType 'Pricing' and category 'conversion'; it lands as a draft, then publish_component. Each prop needs a `target` into your own blockJson: tiers → ONE `table` prop targeting props.rows of a `section` block whose children mirror one row, ui { itemLabel: 'Tier', preview: { title: 'name' }, icon: 'tiers' }; its config.fields hold name, tagline, priceType (select, ui.control 'segmented'), price (number) and interval with `showIf` priceType eq 'fixed', contactSales and cta as `button` sub-fields, featured (boolean) and features as a nested `table` of scalars. comparison → a second `table` prop, one row per feature and one key per tier. currency/toggle/discount → ui.tab 'Display'. Variants are sibling components sharing sectionType 'Pricing' and the same prop keys.","pitfalls":["Store numbers, never '$24/mo'. A price in words is its own text key, shown when the numbers are empty.","The nested features table is the deepest a prop goes: its rows take scalars only.","A tier added to the comparison is a new sub-field, a schema change. No Offer JSON-LD is built from prices; paste it as `schema` on update_page when needed."],"notCarried":["ai","category","variants"],"component":{"name":"Pricing","slug":"pricing","props":[{"key":"heading","label":"Heading","type":"text","required":true,"recommendedMax":48,"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"intro","label":"Intro","type":"longtext","ui":{"rows":2,"tab":{"id":"content"}}},{"key":"tiers","label":"Tiers","type":"table","config":{"minItems":1,"maxItems":4,"fields":[{"key":"name","label":"Name","type":"text","required":true,"config":{"maxLength":24}},{"key":"tagline","label":"Tagline","type":"text","recommendedMax":60},{"key":"priceType","label":"Price","type":"select","defaultValue":"fixed","options":[{"label":"Fixed","value":"fixed"},{"label":"Free","value":"free"},{"label":"Custom","value":"custom"}],"ui":{"control":"segmented"}},{"key":"price","label":"Amount","type":"number","required":true,"showIf":{"match":"all","rules":[{"field":"priceType","op":"eq","value":"fixed"}]},"config":{"unit":"USD","min":0,"step":1},"ui":{"group":{"id":"amount","label":"Amount","kind":"group","columns":2}}},{"key":"interval","label":"Billed","type":"select","showIf":{"match":"all","rules":[{"field":"priceType","op":"eq","value":"fixed"}]},"options":[{"label":"Once","value":"once"},{"label":"Per month","value":"month"},{"label":"Per year","value":"year"}],"ui":{"group":{"id":"amount"}}},{"key":"contactSales","label":"Contact sales button","type":"button","showIf":{"match":"all","rules":[{"field":"priceType","op":"eq","value":"custom"}]}},{"key":"cta","label":"Button","type":"button","showIf":{"match":"all","rules":[{"field":"priceType","op":"in","value":["fixed","free"]}]}},{"key":"features","label":"Features","type":"table","openMode":"inline","fields":[{"key":"label","label":"Feature","type":"text","required":true,"config":{"maxLength":48}},{"key":"included","label":"Included","type":"boolean","defaultValue":true},{"key":"note","label":"Tooltip","type":"text","config":{"maxLength":80}}],"config":{"maxItems":10},"ui":{"itemLabel":"Feature","icon":"circle-check","preview":{"title":"label","subtitle":"note","fallback":"Untitled feature"}}},{"key":"featured","label":"Most popular","type":"boolean"}]},"ui":{"itemLabel":"Tier","icon":"badge","preview":{"title":"name","subtitle":"tagline","fallback":"Untitled tier"},"tab":{"id":"content"}}},{"key":"comparison","label":"Comparison table","type":"object","openMode":"dialog","config":{"fields":[{"key":"title","label":"Table title","type":"text","config":{"maxLength":48}},{"key":"rows","label":"Rows","type":"table","fields":[{"key":"feature","label":"Feature","type":"text","required":true,"config":{"maxLength":48}},{"key":"note","label":"Footnote","type":"text"},{"key":"starter","label":"In Starter","type":"boolean"},{"key":"stockist","label":"In Stockist","type":"boolean"},{"key":"flagship","label":"In Flagship","type":"boolean"}],"config":{"maxItems":20},"ui":{"itemLabel":"Row","icon":"table","preview":{"title":"feature","subtitle":"note","fallback":"Untitled row"}}}]},"ui":{"icon":"table","tab":{"id":"display","label":"Display"}}},{"key":"currency","label":"Currency","type":"select","config":{"options":[{"label":"US dollar","value":"USD"},{"label":"Euro","value":"EUR"},{"label":"Pound sterling","value":"GBP"}]},"ui":{"tab":{"id":"display"}}},{"key":"toggle","label":"Monthly / yearly switch","type":"boolean","ui":{"tab":{"id":"display"}}},{"key":"discount","label":"Yearly discount","type":"number","helpText":"Steps of 5%. Type a value like 17% for anything in between.","showIf":{"match":"all","rules":[{"field":"toggle","op":"eq","value":"true"}]},"config":{"scale":{"steps":{"min":0,"max":40,"step":5},"units":["%"],"allowCustom":{"min":0,"max":90,"units":["%"]}}},"ui":{"tab":{"id":"display"}}}],"description":"Plans or bundles side by side","category":"conversion","icon":"currency-dollar","preview":{"title":"heading"}}},
|
|
1506
|
+
{"id":"core-bento","name":"Bento grid","category":"Core examples","kind":"block","tags":["slots","polymorphic-array","variants","responsive-layout","media-heavy"],"proves":"Slot with per-cell layout settings (size, surface) · variants","why":"A bento is a SLOT of 3–9 cells, and each cell is any allowed block (image, stat, quote, text, CTA card, product card, video). Size (1×1, 2×1, 1×2, 2×2) and surface (light, dark, brand) belong to the cell's place in this grid, not to the block, so they are per-slot settings rather than fields on every block. The gap is a design select; four and three columns are variants.","htmlSignals":["A grid section whose cells span different column/row counts (wide, tall and large tiles beside small ones)","Cells of mixed kinds in one grid: a photo, a big number with a label, a quote, a product card, a CTA card, a video","Some cells on a dark or brand-coloured surface while their neighbours are light"],"mcp":"create_component with a sectionType naming the family (e.g. 'Bento'); each prop needs a `target` into your own blockJson. cells → a `slot` prop with config.multiple, minItems 3, maxItems 9, config.componentIds of your published cell components, and config.slotSettings span (select 1x1/2x1/1x2/2x2, ui.control 'cards') and tone (select light/dark/brand, ui.control 'segmented'); target a container's child list (props.children on a section, props.columns.<n> on a columns block). Each child is `{ componentId, overrides? }` and its settings sit in `_settings`. eyebrow/heading → text props; gap → select with ui.control 'segmented'. 4 columns and 3 columns are two components sharing sectionType 'Bento' with the same prop keys.","pitfalls":["The kit's allow list names kit blocks; config.componentIds must name this project's published components, and the renderer needs their definitions or every cell draws empty.","A slot child has no `_variant`: a variant is its own component.","slotSettings are leaf fields only; a cell's own copy stays in the cell component's props."],"notCarried":["ai","category","variants"],"component":{"name":"Bento grid","slug":"bento","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":32}},{"key":"heading","label":"Heading","type":"text","recommendedMax":56},{"key":"cells","label":"Cells","type":"slot","config":{"multiple":true,"minItems":3,"maxItems":9,"allow":["imageBlock","statBlock","quoteBlock","textBlock","ctaCard","productCard","videoBlock"],"slotSettings":[{"key":"span","label":"Size","type":"select","defaultValue":"1x1","options":[{"label":"Small","value":"1x1","icon":"layout-grid","description":"1 × 1"},{"label":"Wide","value":"2x1","icon":"columns-2","description":"2 × 1"},{"label":"Tall","value":"1x2","icon":"layout-sidebar","description":"1 × 2"},{"label":"Large","value":"2x2","icon":"layout-cards","description":"2 × 2"}],"ui":{"control":"cards"}},{"key":"tone","label":"Surface","type":"select","defaultValue":"light","options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}],"ui":{"control":"segmented"}}]}},{"key":"gap","label":"Gap","type":"select","defaultValue":"md","config":{"options":[{"label":"S","value":"sm"},{"label":"M","value":"md"},{"label":"L","value":"lg"}]},"ui":{"control":"segmented"}}],"description":"A mosaic of cells — images, stats, quotes, products","category":"section","icon":"layout-dashboard","preview":{"title":"heading"}}},
|
|
1507
|
+
{"id":"core-journalStory","name":"Journal story","category":"Core examples","kind":"entry","tags":["rich-text","references","media-heavy","seo-structured","localized","validation","embed"],"proves":"An entry: groups, fieldsets, references, rich text, SEO limits, changed-since-publish","why":"A story is an ENTRY, one of many, with its own URL: content in groups (Content, Media, Settings) and fieldsets (Byline, a collapsed Gear review, Publishing). Authors and topics are references to their own collections, not copied names; the reviewed product is a reference to commerce. The body is the one long rich-text field. Layout, accent, internal tags, related stories and developer-only custom CSS are Settings. It proves references, rich text, SEO-friendly limits (title, excerpt) and changed-since-publish.","htmlSignals":["An <article> (often itemtype BlogPosting) with an <h1>, a dek <p>, a byline with <a rel=\"author\"> and <time datetime>","A cover <figure> with <img> and <figcaption> above a prose body of <h2>, <p>, lists, figures, tables and code","A topic link above the title and a reading time (\"7 min read\") in the byline","One of many pages under a shared route such as /blog/<slug>"],"mcp":"Not a component: create_content_model for the stories (a collection with urlPattern '/blog/:slug' and one dynamic page), and create_content_entry per story. Find or create the author and topic entries first and reference them by id. title, slug, excerpt, cover, gallery and film are fields; body is the ONE `document` field (top level, at most one per schema); dates are ISO 8601 with their offset. Publish authors and topics, then the post, then the template page. The `component` shape below is props-shaped only for comparison; if you place a story teaser as a component, each prop needs a `target` into your own blockJson.","pitfalls":["Never model SEO fields: entry meta (metaTitle, metaDescription, og, canonical) goes in update_content_entry `meta`, and BlogPosting markup is automatic for a collection with a body, a date and an author.","Reading time is computed by the template; no field stores it today, so don't copy \"7 min read\" into readingTime.","Bind the body wrapper as kind `document`, never `richtext`, or a save repaints the article from plain text."],"notCarried":["ai","kind:entry (props)","shopify reference"],"component":{"name":"Story","slug":"journal-story","props":[{"key":"title","label":"Title","type":"text","required":true,"recommendedMax":70,"localized":true,"config":{"maxLength":90},"ui":{"tab":{"id":"content","label":"Content","icon":"file-text"}}},{"key":"slug","label":"Slug","type":"slug","required":true,"ui":{"tab":{"id":"content"}}},{"key":"excerpt","label":"Excerpt","type":"longtext","recommendedMax":160,"localized":true,"ui":{"rows":2,"tab":{"id":"content"}}},{"key":"authors","label":"Authors","type":"multi-reference","config":{"min":1,"max":3,"contentModelId":"col_authors"},"ui":{"tab":{"id":"content"},"group":{"id":"byline","label":"Byline","kind":"group","description":"Who wrote it and what it's about."}}},{"key":"topics","label":"Topics","type":"multi-reference","config":{"max":4,"contentModelId":"col_topics"},"ui":{"tab":{"id":"content"},"group":{"id":"byline"}}},{"key":"readingTime","label":"Reading time","type":"number","config":{"unit":"min","min":1,"max":60},"ui":{"tab":{"id":"content"},"group":{"id":"byline"}}},{"key":"body","label":"Body","type":"richtext","required":true,"localized":true,"ui":{"tab":{"id":"content"}}},{"key":"product","label":"Reviewed product","type":"text","readOnly":true,"lockReason":"A Shopify product id. No Shopify integration backs a reference in BetterCMS yet, so the id is kept as it is.","ui":{"tab":{"id":"content"},"group":{"id":"review","label":"Gear review","kind":"group","collapsed":true,"description":"Only for stories that review a product."}}},{"key":"score","label":"Our score","type":"rating","config":{"step":0.1},"ui":{"tab":{"id":"content"},"group":{"id":"review"}}},{"key":"verdict","label":"Verdict","type":"text","recommendedMax":80,"ui":{"tab":{"id":"content"},"group":{"id":"review"}}},{"key":"cta","label":"Call to action","type":"button","ui":{"tab":{"id":"content"}}},{"key":"cover","label":"Cover image","type":"image","helpText":"1600 × 900 or larger. Also used on the social card.","required":true,"ui":{"tab":{"id":"media","label":"Media","icon":"photo"}}},{"key":"gallery","label":"Gallery","type":"array","config":{"maxItems":12,"itemType":"image"},"ui":{"itemName":"photo","itemTitle":"Photo","tab":{"id":"media"}}},{"key":"film","label":"Film","type":"video","ui":{"tab":{"id":"media"}}},{"key":"location","label":"Where it happened","type":"location","ui":{"tab":{"id":"media"}}},{"key":"publishedAt","label":"Publish date","type":"datetime","ui":{"tab":{"id":"settings","label":"Settings","icon":"settings"},"group":{"id":"publishing","label":"Publishing","kind":"group","columns":2}}},{"key":"featured","label":"Featured on the Journal","type":"boolean","ui":{"tab":{"id":"settings"},"group":{"id":"publishing"}}},{"key":"layout","label":"Layout","type":"select","config":{"options":[{"label":"Standard","value":"standard","icon":"file-text","description":"Text column, inline images"},{"label":"Wide","value":"wide","icon":"layout-columns","description":"Full-width images"},{"label":"Photo essay","value":"essay","icon":"library-photo","description":"Images first, short captions"}]},"ui":{"control":"cards","tab":{"id":"settings"}}},{"key":"accent","label":"Accent colour","type":"color","ui":{"tab":{"id":"settings"}}},{"key":"tags","label":"Internal tags","type":"tags","helpText":"Only your team sees these.","config":{"max":6},"ui":{"tab":{"id":"settings"}}},{"key":"related","label":"Related stories","type":"select","config":{"options":[{"label":"Automatic","value":"auto","description":"Same topics, newest first"},{"label":"Pick them","value":"manual","description":"Choose up to three"},{"label":"Don't show","value":"none"}]},"ui":{"control":"radio","tab":{"id":"settings"}}},{"key":"customCss","label":"Custom CSS","type":"code","advanced":true,"ui":{"access":{"view":"schema.view","edit":"schema.manage"},"tab":{"id":"settings"}}}],"description":"A long-form Journal story","icon":"news","preview":{"title":"title","subtitle":"excerpt","media":"cover"}}},
|
|
1508
|
+
{"id":"core-hero","name":"Hero","category":"Core examples","kind":"block","tags":["variants","media-heavy","localized","a11y"],"proves":"Buttons as one unit · inline proof object · cards, segmented and slider controls","why":"The opening band: an eyebrow, a headline (required, 90 max, 60 recommended), supporting text, two buttons and a proof line of rating plus label. Each button is one unit, so label, link and look travel together. The proof is a small object because the stars are drawn from the number. Media (a required image, an optional background video) and Design (alignment, theme, overlay, fill-the-screen) are their own tabs, so copy, media and look never mix; split, centered and full-bleed are variants.","htmlSignals":["The first <section> after the header with one <h1>, a short eyebrow <p> above it and a lede <p> below","Two links styled as buttons (btn-primary / btn-ghost), sometimes with an arrow icon","A <picture>/<img> beside the copy (split) or absolutely positioned behind it with a dark scrim <div> (full-bleed)","A rating line: star glyphs with aria-label=\"Rated 4.8 out of 5\" and \"from 310 reviews\""],"mcp":"create_component with sectionType 'Hero' and category 'hero'; root is a `section` block with `style`, content in props.children. Each prop needs a `target` into your own blockJson: eyebrow/body → text props on text blocks (props.html), heading → props.text of a level-1 heading block, primary/secondary → one `button` prop each (label, link and look in one unit) on a button block's `props`, image → an `image` prop on an image block's props.src plus a text prop on props.alt, video → a video block's props.url, proof → an `object` prop with config.fields rating (number) and label (text). Put image/video in ui.tab 'Media' and align/theme/overlay/fullHeight in ui.tab 'Design' (theme with ui.control 'segmented'). Split image, Centered and Full-bleed image are three components sharing sectionType 'Hero' with the same prop keys.","pitfalls":["Overlay and height have no section token today: the scrim and spacing stay in the template's classes unless the template reads those props.","object-position is the image's focal point (50% 35% → { x: 0.5, y: 0.35 }); store the number 4.8, never the star glyphs.","A variant that renames a key (title vs heading) silently drops that content on a swap."],"notCarried":["ai","variants"],"component":{"name":"Hero","slug":"hero","props":[{"key":"eyebrow","label":"Eyebrow","type":"text","config":{"maxLength":40},"ui":{"tab":{"id":"content","label":"Content"}}},{"key":"heading","label":"Headline","type":"text","required":true,"recommendedMax":60,"localized":true,"config":{"maxLength":90},"ui":{"tab":{"id":"content"}}},{"key":"body","label":"Supporting text","type":"longtext","recommendedMax":180,"localized":true,"ui":{"rows":3,"tab":{"id":"content"}}},{"key":"primary","label":"Primary button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"secondary","label":"Secondary button","type":"button","ui":{"tab":{"id":"content"}}},{"key":"proof","label":"Proof line","type":"object","config":{"fields":[{"key":"rating","label":"Rating","type":"number","config":{"scale":{"steps":{"min":0,"max":5,"step":0.1}}}},{"key":"label","label":"Label","type":"text","placeholder":"from 2,400 reviews"}]},"ui":{"icon":"star","tab":{"id":"content"}}},{"key":"image","label":"Image","type":"image","required":true,"ui":{"tab":{"id":"media","label":"Media"}}},{"key":"video","label":"Background video","type":"video","ui":{"tab":{"id":"media"}}},{"key":"align","label":"Text alignment","type":"select","config":{"options":[{"label":"Left","value":"left","icon":"layout-sidebar","description":"Reads like a story"},{"label":"Centered","value":"center","icon":"layout-columns","description":"Short, punchy lines"}]},"ui":{"control":"cards","tab":{"id":"design","label":"Design"}}},{"key":"theme","label":"Theme","type":"select","config":{"options":[{"label":"Light","value":"light"},{"label":"Dark","value":"dark"},{"label":"Brand","value":"brand"}]},"ui":{"control":"segmented","tab":{"id":"design"}}},{"key":"overlay","label":"Image overlay","type":"number","config":{"scale":{"steps":{"min":0,"max":80,"step":5},"units":["%"]}},"ui":{"tab":{"id":"design"}}},{"key":"fullHeight","label":"Fill the screen","type":"boolean","ui":{"tab":{"id":"design"}}}],"description":"Headline, supporting copy, actions and media","category":"hero","icon":"template","preview":{"title":"heading","subtitle":"eyebrow","media":"image"}}}
|
|
1509
|
+
]
|
|
1510
|
+
}
|