@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.
Files changed (587) hide show
  1. package/README.md +33 -0
  2. package/dist/adapters/dock-values.d.ts +28 -0
  3. package/dist/adapters/dock-values.js +41 -0
  4. package/dist/adapters/dock-values.js.map +1 -0
  5. package/{adapters → dist/adapters}/icons.js +2 -2
  6. package/dist/adapters/icons.js.map +1 -0
  7. package/{adapters → dist/adapters}/lossy.d.ts +90 -31
  8. package/dist/adapters/lossy.js +414 -0
  9. package/dist/adapters/lossy.js.map +1 -0
  10. package/{adapters → dist/adapters}/prod-types.d.ts +82 -4
  11. package/{adapters → dist/adapters}/prod-types.js +71 -3
  12. package/dist/adapters/prod-types.js.map +1 -0
  13. package/{adapters → dist/adapters}/schema.d.ts +40 -1
  14. package/{adapters → dist/adapters}/schema.js +996 -179
  15. package/dist/adapters/schema.js.map +1 -0
  16. package/{adapters → dist/adapters}/values.d.ts +26 -5
  17. package/{adapters → dist/adapters}/values.js +87 -17
  18. package/dist/adapters/values.js.map +1 -0
  19. package/dist/brand/logo.js +96 -0
  20. package/dist/brand/logo.js.map +1 -0
  21. package/{context.d.ts → dist/context.d.ts} +92 -2
  22. package/{context.js → dist/context.js} +5 -2
  23. package/dist/context.js.map +1 -0
  24. package/{controls → dist/controls}/button.d.ts +19 -5
  25. package/{controls → dist/controls}/button.js +125 -54
  26. package/dist/controls/button.js.map +1 -0
  27. package/{controls → dist/controls}/c1.js +1 -1
  28. package/dist/controls/c1.js.map +1 -0
  29. package/{controls → dist/controls}/c2.js +1 -1
  30. package/dist/controls/c2.js.map +1 -0
  31. package/{controls → dist/controls}/choice.d.ts +7 -2
  32. package/{controls → dist/controls}/choice.js +71 -18
  33. package/dist/controls/choice.js.map +1 -0
  34. package/{controls → dist/controls}/code-register.js +1 -1
  35. package/dist/controls/code-register.js.map +1 -0
  36. package/{controls → dist/controls}/code.js +10 -9
  37. package/dist/controls/code.js.map +1 -0
  38. package/{controls → dist/controls}/color.js +8 -8
  39. package/dist/controls/color.js.map +1 -0
  40. package/{controls → dist/controls}/component-services.d.ts +42 -0
  41. package/dist/controls/component-services.js +7 -0
  42. package/dist/controls/component-services.js.map +1 -0
  43. package/{controls → dist/controls}/date-register.js +1 -1
  44. package/dist/controls/date-register.js.map +1 -0
  45. package/{controls → dist/controls}/date-zone.js +8 -2
  46. package/dist/controls/date-zone.js.map +1 -0
  47. package/{controls → dist/controls}/date.js +32 -24
  48. package/dist/controls/date.js.map +1 -0
  49. package/{controls → dist/controls}/form-picker-register.js +1 -1
  50. package/dist/controls/form-picker-register.js.map +1 -0
  51. package/{controls → dist/controls}/form-picker.js +11 -9
  52. package/dist/controls/form-picker.js.map +1 -0
  53. package/{controls → dist/controls}/icon-register.js +1 -1
  54. package/dist/controls/icon-register.js.map +1 -0
  55. package/{controls → dist/controls}/icon.d.ts +4 -2
  56. package/{controls → dist/controls}/icon.js +32 -27
  57. package/dist/controls/icon.js.map +1 -0
  58. package/{controls → dist/controls}/index.d.ts +1 -1
  59. package/{controls → dist/controls}/index.js +4 -3
  60. package/dist/controls/index.js.map +1 -0
  61. package/{controls → dist/controls}/json-register.js +1 -1
  62. package/dist/controls/json-register.js.map +1 -0
  63. package/{controls → dist/controls}/json.js +13 -3
  64. package/dist/controls/json.js.map +1 -0
  65. package/{controls → dist/controls}/link.js +6 -6
  66. package/dist/controls/link.js.map +1 -0
  67. package/{controls → dist/controls}/location-register.js +1 -1
  68. package/dist/controls/location-register.js.map +1 -0
  69. package/{controls → dist/controls}/location.js +64 -40
  70. package/dist/controls/location.js.map +1 -0
  71. package/{controls → dist/controls}/media-register.js +1 -1
  72. package/dist/controls/media-register.js.map +1 -0
  73. package/{controls → dist/controls}/media-services.d.ts +6 -0
  74. package/dist/controls/media-services.js +0 -0
  75. package/{controls → dist/controls}/media.d.ts +35 -4
  76. package/{controls → dist/controls}/media.js +491 -171
  77. package/dist/controls/media.js.map +1 -0
  78. package/{controls → dist/controls}/number.js +10 -9
  79. package/dist/controls/number.js.map +1 -0
  80. package/{controls → dist/controls}/reference-register.js +1 -1
  81. package/dist/controls/reference-register.js.map +1 -0
  82. package/{controls → dist/controls}/reference-services.d.ts +4 -0
  83. package/dist/controls/reference-services.js +0 -0
  84. package/{controls → dist/controls}/reference.d.ts +1 -1
  85. package/{controls → dist/controls}/reference.js +93 -77
  86. package/dist/controls/reference.js.map +1 -0
  87. package/dist/controls/rich-text/blocks.d.ts +49 -0
  88. package/{controls → dist/controls}/rich-text/blocks.js +471 -144
  89. package/dist/controls/rich-text/blocks.js.map +1 -0
  90. package/{controls → dist/controls}/rich-text/catalog.d.ts +4 -1
  91. package/{controls → dist/controls}/rich-text/catalog.js +75 -65
  92. package/dist/controls/rich-text/catalog.js.map +1 -0
  93. package/{controls → dist/controls}/rich-text/codec.d.ts +9 -1
  94. package/{controls → dist/controls}/rich-text/codec.js +3 -1
  95. package/dist/controls/rich-text/codec.js.map +1 -0
  96. package/{controls → dist/controls}/rich-text/component-card.d.ts +1 -1
  97. package/{controls → dist/controls}/rich-text/component-card.js +417 -99
  98. package/dist/controls/rich-text/component-card.js.map +1 -0
  99. package/dist/controls/rich-text/component-panel.d.ts +47 -0
  100. package/dist/controls/rich-text/component-panel.js +141 -0
  101. package/dist/controls/rich-text/component-panel.js.map +1 -0
  102. package/{controls → dist/controls}/rich-text/control.d.ts +1 -2
  103. package/{controls → dist/controls}/rich-text/control.js +84 -42
  104. package/dist/controls/rich-text/control.js.map +1 -0
  105. package/{controls → dist/controls}/rich-text/dom.d.ts +52 -9
  106. package/{controls → dist/controls}/rich-text/dom.js +379 -50
  107. package/dist/controls/rich-text/dom.js.map +1 -0
  108. package/{controls → dist/controls}/rich-text/editor.d.ts +7 -0
  109. package/{controls → dist/controls}/rich-text/editor.js +148 -48
  110. package/dist/controls/rich-text/editor.js.map +1 -0
  111. package/{controls → dist/controls}/rich-text/floating.d.ts +6 -1
  112. package/{controls → dist/controls}/rich-text/floating.js +35 -18
  113. package/dist/controls/rich-text/floating.js.map +1 -0
  114. package/{controls → dist/controls}/rich-text/index.js +1 -1
  115. package/dist/controls/rich-text/index.js.map +1 -0
  116. package/dist/controls/rich-text/link-form.js +147 -0
  117. package/dist/controls/rich-text/link-form.js.map +1 -0
  118. package/{controls → dist/controls}/rich-text/markdown.js +4 -2
  119. package/dist/controls/rich-text/markdown.js.map +1 -0
  120. package/{controls → dist/controls}/rich-text/ops.js +17 -15
  121. package/dist/controls/rich-text/ops.js.map +1 -0
  122. package/dist/controls/rich-text/plain.d.ts +17 -0
  123. package/dist/controls/rich-text/plain.js +44 -0
  124. package/dist/controls/rich-text/plain.js.map +1 -0
  125. package/{controls → dist/controls}/rich-text/toolbar.d.ts +6 -2
  126. package/{controls → dist/controls}/rich-text/toolbar.js +12 -7
  127. package/dist/controls/rich-text/toolbar.js.map +1 -0
  128. package/{controls → dist/controls}/scale.js +5 -5
  129. package/dist/controls/scale.js.map +1 -0
  130. package/{controls → dist/controls}/slot-register.js +1 -1
  131. package/dist/controls/slot-register.js.map +1 -0
  132. package/{controls → dist/controls}/slot-services.d.ts +25 -0
  133. package/dist/controls/slot-services.js +155 -0
  134. package/dist/controls/slot-services.js.map +1 -0
  135. package/{controls → dist/controls}/slot.js +42 -24
  136. package/dist/controls/slot.js.map +1 -0
  137. package/{controls → dist/controls}/table/index.js +1 -1
  138. package/dist/controls/table/index.js.map +1 -0
  139. package/{controls → dist/controls}/table/prod.js +3 -3
  140. package/dist/controls/table/prod.js.map +1 -0
  141. package/{controls → dist/controls}/table/refs.js +13 -2
  142. package/dist/controls/table/refs.js.map +1 -0
  143. package/{controls → dist/controls}/table/stored.js +1 -1
  144. package/dist/controls/table/stored.js.map +1 -0
  145. package/{controls → dist/controls}/table/table-editor.js +51 -48
  146. package/dist/controls/table/table-editor.js.map +1 -0
  147. package/{controls → dist/controls}/table/table-link.js +3 -3
  148. package/dist/controls/table/table-link.js.map +1 -0
  149. package/{controls → dist/controls}/table/table.js +3 -3
  150. package/dist/controls/table/table.js.map +1 -0
  151. package/{controls → dist/controls}/text.d.ts +2 -2
  152. package/{controls → dist/controls}/text.js +78 -43
  153. package/dist/controls/text.js.map +1 -0
  154. package/dist/controls/unknown-value.d.ts +17 -0
  155. package/dist/controls/unknown-value.js +66 -0
  156. package/dist/controls/unknown-value.js.map +1 -0
  157. package/dist/core/core.d.ts +63 -0
  158. package/dist/core/core.js +171 -0
  159. package/dist/core/core.js.map +1 -0
  160. package/dist/core/facets.d.ts +347 -0
  161. package/dist/core/facets.js +334 -0
  162. package/dist/core/facets.js.map +1 -0
  163. package/dist/core/index.d.ts +3 -0
  164. package/dist/core/index.js +3 -0
  165. package/{engine → dist/engine}/array.js +34 -21
  166. package/dist/engine/array.js.map +1 -0
  167. package/{engine → dist/engine}/bits.js +1 -1
  168. package/dist/engine/bits.js.map +1 -0
  169. package/{engine → dist/engine}/breadcrumb.d.ts +3 -3
  170. package/{engine → dist/engine}/breadcrumb.js +12 -36
  171. package/dist/engine/breadcrumb.js.map +1 -0
  172. package/{engine → dist/engine}/expand.d.ts +2 -4
  173. package/{engine → dist/engine}/expand.js +5 -22
  174. package/dist/engine/expand.js.map +1 -0
  175. package/{engine → dist/engine}/field-list.js +1 -1
  176. package/dist/engine/field-list.js.map +1 -0
  177. package/{engine → dist/engine}/field-shell.js +249 -121
  178. package/dist/engine/field-shell.js.map +1 -0
  179. package/{engine → dist/engine}/frame.d.ts +3 -1
  180. package/{engine → dist/engine}/frame.js +16 -14
  181. package/dist/engine/frame.js.map +1 -0
  182. package/{engine → dist/engine}/group-summary.d.ts +2 -1
  183. package/{engine → dist/engine}/group-summary.js +7 -3
  184. package/dist/engine/group-summary.js.map +1 -0
  185. package/{engine → dist/engine}/grouping.d.ts +19 -5
  186. package/{engine → dist/engine}/grouping.js +193 -21
  187. package/dist/engine/grouping.js.map +1 -0
  188. package/{engine → dist/engine}/item-list.d.ts +11 -1
  189. package/{engine → dist/engine}/item-list.js +35 -13
  190. package/dist/engine/item-list.js.map +1 -0
  191. package/{engine → dist/engine}/kit-field.js +45 -10
  192. package/dist/engine/kit-field.js.map +1 -0
  193. package/{engine → dist/engine}/schema-form.d.ts +27 -2
  194. package/{engine → dist/engine}/schema-form.js +182 -40
  195. package/dist/engine/schema-form.js.map +1 -0
  196. package/{engine → dist/engine}/slot.js +5 -15
  197. package/dist/engine/slot.js.map +1 -0
  198. package/dist/index.js +47 -0
  199. package/dist/index.js.map +1 -0
  200. package/{kit.css → dist/kit.css} +19 -0
  201. package/{lib → dist/lib}/cn.js +7 -4
  202. package/dist/lib/cn.js.map +1 -0
  203. package/{lib → dist/lib}/columns.js +4 -1
  204. package/dist/lib/columns.js.map +1 -0
  205. package/{lib → dist/lib}/frames.js +2 -2
  206. package/dist/lib/frames.js.map +1 -0
  207. package/dist/lib/icon-image.d.ts +10 -0
  208. package/dist/lib/icon-image.js +8 -0
  209. package/dist/lib/icon-image.js.map +1 -0
  210. package/{lib → dist/lib}/icons.js +4 -3
  211. package/dist/lib/icons.js.map +1 -0
  212. package/dist/lib/input-modality.d.ts +11 -0
  213. package/dist/lib/input-modality.js +47 -0
  214. package/dist/lib/input-modality.js.map +1 -0
  215. package/dist/lib/key-event.d.ts +11 -0
  216. package/dist/lib/key-event.js +16 -0
  217. package/dist/lib/key-event.js.map +1 -0
  218. package/{lib → dist/lib}/path.js +4 -2
  219. package/dist/lib/path.js.map +1 -0
  220. package/{lib → dist/lib}/schema.d.ts +34 -3
  221. package/{lib → dist/lib}/schema.js +110 -13
  222. package/dist/lib/schema.js.map +1 -0
  223. package/{lib → dist/lib}/utils.js +1 -1
  224. package/dist/lib/utils.js.map +1 -0
  225. package/{lib → dist/lib}/validate.d.ts +11 -1
  226. package/{lib → dist/lib}/validate.js +85 -11
  227. package/dist/lib/validate.js.map +1 -0
  228. package/dist/library/analysis.d.ts +23 -0
  229. package/dist/library/analysis.js +160 -0
  230. package/dist/library/analysis.js.map +1 -0
  231. package/dist/library/catalog.d.ts +57 -0
  232. package/dist/library/catalog.js +307 -0
  233. package/dist/library/catalog.js.map +1 -0
  234. package/dist/library/coreNotes.d.ts +8 -0
  235. package/dist/library/coreNotes.js +111 -0
  236. package/dist/library/coreNotes.js.map +1 -0
  237. package/dist/library/coverage.d.ts +40 -0
  238. package/dist/library/coverage.js +59 -0
  239. package/dist/library/coverage.js.map +1 -0
  240. package/dist/library/entries/advanced.entries.d.ts +2 -0
  241. package/dist/library/entries/advanced.entries.js +6579 -0
  242. package/dist/library/entries/advanced.entries.js.map +1 -0
  243. package/dist/library/entries/commerce.entries.d.ts +2 -0
  244. package/dist/library/entries/commerce.entries.js +4140 -0
  245. package/dist/library/entries/commerce.entries.js.map +1 -0
  246. package/dist/library/entries/company.entries.d.ts +2 -0
  247. package/dist/library/entries/company.entries.js +2731 -0
  248. package/dist/library/entries/company.entries.js.map +1 -0
  249. package/dist/library/entries/conversion.entries.d.ts +2 -0
  250. package/dist/library/entries/conversion.entries.js +3707 -0
  251. package/dist/library/entries/conversion.entries.js.map +1 -0
  252. package/dist/library/entries/data.entries.d.ts +7 -0
  253. package/dist/library/entries/data.entries.js +4613 -0
  254. package/dist/library/entries/data.entries.js.map +1 -0
  255. package/dist/library/entries/events.entries.d.ts +2 -0
  256. package/dist/library/entries/events.entries.js +3428 -0
  257. package/dist/library/entries/events.entries.js.map +1 -0
  258. package/dist/library/entries/features.entries.d.ts +2 -0
  259. package/dist/library/entries/features.entries.js +3157 -0
  260. package/dist/library/entries/features.entries.js.map +1 -0
  261. package/dist/library/entries/heroes.entries.d.ts +2 -0
  262. package/dist/library/entries/heroes.entries.js +3401 -0
  263. package/dist/library/entries/heroes.entries.js.map +1 -0
  264. package/dist/library/entries/index.d.ts +3 -0
  265. package/dist/library/entries/index.js +35 -0
  266. package/dist/library/entries/index.js.map +1 -0
  267. package/dist/library/entries/listings.entries.d.ts +2 -0
  268. package/dist/library/entries/listings.entries.js +2976 -0
  269. package/dist/library/entries/listings.entries.js.map +1 -0
  270. package/dist/library/entries/longform.entries.d.ts +2 -0
  271. package/dist/library/entries/longform.entries.js +4227 -0
  272. package/dist/library/entries/longform.entries.js.map +1 -0
  273. package/dist/library/entries/media.entries.d.ts +2 -0
  274. package/dist/library/entries/media.entries.js +2781 -0
  275. package/dist/library/entries/media.entries.js.map +1 -0
  276. package/dist/library/entries/navigation.entries.d.ts +2 -0
  277. package/dist/library/entries/navigation.entries.js +4011 -0
  278. package/dist/library/entries/navigation.entries.js.map +1 -0
  279. package/dist/library/entries/pricing.entries.d.ts +2 -0
  280. package/dist/library/entries/pricing.entries.js +2383 -0
  281. package/dist/library/entries/pricing.entries.js.map +1 -0
  282. package/dist/library/entries/social-proof.entries.d.ts +2 -0
  283. package/dist/library/entries/social-proof.entries.js +2297 -0
  284. package/dist/library/entries/social-proof.entries.js.map +1 -0
  285. package/dist/library/examples/bento.d.ts +92 -0
  286. package/dist/library/examples/bento.js +201 -0
  287. package/dist/library/examples/bento.js.map +1 -0
  288. package/dist/library/examples/blocks.d.ts +14 -0
  289. package/dist/library/examples/blocks.js +313 -0
  290. package/dist/library/examples/blocks.js.map +1 -0
  291. package/dist/library/examples/faq.d.ts +28 -0
  292. package/dist/library/examples/faq.js +230 -0
  293. package/dist/library/examples/faq.js.map +1 -0
  294. package/dist/library/examples/hero.d.ts +43 -0
  295. package/dist/library/examples/hero.js +222 -0
  296. package/dist/library/examples/hero.js.map +1 -0
  297. package/dist/library/examples/index.d.ts +36 -0
  298. package/dist/library/examples/index.js +109 -0
  299. package/dist/library/examples/index.js.map +1 -0
  300. package/dist/library/examples/journalStory.d.ts +93 -0
  301. package/dist/library/examples/journalStory.js +338 -0
  302. package/dist/library/examples/journalStory.js.map +1 -0
  303. package/dist/library/examples/megaMenu.d.ts +134 -0
  304. package/dist/library/examples/megaMenu.js +442 -0
  305. package/dist/library/examples/megaMenu.js.map +1 -0
  306. package/dist/library/examples/pricing.d.ts +103 -0
  307. package/dist/library/examples/pricing.js +479 -0
  308. package/dist/library/examples/pricing.js.map +1 -0
  309. package/dist/library/examples/tabs.d.ts +87 -0
  310. package/dist/library/examples/tabs.js +180 -0
  311. package/dist/library/examples/tabs.js.map +1 -0
  312. package/dist/library/export.d.ts +42 -0
  313. package/dist/library/export.js +80 -0
  314. package/dist/library/export.js.map +1 -0
  315. package/dist/library/index.d.ts +13 -0
  316. package/dist/library/index.js +6 -0
  317. package/dist/library/kit-catalog.json +1510 -0
  318. package/dist/library/types.d.ts +42 -0
  319. package/dist/library/types.js +0 -0
  320. package/{schema → dist/schema}/adapters.js +1 -2
  321. package/dist/schema/adapters.js.map +1 -0
  322. package/{schema → dist/schema}/help.d.ts +2 -0
  323. package/{schema → dist/schema}/help.js +6 -4
  324. package/dist/schema/help.js.map +1 -0
  325. package/{schema → dist/schema}/scale.js +1 -1
  326. package/dist/schema/scale.js.map +1 -0
  327. package/{schema → dist/schema}/sources.js +1 -1
  328. package/dist/schema/sources.js.map +1 -0
  329. package/{schema → dist/schema}/table.js +11 -10
  330. package/dist/schema/table.js.map +1 -0
  331. package/{schema → dist/schema}/types.d.ts +29 -1
  332. package/{schema → dist/schema}/types.js +1 -1
  333. package/dist/schema/types.js.map +1 -0
  334. package/dist/styles.css +2 -0
  335. package/dist/table/table-view.d.ts +14 -0
  336. package/dist/table/table-view.js +140 -0
  337. package/dist/table/table-view.js.map +1 -0
  338. package/dist/table/table.css +73 -0
  339. package/dist/testing/corpus.d.ts +63 -0
  340. package/dist/testing/corpus.js +129 -0
  341. package/dist/testing/corpus.js.map +1 -0
  342. package/dist/testing/test-form.d.ts +11 -0
  343. package/dist/testing/test-form.js +38 -0
  344. package/dist/testing/test-form.js.map +1 -0
  345. package/dist/theme.css +345 -0
  346. package/dist/theme.d.ts +22 -0
  347. package/dist/theme.js +43 -0
  348. package/dist/theme.js.map +1 -0
  349. package/dist/tokens.css +542 -0
  350. package/dist/types.js +0 -0
  351. package/{ui → dist/ui}/action-tray.d.ts +1 -1
  352. package/{ui → dist/ui}/action-tray.js +20 -19
  353. package/dist/ui/action-tray.js.map +1 -0
  354. package/dist/ui/autocomplete.d.ts +30 -0
  355. package/dist/ui/autocomplete.js +139 -0
  356. package/dist/ui/autocomplete.js.map +1 -0
  357. package/{ui → dist/ui}/button.d.ts +3 -3
  358. package/{ui → dist/ui}/button.js +6 -7
  359. package/dist/ui/button.js.map +1 -0
  360. package/{ui → dist/ui}/choice-switch.js +1 -1
  361. package/dist/ui/choice-switch.js.map +1 -0
  362. package/{ui → dist/ui}/controls.js +51 -39
  363. package/dist/ui/controls.js.map +1 -0
  364. package/{ui → dist/ui}/display.d.ts +10 -3
  365. package/{ui → dist/ui}/display.js +57 -46
  366. package/dist/ui/display.js.map +1 -0
  367. package/{ui → dist/ui}/expanded-editor-modal.d.ts +2 -2
  368. package/{ui → dist/ui}/expanded-editor-modal.js +6 -6
  369. package/dist/ui/expanded-editor-modal.js.map +1 -0
  370. package/{ui → dist/ui}/field-context.js +1 -1
  371. package/dist/ui/field-context.js.map +1 -0
  372. package/{ui → dist/ui}/floating-panel.d.ts +1 -1
  373. package/{ui → dist/ui}/floating-panel.js +28 -12
  374. package/dist/ui/floating-panel.js.map +1 -0
  375. package/{ui → dist/ui}/index.d.ts +4 -0
  376. package/dist/ui/index.js +18 -0
  377. package/{ui → dist/ui}/input.d.ts +1 -1
  378. package/{ui → dist/ui}/input.js +9 -9
  379. package/dist/ui/input.js.map +1 -0
  380. package/{ui → dist/ui}/mode-bar.js +3 -3
  381. package/dist/ui/mode-bar.js.map +1 -0
  382. package/{ui → dist/ui}/overlay.d.ts +77 -5
  383. package/dist/ui/overlay.js +697 -0
  384. package/dist/ui/overlay.js.map +1 -0
  385. package/{ui → dist/ui}/path-crumbs.js +37 -3
  386. package/dist/ui/path-crumbs.js.map +1 -0
  387. package/{ui → dist/ui}/presence.js +2 -2
  388. package/dist/ui/presence.js.map +1 -0
  389. package/{ui → dist/ui}/select.js +89 -87
  390. package/dist/ui/select.js.map +1 -0
  391. package/{ui → dist/ui}/shortcut-hint.js +1 -1
  392. package/dist/ui/shortcut-hint.js.map +1 -0
  393. package/dist/ui/spinner.d.ts +24 -0
  394. package/dist/ui/spinner.js +42 -0
  395. package/dist/ui/spinner.js.map +1 -0
  396. package/dist/ui/split-button.d.ts +32 -0
  397. package/dist/ui/split-button.js +60 -0
  398. package/dist/ui/split-button.js.map +1 -0
  399. package/{ui → dist/ui}/surfaces.js +1 -1
  400. package/dist/ui/surfaces.js.map +1 -0
  401. package/dist/ui/tab.d.ts +38 -0
  402. package/dist/ui/tab.js +87 -0
  403. package/dist/ui/tab.js.map +1 -0
  404. package/dist/ui/toast.d.ts +20 -0
  405. package/dist/ui/toast.js +115 -0
  406. package/dist/ui/toast.js.map +1 -0
  407. package/{ui → dist/ui}/tooltip-root.d.ts +4 -3
  408. package/{ui → dist/ui}/tooltip-root.js +9 -8
  409. package/dist/ui/tooltip-root.js.map +1 -0
  410. package/{ui → dist/ui}/variants.js +1 -1
  411. package/dist/ui/variants.js.map +1 -0
  412. package/package.json +77 -23
  413. package/adapters/icons.js.map +0 -1
  414. package/adapters/lossy.js +0 -233
  415. package/adapters/lossy.js.map +0 -1
  416. package/adapters/prod-types.js.map +0 -1
  417. package/adapters/schema.js.map +0 -1
  418. package/adapters/values.js.map +0 -1
  419. package/brand/logo.js +0 -9
  420. package/brand/logo.js.map +0 -1
  421. package/context.js.map +0 -1
  422. package/controls/button.js.map +0 -1
  423. package/controls/c1.js.map +0 -1
  424. package/controls/c2.js.map +0 -1
  425. package/controls/choice.js.map +0 -1
  426. package/controls/code-register.js.map +0 -1
  427. package/controls/code.js.map +0 -1
  428. package/controls/color.js.map +0 -1
  429. package/controls/date-register.js.map +0 -1
  430. package/controls/date-zone.js.map +0 -1
  431. package/controls/date.js.map +0 -1
  432. package/controls/form-picker-register.js.map +0 -1
  433. package/controls/form-picker.js.map +0 -1
  434. package/controls/icon-register.js.map +0 -1
  435. package/controls/icon.js.map +0 -1
  436. package/controls/index.js.map +0 -1
  437. package/controls/json-register.js.map +0 -1
  438. package/controls/json.js.map +0 -1
  439. package/controls/link.js.map +0 -1
  440. package/controls/location-register.js.map +0 -1
  441. package/controls/location.js.map +0 -1
  442. package/controls/media-register.js.map +0 -1
  443. package/controls/media.js.map +0 -1
  444. package/controls/number.js.map +0 -1
  445. package/controls/reference-register.js.map +0 -1
  446. package/controls/reference.js.map +0 -1
  447. package/controls/rich-text/blocks.d.ts +0 -28
  448. package/controls/rich-text/blocks.js.map +0 -1
  449. package/controls/rich-text/catalog.js.map +0 -1
  450. package/controls/rich-text/codec.js.map +0 -1
  451. package/controls/rich-text/component-card.js.map +0 -1
  452. package/controls/rich-text/control.js.map +0 -1
  453. package/controls/rich-text/dom.js.map +0 -1
  454. package/controls/rich-text/editor.js.map +0 -1
  455. package/controls/rich-text/floating.js.map +0 -1
  456. package/controls/rich-text/index.js.map +0 -1
  457. package/controls/rich-text/link-form.js +0 -111
  458. package/controls/rich-text/link-form.js.map +0 -1
  459. package/controls/rich-text/markdown.js.map +0 -1
  460. package/controls/rich-text/ops.js.map +0 -1
  461. package/controls/rich-text/toolbar.js.map +0 -1
  462. package/controls/scale.js.map +0 -1
  463. package/controls/slot-register.js.map +0 -1
  464. package/controls/slot-services.js +0 -38
  465. package/controls/slot-services.js.map +0 -1
  466. package/controls/slot.js.map +0 -1
  467. package/controls/table/index.js.map +0 -1
  468. package/controls/table/prod.js.map +0 -1
  469. package/controls/table/refs.js.map +0 -1
  470. package/controls/table/stored.js.map +0 -1
  471. package/controls/table/table-editor.js.map +0 -1
  472. package/controls/table/table-link.js.map +0 -1
  473. package/controls/table/table.js.map +0 -1
  474. package/controls/text.js.map +0 -1
  475. package/engine/array.js.map +0 -1
  476. package/engine/bits.js.map +0 -1
  477. package/engine/breadcrumb.js.map +0 -1
  478. package/engine/expand.js.map +0 -1
  479. package/engine/field-list.js.map +0 -1
  480. package/engine/field-shell.js.map +0 -1
  481. package/engine/frame.js.map +0 -1
  482. package/engine/group-summary.js.map +0 -1
  483. package/engine/grouping.js.map +0 -1
  484. package/engine/item-list.js.map +0 -1
  485. package/engine/kit-field.js.map +0 -1
  486. package/engine/schema-form.js.map +0 -1
  487. package/engine/slot.js.map +0 -1
  488. package/index.js +0 -55
  489. package/index.js.map +0 -1
  490. package/lib/cn.js.map +0 -1
  491. package/lib/columns.js.map +0 -1
  492. package/lib/frames.js.map +0 -1
  493. package/lib/icons.js.map +0 -1
  494. package/lib/path.js.map +0 -1
  495. package/lib/schema.js.map +0 -1
  496. package/lib/utils.js.map +0 -1
  497. package/lib/validate.js.map +0 -1
  498. package/schema/adapters.js.map +0 -1
  499. package/schema/help.js.map +0 -1
  500. package/schema/scale.js.map +0 -1
  501. package/schema/sources.js.map +0 -1
  502. package/schema/table.js.map +0 -1
  503. package/schema/types.js.map +0 -1
  504. package/ui/action-tray.js.map +0 -1
  505. package/ui/button.js.map +0 -1
  506. package/ui/choice-switch.js.map +0 -1
  507. package/ui/controls.js.map +0 -1
  508. package/ui/display.js.map +0 -1
  509. package/ui/expanded-editor-modal.js.map +0 -1
  510. package/ui/field-context.js.map +0 -1
  511. package/ui/floating-panel.js.map +0 -1
  512. package/ui/index.js +0 -13
  513. package/ui/input.js.map +0 -1
  514. package/ui/mode-bar.js.map +0 -1
  515. package/ui/overlay.js +0 -378
  516. package/ui/overlay.js.map +0 -1
  517. package/ui/path-crumbs.js.map +0 -1
  518. package/ui/presence.js.map +0 -1
  519. package/ui/select.js.map +0 -1
  520. package/ui/shortcut-hint.js.map +0 -1
  521. package/ui/surfaces.js.map +0 -1
  522. package/ui/tooltip-root.js.map +0 -1
  523. package/ui/variants.js.map +0 -1
  524. /package/{_virtual → dist/_virtual}/_rolldown/runtime.js +0 -0
  525. /package/{adapters → dist/adapters}/icons.d.ts +0 -0
  526. /package/{brand → dist/brand}/logo.d.ts +0 -0
  527. /package/{controls → dist/controls}/c1.d.ts +0 -0
  528. /package/{controls → dist/controls}/c2.d.ts +0 -0
  529. /package/{controls → dist/controls}/code-register.d.ts +0 -0
  530. /package/{controls → dist/controls}/code.d.ts +0 -0
  531. /package/{controls → dist/controls}/color.d.ts +0 -0
  532. /package/{controls → dist/controls}/date-register.d.ts +0 -0
  533. /package/{controls → dist/controls}/date-zone.d.ts +0 -0
  534. /package/{controls → dist/controls}/date.d.ts +0 -0
  535. /package/{controls → dist/controls}/form-picker-register.d.ts +0 -0
  536. /package/{controls → dist/controls}/form-picker.d.ts +0 -0
  537. /package/{controls → dist/controls}/icon-register.d.ts +0 -0
  538. /package/{controls → dist/controls}/json-register.d.ts +0 -0
  539. /package/{controls → dist/controls}/json.d.ts +0 -0
  540. /package/{controls → dist/controls}/link.d.ts +0 -0
  541. /package/{controls → dist/controls}/location-register.d.ts +0 -0
  542. /package/{controls → dist/controls}/location.d.ts +0 -0
  543. /package/{controls → dist/controls}/media-register.d.ts +0 -0
  544. /package/{controls → dist/controls}/number.d.ts +0 -0
  545. /package/{controls → dist/controls}/reference-register.d.ts +0 -0
  546. /package/{controls → dist/controls}/rich-text/index.d.ts +0 -0
  547. /package/{controls → dist/controls}/rich-text/link-form.d.ts +0 -0
  548. /package/{controls → dist/controls}/rich-text/markdown.d.ts +0 -0
  549. /package/{controls → dist/controls}/rich-text/ops.d.ts +0 -0
  550. /package/{controls → dist/controls}/scale.d.ts +0 -0
  551. /package/{controls → dist/controls}/slot-register.d.ts +0 -0
  552. /package/{controls → dist/controls}/slot.d.ts +0 -0
  553. /package/{controls → dist/controls}/table/index.d.ts +0 -0
  554. /package/{controls → dist/controls}/table/prod.d.ts +0 -0
  555. /package/{controls → dist/controls}/table/refs.d.ts +0 -0
  556. /package/{controls → dist/controls}/table/stored.d.ts +0 -0
  557. /package/{controls → dist/controls}/table/table-editor.d.ts +0 -0
  558. /package/{controls → dist/controls}/table/table-link.d.ts +0 -0
  559. /package/{controls → dist/controls}/table/table.d.ts +0 -0
  560. /package/{engine → dist/engine}/array.d.ts +0 -0
  561. /package/{engine → dist/engine}/bits.d.ts +0 -0
  562. /package/{engine → dist/engine}/field-list.d.ts +0 -0
  563. /package/{engine → dist/engine}/field-shell.d.ts +0 -0
  564. /package/{engine → dist/engine}/kit-field.d.ts +0 -0
  565. /package/{engine → dist/engine}/slot.d.ts +0 -0
  566. /package/{index.d.ts → dist/index.d.ts} +0 -0
  567. /package/{lib → dist/lib}/cn.d.ts +0 -0
  568. /package/{lib → dist/lib}/columns.d.ts +0 -0
  569. /package/{lib → dist/lib}/frames.d.ts +0 -0
  570. /package/{lib → dist/lib}/icons.d.ts +0 -0
  571. /package/{lib → dist/lib}/path.d.ts +0 -0
  572. /package/{lib → dist/lib}/utils.d.ts +0 -0
  573. /package/{schema → dist/schema}/adapters.d.ts +0 -0
  574. /package/{schema → dist/schema}/scale.d.ts +0 -0
  575. /package/{schema → dist/schema}/sources.d.ts +0 -0
  576. /package/{schema → dist/schema}/table.d.ts +0 -0
  577. /package/{types.d.ts → dist/types.d.ts} +0 -0
  578. /package/{ui → dist/ui}/choice-switch.d.ts +0 -0
  579. /package/{ui → dist/ui}/controls.d.ts +0 -0
  580. /package/{ui → dist/ui}/field-context.d.ts +0 -0
  581. /package/{ui → dist/ui}/mode-bar.d.ts +0 -0
  582. /package/{ui → dist/ui}/path-crumbs.d.ts +0 -0
  583. /package/{ui → dist/ui}/presence.d.ts +0 -0
  584. /package/{ui → dist/ui}/select.d.ts +0 -0
  585. /package/{ui → dist/ui}/shortcut-hint.d.ts +0 -0
  586. /package/{ui → dist/ui}/surfaces.d.ts +0 -0
  587. /package/{ui → dist/ui}/variants.d.ts +0 -0
@@ -0,0 +1,4227 @@
1
+ //#region src/library/entries/longform.entries.ts
2
+ var pic = (seed, w = 1600, h = 1e3) => `https://picsum.photos/seed/${seed}/${w}/${h}`;
3
+ var image = (seed, alt, w = 1600, h = 1e3) => ({
4
+ url: pic(seed, w, h),
5
+ alt,
6
+ name: `${seed}.jpg`,
7
+ width: w * 2,
8
+ height: h * 2
9
+ });
10
+ var CONTENT = {
11
+ name: "content",
12
+ title: "Content",
13
+ icon: "file-text",
14
+ default: true
15
+ };
16
+ var MEDIA = {
17
+ name: "media",
18
+ title: "Media",
19
+ icon: "photo"
20
+ };
21
+ var SETTINGS = {
22
+ name: "settings",
23
+ title: "Settings",
24
+ icon: "settings"
25
+ };
26
+ var DESIGN = {
27
+ name: "design",
28
+ title: "Design",
29
+ icon: "palette"
30
+ };
31
+ /** Rich-text body islands, in the editor's stored format. */
32
+ var rt = {
33
+ figure: (seed, alt, caption) => `<figure data-rt="image" data-align="full" data-size="full"><img src="${pic(seed)}" alt="${alt}"><figcaption>${caption}</figcaption></figure>`,
34
+ callout: (tone, title, body) => `<div data-rt="callout" data-callout="${tone}"><p data-title="true">${title}</p><div data-body="true"><p>${body}</p></div></div>`,
35
+ video: (url) => `<div data-rt="video" data-embed="video" data-url="${url}"><a href="${url}">${url}</a></div>`,
36
+ toggle: (q, a) => `<details data-rt="toggle"><summary>${q}</summary><div data-body="true"><p>${a}</p></div></details>`,
37
+ pullquote: (quote, name, role) => `<figure data-rt="pullquote"><blockquote><p>${quote}</p></blockquote><figcaption><span data-name>${name}</span>, <span data-role>${role}</span></figcaption></figure>`,
38
+ takeaways: (title, points) => `<aside data-rt="takeaways"><p data-title>${title}</p><ul>${points.map((p) => `<li>${p}</li>`).join("")}</ul></aside>`,
39
+ fn: (n, note) => `<sup data-rt="fn" id="fnref-${n}" data-note="${note}"><a href="#fn-${n}" aria-label="Footnote ${n}">${n}</a></sup>`,
40
+ footnotes: (notes) => `<ol class="cs-footnotes" data-rt="footnotes" aria-label="Footnotes">${notes.map((n, i) => `<li id="fn-${i + 1}">${n} <a href="#fnref-${i + 1}" aria-label="Back to reference ${i + 1}">↩</a></li>`).join("")}</ol>`,
41
+ component: (type, name, id, overrides) => `<div data-rt="component" data-block="${type}" data-ref="${type}" data-id="${id}" data-name="${name}"${overrides ? ` data-overrides="${JSON.stringify(overrides).replace(/&/g, "&amp;").replace(/"/g, "&quot;")}"` : ""}>⧉&nbsp;${name} · component</div>`
42
+ };
43
+ var refs = (name, title, to, extra = {}) => ({
44
+ name,
45
+ title,
46
+ type: "array",
47
+ of: [{
48
+ name: name.replace(/s$/, ""),
49
+ title: title.replace(/s$/, ""),
50
+ type: "reference",
51
+ reference: { to }
52
+ }],
53
+ ...extra
54
+ });
55
+ var post = {
56
+ id: "rich-blog-post",
57
+ label: "Rich blog post",
58
+ category: "Long-form & docs",
59
+ brand: "Caldera",
60
+ previewLayout: "article",
61
+ tags: [
62
+ "rich-text",
63
+ "references",
64
+ "media-heavy",
65
+ "embed",
66
+ "conditional",
67
+ "validation",
68
+ "seo-structured",
69
+ "localized"
70
+ ],
71
+ proves: "One rich-text body holds key takeaways, callouts, tables, code, images, video, pull quotes, toggles, footnotes and linked components · sponsor disclosure is conditional",
72
+ docTitle: "The 30-minute campfire loaf",
73
+ urlPrefix: "caldera.bettercms.site/journal/",
74
+ schema: {
75
+ name: "journalPost",
76
+ title: "Journal post",
77
+ kind: "entry",
78
+ icon: "news",
79
+ category: "Long-form & docs",
80
+ description: "A Journal story with a rich body",
81
+ preview: {
82
+ title: "title",
83
+ subtitle: "dek",
84
+ media: "coverImage"
85
+ },
86
+ groups: [
87
+ CONTENT,
88
+ MEDIA,
89
+ SETTINGS
90
+ ],
91
+ fieldsets: [
92
+ {
93
+ name: "byline",
94
+ title: "Byline"
95
+ },
96
+ {
97
+ name: "dates",
98
+ title: "Dates"
99
+ },
100
+ {
101
+ name: "sponsor",
102
+ title: "Sponsorship",
103
+ collapsed: true
104
+ }
105
+ ],
106
+ fields: [
107
+ {
108
+ name: "title",
109
+ title: "Title",
110
+ type: "string",
111
+ group: "content",
112
+ validation: {
113
+ required: true,
114
+ max: 90,
115
+ recommendedMax: 70
116
+ },
117
+ ai: true,
118
+ localized: true,
119
+ description: "The page's only H1."
120
+ },
121
+ {
122
+ name: "slug",
123
+ title: "Slug",
124
+ type: "slug",
125
+ group: "content",
126
+ validation: { required: true }
127
+ },
128
+ {
129
+ name: "dek",
130
+ title: "Dek",
131
+ type: "text",
132
+ rows: 2,
133
+ group: "content",
134
+ validation: {
135
+ required: true,
136
+ recommendedMax: 160
137
+ },
138
+ ai: true,
139
+ localized: true,
140
+ description: "Also shown on cards."
141
+ },
142
+ refs("authors", "Authors", ["col_authors"], {
143
+ group: "content",
144
+ fieldset: "byline",
145
+ validation: {
146
+ min: 1,
147
+ max: 3
148
+ },
149
+ description: "The first author leads the byline."
150
+ }),
151
+ refs("topics", "Topics", ["col_topics"], {
152
+ group: "content",
153
+ fieldset: "byline",
154
+ validation: {
155
+ min: 1,
156
+ max: 4
157
+ },
158
+ description: "The first topic is the label on cards."
159
+ }),
160
+ {
161
+ name: "publishedAt",
162
+ title: "Publish date",
163
+ type: "datetime",
164
+ group: "content",
165
+ fieldset: "dates",
166
+ validation: { required: true }
167
+ },
168
+ {
169
+ name: "updatedAt",
170
+ title: "Updated",
171
+ type: "datetime",
172
+ group: "content",
173
+ fieldset: "dates",
174
+ description: "Only for real corrections or updates."
175
+ },
176
+ {
177
+ name: "readingTime",
178
+ title: "Reading time",
179
+ type: "number",
180
+ group: "content",
181
+ fieldset: "dates",
182
+ readOnly: true,
183
+ numeric: { unit: "min" },
184
+ description: "Worked out from the body word count."
185
+ },
186
+ {
187
+ name: "body",
188
+ title: "Body",
189
+ type: "richtext",
190
+ group: "content",
191
+ validation: { required: true },
192
+ ai: true,
193
+ localized: true,
194
+ description: "Type / for images, tables, callouts, code, video and components.",
195
+ helpMore: "H2 and H3 build the table of contents. Components stay shared with Blocks; a field edited here changes only this place."
196
+ },
197
+ {
198
+ name: "closingButton",
199
+ title: "Closing button",
200
+ type: "button",
201
+ group: "content"
202
+ },
203
+ {
204
+ name: "coverImage",
205
+ title: "Cover image",
206
+ type: "image",
207
+ group: "media",
208
+ validation: { required: true },
209
+ description: "1600 × 900 or larger. Used on cards and when shared."
210
+ },
211
+ {
212
+ name: "coverCaption",
213
+ title: "Cover caption",
214
+ type: "string",
215
+ group: "media",
216
+ validation: { max: 140 }
217
+ },
218
+ {
219
+ name: "isSponsored",
220
+ title: "Sponsored",
221
+ type: "boolean",
222
+ group: "settings",
223
+ fieldset: "sponsor",
224
+ description: "Adds a \"Paid partnership\" label and marks partner links sponsored."
225
+ },
226
+ {
227
+ name: "sponsorName",
228
+ title: "Partner",
229
+ type: "string",
230
+ group: "settings",
231
+ fieldset: "sponsor",
232
+ hidden: {
233
+ field: "isSponsored",
234
+ equals: true
235
+ },
236
+ validation: {
237
+ required: true,
238
+ max: 40
239
+ }
240
+ },
241
+ {
242
+ name: "sponsorLink",
243
+ title: "Partner link",
244
+ type: "link",
245
+ group: "settings",
246
+ fieldset: "sponsor",
247
+ hidden: {
248
+ field: "isSponsored",
249
+ equals: true
250
+ },
251
+ initialValue: {
252
+ type: "url",
253
+ href: "",
254
+ rel: "sponsored",
255
+ newTab: true
256
+ }
257
+ },
258
+ {
259
+ name: "isFeatured",
260
+ title: "Featured",
261
+ type: "boolean",
262
+ group: "settings"
263
+ },
264
+ {
265
+ name: "showContents",
266
+ title: "Show table of contents",
267
+ type: "boolean",
268
+ group: "settings",
269
+ initialValue: true
270
+ },
271
+ {
272
+ name: "related",
273
+ title: "Related stories",
274
+ type: "select",
275
+ group: "settings",
276
+ layout: "radio",
277
+ initialValue: "auto",
278
+ options: [
279
+ {
280
+ value: "auto",
281
+ label: "Automatic",
282
+ description: "Same topics, newest first"
283
+ },
284
+ {
285
+ value: "manual",
286
+ label: "Pick them"
287
+ },
288
+ {
289
+ value: "none",
290
+ label: "Don't show"
291
+ }
292
+ ],
293
+ description: ""
294
+ },
295
+ refs("relatedPosts", "Stories", ["col_journal", "col_recipes"], {
296
+ group: "settings",
297
+ hidden: {
298
+ field: "related",
299
+ equals: "manual"
300
+ },
301
+ validation: {
302
+ min: 1,
303
+ max: 3,
304
+ unique: true
305
+ }
306
+ }),
307
+ {
308
+ name: "layout",
309
+ title: "Layout",
310
+ type: "select",
311
+ group: "settings",
312
+ layout: "cards",
313
+ initialValue: "standard",
314
+ options: [
315
+ {
316
+ value: "standard",
317
+ label: "Standard",
318
+ icon: "file-text",
319
+ description: "Text column, inline images"
320
+ },
321
+ {
322
+ value: "wide",
323
+ label: "Wide",
324
+ icon: "layout-columns",
325
+ description: "Full-width images"
326
+ },
327
+ {
328
+ value: "essay",
329
+ label: "Photo essay",
330
+ icon: "library-photo",
331
+ description: "Images first, short captions"
332
+ }
333
+ ],
334
+ description: ""
335
+ }
336
+ ]
337
+ },
338
+ value: {
339
+ title: "The 30-minute campfire loaf",
340
+ slug: "30-minute-campfire-loaf",
341
+ dek: "No starter, no oven, no proofing basket. A Dutch oven, a bag of coals and a dough you mix at lunch.",
342
+ authors: ["en_a2"],
343
+ topics: [
344
+ "tp_recipes",
345
+ "tp_camp",
346
+ "tp_cast-iron"
347
+ ],
348
+ publishedAt: "2026-09-24T09:00:00-07:00[America/Los_Angeles]",
349
+ updatedAt: "2026-09-30T16:20:00-07:00[America/Los_Angeles]",
350
+ readingTime: 7,
351
+ body: [
352
+ "<p>Bread is the thing people assume you can't make outside. You can, and it's easier over coals than in most home ovens: a cast-iron Dutch oven is a steam oven with a lid.</p>",
353
+ rt.takeaways("Key takeaways", [
354
+ "Mix at lunch, bake at dusk: four hours does the proofing for you.",
355
+ "Two-thirds of the coals go on the lid.",
356
+ "Lift the lid once, at 20 minutes."
357
+ ]),
358
+ "<h2>What you need</h2>",
359
+ `<table data-rt="table" data-header="true"><caption>Gear for one 900 g loaf</caption><thead><tr><th scope="col">Item</th><th scope="col">Why</th></tr></thead><tbody><tr><td>5.5 qt Dutch oven</td><td>Holds steam for the first 15 minutes</td></tr><tr><td>18 charcoal briquettes</td><td>About 220°C with 12 on the lid, 6 under</td></tr><tr><td>Parchment sling</td><td>Lowers the dough in without burnt knuckles</td></tr></tbody></table>`,
360
+ rt.callout("tip", "Count your coals", "Two-thirds on the lid, one-third underneath. Bread burns from the bottom first."),
361
+ "<h2>Mix at lunch, bake at dusk</h2>",
362
+ "<p>Stir 500 g flour, 360 g warm water, 10 g salt and 4 g instant yeast until shaggy. Cover it and forget it for four hours.</p>",
363
+ rt.figure("campfire-loaf-dough", "A shaggy dough in a steel bowl on a camp table", "Four hours later: doubled and bubbly. Photo: Daniel Okafor"),
364
+ "<h3>Shaping without a counter</h3>",
365
+ "<p>Flour the parchment, tip the dough out and fold the four edges into the middle. Flip it seam-down and let it sit while the coals catch.</p>",
366
+ rt.video("https://www.youtube.com/watch?v=Kc7sqbW3f8Y"),
367
+ rt.pullquote("Thin coals, patient lid. Lift it once at 20 minutes and never again.", "Priya Nair", "Caldera product engineering"),
368
+ rt.component("article-cta", "Inline CTA", "ci_loaf_cta", { heading: "Baking this weekend? The Ember Dutch Oven ships in two days." }),
369
+ "<h2>Bake</h2>",
370
+ `<p>Lid on for 20 minutes, lid off for 10. Knock the base: hollow means done. A probe should read 96°C.${rt.fn(1, "At the centre of the loaf, with an instant-read probe pushed in from the side.")}</p>`,
371
+ "<pre><code>Lid on 20 min ~220°C 12 coals top, 6 under\nLid off 10 min ~200°C 6 coals under only</code></pre>",
372
+ rt.callout("warning", "Never on a picnic table", "A loaded Dutch oven over coals scorches wood. Use the Cable Fold grate or bare ground."),
373
+ rt.toggle("Can I use whole wheat?", "Swap up to 150 g. Add 20 g more water, the bran drinks it."),
374
+ rt.toggle("What if it rains?", "The lid keeps rain off the dough. Keep the coals under a tarp until you need them."),
375
+ rt.component("product-card", "Product card", "ci_loaf_dutch", { badge: "Used here" }),
376
+ rt.footnotes(["At the centre of the loaf, with an instant-read probe pushed in from the side."])
377
+ ].join(""),
378
+ closingButton: {
379
+ label: "Shop the Dutch oven",
380
+ href: "/products/ember-dutch-oven",
381
+ type: "entry",
382
+ entryId: "en_p2",
383
+ title: "Ember Dutch Oven 5.5qt",
384
+ style: "primary",
385
+ arrow: true
386
+ },
387
+ coverImage: image("campfire-loaf", "A crusty round loaf on a parchment sling beside a smoking Dutch oven"),
388
+ coverCaption: "Loaf number three of the trip, Deschutes National Forest.",
389
+ isSponsored: false,
390
+ isFeatured: true,
391
+ showContents: true,
392
+ related: "auto",
393
+ relatedPosts: [],
394
+ layout: "standard"
395
+ },
396
+ notes: {
397
+ 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.",
398
+ sanity: "Sanity `post` document: `body: array<block | image | table | callout | codeBlock | videoEmbed | reference→ctaBlock>` (Portable Text with custom types), `authors: array<reference>`, `categories: array<reference>`. Our body stores the same islands as data-rt HTML; component instances carry ref + overrides (D28), which Portable Text can only do with a custom object type.",
399
+ htmlSignals: [
400
+ "<article> with one <h1>, a byline (rel=author link, <time datetime>) and a large hero <img>",
401
+ "Body with h2/h3, <figure>/<figcaption>, <table>, <pre><code>, <blockquote>, <details>",
402
+ "Boxed asides with an icon (Tip / Note / Warning) → callouts",
403
+ "Mid-article product or newsletter card unlike the prose → component instance",
404
+ "'Sponsored', 'In partnership with', 'Paid partnership' near the byline"
405
+ ],
406
+ 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.",
407
+ pitfalls: [
408
+ "An H1 inside the body duplicates the title — the renderer demotes it, the converter should too.",
409
+ "Copying the author box into the body instead of referencing the author.",
410
+ "Pasting a CTA card as HTML loses the link to the component — use an instance.",
411
+ "Sponsored links without rel=sponsored — set by the sponsorLink default.",
412
+ "updatedAt bumped for typo fixes makes every post look fresh and erodes trust."
413
+ ]
414
+ }
415
+ };
416
+ var authorBio = {
417
+ id: "author-bio",
418
+ label: "Author bio",
419
+ category: "Long-form & docs",
420
+ brand: "Caldera",
421
+ previewLayout: "split",
422
+ tags: [
423
+ "references",
424
+ "conditional",
425
+ "variants",
426
+ "dynamic-query",
427
+ "a11y"
428
+ ],
429
+ proves: "One reference drives photo, name, role, bio and socials · per-placement bio override · latest posts by that author",
430
+ schema: {
431
+ name: "authorBio",
432
+ title: "Author bio",
433
+ kind: "block",
434
+ icon: "user-circle",
435
+ category: "Long-form & docs",
436
+ description: "Who wrote it, with a link to everything else they wrote",
437
+ variants: [
438
+ {
439
+ id: "card",
440
+ name: "Card"
441
+ },
442
+ {
443
+ id: "inline",
444
+ name: "Inline",
445
+ description: "A slim row under the article"
446
+ },
447
+ {
448
+ id: "profile",
449
+ name: "Profile",
450
+ description: "Large photo, for author pages"
451
+ }
452
+ ],
453
+ groups: [CONTENT, DESIGN],
454
+ preview: {
455
+ title: "heading",
456
+ subtitle: "author",
457
+ icon: "user-circle"
458
+ },
459
+ fields: [
460
+ {
461
+ name: "heading",
462
+ title: "Heading",
463
+ type: "string",
464
+ group: "content",
465
+ initialValue: "About the author",
466
+ validation: { max: 40 },
467
+ description: "Leave empty on author pages."
468
+ },
469
+ {
470
+ name: "author",
471
+ title: "Author",
472
+ type: "reference",
473
+ group: "content",
474
+ reference: { to: ["col_authors"] },
475
+ description: "Leave empty on a post to use its first author."
476
+ },
477
+ {
478
+ name: "bioSource",
479
+ title: "Bio",
480
+ type: "select",
481
+ group: "content",
482
+ layout: "segmented",
483
+ initialValue: "profile",
484
+ options: [{
485
+ value: "profile",
486
+ label: "From their profile"
487
+ }, {
488
+ value: "custom",
489
+ label: "Written here"
490
+ }]
491
+ },
492
+ {
493
+ name: "bio",
494
+ title: "Bio for this placement",
495
+ type: "richtext",
496
+ group: "content",
497
+ hidden: {
498
+ field: "bioSource",
499
+ equals: "custom"
500
+ },
501
+ validation: { required: true }
502
+ },
503
+ {
504
+ name: "showSocial",
505
+ title: "Show social links",
506
+ type: "boolean",
507
+ group: "content",
508
+ initialValue: true
509
+ },
510
+ {
511
+ name: "showLatest",
512
+ title: "Show their latest posts",
513
+ type: "boolean",
514
+ group: "content"
515
+ },
516
+ {
517
+ name: "latestCount",
518
+ title: "How many",
519
+ type: "number",
520
+ group: "content",
521
+ layout: "slider",
522
+ hidden: {
523
+ field: "showLatest",
524
+ equals: true
525
+ },
526
+ numeric: {
527
+ min: 2,
528
+ max: 6,
529
+ step: 1
530
+ },
531
+ initialValue: 3,
532
+ description: ""
533
+ },
534
+ {
535
+ name: "button",
536
+ title: "Button",
537
+ type: "button",
538
+ group: "content",
539
+ placeholder: "All posts by {name}"
540
+ },
541
+ {
542
+ name: "photoShape",
543
+ title: "Photo",
544
+ type: "select",
545
+ group: "design",
546
+ layout: "segmented",
547
+ initialValue: "circle",
548
+ options: [
549
+ {
550
+ value: "circle",
551
+ label: "Circle"
552
+ },
553
+ {
554
+ value: "rounded",
555
+ label: "Rounded"
556
+ },
557
+ {
558
+ value: "none",
559
+ label: "No photo"
560
+ }
561
+ ],
562
+ description: ""
563
+ }
564
+ ]
565
+ },
566
+ value: {
567
+ _variant: "card",
568
+ heading: "About the author",
569
+ author: "en_a2",
570
+ bioSource: "profile",
571
+ bio: "",
572
+ showSocial: true,
573
+ showLatest: true,
574
+ latestCount: 3,
575
+ button: {
576
+ label: "All posts by {name}",
577
+ href: "/authors/priya-nair",
578
+ type: "entry",
579
+ entryId: "en_a2",
580
+ title: "Priya Nair",
581
+ style: "link",
582
+ arrow: true
583
+ },
584
+ photoShape: "circle"
585
+ },
586
+ notes: {
587
+ 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).",
588
+ sanity: "Sanity `author` document + an `authorBio` object with `author: reference→author`. Fallback to the post's author is done in GROQ (`coalesce(author->, ^.authors[0]->)`).",
589
+ htmlSignals: [
590
+ "A box after the article with a round headshot, name, a one-paragraph bio and social icons",
591
+ "rel=\"author\" links or links to /author/{slug}",
592
+ "'Written by', 'About the author', 'More from {name}'",
593
+ "Person JSON-LD with sameAs social URLs (native output from the author entry)"
594
+ ],
595
+ 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.",
596
+ pitfalls: [
597
+ "Typing the author's name and photo into each post — they drift on the first rebrand.",
598
+ "Headshot alt text 'image' or empty — use 'Portrait of {name}'.",
599
+ "'Latest posts' listing the current post.",
600
+ "Two authors: the box must render both or say why only the first."
601
+ ]
602
+ }
603
+ };
604
+ var codeTabs = {
605
+ name: "codeTabs",
606
+ title: "Code tabs",
607
+ kind: "block",
608
+ icon: "code",
609
+ category: "Long-form & docs",
610
+ description: "The same example in several languages",
611
+ variants: [{
612
+ id: "tabs",
613
+ name: "Tabs"
614
+ }, {
615
+ id: "stacked",
616
+ name: "Stacked",
617
+ description: "Every language visible, one after another"
618
+ }],
619
+ groups: [CONTENT, DESIGN],
620
+ preview: {
621
+ title: "title",
622
+ subtitle: "syncKey",
623
+ icon: "code"
624
+ },
625
+ fields: [
626
+ {
627
+ name: "title",
628
+ title: "Title",
629
+ type: "string",
630
+ group: "content",
631
+ validation: { max: 60 },
632
+ placeholder: "Create a journal entry"
633
+ },
634
+ {
635
+ name: "tabs",
636
+ title: "Languages",
637
+ type: "array",
638
+ group: "content",
639
+ itemNoun: "language",
640
+ validation: {
641
+ min: 1,
642
+ max: 8
643
+ },
644
+ of: [{
645
+ name: "snippet",
646
+ title: "Snippet",
647
+ type: "object",
648
+ icon: "code",
649
+ preview: {
650
+ title: "language",
651
+ subtitle: "filename"
652
+ },
653
+ fields: [
654
+ {
655
+ name: "language",
656
+ title: "Language",
657
+ type: "select",
658
+ validation: { required: true },
659
+ options: [
660
+ {
661
+ value: "curl",
662
+ label: "cURL"
663
+ },
664
+ {
665
+ value: "javascript",
666
+ label: "JavaScript"
667
+ },
668
+ {
669
+ value: "typescript",
670
+ label: "TypeScript"
671
+ },
672
+ {
673
+ value: "python",
674
+ label: "Python"
675
+ },
676
+ {
677
+ value: "ruby",
678
+ label: "Ruby"
679
+ },
680
+ {
681
+ value: "go",
682
+ label: "Go"
683
+ },
684
+ {
685
+ value: "java",
686
+ label: "Java"
687
+ },
688
+ {
689
+ value: "php",
690
+ label: "PHP"
691
+ },
692
+ {
693
+ value: "json",
694
+ label: "JSON"
695
+ }
696
+ ]
697
+ },
698
+ {
699
+ name: "label",
700
+ title: "Tab name",
701
+ type: "string",
702
+ validation: { max: 16 },
703
+ description: "Leave empty to use the language name."
704
+ },
705
+ {
706
+ name: "filename",
707
+ title: "File name",
708
+ type: "string",
709
+ validation: { max: 40 },
710
+ placeholder: "create-entry.ts",
711
+ description: ""
712
+ },
713
+ {
714
+ name: "code",
715
+ title: "Code",
716
+ type: "code",
717
+ validation: { required: true },
718
+ openMode: "dialog"
719
+ },
720
+ {
721
+ name: "highlight",
722
+ title: "Highlight lines",
723
+ type: "string",
724
+ validation: {
725
+ pattern: "^\\d+(-\\d+)?(,\\s*\\d+(-\\d+)?)*$",
726
+ patternMessage: "Use line numbers like 3, 7-9."
727
+ },
728
+ placeholder: "3, 7-9",
729
+ description: ""
730
+ }
731
+ ]
732
+ }]
733
+ },
734
+ {
735
+ name: "syncKey",
736
+ title: "Switch together",
737
+ type: "string",
738
+ group: "content",
739
+ initialValue: "sdk",
740
+ validation: {
741
+ pattern: "^[a-z0-9-]+$",
742
+ patternMessage: "Lowercase letters, numbers and hyphens only."
743
+ },
744
+ description: "Code tabs with the same key switch language together."
745
+ },
746
+ {
747
+ name: "showLineNumbers",
748
+ title: "Show line numbers",
749
+ type: "boolean",
750
+ group: "design",
751
+ description: ""
752
+ },
753
+ {
754
+ name: "showCopyButton",
755
+ title: "Show copy button",
756
+ type: "boolean",
757
+ group: "design",
758
+ initialValue: true,
759
+ description: ""
760
+ },
761
+ {
762
+ name: "maxHeight",
763
+ title: "Max height",
764
+ type: "number",
765
+ group: "design",
766
+ numeric: {
767
+ unit: "px",
768
+ min: 160,
769
+ max: 800,
770
+ step: 40
771
+ },
772
+ description: "Longer code scrolls inside the block."
773
+ }
774
+ ]
775
+ };
776
+ var codeTabsEntry = {
777
+ id: "code-tabs",
778
+ label: "Code tabs",
779
+ category: "Long-form & docs",
780
+ brand: "Ledgerline",
781
+ previewLayout: "tabs",
782
+ tags: [
783
+ "embed",
784
+ "repeater",
785
+ "validation",
786
+ "variants"
787
+ ],
788
+ proves: "One example in N languages · language is a select, not a free label · tabs sync across the page by key",
789
+ schema: codeTabs,
790
+ value: {
791
+ _variant: "tabs",
792
+ title: "Post a journal entry",
793
+ tabs: [
794
+ {
795
+ _key: "l1",
796
+ language: "curl",
797
+ filename: "",
798
+ code: "curl https://api.ledgerline.com/v2/journal-entries \\\n -H \"Authorization: Bearer $LEDGERLINE_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"date\": \"2026-09-30\",\n \"lines\": [\n { \"account\": \"6100\", \"debit\": 1250.00 },\n { \"account\": \"2000\", \"credit\": 1250.00 }\n ]\n }'",
799
+ highlight: "5-8"
800
+ },
801
+ {
802
+ _key: "l2",
803
+ language: "typescript",
804
+ label: "Node.js",
805
+ filename: "post-entry.ts",
806
+ code: "import { Ledgerline } from \"@ledgerline/node\";\n\nconst ll = new Ledgerline(process.env.LEDGERLINE_KEY!);\n\nawait ll.journalEntries.create({\n date: \"2026-09-30\",\n lines: [\n { account: \"6100\", debit: 1250 },\n { account: \"2000\", credit: 1250 },\n ],\n});",
807
+ highlight: "5-11"
808
+ },
809
+ {
810
+ _key: "l3",
811
+ language: "python",
812
+ filename: "post_entry.py",
813
+ code: "from ledgerline import Ledgerline\n\nll = Ledgerline(os.environ[\"LEDGERLINE_KEY\"])\n\nll.journal_entries.create(\n date=\"2026-09-30\",\n lines=[\n {\"account\": \"6100\", \"debit\": 1250},\n {\"account\": \"2000\", \"credit\": 1250},\n ],\n)"
814
+ }
815
+ ],
816
+ syncKey: "sdk",
817
+ showLineNumbers: true,
818
+ showCopyButton: true,
819
+ maxHeight: 420
820
+ },
821
+ notes: {
822
+ 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.",
823
+ sanity: "Sanity's @sanity/code-input gives `code` {language, code, filename, highlightedLines}; tabs are an `array<code>` inside a custom `codeGroup` object in Portable Text.",
824
+ htmlSignals: [
825
+ "A tab bar of language names (cURL, Node, Python…) above a <pre><code class=\"language-*\">",
826
+ "Several <pre> siblings where only one is visible at a time (role=tabpanel)",
827
+ "A copy-to-clipboard button in the code header",
828
+ "data-lang / data-language attributes; localStorage keys like 'preferred-language'"
829
+ ],
830
+ 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.",
831
+ pitfalls: [
832
+ "innerHTML of highlighted code carries <span> soup — store textContent only.",
833
+ "Free-text language labels ('Node 18') break highlighting — map to the select, keep the label separately.",
834
+ "API keys in sample code — replace real-looking secrets with $ENV placeholders.",
835
+ "The `code` field's built-in language list (html/css/js/liquid/json) doesn't cover API languages — the select carries it."
836
+ ]
837
+ }
838
+ };
839
+ var ENTRIES = [
840
+ post,
841
+ authorBio,
842
+ {
843
+ id: "docs-page",
844
+ label: "Docs page",
845
+ category: "Long-form & docs",
846
+ brand: "Ledgerline",
847
+ previewLayout: "article",
848
+ tags: [
849
+ "rich-text",
850
+ "references",
851
+ "global-synced",
852
+ "conditional",
853
+ "embed",
854
+ "validation"
855
+ ],
856
+ proves: "Sidebar position by reference to a nav group · auto ToC with depth · prev/next auto or manual · deprecation reveals a notice",
857
+ docTitle: "Multi-entity close",
858
+ urlPrefix: "docs.ledgerline.com/guides/",
859
+ blocks: [codeTabs],
860
+ schema: {
861
+ name: "docPage",
862
+ title: "Docs page",
863
+ kind: "entry",
864
+ icon: "book",
865
+ category: "Long-form & docs",
866
+ description: "A guide or reference page in the docs",
867
+ preview: {
868
+ title: "title",
869
+ subtitle: "summary",
870
+ icon: "book"
871
+ },
872
+ groups: [
873
+ CONTENT,
874
+ {
875
+ name: "navigation",
876
+ title: "Navigation",
877
+ icon: "list-tree"
878
+ },
879
+ SETTINGS
880
+ ],
881
+ fieldsets: [{
882
+ name: "status",
883
+ title: "Status",
884
+ description: "Beta and deprecated pages show a notice at the top."
885
+ }],
886
+ fields: [
887
+ {
888
+ name: "title",
889
+ title: "Title",
890
+ type: "string",
891
+ group: "content",
892
+ validation: {
893
+ required: true,
894
+ max: 70
895
+ },
896
+ localized: true
897
+ },
898
+ {
899
+ name: "slug",
900
+ title: "Slug",
901
+ type: "slug",
902
+ group: "content",
903
+ validation: { required: true }
904
+ },
905
+ {
906
+ name: "summary",
907
+ title: "Summary",
908
+ type: "text",
909
+ rows: 2,
910
+ group: "content",
911
+ validation: {
912
+ required: true,
913
+ recommendedMax: 160
914
+ },
915
+ localized: true
916
+ },
917
+ {
918
+ name: "body",
919
+ title: "Body",
920
+ type: "richtext",
921
+ group: "content",
922
+ validation: { required: true },
923
+ localized: true,
924
+ description: "Type / for callouts, tables, Code tabs and images.",
925
+ helpMore: "H2 and H3 build the \"On this page\" list."
926
+ },
927
+ {
928
+ name: "sidebarGroup",
929
+ title: "Sidebar group",
930
+ type: "reference",
931
+ group: "navigation",
932
+ reference: { to: ["col_doc_groups"] },
933
+ validation: { required: true },
934
+ description: "The sidebar itself is shared."
935
+ },
936
+ {
937
+ name: "sidebarLabel",
938
+ title: "Sidebar label",
939
+ type: "string",
940
+ group: "navigation",
941
+ validation: { max: 28 },
942
+ description: "Leave empty to use the title."
943
+ },
944
+ {
945
+ name: "sidebarPosition",
946
+ title: "Position",
947
+ type: "number",
948
+ group: "navigation",
949
+ numeric: {
950
+ min: 1,
951
+ step: 1
952
+ },
953
+ description: "Lower comes first."
954
+ },
955
+ {
956
+ name: "tocDepth",
957
+ title: "On this page",
958
+ type: "select",
959
+ group: "navigation",
960
+ layout: "segmented",
961
+ initialValue: "h3",
962
+ options: [
963
+ {
964
+ value: "off",
965
+ label: "Off"
966
+ },
967
+ {
968
+ value: "h2",
969
+ label: "H2 only"
970
+ },
971
+ {
972
+ value: "h3",
973
+ label: "H2 + H3"
974
+ }
975
+ ],
976
+ description: ""
977
+ },
978
+ {
979
+ name: "pager",
980
+ title: "Previous / next",
981
+ type: "select",
982
+ group: "navigation",
983
+ layout: "radio",
984
+ initialValue: "auto",
985
+ options: [
986
+ {
987
+ value: "auto",
988
+ label: "Follow the sidebar order"
989
+ },
990
+ {
991
+ value: "manual",
992
+ label: "Choose them"
993
+ },
994
+ {
995
+ value: "off",
996
+ label: "Hide"
997
+ }
998
+ ],
999
+ description: ""
1000
+ },
1001
+ {
1002
+ name: "prev",
1003
+ title: "Previous page",
1004
+ type: "reference",
1005
+ group: "navigation",
1006
+ reference: { to: ["col_docs"] },
1007
+ hidden: {
1008
+ field: "pager",
1009
+ equals: "manual"
1010
+ }
1011
+ },
1012
+ {
1013
+ name: "next",
1014
+ title: "Next page",
1015
+ type: "reference",
1016
+ group: "navigation",
1017
+ reference: { to: ["col_docs"] },
1018
+ hidden: {
1019
+ field: "pager",
1020
+ equals: "manual"
1021
+ }
1022
+ },
1023
+ {
1024
+ name: "status",
1025
+ title: "Status",
1026
+ type: "select",
1027
+ group: "settings",
1028
+ fieldset: "status",
1029
+ layout: "segmented",
1030
+ initialValue: "stable",
1031
+ options: [
1032
+ {
1033
+ value: "stable",
1034
+ label: "Stable"
1035
+ },
1036
+ {
1037
+ value: "beta",
1038
+ label: "Beta"
1039
+ },
1040
+ {
1041
+ value: "deprecated",
1042
+ label: "Deprecated"
1043
+ }
1044
+ ],
1045
+ description: ""
1046
+ },
1047
+ {
1048
+ name: "statusNote",
1049
+ title: "Notice",
1050
+ type: "richtext",
1051
+ group: "settings",
1052
+ fieldset: "status",
1053
+ hidden: {
1054
+ field: "status",
1055
+ equals: ["beta", "deprecated"]
1056
+ },
1057
+ validation: { required: true }
1058
+ },
1059
+ {
1060
+ name: "replacement",
1061
+ title: "Use instead",
1062
+ type: "reference",
1063
+ group: "settings",
1064
+ fieldset: "status",
1065
+ reference: { to: ["col_docs"] },
1066
+ hidden: {
1067
+ field: "status",
1068
+ equals: "deprecated"
1069
+ }
1070
+ },
1071
+ {
1072
+ name: "apiVersion",
1073
+ title: "API version",
1074
+ type: "select",
1075
+ group: "settings",
1076
+ options: [
1077
+ {
1078
+ value: "2026-09-01",
1079
+ label: "2026-09-01 (latest)"
1080
+ },
1081
+ {
1082
+ value: "2026-03-01",
1083
+ label: "2026-03-01"
1084
+ },
1085
+ {
1086
+ value: "2025-09-01",
1087
+ label: "2025-09-01"
1088
+ }
1089
+ ],
1090
+ description: "The version the examples on this page use."
1091
+ },
1092
+ {
1093
+ name: "lastReviewed",
1094
+ title: "Last reviewed",
1095
+ type: "date",
1096
+ group: "settings"
1097
+ },
1098
+ {
1099
+ name: "editUrl",
1100
+ title: "Source file",
1101
+ type: "url",
1102
+ group: "settings",
1103
+ access: {
1104
+ view: "schema.view",
1105
+ edit: "schema.manage"
1106
+ },
1107
+ advanced: true,
1108
+ description: "Powers the \"Edit on GitHub\" link."
1109
+ }
1110
+ ]
1111
+ },
1112
+ value: {
1113
+ title: "Close several entities at once",
1114
+ slug: "multi-entity-close",
1115
+ summary: "Group subsidiaries under a parent, run one checklist, and let Ledgerline post intercompany eliminations for you.",
1116
+ body: [
1117
+ "<p>Multi-entity close lets a parent company close every subsidiary from one checklist. Each entity keeps its own ledger; the checklist and eliminations are shared.</p>",
1118
+ rt.callout("info", "Available on Scale and Enterprise", "Multi-entity close needs at least two entities with the same fiscal calendar."),
1119
+ "<h2>Before you start</h2>",
1120
+ "<ul><li>Every entity has a functional currency set.</li><li>Intercompany accounts are mapped in <a href=\"/guides/intercompany-mapping\">Intercompany mapping</a>.</li></ul>",
1121
+ "<h2>Create a close group</h2>",
1122
+ "<p>A close group is the parent and the entities it closes. Create one from the dashboard or the API.</p>",
1123
+ rt.component("code-tabs", "Code tabs", "ci_doc_close_group", { title: "Create a close group" }),
1124
+ "<h3>Order of operations</h3>",
1125
+ `<table data-rt="table" data-header="true"><thead><tr><th scope="col">Step</th><th scope="col">Runs on</th></tr></thead><tbody><tr><td>Bank reconciliation</td><td>Each entity</td></tr><tr><td>Intercompany eliminations</td><td>The parent</td></tr><tr><td>Consolidated reports</td><td>The parent</td></tr></tbody></table>`,
1126
+ rt.callout("warning", "Lock periods last", "Locking an entity before eliminations post blocks them. Lock the parent after every child."),
1127
+ "<h2>Troubleshooting</h2>",
1128
+ rt.toggle("Eliminations didn't post", "Check that both sides of the intercompany pair use mapped accounts. Unmapped lines are skipped and listed in the close report.")
1129
+ ].join(""),
1130
+ sidebarGroup: "en_dg_closing",
1131
+ sidebarLabel: "Multi-entity close",
1132
+ sidebarPosition: 3,
1133
+ tocDepth: "h3",
1134
+ pager: "auto",
1135
+ status: "beta",
1136
+ statusNote: "<p>Multi-entity close is in beta. The API shape is stable; the dashboard checklist may change before general availability in Q1 2027.</p>",
1137
+ apiVersion: "2026-09-01",
1138
+ lastReviewed: "2026-09-29",
1139
+ editUrl: "https://github.com/ledgerline/docs/blob/main/guides/multi-entity-close.mdx"
1140
+ },
1141
+ notes: {
1142
+ 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.",
1143
+ sanity: "Sanity docs setups use `article` docs with `section: reference→docSection`, `order: number`, Portable Text with `code` and `callout` types; sidebar from GROQ grouped by section. Prev/next is computed in the frontend.",
1144
+ htmlSignals: [
1145
+ "Three columns: left nav tree, article, right 'On this page' list of anchors",
1146
+ "Breadcrumbs and 'Previous' / 'Next' cards at the bottom",
1147
+ "Admonitions (.admonition, .callout, :::note) and tabbed code blocks",
1148
+ "'Edit this page on GitHub' link; 'Last updated on' date",
1149
+ "A version switcher or 'Beta' / 'Deprecated' banners"
1150
+ ],
1151
+ 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.",
1152
+ pitfalls: [
1153
+ "Hard-coding prev/next per page — they break when pages are reordered; leave pager on auto.",
1154
+ "Importing the ToC as content duplicates headings.",
1155
+ "Docusaurus/Mintlify tabs flattened into one <pre> lose the other languages.",
1156
+ "Deprecated without a replacement link — the reader is stranded.",
1157
+ "Relative links (../guides/x) must be rewritten to page references on import."
1158
+ ]
1159
+ }
1160
+ },
1161
+ codeTabsEntry,
1162
+ {
1163
+ id: "kb-article",
1164
+ label: "Knowledge-base article",
1165
+ category: "Long-form & docs",
1166
+ brand: "Ledgerline",
1167
+ previewLayout: "article",
1168
+ tags: [
1169
+ "rich-text",
1170
+ "form",
1171
+ "conditional",
1172
+ "references",
1173
+ "validation"
1174
+ ],
1175
+ proves: "'Was this helpful?' modelled as settings, not markup · 'No' path is quick reasons, a form or a contact button · review ownership",
1176
+ docTitle: "Why is my bank feed missing transactions?",
1177
+ urlPrefix: "help.ledgerline.com/articles/",
1178
+ schema: {
1179
+ name: "kbArticle",
1180
+ title: "Help article",
1181
+ kind: "entry",
1182
+ icon: "lifebuoy",
1183
+ category: "Long-form & docs",
1184
+ description: "A help-center answer with feedback",
1185
+ preview: {
1186
+ title: "title",
1187
+ subtitle: "category",
1188
+ icon: "lifebuoy"
1189
+ },
1190
+ groups: [
1191
+ CONTENT,
1192
+ {
1193
+ name: "feedback",
1194
+ title: "Feedback",
1195
+ icon: "thumb-up"
1196
+ },
1197
+ SETTINGS
1198
+ ],
1199
+ fields: [
1200
+ {
1201
+ name: "title",
1202
+ title: "Title",
1203
+ type: "string",
1204
+ group: "content",
1205
+ validation: {
1206
+ required: true,
1207
+ max: 90
1208
+ },
1209
+ localized: true
1210
+ },
1211
+ {
1212
+ name: "slug",
1213
+ title: "Slug",
1214
+ type: "slug",
1215
+ group: "content",
1216
+ validation: { required: true }
1217
+ },
1218
+ {
1219
+ name: "category",
1220
+ title: "Category",
1221
+ type: "reference",
1222
+ group: "content",
1223
+ reference: { to: ["col_kb_categories"] },
1224
+ validation: { required: true }
1225
+ },
1226
+ {
1227
+ name: "summary",
1228
+ title: "Short answer",
1229
+ type: "text",
1230
+ rows: 2,
1231
+ group: "content",
1232
+ validation: { recommendedMax: 200 },
1233
+ localized: true,
1234
+ description: "Shown above the steps."
1235
+ },
1236
+ {
1237
+ name: "body",
1238
+ title: "Body",
1239
+ type: "richtext",
1240
+ group: "content",
1241
+ validation: { required: true },
1242
+ localized: true
1243
+ },
1244
+ {
1245
+ name: "appliesTo",
1246
+ title: "Applies to",
1247
+ type: "multiselect",
1248
+ group: "content",
1249
+ options: [
1250
+ {
1251
+ value: "starter",
1252
+ label: "Starter"
1253
+ },
1254
+ {
1255
+ value: "growth",
1256
+ label: "Growth"
1257
+ },
1258
+ {
1259
+ value: "scale",
1260
+ label: "Scale"
1261
+ },
1262
+ {
1263
+ value: "enterprise",
1264
+ label: "Enterprise"
1265
+ }
1266
+ ],
1267
+ description: "Leave empty for every plan."
1268
+ },
1269
+ refs("related", "Related articles", ["col_kb"], {
1270
+ group: "content",
1271
+ validation: {
1272
+ max: 5,
1273
+ unique: true
1274
+ }
1275
+ }),
1276
+ {
1277
+ name: "showFeedback",
1278
+ title: "Ask \"Was this helpful?\"",
1279
+ type: "boolean",
1280
+ group: "feedback",
1281
+ initialValue: true,
1282
+ description: "Answers are counted in Growth › Analytics."
1283
+ },
1284
+ {
1285
+ name: "question",
1286
+ title: "Question",
1287
+ type: "string",
1288
+ group: "feedback",
1289
+ hidden: {
1290
+ field: "showFeedback",
1291
+ equals: true
1292
+ },
1293
+ initialValue: "Was this article helpful?",
1294
+ validation: { max: 48 },
1295
+ description: ""
1296
+ },
1297
+ {
1298
+ name: "thanks",
1299
+ title: "After Yes",
1300
+ type: "string",
1301
+ group: "feedback",
1302
+ hidden: {
1303
+ field: "showFeedback",
1304
+ equals: true
1305
+ },
1306
+ initialValue: "Thanks — glad it helped.",
1307
+ validation: { max: 80 },
1308
+ description: ""
1309
+ },
1310
+ {
1311
+ name: "onNo",
1312
+ title: "After No",
1313
+ type: "select",
1314
+ group: "feedback",
1315
+ layout: "radio",
1316
+ hidden: {
1317
+ field: "showFeedback",
1318
+ equals: true
1319
+ },
1320
+ initialValue: "reasons",
1321
+ options: [
1322
+ {
1323
+ value: "reasons",
1324
+ label: "Ask why, with quick reasons"
1325
+ },
1326
+ {
1327
+ value: "form",
1328
+ label: "Show a form"
1329
+ },
1330
+ {
1331
+ value: "contact",
1332
+ label: "Offer to contact support"
1333
+ }
1334
+ ],
1335
+ description: ""
1336
+ },
1337
+ {
1338
+ name: "reasons",
1339
+ title: "Reasons",
1340
+ type: "array",
1341
+ itemNoun: "reason",
1342
+ of: [{
1343
+ name: "reason",
1344
+ title: "Reason",
1345
+ type: "string"
1346
+ }],
1347
+ group: "feedback",
1348
+ hidden: {
1349
+ field: "onNo",
1350
+ equals: "reasons"
1351
+ },
1352
+ validation: {
1353
+ min: 2,
1354
+ max: 5
1355
+ }
1356
+ },
1357
+ {
1358
+ name: "noForm",
1359
+ title: "Form",
1360
+ type: "reference",
1361
+ group: "feedback",
1362
+ reference: { to: ["form"] },
1363
+ hidden: {
1364
+ field: "onNo",
1365
+ equals: "form"
1366
+ },
1367
+ validation: { required: true }
1368
+ },
1369
+ {
1370
+ name: "contactButton",
1371
+ title: "Contact button",
1372
+ type: "button",
1373
+ group: "feedback",
1374
+ hidden: {
1375
+ field: "onNo",
1376
+ equals: "contact"
1377
+ }
1378
+ },
1379
+ {
1380
+ name: "owner",
1381
+ title: "Owner",
1382
+ type: "reference",
1383
+ group: "settings",
1384
+ reference: { to: ["col_people"] },
1385
+ description: "Gets the \"needs review\" reminder and the No answers."
1386
+ },
1387
+ {
1388
+ name: "lastReviewed",
1389
+ title: "Last reviewed",
1390
+ type: "date",
1391
+ group: "settings",
1392
+ validation: { required: true },
1393
+ description: "Articles over 6 months old are flagged."
1394
+ },
1395
+ {
1396
+ name: "internalNotes",
1397
+ title: "Internal notes",
1398
+ type: "text",
1399
+ rows: 3,
1400
+ group: "settings",
1401
+ description: "Never shown on the site."
1402
+ }
1403
+ ]
1404
+ },
1405
+ value: {
1406
+ title: "Why is my bank feed missing transactions?",
1407
+ slug: "bank-feed-missing-transactions",
1408
+ category: "en_kbc_bank-feeds",
1409
+ summary: "Most gaps are a pending sync or an expired bank login. Reconnect the account and run a 90-day backfill.",
1410
+ body: "<ol><li>Open <strong>Banking › Connections</strong> and look for a yellow <em>Reconnect</em> badge.</li><li>Click <strong>Reconnect</strong> and sign in to your bank again.</li><li>Choose <strong>Backfill 90 days</strong>. New lines appear within 15 minutes.</li></ol><figure data-rt=\"image\" data-align=\"center\" data-size=\"m\"><img src=\"https://picsum.photos/seed/ledgerline-reconnect/1600/1000\" alt=\"The Connections list with a yellow Reconnect badge on the Chase operating account\"><figcaption>An expired connection shows Reconnect.</figcaption></figure><div data-rt=\"callout\" data-callout=\"info\"><p data-title=\"true\">Pending transactions</p><div data-body=\"true\"><p>Banks send pending card charges only once they settle, usually 1–3 business days later.</p></div></div>",
1411
+ appliesTo: [],
1412
+ related: ["en_kb_reconnect-bank", "en_kb_duplicate-lines"],
1413
+ showFeedback: true,
1414
+ question: "Was this article helpful?",
1415
+ thanks: "Thanks — glad it helped.",
1416
+ onNo: "reasons",
1417
+ reasons: [
1418
+ "Steps didn't work",
1419
+ "My bank isn't listed",
1420
+ "Hard to follow",
1421
+ "Not what I was looking for"
1422
+ ],
1423
+ owner: "en_p_rina-das",
1424
+ lastReviewed: "2026-08-18",
1425
+ internalNotes: "Plaid outage on 2026-07-02 caused a spike in tickets; link this article in the macro."
1426
+ },
1427
+ notes: {
1428
+ 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.",
1429
+ sanity: "Sanity `article` { category: reference, body: portableText, related: array<reference> }. Feedback is almost always a hard-coded widget; per-article behavior would be an object `showFeedback {enabled, onNo, form: reference}` with hidden callbacks.",
1430
+ htmlSignals: [
1431
+ "'Was this article helpful?' with Yes/No or thumbs buttons at the end",
1432
+ "Breadcrumbs Help Center › Category › Article; 'Updated N days ago'",
1433
+ "Numbered steps with UI screenshots and bolded menu paths",
1434
+ "Zendesk/Intercom/HelpScout markup (article-body, .article-votes)"
1435
+ ],
1436
+ 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.",
1437
+ pitfalls: [
1438
+ "Vote buttons copied as HTML into the body (they can't record anything).",
1439
+ "Screenshots with alt 'screenshot' — describe what the reader should look for.",
1440
+ "No owner → stale answers nobody reviews.",
1441
+ "Plan badges as body text drift from pricing — use appliesTo."
1442
+ ]
1443
+ }
1444
+ },
1445
+ {
1446
+ id: "glossary",
1447
+ label: "Glossary A–Z",
1448
+ category: "Long-form & docs",
1449
+ brand: "Caldera",
1450
+ previewLayout: "list",
1451
+ tags: [
1452
+ "dynamic-query",
1453
+ "repeater",
1454
+ "conditional",
1455
+ "references",
1456
+ "seo-structured",
1457
+ "a11y",
1458
+ "validation"
1459
+ ],
1460
+ proves: "Terms written here or from a Glossary collection · auto letter index · aliases and see-also links · each term deep-linkable",
1461
+ docTitle: "Fire cooking glossary",
1462
+ urlPrefix: "caldera.bettercms.site/learn/",
1463
+ schema: {
1464
+ name: "glossary",
1465
+ title: "Glossary",
1466
+ kind: "block",
1467
+ icon: "abc",
1468
+ category: "Long-form & docs",
1469
+ description: "Terms and definitions, A to Z",
1470
+ variants: [{
1471
+ id: "list",
1472
+ name: "List"
1473
+ }, {
1474
+ id: "columns",
1475
+ name: "Two columns"
1476
+ }],
1477
+ groups: [CONTENT, DESIGN],
1478
+ preview: {
1479
+ title: "heading",
1480
+ subtitle: "intro",
1481
+ icon: "abc"
1482
+ },
1483
+ fields: [
1484
+ {
1485
+ name: "heading",
1486
+ title: "Heading",
1487
+ type: "string",
1488
+ group: "content",
1489
+ validation: {
1490
+ required: true,
1491
+ max: 48
1492
+ },
1493
+ localized: true
1494
+ },
1495
+ {
1496
+ name: "intro",
1497
+ title: "Intro",
1498
+ type: "text",
1499
+ rows: 2,
1500
+ group: "content",
1501
+ localized: true,
1502
+ description: ""
1503
+ },
1504
+ {
1505
+ name: "source",
1506
+ title: "Terms come from",
1507
+ type: "select",
1508
+ group: "content",
1509
+ layout: "segmented",
1510
+ initialValue: "inline",
1511
+ options: [{
1512
+ value: "inline",
1513
+ label: "Written here"
1514
+ }, {
1515
+ value: "collection",
1516
+ label: "Glossary collection"
1517
+ }]
1518
+ },
1519
+ refs("topics", "Only these topics", ["col_topics"], {
1520
+ group: "content",
1521
+ hidden: {
1522
+ field: "source",
1523
+ equals: "collection"
1524
+ },
1525
+ validation: { max: 4 },
1526
+ description: "Leave empty for every term."
1527
+ }),
1528
+ {
1529
+ name: "terms",
1530
+ title: "Terms",
1531
+ type: "array",
1532
+ group: "content",
1533
+ itemNoun: "term",
1534
+ hidden: {
1535
+ field: "source",
1536
+ equals: "inline"
1537
+ },
1538
+ validation: {
1539
+ min: 1,
1540
+ max: 300
1541
+ },
1542
+ description: "Sorted A to Z on the page.",
1543
+ of: [{
1544
+ name: "term",
1545
+ title: "Term",
1546
+ type: "object",
1547
+ icon: "abc",
1548
+ preview: {
1549
+ title: "term",
1550
+ subtitle: "definition",
1551
+ fallback: "Untitled term"
1552
+ },
1553
+ fields: [
1554
+ {
1555
+ name: "term",
1556
+ title: "Term",
1557
+ type: "string",
1558
+ validation: {
1559
+ required: true,
1560
+ max: 48,
1561
+ unique: true
1562
+ },
1563
+ localized: true
1564
+ },
1565
+ {
1566
+ name: "anchor",
1567
+ title: "Anchor",
1568
+ type: "string",
1569
+ validation: {
1570
+ pattern: "^[a-z0-9-]+$",
1571
+ patternMessage: "Use lowercase letters, numbers and hyphens, like camp-kitchen."
1572
+ },
1573
+ description: "Made from the term if empty."
1574
+ },
1575
+ {
1576
+ name: "definition",
1577
+ title: "Definition",
1578
+ type: "richtext",
1579
+ validation: { required: true },
1580
+ localized: true
1581
+ },
1582
+ {
1583
+ name: "aliases",
1584
+ title: "Also called",
1585
+ type: "tags",
1586
+ validation: { max: 5 },
1587
+ description: "Other names people search for."
1588
+ },
1589
+ {
1590
+ name: "seeAlso",
1591
+ title: "See also",
1592
+ type: "tags",
1593
+ validation: { max: 4 },
1594
+ description: "Other terms in this list, spelled exactly as written."
1595
+ },
1596
+ {
1597
+ name: "link",
1598
+ title: "Learn more",
1599
+ type: "link"
1600
+ }
1601
+ ]
1602
+ }]
1603
+ },
1604
+ {
1605
+ name: "showLetterNav",
1606
+ title: "Show A to Z bar",
1607
+ type: "boolean",
1608
+ group: "design",
1609
+ initialValue: true
1610
+ },
1611
+ {
1612
+ name: "isNavSticky",
1613
+ title: "Keep the A to Z bar in view",
1614
+ type: "boolean",
1615
+ group: "design",
1616
+ hidden: {
1617
+ field: "showLetterNav",
1618
+ equals: true
1619
+ },
1620
+ description: ""
1621
+ },
1622
+ {
1623
+ name: "showSearch",
1624
+ title: "Show search box",
1625
+ type: "boolean",
1626
+ group: "design",
1627
+ initialValue: true
1628
+ }
1629
+ ]
1630
+ },
1631
+ value: {
1632
+ _variant: "list",
1633
+ heading: "Fire cooking glossary",
1634
+ intro: "The words our cooks use, explained without the jargon.",
1635
+ source: "inline",
1636
+ topics: [],
1637
+ terms: [
1638
+ {
1639
+ _key: "g1",
1640
+ term: "Bark",
1641
+ anchor: "bark",
1642
+ definition: "<p>The dark, crusty outer layer that forms on smoked meat as rub, smoke and rendered fat dry on the surface.</p>",
1643
+ aliases: [],
1644
+ seeAlso: ["Maillard reaction", "Stall"]
1645
+ },
1646
+ {
1647
+ _key: "g2",
1648
+ term: "Carryover cooking",
1649
+ anchor: "carryover",
1650
+ definition: "<p>The few degrees food keeps rising after it leaves the heat. Pull a steak 3–5°C before your target.</p>",
1651
+ aliases: ["carry-over"],
1652
+ seeAlso: []
1653
+ },
1654
+ {
1655
+ _key: "g3",
1656
+ term: "Maillard reaction",
1657
+ anchor: "maillard",
1658
+ definition: "<p>The browning of proteins and sugars above about 140°C that gives seared food its flavour.</p>",
1659
+ aliases: ["browning", "maillard"],
1660
+ seeAlso: ["Bark"],
1661
+ link: {
1662
+ type: "entry",
1663
+ href: "/journal/why-dry-meat-sears-better",
1664
+ title: "Why dry meat sears better"
1665
+ }
1666
+ },
1667
+ {
1668
+ _key: "g4",
1669
+ term: "Polymerisation",
1670
+ anchor: "polymerisation",
1671
+ definition: "<p>What happens when a thin coat of oil is heated past its smoke point and bonds into the hard, slick layer we call seasoning.</p>",
1672
+ aliases: ["seasoning layer", "polymerization"],
1673
+ seeAlso: [],
1674
+ link: {
1675
+ type: "entry",
1676
+ href: "/journal/seasoning-cast-iron-open-fire",
1677
+ entryId: "en_1",
1678
+ title: "Seasoning cast iron over an open fire"
1679
+ }
1680
+ },
1681
+ {
1682
+ _key: "g5",
1683
+ term: "Stall",
1684
+ anchor: "stall",
1685
+ definition: "<p>The long plateau around 65–75°C when a large cut stops rising because evaporation cools its surface.</p>",
1686
+ aliases: ["the stall", "plateau"],
1687
+ seeAlso: ["Bark"]
1688
+ }
1689
+ ],
1690
+ showLetterNav: true,
1691
+ isNavSticky: true,
1692
+ showSearch: true
1693
+ },
1694
+ notes: {
1695
+ 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.",
1696
+ sanity: "Sanity: `glossaryTerm` documents { term, slug, definition: portableText, synonyms: array<string>, related: array<reference→glossaryTerm> } listed with GROQ ordered by lower(term).",
1697
+ htmlSignals: [
1698
+ "A row of A–Z letter links (#a, #b…) above alphabetised groups",
1699
+ "<dl> with <dt> terms and <dd> definitions, or h3 + p pairs",
1700
+ "Each term has an id for deep links",
1701
+ "DefinedTermSet / DefinedTerm JSON-LD in the source (native output on our side)"
1702
+ ],
1703
+ 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.",
1704
+ pitfalls: [
1705
+ "Storing the letter headings ('A', 'B') as terms.",
1706
+ "See-also tags that don't match a term exactly render as dead text.",
1707
+ "300 inline terms is the ceiling — beyond that, use the collection.",
1708
+ "Localised term lists re-sort per language (Ä, Ø, Ł) — sort with the locale's collation."
1709
+ ]
1710
+ }
1711
+ },
1712
+ {
1713
+ id: "recipe",
1714
+ label: "Recipe",
1715
+ category: "Long-form & docs",
1716
+ brand: "Caldera",
1717
+ previewLayout: "article",
1718
+ tags: [
1719
+ "deep-nesting",
1720
+ "repeater",
1721
+ "seo-structured",
1722
+ "media-heavy",
1723
+ "conditional",
1724
+ "references",
1725
+ "validation",
1726
+ "localized"
1727
+ ],
1728
+ proves: "Ingredient groups → items with amount/unit/prep · steps with images and optional timers · nutrition · Recipe JSON-LD from fields",
1729
+ docTitle: "Cast-iron skillet cornbread",
1730
+ urlPrefix: "caldera.bettercms.site/recipes/",
1731
+ schema: {
1732
+ name: "recipe",
1733
+ title: "Recipe",
1734
+ kind: "entry",
1735
+ icon: "chef-hat",
1736
+ category: "Long-form & docs",
1737
+ description: "A recipe with structured ingredients, steps and nutrition",
1738
+ preview: {
1739
+ title: "title",
1740
+ subtitle: "summary",
1741
+ media: "image"
1742
+ },
1743
+ groups: [
1744
+ {
1745
+ name: "overview",
1746
+ title: "Overview",
1747
+ icon: "file-text",
1748
+ default: true
1749
+ },
1750
+ {
1751
+ name: "ingredients",
1752
+ title: "Ingredients",
1753
+ icon: "list"
1754
+ },
1755
+ {
1756
+ name: "method",
1757
+ title: "Method",
1758
+ icon: "list-numbers"
1759
+ },
1760
+ {
1761
+ name: "nutrition",
1762
+ title: "Nutrition",
1763
+ icon: "apple"
1764
+ }
1765
+ ],
1766
+ fieldsets: [
1767
+ {
1768
+ name: "times",
1769
+ title: "Times",
1770
+ columns: 2
1771
+ },
1772
+ {
1773
+ name: "yield",
1774
+ title: "Makes"
1775
+ },
1776
+ {
1777
+ name: "facts",
1778
+ title: "Per serving",
1779
+ columns: 2
1780
+ }
1781
+ ],
1782
+ fields: [
1783
+ {
1784
+ name: "title",
1785
+ title: "Title",
1786
+ type: "string",
1787
+ group: "overview",
1788
+ validation: {
1789
+ required: true,
1790
+ max: 80
1791
+ },
1792
+ localized: true
1793
+ },
1794
+ {
1795
+ name: "slug",
1796
+ title: "Slug",
1797
+ type: "slug",
1798
+ group: "overview",
1799
+ validation: { required: true }
1800
+ },
1801
+ {
1802
+ name: "summary",
1803
+ title: "Summary",
1804
+ type: "text",
1805
+ rows: 2,
1806
+ group: "overview",
1807
+ validation: {
1808
+ required: true,
1809
+ recommendedMax: 160
1810
+ },
1811
+ localized: true
1812
+ },
1813
+ {
1814
+ name: "image",
1815
+ title: "Image",
1816
+ type: "image",
1817
+ group: "overview",
1818
+ validation: { required: true },
1819
+ description: "1600 × 1200 or larger. Google shows it in recipe results."
1820
+ },
1821
+ {
1822
+ name: "video",
1823
+ title: "Video",
1824
+ type: "video",
1825
+ group: "overview"
1826
+ },
1827
+ refs("authors", "Authors", ["col_authors"], {
1828
+ group: "overview",
1829
+ validation: {
1830
+ min: 1,
1831
+ max: 2
1832
+ }
1833
+ }),
1834
+ {
1835
+ name: "course",
1836
+ title: "Course",
1837
+ type: "select",
1838
+ group: "overview",
1839
+ validation: { required: true },
1840
+ options: [
1841
+ {
1842
+ value: "breakfast",
1843
+ label: "Breakfast"
1844
+ },
1845
+ {
1846
+ value: "main",
1847
+ label: "Main"
1848
+ },
1849
+ {
1850
+ value: "side",
1851
+ label: "Side"
1852
+ },
1853
+ {
1854
+ value: "bread",
1855
+ label: "Bread"
1856
+ },
1857
+ {
1858
+ value: "dessert",
1859
+ label: "Dessert"
1860
+ }
1861
+ ],
1862
+ description: ""
1863
+ },
1864
+ {
1865
+ name: "methods",
1866
+ title: "Cooked on",
1867
+ type: "multiselect",
1868
+ group: "overview",
1869
+ options: [
1870
+ {
1871
+ value: "campfire",
1872
+ label: "Campfire"
1873
+ },
1874
+ {
1875
+ value: "coals",
1876
+ label: "Coals"
1877
+ },
1878
+ {
1879
+ value: "grill",
1880
+ label: "Grill"
1881
+ },
1882
+ {
1883
+ value: "stove",
1884
+ label: "Stovetop"
1885
+ },
1886
+ {
1887
+ value: "oven",
1888
+ label: "Oven"
1889
+ }
1890
+ ]
1891
+ },
1892
+ {
1893
+ name: "difficulty",
1894
+ title: "Difficulty",
1895
+ type: "select",
1896
+ group: "overview",
1897
+ layout: "segmented",
1898
+ options: [
1899
+ {
1900
+ value: "easy",
1901
+ label: "Easy"
1902
+ },
1903
+ {
1904
+ value: "medium",
1905
+ label: "Medium"
1906
+ },
1907
+ {
1908
+ value: "hard",
1909
+ label: "Hard"
1910
+ }
1911
+ ],
1912
+ description: ""
1913
+ },
1914
+ {
1915
+ name: "dietary",
1916
+ title: "Dietary",
1917
+ type: "multiselect",
1918
+ group: "overview",
1919
+ options: [
1920
+ {
1921
+ value: "vegetarian",
1922
+ label: "Vegetarian"
1923
+ },
1924
+ {
1925
+ value: "vegan",
1926
+ label: "Vegan"
1927
+ },
1928
+ {
1929
+ value: "gluten-free",
1930
+ label: "Gluten-free"
1931
+ },
1932
+ {
1933
+ value: "dairy-free",
1934
+ label: "Dairy-free"
1935
+ }
1936
+ ]
1937
+ },
1938
+ {
1939
+ name: "prepMinutes",
1940
+ title: "Prep",
1941
+ type: "number",
1942
+ group: "overview",
1943
+ fieldset: "times",
1944
+ numeric: {
1945
+ unit: "min",
1946
+ min: 0,
1947
+ step: 5
1948
+ },
1949
+ validation: { required: true }
1950
+ },
1951
+ {
1952
+ name: "cookMinutes",
1953
+ title: "Cook",
1954
+ type: "number",
1955
+ group: "overview",
1956
+ fieldset: "times",
1957
+ numeric: {
1958
+ unit: "min",
1959
+ min: 0,
1960
+ step: 5
1961
+ },
1962
+ validation: { required: true }
1963
+ },
1964
+ {
1965
+ name: "restMinutes",
1966
+ title: "Rest",
1967
+ type: "number",
1968
+ group: "overview",
1969
+ fieldset: "times",
1970
+ numeric: {
1971
+ unit: "min",
1972
+ min: 0,
1973
+ step: 5
1974
+ },
1975
+ description: "In minutes. Counts toward the total."
1976
+ },
1977
+ {
1978
+ name: "totalMinutes",
1979
+ title: "Total",
1980
+ type: "number",
1981
+ group: "overview",
1982
+ fieldset: "times",
1983
+ readOnly: true,
1984
+ numeric: { unit: "min" },
1985
+ description: "In minutes: prep + cook + rest."
1986
+ },
1987
+ {
1988
+ name: "servings",
1989
+ title: "Servings",
1990
+ type: "number",
1991
+ group: "overview",
1992
+ fieldset: "yield",
1993
+ numeric: {
1994
+ min: 1,
1995
+ max: 40,
1996
+ step: 1
1997
+ },
1998
+ validation: { required: true },
1999
+ description: "Readers can scale amounts from this."
2000
+ },
2001
+ {
2002
+ name: "yieldLabel",
2003
+ title: "Yield",
2004
+ type: "string",
2005
+ group: "overview",
2006
+ fieldset: "yield",
2007
+ validation: { max: 32 },
2008
+ placeholder: "one 10-inch skillet",
2009
+ description: "Shown instead of \"Serves 8\"."
2010
+ },
2011
+ refs("equipment", "Cooked in", ["shopify"], {
2012
+ group: "overview",
2013
+ validation: { max: 4 },
2014
+ description: "Caldera gear used. Shown with a link to each product."
2015
+ }),
2016
+ {
2017
+ name: "ingredientGroups",
2018
+ title: "Ingredients",
2019
+ type: "array",
2020
+ group: "ingredients",
2021
+ itemNoun: "group",
2022
+ validation: {
2023
+ min: 1,
2024
+ max: 8
2025
+ },
2026
+ of: [{
2027
+ name: "ingredientGroup",
2028
+ title: "Group",
2029
+ type: "object",
2030
+ icon: "list",
2031
+ preview: {
2032
+ title: "title",
2033
+ fallback: "Untitled ingredient group"
2034
+ },
2035
+ fields: [{
2036
+ name: "title",
2037
+ title: "Group name",
2038
+ type: "string",
2039
+ validation: { max: 40 },
2040
+ placeholder: "For the glaze",
2041
+ description: "Leave empty for a single list."
2042
+ }, {
2043
+ name: "items",
2044
+ title: "Items",
2045
+ type: "array",
2046
+ itemNoun: "ingredient",
2047
+ validation: {
2048
+ min: 1,
2049
+ max: 30
2050
+ },
2051
+ of: [{
2052
+ name: "ingredient",
2053
+ title: "Ingredient",
2054
+ type: "object",
2055
+ icon: "point",
2056
+ preview: {
2057
+ title: "name",
2058
+ subtitle: "amount",
2059
+ fallback: "Untitled ingredient"
2060
+ },
2061
+ fields: [
2062
+ {
2063
+ name: "amount",
2064
+ title: "Amount",
2065
+ type: "number",
2066
+ numeric: {
2067
+ min: 0,
2068
+ step: .25
2069
+ },
2070
+ description: "Leave empty for \"to taste\"."
2071
+ },
2072
+ {
2073
+ name: "amountMax",
2074
+ title: "Up to",
2075
+ type: "number",
2076
+ numeric: {
2077
+ min: 0,
2078
+ step: .25
2079
+ },
2080
+ hidden: {
2081
+ field: "amount",
2082
+ notEmpty: true
2083
+ },
2084
+ description: "For ranges like 2–3 cloves."
2085
+ },
2086
+ {
2087
+ name: "unit",
2088
+ title: "Unit",
2089
+ type: "select",
2090
+ initialValue: "",
2091
+ options: [
2092
+ {
2093
+ value: "",
2094
+ label: "None"
2095
+ },
2096
+ {
2097
+ value: "g",
2098
+ label: "g"
2099
+ },
2100
+ {
2101
+ value: "kg",
2102
+ label: "kg"
2103
+ },
2104
+ {
2105
+ value: "ml",
2106
+ label: "ml"
2107
+ },
2108
+ {
2109
+ value: "l",
2110
+ label: "l"
2111
+ },
2112
+ {
2113
+ value: "tsp",
2114
+ label: "tsp"
2115
+ },
2116
+ {
2117
+ value: "tbsp",
2118
+ label: "tbsp"
2119
+ },
2120
+ {
2121
+ value: "cup",
2122
+ label: "cup"
2123
+ },
2124
+ {
2125
+ value: "oz",
2126
+ label: "oz"
2127
+ },
2128
+ {
2129
+ value: "lb",
2130
+ label: "lb"
2131
+ },
2132
+ {
2133
+ value: "clove",
2134
+ label: "clove"
2135
+ },
2136
+ {
2137
+ value: "pinch",
2138
+ label: "pinch"
2139
+ }
2140
+ ],
2141
+ description: "Converts for readers who switch between metric and imperial."
2142
+ },
2143
+ {
2144
+ name: "name",
2145
+ title: "Ingredient",
2146
+ type: "string",
2147
+ validation: {
2148
+ required: true,
2149
+ max: 60
2150
+ },
2151
+ localized: true
2152
+ },
2153
+ {
2154
+ name: "prep",
2155
+ title: "Prep",
2156
+ type: "string",
2157
+ validation: { max: 40 },
2158
+ placeholder: "melted and cooled",
2159
+ localized: true,
2160
+ description: ""
2161
+ },
2162
+ {
2163
+ name: "isOptional",
2164
+ title: "Optional",
2165
+ type: "boolean",
2166
+ description: ""
2167
+ },
2168
+ {
2169
+ name: "product",
2170
+ title: "Shop link",
2171
+ type: "reference",
2172
+ reference: { to: ["shopify"] },
2173
+ advanced: true,
2174
+ description: "Caldera pantry items only."
2175
+ }
2176
+ ]
2177
+ }]
2178
+ }]
2179
+ }]
2180
+ },
2181
+ {
2182
+ name: "steps",
2183
+ title: "Steps",
2184
+ type: "array",
2185
+ group: "method",
2186
+ itemNoun: "step",
2187
+ validation: {
2188
+ min: 1,
2189
+ max: 20
2190
+ },
2191
+ of: [{
2192
+ name: "step",
2193
+ title: "Step",
2194
+ type: "object",
2195
+ icon: "list-numbers",
2196
+ preview: {
2197
+ title: "title",
2198
+ subtitle: "instruction",
2199
+ media: "image",
2200
+ fallback: "Untitled step"
2201
+ },
2202
+ fields: [
2203
+ {
2204
+ name: "title",
2205
+ title: "Short name",
2206
+ type: "string",
2207
+ validation: { max: 40 }
2208
+ },
2209
+ {
2210
+ name: "instruction",
2211
+ title: "Instruction",
2212
+ type: "richtext",
2213
+ validation: { required: true },
2214
+ localized: true
2215
+ },
2216
+ {
2217
+ name: "image",
2218
+ title: "Image",
2219
+ type: "image"
2220
+ },
2221
+ {
2222
+ name: "hasTimer",
2223
+ title: "Add a timer",
2224
+ type: "boolean"
2225
+ },
2226
+ {
2227
+ name: "timerMinutes",
2228
+ title: "Timer",
2229
+ type: "number",
2230
+ hidden: {
2231
+ field: "hasTimer",
2232
+ equals: true
2233
+ },
2234
+ numeric: {
2235
+ unit: "min",
2236
+ min: 1,
2237
+ max: 720,
2238
+ step: 1
2239
+ },
2240
+ validation: { required: true },
2241
+ description: ""
2242
+ },
2243
+ {
2244
+ name: "timerLabel",
2245
+ title: "Timer label",
2246
+ type: "string",
2247
+ hidden: {
2248
+ field: "hasTimer",
2249
+ equals: true
2250
+ },
2251
+ validation: { max: 20 },
2252
+ placeholder: "Bake",
2253
+ description: ""
2254
+ },
2255
+ {
2256
+ name: "tip",
2257
+ title: "Tip",
2258
+ type: "text",
2259
+ rows: 2,
2260
+ validation: { max: 200 },
2261
+ description: "Shown as a note under the step."
2262
+ }
2263
+ ]
2264
+ }]
2265
+ },
2266
+ {
2267
+ name: "notes",
2268
+ title: "Cook's notes",
2269
+ type: "richtext",
2270
+ group: "method"
2271
+ },
2272
+ {
2273
+ name: "showNutrition",
2274
+ title: "Show nutrition",
2275
+ type: "boolean",
2276
+ group: "nutrition",
2277
+ initialValue: true,
2278
+ description: "Shown as estimates, with a disclaimer."
2279
+ },
2280
+ {
2281
+ name: "calories",
2282
+ title: "Calories",
2283
+ type: "number",
2284
+ group: "nutrition",
2285
+ fieldset: "facts",
2286
+ numeric: {
2287
+ unit: "kcal",
2288
+ min: 0
2289
+ },
2290
+ hidden: {
2291
+ field: "showNutrition",
2292
+ equals: true
2293
+ }
2294
+ },
2295
+ {
2296
+ name: "protein",
2297
+ title: "Protein",
2298
+ type: "number",
2299
+ group: "nutrition",
2300
+ fieldset: "facts",
2301
+ numeric: {
2302
+ unit: "g",
2303
+ min: 0
2304
+ },
2305
+ hidden: {
2306
+ field: "showNutrition",
2307
+ equals: true
2308
+ }
2309
+ },
2310
+ {
2311
+ name: "fat",
2312
+ title: "Fat",
2313
+ type: "number",
2314
+ group: "nutrition",
2315
+ fieldset: "facts",
2316
+ numeric: {
2317
+ unit: "g",
2318
+ min: 0
2319
+ },
2320
+ hidden: {
2321
+ field: "showNutrition",
2322
+ equals: true
2323
+ }
2324
+ },
2325
+ {
2326
+ name: "carbs",
2327
+ title: "Carbohydrates",
2328
+ type: "number",
2329
+ group: "nutrition",
2330
+ fieldset: "facts",
2331
+ numeric: {
2332
+ unit: "g",
2333
+ min: 0
2334
+ },
2335
+ hidden: {
2336
+ field: "showNutrition",
2337
+ equals: true
2338
+ }
2339
+ },
2340
+ {
2341
+ name: "fiber",
2342
+ title: "Fibre",
2343
+ type: "number",
2344
+ group: "nutrition",
2345
+ fieldset: "facts",
2346
+ numeric: {
2347
+ unit: "g",
2348
+ min: 0
2349
+ },
2350
+ hidden: {
2351
+ field: "showNutrition",
2352
+ equals: true
2353
+ }
2354
+ },
2355
+ {
2356
+ name: "sodium",
2357
+ title: "Sodium",
2358
+ type: "number",
2359
+ group: "nutrition",
2360
+ fieldset: "facts",
2361
+ numeric: {
2362
+ unit: "mg",
2363
+ min: 0
2364
+ },
2365
+ hidden: {
2366
+ field: "showNutrition",
2367
+ equals: true
2368
+ }
2369
+ },
2370
+ {
2371
+ name: "rating",
2372
+ title: "Reader rating",
2373
+ type: "rating",
2374
+ group: "nutrition",
2375
+ readOnly: true,
2376
+ description: "Average of reader ratings. Not editable."
2377
+ }
2378
+ ]
2379
+ },
2380
+ value: {
2381
+ title: "Cast-iron skillet cornbread",
2382
+ slug: "cast-iron-skillet-cornbread",
2383
+ summary: "Crisp-edged, barely sweet cornbread baked in a screaming-hot skillet over coals — or in the oven at home.",
2384
+ image: image("skillet-cornbread", "A golden cornbread in a 10-inch cast iron skillet, one wedge cut out", 1600, 1200),
2385
+ video: { url: "https://www.youtube.com/watch?v=3n1u3pTw1kM" },
2386
+ authors: ["en_a1"],
2387
+ course: "bread",
2388
+ methods: ["coals", "oven"],
2389
+ difficulty: "easy",
2390
+ dietary: ["vegetarian"],
2391
+ prepMinutes: 10,
2392
+ cookMinutes: 25,
2393
+ restMinutes: 10,
2394
+ totalMinutes: 45,
2395
+ servings: 8,
2396
+ yieldLabel: "one 10-inch skillet",
2397
+ equipment: ["gid://shopify/Product/8814"],
2398
+ ingredientGroups: [{
2399
+ _key: "ig1",
2400
+ title: "",
2401
+ items: [
2402
+ {
2403
+ _key: "i1",
2404
+ amount: 240,
2405
+ unit: "g",
2406
+ name: "medium-grind cornmeal"
2407
+ },
2408
+ {
2409
+ _key: "i2",
2410
+ amount: 125,
2411
+ unit: "g",
2412
+ name: "plain flour"
2413
+ },
2414
+ {
2415
+ _key: "i3",
2416
+ amount: 2,
2417
+ unit: "tsp",
2418
+ name: "baking powder"
2419
+ },
2420
+ {
2421
+ _key: "i4",
2422
+ amount: 1,
2423
+ unit: "tsp",
2424
+ name: "fine salt"
2425
+ },
2426
+ {
2427
+ _key: "i5",
2428
+ amount: 2,
2429
+ unit: "tbsp",
2430
+ name: "honey"
2431
+ },
2432
+ {
2433
+ _key: "i6",
2434
+ amount: 480,
2435
+ unit: "ml",
2436
+ name: "buttermilk"
2437
+ },
2438
+ {
2439
+ _key: "i7",
2440
+ amount: 2,
2441
+ unit: "",
2442
+ name: "eggs",
2443
+ prep: "beaten"
2444
+ },
2445
+ {
2446
+ _key: "i8",
2447
+ amount: 60,
2448
+ unit: "g",
2449
+ name: "butter",
2450
+ prep: "melted, plus 1 tbsp for the pan"
2451
+ }
2452
+ ]
2453
+ }, {
2454
+ _key: "ig2",
2455
+ title: "To serve",
2456
+ items: [{
2457
+ _key: "i9",
2458
+ amount: 2,
2459
+ unit: "tbsp",
2460
+ name: "hot honey",
2461
+ isOptional: true
2462
+ }, {
2463
+ _key: "i10",
2464
+ unit: "",
2465
+ name: "flaky salt",
2466
+ prep: "to taste"
2467
+ }]
2468
+ }],
2469
+ steps: [
2470
+ {
2471
+ _key: "s1",
2472
+ title: "Heat the pan",
2473
+ instruction: "<p>Set the skillet over medium coals or in a <strong>220°C</strong> oven with 1 tbsp butter inside for <strong>10 minutes</strong>.</p>",
2474
+ hasTimer: true,
2475
+ timerMinutes: 10,
2476
+ timerLabel: "Preheat",
2477
+ image: image("cornbread-pan-heat", "Butter foaming in a hot cast iron skillet", 1200, 900)
2478
+ },
2479
+ {
2480
+ _key: "s2",
2481
+ title: "Mix",
2482
+ instruction: "<p>Whisk the dry ingredients. Stir in the honey, buttermilk, eggs and melted butter until just combined — lumps are fine.</p>"
2483
+ },
2484
+ {
2485
+ _key: "s3",
2486
+ title: "Bake",
2487
+ instruction: "<p>Pour the batter into the sizzling pan. Cover with the lid and pile coals on top, or bake uncovered in the oven, for <strong>25 minutes</strong>.</p>",
2488
+ hasTimer: true,
2489
+ timerMinutes: 25,
2490
+ timerLabel: "Bake",
2491
+ tip: "It's done when a skewer comes out clean and the edges pull away from the pan."
2492
+ },
2493
+ {
2494
+ _key: "s4",
2495
+ title: "Rest",
2496
+ instruction: "<p>Rest 10 minutes in the pan so it slices cleanly. Drizzle with hot honey and flaky salt.</p>",
2497
+ hasTimer: true,
2498
+ timerMinutes: 10,
2499
+ timerLabel: "Rest"
2500
+ }
2501
+ ],
2502
+ notes: "<p><strong>No buttermilk?</strong> Stir 1 tbsp lemon juice into 480 ml milk and wait 5 minutes.</p><p>Keeps two days wrapped. Reheat wedges cut-side down in a dry skillet.</p>",
2503
+ showNutrition: true,
2504
+ calories: 290,
2505
+ protein: 7,
2506
+ fat: 10,
2507
+ carbs: 43,
2508
+ fiber: 2,
2509
+ sodium: 420,
2510
+ rating: 4.8
2511
+ },
2512
+ notes: {
2513
+ 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.",
2514
+ sanity: "Sanity: `recipe` { ingredients: array<object{title, items: array<object{amount, unit, name, note}>}>, steps: array<object{body: portableText, image, timer}>, nutrition: object }. Same shape; Sanity has no derived/read-only total, it's computed in GROQ.",
2515
+ htmlSignals: [
2516
+ "Recipe JSON-LD in the source (the most reliable signal) or WPRM/Tasty Recipes plugin markup (.wprm-recipe)",
2517
+ "A 'Prep · Cook · Total' strip and 'Serves N' / servings stepper",
2518
+ "<ul> of ingredients with amounts and units; group sub-headings like 'For the glaze'",
2519
+ "<ol> of steps, often with step photos; 'Jump to recipe' / 'Print' buttons",
2520
+ "A nutrition table (Calories, Protein, Fat, Carbs)"
2521
+ ],
2522
+ 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.",
2523
+ pitfalls: [
2524
+ "Ingredient lines as plain strings — scaling and unit switches become impossible.",
2525
+ "Unicode fractions (½ ⅓) and ranges parsed as one number.",
2526
+ "Total time typed by hand disagrees with prep + cook — keep it derived.",
2527
+ "Dietary claims set at recipe level while an optional ingredient breaks them.",
2528
+ "Timer fields filled but hasTimer off — hidden values still ship unless cleared on save."
2529
+ ]
2530
+ }
2531
+ },
2532
+ {
2533
+ id: "course-curriculum",
2534
+ label: "Course curriculum",
2535
+ category: "Long-form & docs",
2536
+ brand: "Kestrel Academy",
2537
+ previewLayout: "accordion",
2538
+ tags: [
2539
+ "deep-nesting",
2540
+ "repeater",
2541
+ "conditional",
2542
+ "references",
2543
+ "validation",
2544
+ "media-heavy",
2545
+ "seo-structured"
2546
+ ],
2547
+ proves: "Modules → lessons → resources → file (4 drill levels) · lesson type reveals video / reading / quiz / live fields · free previews",
2548
+ docTitle: "Brand Identity Systems",
2549
+ urlPrefix: "kestrel.academy/courses/",
2550
+ schema: {
2551
+ name: "curriculum",
2552
+ title: "Course curriculum",
2553
+ kind: "block",
2554
+ icon: "school",
2555
+ category: "Long-form & docs",
2556
+ description: "Modules and lessons, with free previews and resources",
2557
+ variants: [{
2558
+ id: "accordion",
2559
+ name: "Accordion"
2560
+ }, {
2561
+ id: "timeline",
2562
+ name: "Week by week"
2563
+ }],
2564
+ groups: [CONTENT, DESIGN],
2565
+ preview: {
2566
+ title: "heading",
2567
+ subtitle: "course",
2568
+ icon: "school"
2569
+ },
2570
+ fields: [
2571
+ {
2572
+ name: "heading",
2573
+ title: "Heading",
2574
+ type: "string",
2575
+ group: "content",
2576
+ initialValue: "What you'll learn",
2577
+ validation: { max: 48 },
2578
+ localized: true
2579
+ },
2580
+ {
2581
+ name: "course",
2582
+ title: "Course",
2583
+ type: "reference",
2584
+ group: "content",
2585
+ reference: { to: ["col_courses"] },
2586
+ validation: { required: true },
2587
+ description: "Totals, enrolment and the course page come from here."
2588
+ },
2589
+ {
2590
+ name: "modules",
2591
+ title: "Modules",
2592
+ type: "array",
2593
+ group: "content",
2594
+ itemNoun: "module",
2595
+ validation: {
2596
+ min: 1,
2597
+ max: 16
2598
+ },
2599
+ of: [{
2600
+ name: "module",
2601
+ title: "Module",
2602
+ type: "object",
2603
+ icon: "folder",
2604
+ preview: {
2605
+ title: "title",
2606
+ subtitle: "summary",
2607
+ fallback: "Untitled module"
2608
+ },
2609
+ fields: [
2610
+ {
2611
+ name: "title",
2612
+ title: "Title",
2613
+ type: "string",
2614
+ validation: {
2615
+ required: true,
2616
+ max: 60
2617
+ },
2618
+ localized: true
2619
+ },
2620
+ {
2621
+ name: "summary",
2622
+ title: "Summary",
2623
+ type: "text",
2624
+ rows: 2,
2625
+ validation: { recommendedMax: 200 },
2626
+ localized: true
2627
+ },
2628
+ {
2629
+ name: "unlock",
2630
+ title: "Opens",
2631
+ type: "select",
2632
+ layout: "segmented",
2633
+ initialValue: "now",
2634
+ options: [
2635
+ {
2636
+ value: "now",
2637
+ label: "At enrolment"
2638
+ },
2639
+ {
2640
+ value: "previous",
2641
+ label: "After the last module"
2642
+ },
2643
+ {
2644
+ value: "date",
2645
+ label: "On a date"
2646
+ }
2647
+ ],
2648
+ description: ""
2649
+ },
2650
+ {
2651
+ name: "unlockDate",
2652
+ title: "Opens on",
2653
+ type: "date",
2654
+ hidden: {
2655
+ field: "unlock",
2656
+ equals: "date"
2657
+ },
2658
+ validation: { required: true }
2659
+ },
2660
+ {
2661
+ name: "lessons",
2662
+ title: "Lessons",
2663
+ type: "array",
2664
+ itemNoun: "lesson",
2665
+ validation: {
2666
+ min: 1,
2667
+ max: 20
2668
+ },
2669
+ of: [{
2670
+ name: "lesson",
2671
+ title: "Lesson",
2672
+ type: "object",
2673
+ icon: "player-play",
2674
+ preview: {
2675
+ title: "title",
2676
+ subtitle: "type",
2677
+ fallback: "Untitled lesson"
2678
+ },
2679
+ fields: [
2680
+ {
2681
+ name: "title",
2682
+ title: "Title",
2683
+ type: "string",
2684
+ validation: {
2685
+ required: true,
2686
+ max: 80
2687
+ },
2688
+ localized: true
2689
+ },
2690
+ {
2691
+ name: "type",
2692
+ title: "Type",
2693
+ type: "select",
2694
+ layout: "segmented",
2695
+ validation: { required: true },
2696
+ initialValue: "video",
2697
+ options: [
2698
+ {
2699
+ value: "video",
2700
+ label: "Video",
2701
+ icon: "player-play"
2702
+ },
2703
+ {
2704
+ value: "reading",
2705
+ label: "Reading",
2706
+ icon: "file-text"
2707
+ },
2708
+ {
2709
+ value: "quiz",
2710
+ label: "Quiz",
2711
+ icon: "checklist"
2712
+ },
2713
+ {
2714
+ value: "assignment",
2715
+ label: "Assignment",
2716
+ icon: "pencil"
2717
+ },
2718
+ {
2719
+ value: "live",
2720
+ label: "Live",
2721
+ icon: "broadcast"
2722
+ }
2723
+ ],
2724
+ description: ""
2725
+ },
2726
+ {
2727
+ name: "minutes",
2728
+ title: "Length",
2729
+ type: "number",
2730
+ numeric: {
2731
+ unit: "min",
2732
+ min: 1,
2733
+ max: 240,
2734
+ step: 1
2735
+ },
2736
+ validation: { required: true },
2737
+ description: "In minutes. Adds up to the module and course totals."
2738
+ },
2739
+ {
2740
+ name: "video",
2741
+ title: "Video",
2742
+ type: "video",
2743
+ hidden: {
2744
+ field: "type",
2745
+ equals: "video"
2746
+ },
2747
+ validation: { required: true }
2748
+ },
2749
+ {
2750
+ name: "captions",
2751
+ title: "Captions",
2752
+ type: "file",
2753
+ hidden: {
2754
+ field: "type",
2755
+ equals: "video"
2756
+ },
2757
+ description: "A .vtt file. Required on every video lesson."
2758
+ },
2759
+ {
2760
+ name: "reading",
2761
+ title: "Reading",
2762
+ type: "richtext",
2763
+ hidden: {
2764
+ field: "type",
2765
+ equals: ["reading", "assignment"]
2766
+ },
2767
+ validation: { required: true }
2768
+ },
2769
+ {
2770
+ name: "quiz",
2771
+ title: "Quiz",
2772
+ type: "reference",
2773
+ reference: { to: ["col_quizzes"] },
2774
+ hidden: {
2775
+ field: "type",
2776
+ equals: "quiz"
2777
+ },
2778
+ validation: { required: true }
2779
+ },
2780
+ {
2781
+ name: "liveAt",
2782
+ title: "Starts",
2783
+ type: "datetime",
2784
+ hidden: {
2785
+ field: "type",
2786
+ equals: "live"
2787
+ },
2788
+ validation: { required: true },
2789
+ description: "Shown in each student's own time zone."
2790
+ },
2791
+ {
2792
+ name: "isFreePreview",
2793
+ title: "Free preview",
2794
+ type: "boolean",
2795
+ description: "Visitors can open it before enrolling."
2796
+ },
2797
+ {
2798
+ name: "resources",
2799
+ title: "Resources",
2800
+ type: "array",
2801
+ itemNoun: "resource",
2802
+ openMode: "inline",
2803
+ validation: { max: 8 },
2804
+ of: [{
2805
+ name: "resource",
2806
+ title: "Resource",
2807
+ type: "object",
2808
+ icon: "paperclip",
2809
+ preview: {
2810
+ title: "label",
2811
+ subtitle: "kind"
2812
+ },
2813
+ fields: [
2814
+ {
2815
+ name: "label",
2816
+ title: "Label",
2817
+ type: "string",
2818
+ validation: {
2819
+ required: true,
2820
+ max: 48
2821
+ }
2822
+ },
2823
+ {
2824
+ name: "kind",
2825
+ title: "Kind",
2826
+ type: "select",
2827
+ layout: "segmented",
2828
+ initialValue: "file",
2829
+ options: [{
2830
+ value: "file",
2831
+ label: "File"
2832
+ }, {
2833
+ value: "link",
2834
+ label: "Link"
2835
+ }],
2836
+ description: ""
2837
+ },
2838
+ {
2839
+ name: "file",
2840
+ title: "File",
2841
+ type: "file",
2842
+ hidden: {
2843
+ field: "kind",
2844
+ equals: "file"
2845
+ },
2846
+ validation: { required: true }
2847
+ },
2848
+ {
2849
+ name: "link",
2850
+ title: "Link",
2851
+ type: "link",
2852
+ hidden: {
2853
+ field: "kind",
2854
+ equals: "link"
2855
+ },
2856
+ validation: { required: true }
2857
+ }
2858
+ ]
2859
+ }]
2860
+ }
2861
+ ]
2862
+ }]
2863
+ }
2864
+ ]
2865
+ }]
2866
+ },
2867
+ {
2868
+ name: "isFirstOpen",
2869
+ title: "Open the first module",
2870
+ type: "boolean",
2871
+ group: "design",
2872
+ initialValue: true,
2873
+ description: ""
2874
+ },
2875
+ {
2876
+ name: "showLengths",
2877
+ title: "Show lengths",
2878
+ type: "boolean",
2879
+ group: "design",
2880
+ initialValue: true
2881
+ },
2882
+ {
2883
+ name: "showNumbers",
2884
+ title: "Number lessons",
2885
+ type: "boolean",
2886
+ group: "design",
2887
+ initialValue: true,
2888
+ description: "1.1, 1.2 … restarting each module."
2889
+ }
2890
+ ]
2891
+ },
2892
+ value: {
2893
+ _variant: "accordion",
2894
+ heading: "What you'll learn",
2895
+ course: "en_course_brand-identity-systems",
2896
+ modules: [{
2897
+ _key: "m1",
2898
+ title: "Foundations of identity",
2899
+ summary: "Read a brief, audit an existing identity and write a positioning line the system can grow from.",
2900
+ unlock: "now",
2901
+ lessons: [
2902
+ {
2903
+ _key: "l1",
2904
+ title: "What an identity system actually is",
2905
+ type: "video",
2906
+ minutes: 12,
2907
+ video: { url: "https://vimeo.com/912345678" },
2908
+ captions: {
2909
+ url: "https://kestrel.academy/media/captions/bis-1-1.vtt",
2910
+ name: "bis-1-1.vtt",
2911
+ size: 18432
2912
+ },
2913
+ isFreePreview: true,
2914
+ resources: [{
2915
+ _key: "r1",
2916
+ label: "Slides (PDF)",
2917
+ kind: "file",
2918
+ file: {
2919
+ url: "https://kestrel.academy/media/bis-1-1-slides.pdf",
2920
+ name: "bis-1-1-slides.pdf",
2921
+ size: 4194304
2922
+ }
2923
+ }]
2924
+ },
2925
+ {
2926
+ _key: "l2",
2927
+ title: "Auditing an existing brand",
2928
+ type: "reading",
2929
+ minutes: 20,
2930
+ reading: "<p>Before you design anything, inventory what's already out there: every logo lock-up, colour, typeface and template in use.</p>",
2931
+ resources: [{
2932
+ _key: "r2",
2933
+ label: "Audit spreadsheet",
2934
+ kind: "link",
2935
+ link: {
2936
+ type: "url",
2937
+ href: "https://docs.google.com/spreadsheets/d/1kestrel-audit-template/copy",
2938
+ newTab: true
2939
+ }
2940
+ }]
2941
+ },
2942
+ {
2943
+ _key: "l3",
2944
+ title: "Check your understanding",
2945
+ type: "quiz",
2946
+ minutes: 8,
2947
+ quiz: "en_quiz_bis-m1"
2948
+ }
2949
+ ]
2950
+ }, {
2951
+ _key: "m2",
2952
+ title: "Marks, type and colour",
2953
+ summary: "Build a logo suite, pair typefaces with a reason, and make a colour system that passes contrast.",
2954
+ unlock: "previous",
2955
+ lessons: [
2956
+ {
2957
+ _key: "l4",
2958
+ title: "Designing a flexible logo suite",
2959
+ type: "video",
2960
+ minutes: 26,
2961
+ video: { url: "https://vimeo.com/912345690" },
2962
+ resources: []
2963
+ },
2964
+ {
2965
+ _key: "l5",
2966
+ title: "Critique with Ana Kovač",
2967
+ type: "live",
2968
+ minutes: 60,
2969
+ liveAt: "2026-11-12T17:00:00+00:00[Europe/London]",
2970
+ resources: []
2971
+ },
2972
+ {
2973
+ _key: "l6",
2974
+ title: "Assignment: a three-colour system",
2975
+ type: "assignment",
2976
+ minutes: 90,
2977
+ reading: "<p>Build primary, secondary and neutral ramps. Every text pairing must reach 4.5:1.</p>",
2978
+ resources: [{
2979
+ _key: "r3",
2980
+ label: "Figma starter file",
2981
+ kind: "link",
2982
+ link: {
2983
+ type: "url",
2984
+ href: "https://www.figma.com/community/file/1300000000000000001",
2985
+ newTab: true
2986
+ }
2987
+ }]
2988
+ }
2989
+ ]
2990
+ }],
2991
+ isFirstOpen: true,
2992
+ showLengths: true,
2993
+ showNumbers: true
2994
+ },
2995
+ notes: {
2996
+ 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.",
2997
+ sanity: "Sanity: `course` document with `modules: array<object{title, lessons: array<reference→lesson>}>` — lessons are usually their own documents. Inline lessons are fine until lessons are shared between courses; then promote them to a Lessons collection and make `lessons` an array of references.",
2998
+ htmlSignals: [
2999
+ "Accordion of 'Module 1 / Week 1' headings with lesson rows inside",
3000
+ "Per-row icons for video / article / quiz and durations like '12 min'",
3001
+ "'Preview' or 'Free' badges on some lessons; lock icons on others",
3002
+ "Totals like '6 modules · 42 lessons · 9h 30m'"
3003
+ ],
3004
+ 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.",
3005
+ pitfalls: [
3006
+ "Durations typed as text ('1h 05m') can't be summed.",
3007
+ "Video lessons without captions — the captions field should be checked before publish (a11y).",
3008
+ "Live lesson times stored in local time without a zone.",
3009
+ "Five levels inline (course → module → lesson → resource → file) — keep resources tiny and inline."
3010
+ ]
3011
+ }
3012
+ },
3013
+ {
3014
+ id: "legal-policy",
3015
+ label: "Legal / policy page",
3016
+ category: "Long-form & docs",
3017
+ brand: "Caldera",
3018
+ previewLayout: "article",
3019
+ tags: [
3020
+ "deep-nesting",
3021
+ "repeater",
3022
+ "rich-text",
3023
+ "validation",
3024
+ "conditional",
3025
+ "a11y",
3026
+ "localized"
3027
+ ],
3028
+ proves: "Numbered sections → clauses with optional plain-English summaries · effective date + version history with archived copies",
3029
+ docTitle: "Warranty & returns policy",
3030
+ urlPrefix: "caldera.bettercms.site/legal/",
3031
+ schema: {
3032
+ name: "policyPage",
3033
+ title: "Policy page",
3034
+ kind: "page",
3035
+ icon: "scale",
3036
+ category: "Long-form & docs",
3037
+ description: "Terms, privacy, warranty or returns, numbered and versioned",
3038
+ preview: {
3039
+ title: "title",
3040
+ subtitle: "version",
3041
+ icon: "scale"
3042
+ },
3043
+ groups: [
3044
+ CONTENT,
3045
+ {
3046
+ name: "versions",
3047
+ title: "Versions",
3048
+ icon: "history"
3049
+ },
3050
+ SETTINGS
3051
+ ],
3052
+ fieldsets: [{
3053
+ name: "dates",
3054
+ title: "Dates"
3055
+ }],
3056
+ fields: [
3057
+ {
3058
+ name: "title",
3059
+ title: "Title",
3060
+ type: "string",
3061
+ group: "content",
3062
+ validation: {
3063
+ required: true,
3064
+ max: 60
3065
+ },
3066
+ localized: true
3067
+ },
3068
+ {
3069
+ name: "slug",
3070
+ title: "Slug",
3071
+ type: "slug",
3072
+ group: "content",
3073
+ validation: { required: true }
3074
+ },
3075
+ {
3076
+ name: "policyType",
3077
+ title: "Policy",
3078
+ type: "select",
3079
+ group: "content",
3080
+ access: { edit: "schema.manage" },
3081
+ lockReason: "Set by the developer. Footer and checkout links depend on it.",
3082
+ options: [
3083
+ {
3084
+ value: "terms",
3085
+ label: "Terms of service"
3086
+ },
3087
+ {
3088
+ value: "privacy",
3089
+ label: "Privacy"
3090
+ },
3091
+ {
3092
+ value: "cookies",
3093
+ label: "Cookies"
3094
+ },
3095
+ {
3096
+ value: "returns",
3097
+ label: "Returns"
3098
+ },
3099
+ {
3100
+ value: "warranty",
3101
+ label: "Warranty"
3102
+ },
3103
+ {
3104
+ value: "accessibility",
3105
+ label: "Accessibility"
3106
+ }
3107
+ ],
3108
+ description: "Footer and checkout links use this."
3109
+ },
3110
+ {
3111
+ name: "effectiveDate",
3112
+ title: "Effective from",
3113
+ type: "date",
3114
+ group: "content",
3115
+ fieldset: "dates",
3116
+ validation: { required: true }
3117
+ },
3118
+ {
3119
+ name: "version",
3120
+ title: "Version",
3121
+ type: "string",
3122
+ group: "content",
3123
+ fieldset: "dates",
3124
+ validation: {
3125
+ required: true,
3126
+ pattern: "^\\d+\\.\\d+$",
3127
+ patternMessage: "Use a version like 2.1."
3128
+ },
3129
+ description: "Bump the first number for material changes."
3130
+ },
3131
+ {
3132
+ name: "summary",
3133
+ title: "In short",
3134
+ type: "richtext",
3135
+ group: "content",
3136
+ localized: true
3137
+ },
3138
+ {
3139
+ name: "sections",
3140
+ title: "Sections",
3141
+ type: "array",
3142
+ group: "content",
3143
+ itemNoun: "section",
3144
+ validation: {
3145
+ min: 1,
3146
+ max: 40
3147
+ },
3148
+ description: "Numbered automatically in this order.",
3149
+ of: [{
3150
+ name: "section",
3151
+ title: "Section",
3152
+ type: "object",
3153
+ icon: "section",
3154
+ preview: {
3155
+ title: "heading",
3156
+ subtitle: "anchor",
3157
+ fallback: "Untitled section"
3158
+ },
3159
+ fields: [
3160
+ {
3161
+ name: "heading",
3162
+ title: "Heading",
3163
+ type: "string",
3164
+ validation: {
3165
+ required: true,
3166
+ max: 80
3167
+ },
3168
+ localized: true
3169
+ },
3170
+ {
3171
+ name: "anchor",
3172
+ title: "Anchor",
3173
+ type: "string",
3174
+ validation: {
3175
+ pattern: "^[a-z0-9-]+$",
3176
+ patternMessage: "Use lowercase letters, numbers and hyphens, like camp-kitchen."
3177
+ },
3178
+ description: "Changing it once live breaks existing links."
3179
+ },
3180
+ {
3181
+ name: "clauses",
3182
+ title: "Clauses",
3183
+ type: "array",
3184
+ itemNoun: "clause",
3185
+ validation: {
3186
+ min: 1,
3187
+ max: 30
3188
+ },
3189
+ of: [{
3190
+ name: "clause",
3191
+ title: "Clause",
3192
+ type: "object",
3193
+ icon: "align-left",
3194
+ preview: {
3195
+ title: "heading",
3196
+ subtitle: "body",
3197
+ fallback: "Untitled clause"
3198
+ },
3199
+ fields: [
3200
+ {
3201
+ name: "heading",
3202
+ title: "Heading",
3203
+ type: "string",
3204
+ validation: { max: 80 },
3205
+ localized: true
3206
+ },
3207
+ {
3208
+ name: "body",
3209
+ title: "Body",
3210
+ type: "richtext",
3211
+ validation: { required: true },
3212
+ localized: true
3213
+ },
3214
+ {
3215
+ name: "hasSummary",
3216
+ title: "Add a plain-English note",
3217
+ type: "boolean"
3218
+ },
3219
+ {
3220
+ name: "plain",
3221
+ title: "Plain-English note",
3222
+ type: "text",
3223
+ rows: 2,
3224
+ hidden: {
3225
+ field: "hasSummary",
3226
+ equals: true
3227
+ },
3228
+ validation: {
3229
+ required: true,
3230
+ max: 240
3231
+ },
3232
+ localized: true,
3233
+ description: ""
3234
+ }
3235
+ ]
3236
+ }]
3237
+ }
3238
+ ]
3239
+ }]
3240
+ },
3241
+ {
3242
+ name: "contactEmail",
3243
+ title: "Questions go to",
3244
+ type: "email",
3245
+ group: "content",
3246
+ validation: { required: true }
3247
+ },
3248
+ {
3249
+ name: "versions",
3250
+ title: "Version history",
3251
+ type: "array",
3252
+ group: "versions",
3253
+ itemNoun: "version",
3254
+ validation: { max: 30 },
3255
+ of: [{
3256
+ name: "pastVersion",
3257
+ title: "Past version",
3258
+ type: "object",
3259
+ icon: "history",
3260
+ preview: {
3261
+ title: "version",
3262
+ subtitle: "effectiveDate"
3263
+ },
3264
+ fields: [
3265
+ {
3266
+ name: "version",
3267
+ title: "Version",
3268
+ type: "string",
3269
+ validation: {
3270
+ required: true,
3271
+ pattern: "^\\d+\\.\\d+$",
3272
+ patternMessage: "Use a version like 2.1."
3273
+ }
3274
+ },
3275
+ {
3276
+ name: "effectiveDate",
3277
+ title: "Effective from",
3278
+ type: "date",
3279
+ validation: { required: true }
3280
+ },
3281
+ {
3282
+ name: "changes",
3283
+ title: "What changed",
3284
+ type: "text",
3285
+ rows: 2,
3286
+ validation: {
3287
+ required: true,
3288
+ max: 300
3289
+ }
3290
+ },
3291
+ {
3292
+ name: "archive",
3293
+ title: "Archived copy",
3294
+ type: "file",
3295
+ description: "A PDF of the full text as it stood."
3296
+ }
3297
+ ]
3298
+ }]
3299
+ },
3300
+ {
3301
+ name: "numbering",
3302
+ title: "Numbering",
3303
+ type: "select",
3304
+ group: "settings",
3305
+ layout: "segmented",
3306
+ initialValue: "decimal",
3307
+ options: [
3308
+ {
3309
+ value: "decimal",
3310
+ label: "1. 1.1"
3311
+ },
3312
+ {
3313
+ value: "roman",
3314
+ label: "I, A"
3315
+ },
3316
+ {
3317
+ value: "none",
3318
+ label: "None"
3319
+ }
3320
+ ],
3321
+ description: ""
3322
+ },
3323
+ {
3324
+ name: "showContents",
3325
+ title: "Show table of contents",
3326
+ type: "boolean",
3327
+ group: "settings",
3328
+ initialValue: true
3329
+ },
3330
+ {
3331
+ name: "showChangeBanner",
3332
+ title: "Show a banner when it changes",
3333
+ type: "boolean",
3334
+ group: "settings",
3335
+ description: "Shows a site banner for 30 days after a new effective date."
3336
+ }
3337
+ ]
3338
+ },
3339
+ value: {
3340
+ title: "Warranty & returns",
3341
+ slug: "warranty-returns",
3342
+ policyType: "warranty",
3343
+ effectiveDate: "2026-10-01",
3344
+ version: "3.0",
3345
+ summary: "<p>Cast iron and carbon steel are covered for life against cracks, warps and failed welds. Unused items can come back within 60 days for a full refund. Seasoning wear isn't a defect.</p>",
3346
+ sections: [{
3347
+ _key: "s1",
3348
+ heading: "Lifetime warranty",
3349
+ anchor: "lifetime-warranty",
3350
+ clauses: [{
3351
+ _key: "c1",
3352
+ heading: "What's covered",
3353
+ body: "<p>Caldera warrants every cast iron and carbon steel product against defects in materials and workmanship, including cracking, warping beyond 1.5 mm and weld failure, for the life of the product.</p>",
3354
+ hasSummary: true,
3355
+ plain: "If it cracks, warps or a weld fails, we replace it. Forever."
3356
+ }, {
3357
+ _key: "c2",
3358
+ heading: "What isn't",
3359
+ body: "<p>Rust, seasoning loss, discolouration and damage from misuse — including thermal shock from cold water on a hot pan — are not defects.</p>",
3360
+ hasSummary: true,
3361
+ plain: "Rust and worn seasoning are fixable, not faults. We'll show you how."
3362
+ }]
3363
+ }, {
3364
+ _key: "s2",
3365
+ heading: "Returns",
3366
+ anchor: "returns",
3367
+ clauses: [{
3368
+ _key: "c3",
3369
+ heading: "60-day window",
3370
+ body: "<p>Unused items in original packaging may be returned within 60 days of delivery for a full refund to the original payment method.</p>",
3371
+ hasSummary: false
3372
+ }, {
3373
+ _key: "c4",
3374
+ heading: "How to start a return",
3375
+ body: "<p>Start a return at <a href=\"/returns\">caldera.bettercms.site/returns</a> with your order number. We email a prepaid label within one business day.</p>",
3376
+ hasSummary: false
3377
+ }]
3378
+ }],
3379
+ contactEmail: "care@caldera.co",
3380
+ versions: [{
3381
+ _key: "h1",
3382
+ version: "2.1",
3383
+ effectiveDate: "2025-03-01",
3384
+ changes: "Returns window extended from 30 to 45 days.",
3385
+ archive: {
3386
+ url: "https://caldera.bettercms.site/media/legal/warranty-returns-v2-1.pdf",
3387
+ name: "warranty-returns-v2-1.pdf",
3388
+ size: 186e3
3389
+ }
3390
+ }, {
3391
+ _key: "h2",
3392
+ version: "2.0",
3393
+ effectiveDate: "2024-01-15",
3394
+ changes: "Carbon steel added to the lifetime warranty.",
3395
+ archive: {
3396
+ url: "https://caldera.bettercms.site/media/legal/warranty-returns-v2-0.pdf",
3397
+ name: "warranty-returns-v2-0.pdf",
3398
+ size: 171e3
3399
+ }
3400
+ }],
3401
+ numbering: "decimal",
3402
+ showContents: true,
3403
+ showChangeBanner: true
3404
+ },
3405
+ notes: {
3406
+ 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.",
3407
+ sanity: "Sanity: `legalPage` { sections: array<object{title, body: portableText}>, effectiveDate, version }. Numbering is usually typed into headings, which breaks on insert. Version history lives in Sanity's history, not in content — not something you can link customers to.",
3408
+ htmlSignals: [
3409
+ "Headings numbered '1.', '1.1', 'Section 4' or roman numerals",
3410
+ "'Effective date', 'Last updated', 'Version' near the title",
3411
+ "A contents list of #anchors at the top",
3412
+ "Two-column 'In plain English' or 'TL;DR' asides beside clauses",
3413
+ "Links to previous versions or PDFs"
3414
+ ],
3415
+ 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.",
3416
+ pitfalls: [
3417
+ "Changing an anchor breaks links in sent emails and order confirmations.",
3418
+ "Typed numbering drifts on insert — numbering is generated.",
3419
+ "'Last updated' bumped for typos implies the terms changed.",
3420
+ "Localized policies must be reviewed per locale, not machine-translated — flag localized fields."
3421
+ ]
3422
+ }
3423
+ },
3424
+ {
3425
+ id: "case-study",
3426
+ label: "Case study",
3427
+ category: "Long-form & docs",
3428
+ brand: "Ledgerline",
3429
+ previewLayout: "article",
3430
+ tags: [
3431
+ "rich-text",
3432
+ "repeater",
3433
+ "references",
3434
+ "conditional",
3435
+ "media-heavy",
3436
+ "validation"
3437
+ ],
3438
+ proves: "Customer facts as an object · headline metrics repeater · challenge/solution/results as three rich-text bodies · quote written here or referenced",
3439
+ docTitle: "How Northbeam closes its books in 3 days",
3440
+ urlPrefix: "ledgerline.com/customers/",
3441
+ schema: {
3442
+ name: "caseStudy",
3443
+ title: "Case study",
3444
+ kind: "entry",
3445
+ icon: "briefcase",
3446
+ category: "Long-form & docs",
3447
+ description: "A customer story with results",
3448
+ preview: {
3449
+ title: "title",
3450
+ subtitle: "customerName",
3451
+ media: "customerLogo"
3452
+ },
3453
+ groups: [
3454
+ CONTENT,
3455
+ {
3456
+ name: "customer",
3457
+ title: "Customer",
3458
+ icon: "building"
3459
+ },
3460
+ MEDIA
3461
+ ],
3462
+ fieldsets: [{
3463
+ name: "quote",
3464
+ title: "Quote"
3465
+ }],
3466
+ fields: [
3467
+ {
3468
+ name: "title",
3469
+ title: "Title",
3470
+ type: "string",
3471
+ group: "content",
3472
+ validation: {
3473
+ required: true,
3474
+ max: 90
3475
+ },
3476
+ localized: true
3477
+ },
3478
+ {
3479
+ name: "slug",
3480
+ title: "Slug",
3481
+ type: "slug",
3482
+ group: "content",
3483
+ validation: { required: true }
3484
+ },
3485
+ {
3486
+ name: "summary",
3487
+ title: "Summary",
3488
+ type: "text",
3489
+ rows: 2,
3490
+ group: "content",
3491
+ validation: {
3492
+ required: true,
3493
+ recommendedMax: 180
3494
+ },
3495
+ localized: true
3496
+ },
3497
+ {
3498
+ name: "metrics",
3499
+ title: "Headline results",
3500
+ type: "array",
3501
+ group: "content",
3502
+ itemNoun: "result",
3503
+ openMode: "inline",
3504
+ validation: {
3505
+ min: 2,
3506
+ max: 4
3507
+ },
3508
+ of: [{
3509
+ name: "metric",
3510
+ title: "Result",
3511
+ type: "object",
3512
+ icon: "chart-bar",
3513
+ preview: {
3514
+ title: "value",
3515
+ subtitle: "label"
3516
+ },
3517
+ fields: [
3518
+ {
3519
+ name: "value",
3520
+ title: "Number",
3521
+ type: "string",
3522
+ validation: {
3523
+ required: true,
3524
+ max: 10
3525
+ },
3526
+ placeholder: "3 days"
3527
+ },
3528
+ {
3529
+ name: "label",
3530
+ title: "Label",
3531
+ type: "string",
3532
+ validation: {
3533
+ required: true,
3534
+ max: 40
3535
+ }
3536
+ },
3537
+ {
3538
+ name: "context",
3539
+ title: "Compared with",
3540
+ type: "string",
3541
+ validation: { max: 40 },
3542
+ placeholder: "down from 11"
3543
+ }
3544
+ ]
3545
+ }]
3546
+ },
3547
+ {
3548
+ name: "challenge",
3549
+ title: "The challenge",
3550
+ type: "richtext",
3551
+ group: "content",
3552
+ validation: { required: true },
3553
+ ai: true,
3554
+ localized: true
3555
+ },
3556
+ {
3557
+ name: "solution",
3558
+ title: "The solution",
3559
+ type: "richtext",
3560
+ group: "content",
3561
+ validation: { required: true },
3562
+ ai: true,
3563
+ localized: true
3564
+ },
3565
+ {
3566
+ name: "results",
3567
+ title: "The results",
3568
+ type: "richtext",
3569
+ group: "content",
3570
+ validation: { required: true },
3571
+ ai: true,
3572
+ localized: true
3573
+ },
3574
+ {
3575
+ name: "quoteSource",
3576
+ title: "Quote",
3577
+ type: "select",
3578
+ group: "content",
3579
+ fieldset: "quote",
3580
+ layout: "segmented",
3581
+ initialValue: "written",
3582
+ options: [
3583
+ {
3584
+ value: "written",
3585
+ label: "Write it here"
3586
+ },
3587
+ {
3588
+ value: "testimonial",
3589
+ label: "From Testimonials"
3590
+ },
3591
+ {
3592
+ value: "none",
3593
+ label: "None"
3594
+ }
3595
+ ],
3596
+ description: "Testimonials are reused on pricing and the homepage."
3597
+ },
3598
+ {
3599
+ name: "quote",
3600
+ title: "Quote",
3601
+ type: "text",
3602
+ rows: 3,
3603
+ group: "content",
3604
+ fieldset: "quote",
3605
+ hidden: {
3606
+ field: "quoteSource",
3607
+ equals: "written"
3608
+ },
3609
+ validation: {
3610
+ required: true,
3611
+ recommendedMax: 220
3612
+ }
3613
+ },
3614
+ {
3615
+ name: "quoteName",
3616
+ title: "Name",
3617
+ type: "string",
3618
+ group: "content",
3619
+ fieldset: "quote",
3620
+ hidden: {
3621
+ field: "quoteSource",
3622
+ equals: "written"
3623
+ },
3624
+ validation: { required: true }
3625
+ },
3626
+ {
3627
+ name: "quoteRole",
3628
+ title: "Role",
3629
+ type: "string",
3630
+ group: "content",
3631
+ fieldset: "quote",
3632
+ hidden: {
3633
+ field: "quoteSource",
3634
+ equals: "written"
3635
+ }
3636
+ },
3637
+ {
3638
+ name: "quotePhoto",
3639
+ title: "Photo",
3640
+ type: "image",
3641
+ group: "content",
3642
+ fieldset: "quote",
3643
+ hidden: {
3644
+ field: "quoteSource",
3645
+ equals: "written"
3646
+ }
3647
+ },
3648
+ {
3649
+ name: "testimonial",
3650
+ title: "Testimonial",
3651
+ type: "reference",
3652
+ group: "content",
3653
+ fieldset: "quote",
3654
+ reference: { to: ["col_testimonials"] },
3655
+ hidden: {
3656
+ field: "quoteSource",
3657
+ equals: "testimonial"
3658
+ },
3659
+ validation: { required: true }
3660
+ },
3661
+ {
3662
+ name: "productsUsed",
3663
+ title: "Products used",
3664
+ type: "multiselect",
3665
+ group: "content",
3666
+ options: [
3667
+ {
3668
+ value: "close",
3669
+ label: "Close"
3670
+ },
3671
+ {
3672
+ value: "reconcile",
3673
+ label: "Reconcile"
3674
+ },
3675
+ {
3676
+ value: "spend",
3677
+ label: "Spend"
3678
+ },
3679
+ {
3680
+ value: "reports",
3681
+ label: "Reports"
3682
+ }
3683
+ ],
3684
+ description: "Also filters the customer list."
3685
+ },
3686
+ {
3687
+ name: "button",
3688
+ title: "Button",
3689
+ type: "button",
3690
+ group: "content"
3691
+ },
3692
+ {
3693
+ name: "customerName",
3694
+ title: "Company",
3695
+ type: "string",
3696
+ group: "customer",
3697
+ validation: {
3698
+ required: true,
3699
+ max: 40
3700
+ }
3701
+ },
3702
+ {
3703
+ name: "customerLogo",
3704
+ title: "Logo",
3705
+ type: "image",
3706
+ group: "customer",
3707
+ validation: { required: true },
3708
+ description: "SVG or a transparent PNG."
3709
+ },
3710
+ {
3711
+ name: "industry",
3712
+ title: "Industry",
3713
+ type: "select",
3714
+ group: "customer",
3715
+ options: [
3716
+ {
3717
+ value: "saas",
3718
+ label: "Software"
3719
+ },
3720
+ {
3721
+ value: "ecommerce",
3722
+ label: "E-commerce"
3723
+ },
3724
+ {
3725
+ value: "marketplace",
3726
+ label: "Marketplace"
3727
+ },
3728
+ {
3729
+ value: "fintech",
3730
+ label: "Fintech"
3731
+ },
3732
+ {
3733
+ value: "healthcare",
3734
+ label: "Healthcare"
3735
+ }
3736
+ ],
3737
+ description: ""
3738
+ },
3739
+ {
3740
+ name: "companySize",
3741
+ title: "Company size",
3742
+ type: "select",
3743
+ group: "customer",
3744
+ options: [
3745
+ {
3746
+ value: "1-50",
3747
+ label: "1–50"
3748
+ },
3749
+ {
3750
+ value: "51-200",
3751
+ label: "51–200"
3752
+ },
3753
+ {
3754
+ value: "201-1000",
3755
+ label: "201–1,000"
3756
+ },
3757
+ {
3758
+ value: "1000+",
3759
+ label: "1,000+"
3760
+ }
3761
+ ],
3762
+ description: ""
3763
+ },
3764
+ {
3765
+ name: "region",
3766
+ title: "Region",
3767
+ type: "select",
3768
+ group: "customer",
3769
+ options: [
3770
+ {
3771
+ value: "na",
3772
+ label: "North America"
3773
+ },
3774
+ {
3775
+ value: "emea",
3776
+ label: "Europe & Middle East"
3777
+ },
3778
+ {
3779
+ value: "apac",
3780
+ label: "Asia Pacific"
3781
+ }
3782
+ ],
3783
+ description: ""
3784
+ },
3785
+ {
3786
+ name: "website",
3787
+ title: "Website",
3788
+ type: "url",
3789
+ group: "customer"
3790
+ },
3791
+ {
3792
+ name: "coverImage",
3793
+ title: "Cover image",
3794
+ type: "image",
3795
+ group: "media",
3796
+ validation: { required: true }
3797
+ },
3798
+ {
3799
+ name: "pdf",
3800
+ title: "PDF version",
3801
+ type: "file",
3802
+ group: "media",
3803
+ description: "Offered as a download on the page."
3804
+ }
3805
+ ]
3806
+ },
3807
+ value: {
3808
+ title: "How Northbeam closes its books in 3 days",
3809
+ slug: "northbeam",
3810
+ summary: "Northbeam's four-person finance team cut month-end close from 11 days to 3 while revenue doubled — without adding headcount.",
3811
+ metrics: [
3812
+ {
3813
+ _key: "m1",
3814
+ value: "3 days",
3815
+ label: "Month-end close",
3816
+ context: "down from 11"
3817
+ },
3818
+ {
3819
+ _key: "m2",
3820
+ value: "92%",
3821
+ label: "Transactions auto-matched",
3822
+ context: "up from 40%"
3823
+ },
3824
+ {
3825
+ _key: "m3",
3826
+ value: "0",
3827
+ label: "New finance hires",
3828
+ context: "while revenue doubled"
3829
+ }
3830
+ ],
3831
+ challenge: "<p>Northbeam sells to 6,000 Shopify brands across three currencies. Its finance team reconciled Stripe payouts against bank lines in spreadsheets — <strong>11 days</strong> every month, and growing.</p>",
3832
+ solution: "<p>Ledgerline connected Stripe, two banks and NetSuite in an afternoon. Bank rules matched payouts automatically, and the multi-entity checklist replaced a 140-row spreadsheet.</p>",
3833
+ results: "<p>Close now takes three days. The controller spends the rest of the month on forecasting instead of matching, and auditors pull evidence straight from the close report.</p>",
3834
+ quoteSource: "written",
3835
+ quote: "We closed March in three days and nobody stayed late. I didn't think that was possible at our volume.",
3836
+ quoteName: "Dana Whitfield",
3837
+ quoteRole: "Controller, Northbeam",
3838
+ quotePhoto: image("dana-whitfield", "Portrait of Dana Whitfield", 400, 400),
3839
+ productsUsed: ["close", "reconcile"],
3840
+ button: {
3841
+ label: "Book a demo",
3842
+ href: "/demo",
3843
+ type: "page",
3844
+ title: "Book a demo",
3845
+ style: "primary",
3846
+ arrow: true
3847
+ },
3848
+ customerName: "Northbeam",
3849
+ customerLogo: {
3850
+ url: "https://picsum.photos/seed/northbeam-logo/480/160",
3851
+ alt: "Northbeam",
3852
+ name: "northbeam-logo.svg",
3853
+ width: 480,
3854
+ height: 160
3855
+ },
3856
+ industry: "saas",
3857
+ companySize: "201-1000",
3858
+ region: "na",
3859
+ website: "https://www.northbeam.io",
3860
+ coverImage: image("northbeam-team", "Northbeam's finance team at a whiteboard covered in a close calendar"),
3861
+ pdf: {
3862
+ url: "https://ledgerline.com/media/customers/northbeam-one-pager.pdf",
3863
+ name: "northbeam-one-pager.pdf",
3864
+ size: 612e3
3865
+ }
3866
+ },
3867
+ notes: {
3868
+ 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.",
3869
+ sanity: "Sanity: `caseStudy` { customer: reference→company, stats: array<object>, body: portableText, testimonial: reference→testimonial }. Many teams put everything in one body; three bodies make the structure enforceable.",
3870
+ htmlSignals: [
3871
+ "A customer logo and big numbers row near the top ('3 days', '92%')",
3872
+ "Sections headed Challenge / Solution / Results (or Problem / Approach / Outcome)",
3873
+ "A large pull quote with name, title and headshot",
3874
+ "A side panel: Industry, Company size, Location, Products used",
3875
+ "URL under /customers/ or /case-studies/"
3876
+ ],
3877
+ 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.",
3878
+ pitfalls: [
3879
+ "Metrics without approval — help text says so; numbers are what legal checks.",
3880
+ "One undivided body: the Challenge/Solution/Results layout can't be enforced.",
3881
+ "Customer logos as JPG with a white box — ask for SVG/transparent PNG.",
3882
+ "Duplicate quotes typed in three places drift — reference Testimonials."
3883
+ ]
3884
+ }
3885
+ },
3886
+ {
3887
+ id: "podcast-episode",
3888
+ label: "Podcast episode",
3889
+ category: "Long-form & docs",
3890
+ brand: "Caldera",
3891
+ previewLayout: "article",
3892
+ tags: [
3893
+ "media-heavy",
3894
+ "embed",
3895
+ "references",
3896
+ "repeater",
3897
+ "seo-structured",
3898
+ "a11y",
3899
+ "validation",
3900
+ "conditional"
3901
+ ],
3902
+ proves: "Audio file + show notes + timestamped chapters + transcript (a11y) · guests by reference · feeds the RSS and PodcastEpisode JSON-LD",
3903
+ docTitle: "Ep. 42 — Cooking at 3,000 metres",
3904
+ urlPrefix: "caldera.bettercms.site/podcast/",
3905
+ schema: {
3906
+ name: "podcastEpisode",
3907
+ title: "Podcast episode",
3908
+ kind: "entry",
3909
+ icon: "microphone",
3910
+ category: "Long-form & docs",
3911
+ description: "One episode of the show, with audio, notes and transcript",
3912
+ preview: {
3913
+ title: "title",
3914
+ subtitle: "episodeNumber",
3915
+ media: "art"
3916
+ },
3917
+ groups: [
3918
+ CONTENT,
3919
+ {
3920
+ name: "audio",
3921
+ title: "Audio",
3922
+ icon: "headphones"
3923
+ },
3924
+ {
3925
+ name: "transcript",
3926
+ title: "Transcript",
3927
+ icon: "file-text"
3928
+ }
3929
+ ],
3930
+ fieldsets: [{
3931
+ name: "numbering",
3932
+ title: "Numbering",
3933
+ columns: 2
3934
+ }],
3935
+ fields: [
3936
+ {
3937
+ name: "title",
3938
+ title: "Title",
3939
+ type: "string",
3940
+ group: "content",
3941
+ validation: {
3942
+ required: true,
3943
+ max: 90
3944
+ },
3945
+ description: "Without the episode number. Apps add it."
3946
+ },
3947
+ {
3948
+ name: "slug",
3949
+ title: "Slug",
3950
+ type: "slug",
3951
+ group: "content",
3952
+ validation: { required: true }
3953
+ },
3954
+ {
3955
+ name: "episodeType",
3956
+ title: "Type",
3957
+ type: "select",
3958
+ group: "content",
3959
+ fieldset: "numbering",
3960
+ layout: "segmented",
3961
+ initialValue: "full",
3962
+ options: [
3963
+ {
3964
+ value: "full",
3965
+ label: "Full"
3966
+ },
3967
+ {
3968
+ value: "bonus",
3969
+ label: "Bonus"
3970
+ },
3971
+ {
3972
+ value: "trailer",
3973
+ label: "Trailer"
3974
+ }
3975
+ ],
3976
+ description: ""
3977
+ },
3978
+ {
3979
+ name: "season",
3980
+ title: "Season",
3981
+ type: "number",
3982
+ group: "content",
3983
+ fieldset: "numbering",
3984
+ numeric: {
3985
+ min: 1,
3986
+ step: 1
3987
+ }
3988
+ },
3989
+ {
3990
+ name: "episodeNumber",
3991
+ title: "Episode",
3992
+ type: "number",
3993
+ group: "content",
3994
+ fieldset: "numbering",
3995
+ numeric: {
3996
+ min: 1,
3997
+ step: 1
3998
+ },
3999
+ hidden: {
4000
+ field: "episodeType",
4001
+ equals: "full"
4002
+ },
4003
+ validation: { required: true }
4004
+ },
4005
+ {
4006
+ name: "publishedAt",
4007
+ title: "Publish date",
4008
+ type: "datetime",
4009
+ group: "content",
4010
+ validation: { required: true },
4011
+ description: "Also the release time in podcast apps."
4012
+ },
4013
+ {
4014
+ name: "summary",
4015
+ title: "Summary",
4016
+ type: "text",
4017
+ rows: 2,
4018
+ group: "content",
4019
+ validation: {
4020
+ required: true,
4021
+ recommendedMax: 250
4022
+ },
4023
+ description: "The first lines podcast apps show. No links."
4024
+ },
4025
+ {
4026
+ name: "showNotes",
4027
+ title: "Show notes",
4028
+ type: "richtext",
4029
+ group: "content",
4030
+ validation: { required: true },
4031
+ description: "Also goes into the RSS feed."
4032
+ },
4033
+ refs("hosts", "Hosts", ["col_authors"], {
4034
+ group: "content",
4035
+ validation: {
4036
+ min: 1,
4037
+ max: 3
4038
+ }
4039
+ }),
4040
+ refs("guests", "Guests", ["col_people"], {
4041
+ group: "content",
4042
+ validation: { max: 4 },
4043
+ description: "Each guest gets a card and links back from their profile."
4044
+ }),
4045
+ {
4046
+ name: "chapters",
4047
+ title: "Chapters",
4048
+ type: "array",
4049
+ group: "content",
4050
+ itemNoun: "chapter",
4051
+ openMode: "inline",
4052
+ validation: { max: 30 },
4053
+ of: [{
4054
+ name: "chapter",
4055
+ title: "Chapter",
4056
+ type: "object",
4057
+ icon: "clock",
4058
+ preview: {
4059
+ title: "title",
4060
+ subtitle: "start"
4061
+ },
4062
+ fields: [{
4063
+ name: "start",
4064
+ title: "Starts at",
4065
+ type: "string",
4066
+ validation: {
4067
+ required: true,
4068
+ pattern: "^(\\d{1,2}:)?[0-5]?\\d:[0-5]\\d$",
4069
+ patternMessage: "Use mm:ss or h:mm:ss, like 12:40."
4070
+ },
4071
+ placeholder: "12:30"
4072
+ }, {
4073
+ name: "title",
4074
+ title: "Title",
4075
+ type: "string",
4076
+ validation: {
4077
+ required: true,
4078
+ max: 60
4079
+ }
4080
+ }]
4081
+ }]
4082
+ },
4083
+ {
4084
+ name: "audio",
4085
+ title: "Audio file",
4086
+ type: "file",
4087
+ group: "audio",
4088
+ validation: { required: true },
4089
+ description: "MP3 or M4A. The player and the RSS feed both use it."
4090
+ },
4091
+ {
4092
+ name: "durationSeconds",
4093
+ title: "Length",
4094
+ type: "number",
4095
+ group: "audio",
4096
+ readOnly: true,
4097
+ numeric: { unit: "s" },
4098
+ description: "Read from the audio file."
4099
+ },
4100
+ {
4101
+ name: "isExplicit",
4102
+ title: "Explicit content",
4103
+ type: "boolean",
4104
+ group: "audio",
4105
+ description: "Marks it explicit in podcast apps."
4106
+ },
4107
+ {
4108
+ name: "art",
4109
+ title: "Episode art",
4110
+ type: "image",
4111
+ group: "audio",
4112
+ description: "Square, 3000 × 3000. Leave empty to use the show art."
4113
+ },
4114
+ {
4115
+ name: "video",
4116
+ title: "Video version",
4117
+ type: "video",
4118
+ group: "audio"
4119
+ },
4120
+ {
4121
+ name: "transcriptSource",
4122
+ title: "Transcript",
4123
+ type: "select",
4124
+ group: "transcript",
4125
+ layout: "segmented",
4126
+ initialValue: "text",
4127
+ options: [{
4128
+ value: "text",
4129
+ label: "Write or paste"
4130
+ }, {
4131
+ value: "file",
4132
+ label: "Upload file"
4133
+ }]
4134
+ },
4135
+ {
4136
+ name: "transcript",
4137
+ title: "Transcript text",
4138
+ type: "richtext",
4139
+ group: "transcript",
4140
+ hidden: {
4141
+ field: "transcriptSource",
4142
+ equals: "text"
4143
+ },
4144
+ description: "Bold each speaker's name at the start of their turn."
4145
+ },
4146
+ {
4147
+ name: "transcriptFile",
4148
+ title: "Transcript file",
4149
+ type: "file",
4150
+ group: "transcript",
4151
+ hidden: {
4152
+ field: "transcriptSource",
4153
+ equals: "file"
4154
+ },
4155
+ description: "A .vtt or .srt file. Also used as player captions."
4156
+ }
4157
+ ]
4158
+ },
4159
+ value: {
4160
+ title: "Cooking at 3,000 metres",
4161
+ slug: "cooking-at-3000-metres",
4162
+ episodeType: "full",
4163
+ season: 3,
4164
+ episodeNumber: 42,
4165
+ publishedAt: "2026-09-29T05:00:00-07:00[America/Los_Angeles]",
4166
+ summary: "Mountain guide Inés Calderón on why water boils at 90°C up high, the one pan she carries up Aconcagua, and dinner for twelve in a snowstorm.",
4167
+ showNotes: "<p>Inés guides on Aconcagua and in the Cordillera Blanca. We talk altitude, fuel and feeding a team at the end of a 14-hour day.</p><h3>Mentioned</h3><ul><li><a href=\"/products/carbon-pan-8\">Carbon Steel Pan 8\"</a></li><li><a href=\"/recipes/one-pot-lentils\">One-pot lentils with chorizo</a></li><li><a href=\"https://www.ineshighaltitude.com\" rel=\"nofollow\">Inés's guiding company</a></li></ul>",
4168
+ hosts: ["au_mei"],
4169
+ guests: ["en_p_ines-calderon"],
4170
+ chapters: [
4171
+ {
4172
+ _key: "ch1",
4173
+ start: "0:00",
4174
+ title: "Intro"
4175
+ },
4176
+ {
4177
+ _key: "ch2",
4178
+ start: "3:12",
4179
+ title: "Why water boils at 90°C"
4180
+ },
4181
+ {
4182
+ _key: "ch3",
4183
+ start: "17:45",
4184
+ title: "The one-pan kit list"
4185
+ },
4186
+ {
4187
+ _key: "ch4",
4188
+ start: "34:20",
4189
+ title: "Dinner for twelve in a storm"
4190
+ }
4191
+ ],
4192
+ audio: {
4193
+ url: "https://media.caldera.co/podcast/low-slow-s3e42.mp3",
4194
+ name: "low-slow-s3e42.mp3",
4195
+ size: 61234567
4196
+ },
4197
+ durationSeconds: 2934,
4198
+ isExplicit: false,
4199
+ art: null,
4200
+ video: { url: "https://www.youtube.com/watch?v=Fq3J2xQm8Hk" },
4201
+ transcriptSource: "text",
4202
+ transcript: "<p><strong>Mei:</strong> Inés, you've cooked on more summits than anyone I know. What surprises people first?</p><p><strong>Inés:</strong> That pasta never cooks. Water boils at about ninety degrees up there, so everything takes twice as long and you burn twice the fuel.</p>"
4203
+ },
4204
+ notes: {
4205
+ 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.",
4206
+ sanity: "Sanity: `episode` { audio: file, duration: number, season, episodeNumber, showNotes: portableText, guests: array<reference→person>, chapters: array<object{start, title}>, transcript: portableText }. RSS is generated by the frontend.",
4207
+ htmlSignals: [
4208
+ "An <audio> player or a Transistor/Buzzsprout/Simplecast iframe as the page's main media",
4209
+ "'Episode 42', 'S3 E42', a duration like '48:54'",
4210
+ "Timestamps (12:30) linking to positions in the player",
4211
+ "'Show notes', 'Mentioned in this episode', 'Transcript' sections",
4212
+ "Guest cards with headshots and links"
4213
+ ],
4214
+ 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.",
4215
+ pitfalls: [
4216
+ "Episode number typed into the title ('Ep. 42 —') duplicates in apps.",
4217
+ "Summary with HTML/links — some apps show raw tags.",
4218
+ "Missing transcript fails accessibility and loses search traffic.",
4219
+ "Changing the audio file URL after publish re-downloads in every subscriber's app — replace in place."
4220
+ ]
4221
+ }
4222
+ }
4223
+ ];
4224
+ //#endregion
4225
+ export { ENTRIES };
4226
+
4227
+ //# sourceMappingURL=longform.entries.js.map