@velastack/cms 0.4.0 → 0.5.1

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 (205) hide show
  1. package/README.md +140 -28
  2. package/dist/backend/index.d.ts +1 -1
  3. package/dist/backend/routes/seed.d.ts +6 -0
  4. package/dist/backend/routes/seed.js +96 -0
  5. package/dist/backend/routes/table.js +3 -0
  6. package/dist/backend/store/queries.d.ts +6 -1
  7. package/dist/backend/store/queries.js +89 -0
  8. package/dist/cms.js +13 -16
  9. package/dist/components/admin-bar/admin-bar-internal.svelte +4 -4
  10. package/dist/components/admin-bar/admin-bar.css +1 -1
  11. package/dist/components/admin-bar/field-input.svelte +239 -0
  12. package/dist/components/admin-bar/field-input.svelte.d.ts +30 -0
  13. package/dist/components/admin-bar/fields-form.svelte +218 -0
  14. package/dist/components/admin-bar/fields-form.svelte.d.ts +16 -0
  15. package/dist/components/admin-bar/icons/pencil.svelte +19 -0
  16. package/dist/components/admin-bar/icons/pencil.svelte.d.ts +3 -0
  17. package/dist/components/admin-bar/keyboard-shortcuts-dialog.svelte +1 -1
  18. package/dist/components/admin-bar/link-input.svelte +102 -0
  19. package/dist/components/admin-bar/link-input.svelte.d.ts +9 -0
  20. package/dist/components/admin-bar/list-field.svelte +93 -0
  21. package/dist/components/admin-bar/list-field.svelte.d.ts +20 -0
  22. package/dist/components/admin-bar/locales-panel.svelte +42 -1
  23. package/dist/components/admin-bar/page-config.d.ts +8 -1
  24. package/dist/components/admin-bar/page-config.js +15 -2
  25. package/dist/components/admin-bar/page-options.d.ts +24 -0
  26. package/dist/components/admin-bar/page-options.js +53 -0
  27. package/dist/components/admin-bar/reorder-buttons.svelte +52 -0
  28. package/dist/components/admin-bar/reorder-buttons.svelte.d.ts +10 -0
  29. package/dist/components/admin-bar/rich-text-input.svelte +92 -0
  30. package/dist/components/admin-bar/rich-text-input.svelte.d.ts +9 -0
  31. package/dist/components/admin-bar/seo-panel.svelte +144 -189
  32. package/dist/components/admin-bar/site-options-panel.svelte +115 -0
  33. package/dist/components/admin-bar/site-options-panel.svelte.d.ts +7 -0
  34. package/dist/components/admin-bar/structured-editor-popover.svelte +259 -0
  35. package/dist/components/admin-bar/structured-editor-popover.svelte.d.ts +48 -0
  36. package/dist/components/admin-bar/ui/checkbox/checkbox.svelte +36 -0
  37. package/dist/components/admin-bar/ui/checkbox/checkbox.svelte.d.ts +5 -0
  38. package/dist/components/admin-bar/ui/checkbox/index.d.ts +2 -0
  39. package/dist/components/admin-bar/ui/checkbox/index.js +2 -0
  40. package/dist/components/admin-bar/ui/popover/index.d.ts +5 -0
  41. package/dist/components/admin-bar/ui/popover/index.js +7 -0
  42. package/dist/components/admin-bar/ui/popover/popover-close.svelte +7 -0
  43. package/dist/components/admin-bar/ui/popover/popover-close.svelte.d.ts +4 -0
  44. package/dist/components/admin-bar/ui/popover/popover-content.svelte +35 -0
  45. package/dist/components/admin-bar/ui/popover/popover-content.svelte.d.ts +10 -0
  46. package/dist/components/admin-bar/ui/popover/popover-trigger.svelte +7 -0
  47. package/dist/components/admin-bar/ui/popover/popover-trigger.svelte.d.ts +4 -0
  48. package/dist/components/admin-bar/ui/popover/popover.svelte +7 -0
  49. package/dist/components/admin-bar/ui/popover/popover.svelte.d.ts +3 -0
  50. package/dist/components/admin-bar/ui/sheet/index.d.ts +10 -0
  51. package/dist/components/admin-bar/ui/sheet/index.js +12 -0
  52. package/dist/components/admin-bar/ui/sheet/sheet-close.svelte +7 -0
  53. package/dist/components/admin-bar/ui/sheet/sheet-close.svelte.d.ts +4 -0
  54. package/dist/components/admin-bar/ui/sheet/sheet-content.svelte +64 -0
  55. package/dist/components/admin-bar/ui/sheet/sheet-content.svelte.d.ts +17 -0
  56. package/dist/components/admin-bar/ui/sheet/sheet-description.svelte +17 -0
  57. package/dist/components/admin-bar/ui/sheet/sheet-description.svelte.d.ts +4 -0
  58. package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte +23 -0
  59. package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte.d.ts +5 -0
  60. package/dist/components/admin-bar/ui/sheet/sheet-header.svelte +20 -0
  61. package/dist/components/admin-bar/ui/sheet/sheet-header.svelte.d.ts +5 -0
  62. package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte +20 -0
  63. package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte.d.ts +4 -0
  64. package/dist/components/admin-bar/ui/sheet/sheet-title.svelte +17 -0
  65. package/dist/components/admin-bar/ui/sheet/sheet-title.svelte.d.ts +4 -0
  66. package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte +7 -0
  67. package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte.d.ts +4 -0
  68. package/dist/components/admin-bar/ui/sheet/sheet.svelte +7 -0
  69. package/dist/components/admin-bar/ui/sheet/sheet.svelte.d.ts +3 -0
  70. package/dist/components/admin-bar/ui/switch/index.d.ts +2 -0
  71. package/dist/components/admin-bar/ui/switch/index.js +2 -0
  72. package/dist/components/admin-bar/ui/switch/switch.svelte +27 -0
  73. package/dist/components/admin-bar/ui/switch/switch.svelte.d.ts +5 -0
  74. package/dist/components/admin-bar/ui/tabs/index.d.ts +5 -0
  75. package/dist/components/admin-bar/ui/tabs/index.js +7 -0
  76. package/dist/components/admin-bar/ui/tabs/tabs-content.svelte +17 -0
  77. package/dist/components/admin-bar/ui/tabs/tabs-content.svelte.d.ts +4 -0
  78. package/dist/components/admin-bar/ui/tabs/tabs-list.svelte +16 -0
  79. package/dist/components/admin-bar/ui/tabs/tabs-list.svelte.d.ts +4 -0
  80. package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte +20 -0
  81. package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  82. package/dist/components/admin-bar/ui/tabs/tabs.svelte +19 -0
  83. package/dist/components/admin-bar/ui/tabs/tabs.svelte.d.ts +4 -0
  84. package/dist/components/cms/cms-boolean.svelte +9 -18
  85. package/dist/components/cms/cms-boolean.svelte.d.ts +1 -0
  86. package/dist/components/cms/cms-collection.svelte +46 -0
  87. package/dist/components/cms/cms-collection.svelte.d.ts +25 -0
  88. package/dist/components/cms/cms-contact.svelte +26 -0
  89. package/dist/components/cms/cms-contact.svelte.d.ts +14 -0
  90. package/dist/components/cms/cms-date-time-editable.svelte +3 -3
  91. package/dist/components/cms/cms-date-time.svelte +17 -17
  92. package/dist/components/cms/cms-date-time.svelte.d.ts +2 -1
  93. package/dist/components/cms/cms-faq.svelte +22 -0
  94. package/dist/components/cms/cms-faq.svelte.d.ts +15 -0
  95. package/dist/components/cms/cms-gallery.svelte +31 -0
  96. package/dist/components/cms/cms-gallery.svelte.d.ts +16 -0
  97. package/dist/components/cms/cms-hours-editable.svelte +160 -0
  98. package/dist/components/cms/cms-hours-editable.svelte.d.ts +17 -0
  99. package/dist/components/cms/cms-hours.svelte +42 -0
  100. package/dist/components/cms/cms-hours.svelte.d.ts +19 -0
  101. package/dist/components/cms/cms-image-editable.svelte +13 -4
  102. package/dist/components/cms/cms-image.svelte +19 -34
  103. package/dist/components/cms/cms-image.svelte.d.ts +7 -8
  104. package/dist/components/cms/cms-link-editable.svelte +4 -60
  105. package/dist/components/cms/cms-link.svelte +15 -28
  106. package/dist/components/cms/cms-link.svelte.d.ts +4 -9
  107. package/dist/components/cms/cms-logos.svelte +29 -0
  108. package/dist/components/cms/cms-logos.svelte.d.ts +16 -0
  109. package/dist/components/cms/cms-markdown.svelte +15 -16
  110. package/dist/components/cms/cms-markdown.svelte.d.ts +1 -0
  111. package/dist/components/cms/cms-nav.svelte +22 -0
  112. package/dist/components/cms/cms-nav.svelte.d.ts +15 -0
  113. package/dist/components/cms/cms-number-editable.svelte +4 -2
  114. package/dist/components/cms/cms-number.svelte +18 -17
  115. package/dist/components/cms/cms-number.svelte.d.ts +2 -1
  116. package/dist/components/cms/cms-pricing.svelte +36 -0
  117. package/dist/components/cms/cms-pricing.svelte.d.ts +14 -0
  118. package/dist/components/cms/cms-rich-text.svelte +17 -22
  119. package/dist/components/cms/cms-rich-text.svelte.d.ts +1 -0
  120. package/dist/components/cms/cms-schedule.svelte +35 -0
  121. package/dist/components/cms/cms-schedule.svelte.d.ts +16 -0
  122. package/dist/components/cms/cms-social-links.svelte +35 -0
  123. package/dist/components/cms/cms-social-links.svelte.d.ts +15 -0
  124. package/dist/components/cms/cms-stats.svelte +31 -0
  125. package/dist/components/cms/cms-stats.svelte.d.ts +16 -0
  126. package/dist/components/cms/cms-steps.svelte +31 -0
  127. package/dist/components/cms/cms-steps.svelte.d.ts +16 -0
  128. package/dist/components/cms/cms-store.svelte.d.ts +14 -0
  129. package/dist/components/cms/cms-store.svelte.js +39 -2
  130. package/dist/components/cms/cms-structured-editable.svelte +29 -0
  131. package/dist/components/cms/{cms-repeater-editable.svelte.d.ts → cms-structured-editable.svelte.d.ts} +12 -8
  132. package/dist/components/cms/cms-structured.svelte +50 -0
  133. package/dist/components/cms/cms-structured.svelte.d.ts +49 -0
  134. package/dist/components/cms/cms-team.svelte +22 -0
  135. package/dist/components/cms/cms-team.svelte.d.ts +15 -0
  136. package/dist/components/cms/cms-testimonials.svelte +36 -0
  137. package/dist/components/cms/cms-testimonials.svelte.d.ts +15 -0
  138. package/dist/components/cms/cms-text.svelte +23 -30
  139. package/dist/components/cms/cms-text.svelte.d.ts +3 -1
  140. package/dist/components/cms/cms-timeline.svelte +35 -0
  141. package/dist/components/cms/cms-timeline.svelte.d.ts +16 -0
  142. package/dist/components/cms/index.d.ts +15 -1
  143. package/dist/components/cms/index.js +15 -1
  144. package/dist/components/cms/scope.d.ts +25 -5
  145. package/dist/components/cms/use-cms-field.svelte.d.ts +54 -0
  146. package/dist/components/cms/use-cms-field.svelte.js +83 -0
  147. package/dist/core/metadata.d.ts +44 -0
  148. package/dist/core/metadata.js +59 -0
  149. package/dist/core/shapes/collection.d.ts +60 -0
  150. package/dist/core/shapes/collection.js +194 -0
  151. package/dist/core/shapes/contact.d.ts +70 -0
  152. package/dist/core/shapes/contact.js +214 -0
  153. package/dist/core/shapes/faq.d.ts +12 -0
  154. package/dist/core/shapes/faq.js +48 -0
  155. package/dist/core/shapes/form.d.ts +46 -0
  156. package/dist/core/shapes/form.js +7 -0
  157. package/dist/core/shapes/hours.d.ts +91 -0
  158. package/dist/core/shapes/hours.js +325 -0
  159. package/dist/core/shapes/image.d.ts +15 -0
  160. package/dist/core/shapes/image.js +27 -0
  161. package/dist/core/shapes/index.d.ts +31 -0
  162. package/dist/core/shapes/index.js +13 -0
  163. package/dist/core/shapes/link.d.ts +15 -0
  164. package/dist/core/shapes/link.js +42 -0
  165. package/dist/core/shapes/nav.d.ts +21 -0
  166. package/dist/core/shapes/nav.js +71 -0
  167. package/dist/core/shapes/presets.d.ts +65 -0
  168. package/dist/core/shapes/presets.js +152 -0
  169. package/dist/core/shapes/pricing.d.ts +40 -0
  170. package/dist/core/shapes/pricing.js +115 -0
  171. package/dist/core/shapes/social-links.d.ts +19 -0
  172. package/dist/core/shapes/social-links.js +83 -0
  173. package/dist/core/shapes/team.d.ts +22 -0
  174. package/dist/core/shapes/team.js +70 -0
  175. package/dist/core/shapes/testimonials.d.ts +24 -0
  176. package/dist/core/shapes/testimonials.js +86 -0
  177. package/dist/core/structured-registry.d.ts +10 -0
  178. package/dist/core/structured-registry.js +6 -0
  179. package/dist/core/structured.d.ts +131 -0
  180. package/dist/core/structured.js +235 -0
  181. package/dist/core/wire.d.ts +31 -1
  182. package/dist/editor.d.ts +26 -0
  183. package/dist/editor.js +25 -0
  184. package/dist/index.d.ts +26 -4
  185. package/dist/index.js +31 -2
  186. package/dist/server/load-cms.js +1 -0
  187. package/package.json +12 -3
  188. package/plugin/README.md +22 -2
  189. package/plugin/dist/build.d.ts +74 -0
  190. package/plugin/dist/build.js +210 -0
  191. package/plugin/dist/build.js.map +1 -0
  192. package/plugin/dist/index.d.ts +10 -1
  193. package/plugin/dist/index.js +63 -4
  194. package/plugin/dist/index.js.map +1 -1
  195. package/plugin/dist/manifest.d.ts +31 -0
  196. package/plugin/dist/manifest.js +146 -41
  197. package/plugin/dist/manifest.js.map +1 -1
  198. package/plugin/dist/parse-svelte.d.ts +15 -0
  199. package/plugin/dist/parse-svelte.js +26 -16
  200. package/plugin/dist/parse-svelte.js.map +1 -1
  201. package/dist/components/admin-bar/site-settings-panel.svelte +0 -225
  202. package/dist/components/admin-bar/site-settings-panel.svelte.d.ts +0 -7
  203. package/dist/components/cms/cms-repeater-editable.svelte +0 -137
  204. package/dist/components/cms/cms-repeater.svelte +0 -55
  205. package/dist/components/cms/cms-repeater.svelte.d.ts +0 -30
package/README.md CHANGED
@@ -201,13 +201,18 @@ If `endpoint` is omitted (e.g. when using `mockAdapter` for tests/demos), the pl
201
201
 
202
202
  ## CMS components
203
203
 
204
- All four included CMS components share the same prop shape:
204
+ Every CMS component shares the same prop shape:
205
205
 
206
- | Prop | Type | Notes |
207
- | ---------- | ------------------ | ------------------------------------------------------------------------ |
208
- | `name` | `string` | Field path inside this scope. Must be a static string at v1. |
209
- | `fallback` | varies | Rendered when no value is stored. `string` for text, HTML for rich text. |
210
- | `value` | `unknown` (opt-in) | Per-item override used inside `<CmsRepeater/>`; bypasses scope lookup. |
206
+ | Prop | Type | Notes |
207
+ | ---------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
208
+ | `name` | `string` | Field path inside this scope. Must be a static string at v1. |
209
+ | `scope` | `'root' \| string` | Read and write a value that lives in the root layout scope (`'root'`, i.e. `layout:/`) or another param-less layout scope (`layout:/(public)`). |
210
+ | `fallback` | varies | Rendered when **nothing** is stored. `string` for text, HTML for rich text, `{ url, alt }` or a URL for images. |
211
+ | `value` | `unknown` (opt-in) | Per-item override; bypasses scope lookup and disables editing. |
212
+
213
+ Two stored states are distinct: `undefined` (nothing stored) renders the fallback, `null` (the editor cleared the field) renders empty. Editables write `null` on clear, never drop the key, so a cleared field can't fall back to the template's demo copy after publish.
214
+
215
+ `scope="root"` is how site-wide content — hours, contact, navigation, branding — is edited where it renders: put the component in the root `+layout.svelte` and reference the same `name` from any page with `scope="root"`. The payload already carries every enclosing layout tree, so no extra fetch happens.
211
216
 
212
217
  ### `<CmsText />`
213
218
 
@@ -233,22 +238,93 @@ Rendered with `{@html}`. Edit mode swaps for a `<textarea>`.
233
238
 
234
239
  Renders an `<img>`. Edit mode shows the current image plus a URL input.
235
240
 
236
- ### `<CmsRepeater />`
241
+ ## Structured components
242
+
243
+ Hours, navigation, a team, a price list: one value with a fixed shape, edited in a slot-anchored popover and rendered by the template through a snippet. Every structured component is headless: it reads the value at `name`, normalizes it, derives the view a template needs and hands it to `children`. It never emits markup of its own.
237
244
 
238
245
  ```svelte
239
- <CmsRepeater name="gallery.items">
240
- {#snippet children(item)}
241
- <figure>
242
- <CmsImage name="src" value={item.src} alt={item.caption} />
243
- <figcaption>
244
- <CmsText name="caption" value={item.caption} />
245
- </figcaption>
246
- </figure>
246
+ <script lang="ts">
247
+ import { CmsHours, CmsNav, isActive } from '@velastack/cms';
248
+ </script>
249
+
250
+ <!-- In the root layout: site-wide by construction. -->
251
+ <CmsNav name="nav.primary">
252
+ {#snippet children(items)}
253
+ {#each items as item (item.id)}
254
+ <a href={item.href} aria-current={isActive(item, page.url) ? 'page' : undefined}
255
+ >{item.label}</a
256
+ >
257
+ {/each}
247
258
  {/snippet}
248
- </CmsRepeater>
259
+ </CmsNav>
260
+
261
+ <!-- On the contact page: the same value, read and written through the root scope. -->
262
+ <CmsHours name="hours" scope="root">
263
+ {#snippet children(h)}
264
+ <p>{h.today.label}: {h.today.text} · {h.status}</p>
265
+ {#each h.rows as row (row.id)}<dt>{row.days}</dt>
266
+ <dd>{row.text}</dd>{/each}
267
+ {/snippet}
268
+ </CmsHours>
249
269
  ```
250
270
 
251
- Iterates an array stored at `name`. Inside the snippet, pass per-item values via `value=` to bypass scope lookup. Edit mode renders the same snippet for each item plus add/remove controls and per-key inputs.
271
+ | Component | Value | Snippet receives | Helpers |
272
+ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------- |
273
+ | `CmsNav` | `{ items: [{ id, label, link, children }] }` | items with resolved `href` | `isActive(item, url)` |
274
+ | `CmsHours` | week grid, `exceptions`, `note`, `labels`, `timezone` | `rows`, `today`, `isOpenNow`, `status`, `exceptionRows` in the visitor's locale | `toOpeningHoursSpecification` |
275
+ | `CmsContact` | `name`, `address`, `phones`, `emails`, `whatsapp`, `directionsUrl`, `geo`, `labels` | plus `telHref`, `mailto`, `whatsappHref`, `mapsHref`, `formattedAddress` | `toPostalAddress` |
276
+ | `CmsSocialLinks` | `{ items: [{ id, platform, url, label }] }` | items with `name` (label or platform name) | `toSameAs`, `SOCIAL_PLATFORMS` |
277
+ | `CmsCollection` | rooms, services, tours: title, summary, body, image, gallery, price, link, tags, features, `details` key-value list, featured, hidden | visible items with `href`; props `limit`, `featured`, `tag` slice them | `formatCollectionPrice`, `collectionTags`, `toItemList` |
278
+ | `CmsTeam` | name, role, rich bio, photo, email, phone, profile links | members | |
279
+ | `CmsTestimonials` | rich quote, author, role, company, photo, rating, source, date | testimonials | `toReview` |
280
+ | `CmsPricing` | `currency`, `labels.custom`, tiers with a feature checklist and a CTA | tiers with `priceText` and `href` | `formatPrice` |
281
+ | `CmsFaq` | question, rich answer | questions | `toFaqPage` |
282
+ | `CmsStats`, `CmsSteps`, `CmsTimeline`, `CmsGallery`, `CmsLogos`, `CmsSchedule` | fixed list presets | items | `logosView`, `scheduleByDay` |
283
+
284
+ Templates use only these exports; there is no open item schema. A shape outside the presets is a new preset in the package. `fallback` takes the full value or a bare item list and is normalized like a stored value, so a hand-written seed with missing fields is safe. Prices, times, dates and day names format in the visitor's locale; `isOpenNow` is computed when the page renders, so a prerendered page shows the state at build time.
285
+
286
+ Every value type (`CmsHoursValue`, `NavItem`, …), schema (`cmsHours`, …) and JSON-LD helper is exported from the package root.
287
+
288
+ ### Building one
289
+
290
+ The package ships the pieces a structured component is made of, and the components above are built from nothing else.
291
+
292
+ ```ts
293
+ // The value's rules: a versioned shape, its translatable strings, and the form the popover draws.
294
+ import { defineForm, asItems, asString } from '@velastack/cms';
295
+
296
+ export const cmsFaq = defineForm<CmsFaqValue>({
297
+ component: 'CmsFaq', // canonical export name, recorded in the manifest
298
+ label: 'FAQ', // popover header
299
+ version: 1, // written as `v` on every value
300
+ translatable: [], // root fields that translate (dotted paths allowed: 'labels.closed')
301
+ items: { key: 'items', translatable: ['question', 'answer'] }, // lists and their translatable fields; nest `items` for lists inside items
302
+ normalize: (raw) => ({ v: 1, items: asItems(raw?.items, …) }),
303
+ empty: () => ({ v: 1, items: [] }),
304
+ fields: [{ key: 'items', type: 'list', label: 'Questions', itemLabel: 'question', titleKey: 'question', blank: () => ({ question: '', answer: '' }), fields: [
305
+ { key: 'question', label: 'Question', type: 'text' },
306
+ { key: 'answer', label: 'Answer', type: 'html' }
307
+ ] }]
308
+ });
309
+ ```
310
+
311
+ ```svelte
312
+ <!-- The display component, inside the package: the generic one, given the schema and a view function. -->
313
+ <script lang="ts">
314
+ import Structured from './cms-structured.svelte'; // src/lib/components/cms, inside the package
315
+ let { name, scope, fallback, value, children } = $props();
316
+ </script>
317
+
318
+ <Structured schema={cmsFaq} {name} {scope} {fallback} {value} view={faqView} {children} />
319
+ ```
320
+
321
+ Field types: `text`, `long-string`, `html` (TipTap), `number`, `boolean`, `date`, `time`, `url`, `image` and `images` (media picker, alt text), `enum`, `link` (page picker or URL, new tab), `strings` (one per line), `rating`, `list` (collapsible cards with reorder). `group` puts a field in a tab; `half` shares a row with the next field. A bespoke editor (the hours week grid) wraps `StructuredEditorPopover` from `@velastack/cms/editor` and renders `FieldsForm` for the rest; `FieldInput`, `ListField`, `LinkInput`, `RichTextInput`, `ReorderButtons` and the Popover / Tabs / Switch / Checkbox / Sheet primitives are exported there too. The popover commits on **Done**; edit mode is page-wide and the bar's Save flushes the draft.
322
+
323
+ Rules every structured value follows (`core/structured.ts`):
324
+
325
+ - **Whole-object writes, explicit `null` clears.** Components never patch a leaf; they write the full `{ v, … }` object. Cleared fields are `null`.
326
+ - **List items carry a stable `id`** (`newItemId()`) at every nesting level; reorder and delete work because arrays replace wholesale on merge.
327
+ - **Localisation is a string overlay.** The default locale stores the full value at `<name>`; every other locale stores only strings at `<name>.$t.<id>.<field>` (`_` is the id for root fields, `field` may be a dotted path, and a string array translates per index as `tags.0`). `read()` folds the overlay in, structure never forks between languages, and the popover's locale tabs write only the overlay. The Locales panel counts translatable strings per locale for every structured usage on the current route.
252
328
 
253
329
  ### `<CmsEntries />`
254
330
 
@@ -296,7 +372,7 @@ A component is treated as a CMS component if **any** of the following hold:
296
372
 
297
373
  2. **It's a named export from your local `src/lib/components/cms/index.{ts,js}` barrel.** Re-export your component there if you prefer a single import path..
298
374
 
299
- 3. **It's imported from the `@velastack/cms` package itself.** The four built-ins (`CmsText`, `CmsRichText`, `CmsImage`, `CmsRepeater`) work this way: any import resolving inside the installed `@velastack/cms` package is auto-classified.
375
+ 3. **It's imported from the `@velastack/cms` package itself.** Every named import from `@velastack/cms` is a CMS component, whether or not the bundler resolved the package.
300
376
 
301
377
  ### Third-party packs (opt-in)
302
378
 
@@ -332,13 +408,12 @@ String patterns match `source === pattern || source.startsWith(pattern + '/')`.
332
408
 
333
409
  A CMS component should:
334
410
 
335
- - Accept `{ name: string; fallback?: string; value?: unknown }` props (and any extras you need).
336
- - Call `getCmsScope()` from `cms` to find its scope.
337
- - Read drafts via `cmsStore.hasDraft(scopeKey, name)` / `cmsStore.getValue(scopeKey, name)` when `cmsStore.isEditing` is true; fall back to `page.data.cms.docs[scopeKey][name]` otherwise.
338
- - Skip scope lookup when `value !== undefined` — that prop is the per-item override used inside `<CmsRepeater/>`.
339
- - Optionally provide an editable sibling that's dynamically `import()`'d when `cmsStore.isEditing` flips on, so editing code doesn't ship to public visitors.
411
+ - Accept `{ name: string; scope?: string; fallback?: …; value?: unknown }` props (and any extras you need).
412
+ - Call `useCmsField(() => ({ name, scope, value }), read)` once at init. It resolves the scope (context, `'root'`, or an explicit id), reads the merged value, reports `editable`, and exposes `set` / `clear`.
413
+ - Treat `undefined` as "render the fallback" and `null` as "render empty" in `read`.
414
+ - Optionally provide an editable sibling that's dynamically `import()`'d when `field.editable` flips on, so editing code doesn't ship to public visitors.
340
415
 
341
- The four built-ins are reference implementations; copy `src/lib/components/cms/cms-text.svelte` (or one of the other built-ins with a paired `*-editable.svelte` sibling) as a starting point.
416
+ The built-ins are reference implementations; copy `src/lib/components/cms/cms-text.svelte` (or one of the others with a paired `*-editable.svelte` sibling) as a starting point.
342
417
 
343
418
  ## Architecture
344
419
 
@@ -399,7 +474,7 @@ Display components prefer drafts over published values when `isEditing` is true.
399
474
 
400
475
  ### Page metadata
401
476
 
402
- Page scopes carry an editable metadata field list (default: `title`, `description`, `canonical`, `robots`). Resolution order, per design:
477
+ Every page scope carries the full metadata set by default — `title`, `description`, `ogTitle`, `ogDescription`, `ogImage`, `twitterCard`, `canonical`, `noindex` — and a route's `page.cms.ts` `metadata` schema adds to or overrides it. The SEO panel previews the search snippet and the share card; blank share fields reuse the search fields. Resolution order, per design:
403
478
 
404
479
  ```
405
480
  editable page metadata (cms.metadata)
@@ -407,7 +482,19 @@ editable page metadata (cms.metadata)
407
482
  → static site defaults (defineBaseMetaTags)
408
483
  ```
409
484
 
410
- Hand `cms.metadata` directly to `definePageMetaTags(...)` from `svelte-meta-tags` and you're done.
485
+ `toMetaTags(cms.metadata, { siteName })` turns the branch into `svelte-meta-tags` props, with the title template and Open Graph site name from the root layout's `branding.name`:
486
+
487
+ ```svelte
488
+ <script lang="ts">
489
+ import { cms, toMetaTags } from '@velastack/cms';
490
+ const siteName = $derived(cms.docs['layout:/']?.branding?.name as string | undefined);
491
+ const metaTags = $derived(deepMerge(data.baseMetaTags, toMetaTags(cms.metadata, { siteName })));
492
+ </script>
493
+ ```
494
+
495
+ ### Site options
496
+
497
+ `createCms({ site })` declares the **Site Options** panel: project-wide values that never render on the page — the schema.org type (`enum`), the default share image (`image`), which locales are enabled (`boolean`). Field types are `text`, `markdown`, `number`, `datetime`, `url`, `color`, `image`, `enum` (with `values`) and `boolean`. Anything a visitor sees belongs in the root layout scope, edited in place with `scope="root"`; the site tree is one non-localised tree per project.
411
498
 
412
499
  ## Adapters
413
500
 
@@ -578,6 +665,24 @@ cms.locale; // 'es'
578
665
  cms.locales; // ['en', 'es', 'fr']
579
666
  ```
580
667
 
668
+ ## Seeding a project
669
+
670
+ A fresh project should open with content, not a blank site. The backend accepts a project's whole published state as one object and returns it in the same shape:
671
+
672
+ ```ts
673
+ type CmsSeed = {
674
+ layouts?: Record<locale, Record<routeId, Tree>>;
675
+ pages?: Record<locale, Record<routeId, Tree | PageEntry[]>>; // a bare tree is a static page
676
+ site?: Tree;
677
+ };
678
+ ```
679
+
680
+ - `POST /seed` with a `CmsSeed` (plus `force?: true`) writes the published rows directly, outside the release flow. It answers `409 { ok: false, reason: 'already-seeded' }` when the project already has published rows unless `force` is set, so it can never silently overwrite edits. Image values are seeded as URLs; nothing is uploaded.
681
+ - `GET /export` returns the published rows as a `CmsSeed` (tombstoned pages omitted).
682
+ - In-process: `backend.store.seed(projectId, seed, { force })` and `backend.store.exportPublished(projectId)`.
683
+
684
+ Both routes require an editor session. A host that creates projects (velastack.dev's `/setup`) reads the template's published `content/` and calls `store.seed` right after creating the project, in the locales the owner enabled.
685
+
581
686
  ## Redirects & tombstones
582
687
 
583
688
  Pages can be deleted with three different outcomes:
@@ -611,17 +716,24 @@ cms({
611
716
  endpoint: 'https://cms.example.com/v1/projects/p1/cms',
612
717
  locales: ['en'], // mirror createCms({ locales })
613
718
  mediaDir: 'static/cms-media', // download target (default)
614
- mediaPrefix: '/cms-media' // URL prefix in rewritten content (default)
719
+ mediaPrefix: '/cms-media', // URL prefix in rewritten content (default)
720
+
721
+ // Content manifest: `content/<locale>.json` keyed by scope id. When the
722
+ // default locale's file exists its values are injected as `fallback`
723
+ // props into every walked .svelte file, so components stay name-only.
724
+ content: 'content' // default; `false` disables
615
725
  });
616
726
  ```
617
727
 
728
+ Build tooling that needs the same parser — a `pack` step inlining fallbacks into a tarball, a `verify` step checking usages against a content manifest — imports it from `@velastack/cms/build`: `scanUsages(source)`, `injectFallbacks(source, treeOrResolver)` / `inlineFallbacks`, `buildManifest(...)` and `fallbackResolverFor(result, file, content)`. The manifest records each usage's component type (`scope.usages`), and the walk reports dynamic `name`s and out-of-chain `scope=` attributes as warnings instead of dropping them silently.
729
+
618
730
  The plugin also exposes `virtual:vela-cms/manifest` (typed via the package's ambient declaration). You almost never need to import it directly — `loadCms` does that internally — but it's there if you want to introspect the manifest at build time.
619
731
 
620
732
  ## Constraints (v1)
621
733
 
622
734
  Static analysis is conservative on purpose:
623
735
 
624
- - `name=` props must be string literals or `name={'literal'}`. Computed names are not extracted.
736
+ - `name=` props must be string literals or `name={'literal'}`. Computed names are not extracted; the plugin warns about each one.
625
737
  - Only static `import` of `.svelte` files is followed when walking the component graph. Bare specifiers (npm packages) are skipped unless they're the @velastack/cms package itself, are listed under `components`, or match a `traverse` pattern.
626
738
  - `<svelte:component this={…} />` and dynamic component selection are not traced.
627
739
  - Layout reset segments (`+page@layout.svelte`) are not yet supported.
@@ -49,6 +49,6 @@ export type { CmsStore } from './store/queries.js';
49
49
  export type { SqliteDb, SqliteStatement } from './store/sqlite.js';
50
50
  export type { Migration } from './store/migrations/index.js';
51
51
  export { MIGRATIONS } from './store/migrations/index.js';
52
- export type { DocsResponse, MediaItem, OpenRelease, PageMapEntry, PageMapRoute, PagesResponse, PagesRoute, PublishedRelease, ReleaseItem, ReleasePublisher, SiteResponse } from '../core/wire.js';
52
+ export type { CmsSeed, DocsResponse, MediaItem, OpenRelease, PageMapEntry, PageMapRoute, PagesResponse, PagesRoute, PublishedRelease, ReleaseItem, ReleasePublisher, SeedRequest, SeedResponse, SeedSummary, SiteResponse } from '../core/wire.js';
53
53
  export type { PageDeleteOutcome, PageEntry } from '../core/page-entry.js';
54
54
  export type { Tree } from '../core/path.js';
@@ -0,0 +1,6 @@
1
+ import type { CmsSeed } from '../../core/wire.js';
2
+ import { type RouteCtx } from './context.js';
3
+ /** Validate a request body into a {@link CmsSeed}, or explain why not. */
4
+ export declare const parseSeed: (body: unknown) => CmsSeed | string;
5
+ export declare const postSeed: (ctx: RouteCtx) => Promise<Response>;
6
+ export declare const getExport: (ctx: RouteCtx) => Response;
@@ -0,0 +1,96 @@
1
+ import { badRequest, isObjectRecord, isStringRecord, readJson } from './context.js';
2
+ const parseEntries = (raw) => {
3
+ const out = [];
4
+ for (const e of raw) {
5
+ if (!isObjectRecord(e))
6
+ return 'page entries must be objects';
7
+ if (!isStringRecord(e.params))
8
+ return 'page entry params must be a string-keyed object';
9
+ if (!isObjectRecord(e.published))
10
+ return 'page entry published must be an object';
11
+ const entry = { params: e.params, published: e.published };
12
+ const t = e.tombstone;
13
+ if (isObjectRecord(t)) {
14
+ if (t.kind === 'gone')
15
+ entry.tombstone = { kind: 'gone' };
16
+ else if (t.kind === 'redirect' && typeof t.to === 'string') {
17
+ entry.tombstone = { kind: 'redirect', to: t.to };
18
+ }
19
+ else
20
+ return 'page entry tombstone must be gone or redirect';
21
+ }
22
+ out.push(entry);
23
+ }
24
+ return out;
25
+ };
26
+ /** Validate a request body into a {@link CmsSeed}, or explain why not. */
27
+ export const parseSeed = (body) => {
28
+ if (!isObjectRecord(body))
29
+ return 'object body required';
30
+ const seed = {};
31
+ if (body.layouts !== undefined) {
32
+ if (!isObjectRecord(body.layouts))
33
+ return 'layouts must be keyed by locale';
34
+ seed.layouts = {};
35
+ for (const [locale, byRoute] of Object.entries(body.layouts)) {
36
+ if (!isObjectRecord(byRoute))
37
+ return `layouts.${locale} must be keyed by route id`;
38
+ const bucket = {};
39
+ for (const [routeId, tree] of Object.entries(byRoute)) {
40
+ if (!routeId.startsWith('/'))
41
+ return `layouts.${locale}: "${routeId}" is not a route id`;
42
+ if (!isObjectRecord(tree))
43
+ return `layouts.${locale}.${routeId} must be an object`;
44
+ bucket[routeId] = tree;
45
+ }
46
+ seed.layouts[locale] = bucket;
47
+ }
48
+ }
49
+ if (body.pages !== undefined) {
50
+ if (!isObjectRecord(body.pages))
51
+ return 'pages must be keyed by locale';
52
+ seed.pages = {};
53
+ for (const [locale, byRoute] of Object.entries(body.pages)) {
54
+ if (!isObjectRecord(byRoute))
55
+ return `pages.${locale} must be keyed by route id`;
56
+ const bucket = {};
57
+ for (const [routeId, value] of Object.entries(byRoute)) {
58
+ if (!routeId.startsWith('/'))
59
+ return `pages.${locale}: "${routeId}" is not a route id`;
60
+ if (Array.isArray(value)) {
61
+ const entries = parseEntries(value);
62
+ if (typeof entries === 'string')
63
+ return `pages.${locale}.${routeId}: ${entries}`;
64
+ bucket[routeId] = entries;
65
+ }
66
+ else if (isObjectRecord(value)) {
67
+ bucket[routeId] = value;
68
+ }
69
+ else
70
+ return `pages.${locale}.${routeId} must be an object or an entry list`;
71
+ }
72
+ seed.pages[locale] = bucket;
73
+ }
74
+ }
75
+ if (body.site !== undefined) {
76
+ if (!isObjectRecord(body.site))
77
+ return 'site must be an object';
78
+ seed.site = body.site;
79
+ }
80
+ return seed;
81
+ };
82
+ export const postSeed = async (ctx) => {
83
+ const body = await readJson(ctx.event);
84
+ if (body === undefined)
85
+ return badRequest('invalid json');
86
+ const seed = parseSeed(body);
87
+ if (typeof seed === 'string')
88
+ return badRequest(seed);
89
+ const force = isObjectRecord(body) && body.force === true;
90
+ const result = ctx.store.seed(ctx.projectId, seed, { force });
91
+ if (!result.ok)
92
+ return Response.json(result, { status: 409 });
93
+ ctx.cache.clear();
94
+ return Response.json(result);
95
+ };
96
+ export const getExport = (ctx) => Response.json(ctx.store.exportPublished(ctx.projectId));
@@ -6,6 +6,7 @@ import { getLogin, getLoginSuccess, postLogin } from './login.js';
6
6
  import { deleteReleaseItems, getRelease, getReleaseHistory, postReleaseDiscard, postReleaseHistoryPreviewKey, postReleaseItems, postReleasePreviewKey, postReleasePublish, postReleaseRevert } from './release.js';
7
7
  import { deleteMediaItem, getMedia, getUpload, postMedia } from './media.js';
8
8
  import { getDeploy, postDeploy } from './deploy.js';
9
+ import { getExport, postSeed } from './seed.js';
9
10
  import { postTestReset } from './test-reset.js';
10
11
  export const ROUTES = [
11
12
  { method: 'GET', segments: ['docs'], auth: 'public', handler: getDocs },
@@ -65,6 +66,8 @@ export const ROUTES = [
65
66
  },
66
67
  { method: 'GET', segments: ['deploy'], auth: 'required', handler: getDeploy },
67
68
  { method: 'POST', segments: ['deploy'], auth: 'required', handler: postDeploy },
69
+ { method: 'POST', segments: ['seed'], auth: 'required', handler: postSeed },
70
+ { method: 'GET', segments: ['export'], auth: 'required', handler: getExport },
68
71
  { method: 'GET', segments: ['media'], auth: 'required', handler: getMedia },
69
72
  { method: 'POST', segments: ['media'], auth: 'required', handler: postMedia },
70
73
  { method: 'DELETE', segments: ['media', ':id'], auth: 'required', handler: deleteMediaItem },
@@ -1,6 +1,6 @@
1
1
  import { type PageEntry, type PageDeleteOutcome } from '../../core/page-entry.js';
2
2
  import { type Tree } from '../../core/path.js';
3
- import type { AddReleaseItemInput, CreatePageResult, DiscardItemTarget, ListMediaOptions, ListMediaResult, MediaItem, OpenRelease, PageMapRoute, PublishedRelease, ReleaseItem, RenamePageResult, StagePageDeleteResult } from '../../core/wire.js';
3
+ import type { AddReleaseItemInput, CmsSeed, CreatePageResult, DiscardItemTarget, ListMediaOptions, ListMediaResult, MediaItem, OpenRelease, PageMapRoute, PublishedRelease, ReleaseItem, RenamePageResult, SeedResponse, StagePageDeleteResult } from '../../core/wire.js';
4
4
  import type { SqliteDb } from './sqlite.js';
5
5
  import type { CmsEditor } from '../types.js';
6
6
  /** The locale a request falls back to when it names none. */
@@ -33,6 +33,7 @@ export declare const createStore: (db: SqliteDb) => {
33
33
  createPage: (projectId: string, userId: string, routeId: string, locale: string, params: Record<string, string>, metadata?: Record<string, unknown>) => CreatePageResult;
34
34
  deleteMediaItem: (projectId: string, id: string) => MediaItem | null;
35
35
  discardOpenRelease: (projectId: string, userId: string) => boolean;
36
+ exportPublished: (projectId: string) => CmsSeed;
36
37
  discardReleaseItem: (projectId: string, userId: string, target: DiscardItemTarget) => boolean;
37
38
  findMediaItem: (projectId: string, id: string) => MediaItem | null;
38
39
  findOpenReleaseByPreviewKey: (projectId: string, key: string) => OpenRelease | null;
@@ -44,6 +45,7 @@ export declare const createStore: (db: SqliteDb) => {
44
45
  getPublishedPageEntry: (projectId: string, locale: string, routeId: string, params: Record<string, string>) => PageEntry | null;
45
46
  getPublishedSiteTree: (projectId: string) => Tree;
46
47
  getReleaseHistory: (projectId: string) => PublishedRelease[];
48
+ hasPublishedContent: (projectId: string) => boolean;
47
49
  listAllPages: (projectId: string, userId: string, locale: string) => PageMapRoute[];
48
50
  listMediaItems: (projectId: string, { offset, limit }?: ListMediaOptions) => ListMediaResult;
49
51
  listPageParamValues: (projectId: string, routeId: string, param: string) => string[];
@@ -62,6 +64,9 @@ export declare const createStore: (db: SqliteDb) => {
62
64
  toUrl: string;
63
65
  }) => RenamePageResult;
64
66
  revertRelease: (projectId: string, releaseId: string, user: CmsEditor) => PublishedRelease | null;
67
+ seed: (projectId: string, input: CmsSeed, opts?: {
68
+ force?: boolean;
69
+ }) => SeedResponse;
65
70
  seedPublishedDocs: (projectId: string, layouts: Record<string, Record<string, Tree>>, pages: Record<string, Record<string, PageEntry[]>>) => void;
66
71
  stagePageDelete: (projectId: string, userId: string, routeId: string, locale: string, params: Record<string, string>, outcome?: PageDeleteOutcome) => StagePageDeleteResult;
67
72
  };
@@ -1177,6 +1177,92 @@ export const createStore = (db) => {
1177
1177
  });
1178
1178
  tx();
1179
1179
  };
1180
+ // ---------------------------------------------------------------------------
1181
+ // Seed / export
1182
+ // ---------------------------------------------------------------------------
1183
+ const countPublishedStmt = () => db.prepare(`SELECT (SELECT COUNT(*) FROM published_pages WHERE project_id = ?)
1184
+ + (SELECT COUNT(*) FROM published_layouts WHERE project_id = ?)
1185
+ + (SELECT COUNT(*) FROM published_site WHERE project_id = ?) AS n`);
1186
+ /** Whether the project holds any published page, layout or site row. */
1187
+ const hasPublishedContent = (projectId) => (countPublishedStmt().get(projectId, projectId, projectId)?.n ?? 0) > 0;
1188
+ /**
1189
+ * Seed a project's published content from a {@link CmsSeed} — a template's
1190
+ * `content/` at project creation, or an export of another project. Writes
1191
+ * the published rows directly, outside the release flow: there is no editor
1192
+ * and nothing to revert to. Refuses when the project already has published
1193
+ * rows unless `force`, so a second run cannot silently overwrite edits.
1194
+ * Rows a forced seed does not name are left alone. Bumps `cms_version` so
1195
+ * cached public reads are invalidated.
1196
+ */
1197
+ const seed = (projectId, input, opts = {}) => {
1198
+ const tx = db.transaction(() => {
1199
+ if (!opts.force && hasPublishedContent(projectId)) {
1200
+ return { ok: false, reason: 'already-seeded' };
1201
+ }
1202
+ ensureProjectState(projectId);
1203
+ const seedReleaseId = `seed-${projectId}`;
1204
+ const at = nowIso();
1205
+ const locales = new Set();
1206
+ let layouts = 0;
1207
+ let pages = 0;
1208
+ for (const [locale, byRoute] of Object.entries(input.layouts ?? {})) {
1209
+ for (const [routeId, tree] of Object.entries(byRoute)) {
1210
+ locales.add(locale);
1211
+ layouts += 1;
1212
+ upsertPublishedLayoutStmt().run(projectId, locale, routeId, JSON.stringify(tree), at, seedReleaseId);
1213
+ }
1214
+ }
1215
+ for (const [locale, byRoute] of Object.entries(input.pages ?? {})) {
1216
+ for (const [routeId, value] of Object.entries(byRoute)) {
1217
+ const entries = Array.isArray(value)
1218
+ ? value
1219
+ : [{ params: {}, published: value }];
1220
+ for (const entry of entries) {
1221
+ locales.add(locale);
1222
+ pages += 1;
1223
+ upsertPublishedPageStmt().run(projectId, locale, routeId, paramsHash(entry.params), JSON.stringify(entry.params), JSON.stringify(entry.published), entry.tombstone ? JSON.stringify(entry.tombstone) : null, at, seedReleaseId);
1224
+ }
1225
+ }
1226
+ }
1227
+ const site = !!input.site;
1228
+ if (input.site) {
1229
+ upsertPublishedSiteStmt().run(projectId, JSON.stringify(input.site), at, seedReleaseId);
1230
+ }
1231
+ bumpProjectVersionStmt().run(projectId);
1232
+ return { ok: true, seeded: { locales: [...locales].sort(), layouts, pages, site } };
1233
+ });
1234
+ return tx();
1235
+ };
1236
+ /**
1237
+ * Every published row, in the shape {@link seed} accepts. Tombstoned pages
1238
+ * are omitted: a seed describes content, not deletions.
1239
+ */
1240
+ const exportPublished = (projectId) => {
1241
+ const out = { layouts: {}, pages: {} };
1242
+ for (const { locale } of allLocalesForProjectStmt().all(projectId, projectId)) {
1243
+ const layouts = {};
1244
+ for (const r of allLayoutsByLocaleStmt().all(projectId, locale)) {
1245
+ layouts[r.route_id] = parseTree(r.tree) ?? {};
1246
+ }
1247
+ if (Object.keys(layouts).length > 0)
1248
+ out.layouts[locale] = layouts;
1249
+ const pages = {};
1250
+ for (const r of allPagesByLocaleStmt().all(projectId, locale)) {
1251
+ if (parseOutcome(r.tombstone))
1252
+ continue;
1253
+ (pages[r.route_id] ??= []).push({
1254
+ params: parseParams(r.params),
1255
+ published: parseTree(r.tree) ?? {}
1256
+ });
1257
+ }
1258
+ if (Object.keys(pages).length > 0)
1259
+ out.pages[locale] = pages;
1260
+ }
1261
+ const site = getPublishedSiteTree(projectId);
1262
+ if (Object.keys(site).length > 0)
1263
+ out.site = site;
1264
+ return out;
1265
+ };
1180
1266
  return {
1181
1267
  __resetProjectForTests,
1182
1268
  addReleaseItems,
@@ -1186,6 +1272,7 @@ export const createStore = (db) => {
1186
1272
  createPage,
1187
1273
  deleteMediaItem,
1188
1274
  discardOpenRelease,
1275
+ exportPublished,
1189
1276
  discardReleaseItem,
1190
1277
  findMediaItem,
1191
1278
  findOpenReleaseByPreviewKey,
@@ -1197,6 +1284,7 @@ export const createStore = (db) => {
1197
1284
  getPublishedPageEntry,
1198
1285
  getPublishedSiteTree,
1199
1286
  getReleaseHistory,
1287
+ hasPublishedContent,
1200
1288
  listAllPages,
1201
1289
  listMediaItems,
1202
1290
  listPageParamValues,
@@ -1206,6 +1294,7 @@ export const createStore = (db) => {
1206
1294
  regeneratePublishedPreviewKey,
1207
1295
  renamePage,
1208
1296
  revertRelease,
1297
+ seed,
1209
1298
  seedPublishedDocs,
1210
1299
  stagePageDelete
1211
1300
  };
package/dist/cms.js CHANGED
@@ -3,27 +3,24 @@ import { locales } from '../locales/data.js';
3
3
  export const { load: loadCms, generateEntries } = createCms({
4
4
  adapter: apiAdapter({ endpoint: 'https://velastack.dev/v1/projects/velastack-cms/cms' }),
5
5
  locales,
6
+ // Site options: values that never render on the page. Branding, contact
7
+ // and social links live in the root layout scope and are edited in place.
6
8
  site: {
7
- branding: {
8
- label: 'Branding',
9
+ seo: {
10
+ label: 'Search & sharing',
9
11
  fields: {
10
- name: { type: 'text', label: 'Business name' },
11
- tagline: { type: 'text', label: 'Tagline' },
12
- logo: { type: 'image', label: 'Logo' }
12
+ schemaType: {
13
+ type: 'enum',
14
+ label: 'Business type (schema.org)',
15
+ values: ['LocalBusiness', 'Restaurant', 'Hotel', 'Store', 'ProfessionalService']
16
+ },
17
+ shareImage: { type: 'image', label: 'Default share image' }
13
18
  }
14
19
  },
15
- contact: {
16
- label: 'Contact',
20
+ locales: {
21
+ label: 'Languages',
17
22
  fields: {
18
- email: { type: 'text', label: 'Email', placeholder: 'hello@example.com' },
19
- phone: { type: 'text', label: 'Phone' }
20
- }
21
- },
22
- social: {
23
- label: 'Social',
24
- fields: {
25
- twitter: { type: 'url', label: 'Twitter / X' },
26
- instagram: { type: 'url', label: 'Instagram' }
23
+ es: { type: 'boolean', label: 'Spanish (es) enabled' }
27
24
  }
28
25
  }
29
26
  }
@@ -1041,7 +1041,7 @@
1041
1041
  siteSettingsOpen = true;
1042
1042
  };
1043
1043
 
1044
- // Mirrors `onSaveSeo`: flush the site-settings draft as part of the open
1044
+ // Mirrors `onSaveSeo`: flush the site-options draft as part of the open
1045
1045
  // release, refresh overlays so the just-saved values show via the
1046
1046
  // preview-overlay path on the live page.
1047
1047
  const onSaveSiteSettings = async () => {
@@ -1440,7 +1440,7 @@
1440
1440
 
1441
1441
  <Menubar.Label class={menuLabelClass}>Settings</Menubar.Label>
1442
1442
  <Menubar.Item class={menuItemClass} onSelect={onOpenSiteSettings}>
1443
- Site Settings
1443
+ Site Options
1444
1444
  <KbdShortcut keys="⌘," class={menuShortcutClass} />
1445
1445
  </Menubar.Item>
1446
1446
  </Menubar.Content>
@@ -1662,8 +1662,8 @@
1662
1662
  {/if}
1663
1663
 
1664
1664
  {#if siteSettingsOpen}
1665
- {#await import('./site-settings-panel.svelte') then { default: SiteSettingsPanel }}
1666
- <SiteSettingsPanel onClose={() => (siteSettingsOpen = false)} onSave={onSaveSiteSettings} />
1665
+ {#await import('./site-options-panel.svelte') then { default: SiteOptionsPanel }}
1666
+ <SiteOptionsPanel onClose={() => (siteSettingsOpen = false)} onSave={onSaveSiteSettings} />
1667
1667
  {/await}
1668
1668
  {/if}
1669
1669