@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.
- package/README.md +140 -28
- package/dist/backend/index.d.ts +1 -1
- package/dist/backend/routes/seed.d.ts +6 -0
- package/dist/backend/routes/seed.js +96 -0
- package/dist/backend/routes/table.js +3 -0
- package/dist/backend/store/queries.d.ts +6 -1
- package/dist/backend/store/queries.js +89 -0
- package/dist/cms.js +13 -16
- package/dist/components/admin-bar/admin-bar-internal.svelte +4 -4
- package/dist/components/admin-bar/admin-bar.css +1 -1
- package/dist/components/admin-bar/field-input.svelte +239 -0
- package/dist/components/admin-bar/field-input.svelte.d.ts +30 -0
- package/dist/components/admin-bar/fields-form.svelte +218 -0
- package/dist/components/admin-bar/fields-form.svelte.d.ts +16 -0
- package/dist/components/admin-bar/icons/pencil.svelte +19 -0
- package/dist/components/admin-bar/icons/pencil.svelte.d.ts +3 -0
- package/dist/components/admin-bar/keyboard-shortcuts-dialog.svelte +1 -1
- package/dist/components/admin-bar/link-input.svelte +102 -0
- package/dist/components/admin-bar/link-input.svelte.d.ts +9 -0
- package/dist/components/admin-bar/list-field.svelte +93 -0
- package/dist/components/admin-bar/list-field.svelte.d.ts +20 -0
- package/dist/components/admin-bar/locales-panel.svelte +42 -1
- package/dist/components/admin-bar/page-config.d.ts +8 -1
- package/dist/components/admin-bar/page-config.js +15 -2
- package/dist/components/admin-bar/page-options.d.ts +24 -0
- package/dist/components/admin-bar/page-options.js +53 -0
- package/dist/components/admin-bar/reorder-buttons.svelte +52 -0
- package/dist/components/admin-bar/reorder-buttons.svelte.d.ts +10 -0
- package/dist/components/admin-bar/rich-text-input.svelte +92 -0
- package/dist/components/admin-bar/rich-text-input.svelte.d.ts +9 -0
- package/dist/components/admin-bar/seo-panel.svelte +144 -189
- package/dist/components/admin-bar/site-options-panel.svelte +115 -0
- package/dist/components/admin-bar/site-options-panel.svelte.d.ts +7 -0
- package/dist/components/admin-bar/structured-editor-popover.svelte +259 -0
- package/dist/components/admin-bar/structured-editor-popover.svelte.d.ts +48 -0
- package/dist/components/admin-bar/ui/checkbox/checkbox.svelte +36 -0
- package/dist/components/admin-bar/ui/checkbox/checkbox.svelte.d.ts +5 -0
- package/dist/components/admin-bar/ui/checkbox/index.d.ts +2 -0
- package/dist/components/admin-bar/ui/checkbox/index.js +2 -0
- package/dist/components/admin-bar/ui/popover/index.d.ts +5 -0
- package/dist/components/admin-bar/ui/popover/index.js +7 -0
- package/dist/components/admin-bar/ui/popover/popover-close.svelte +7 -0
- package/dist/components/admin-bar/ui/popover/popover-close.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/popover/popover-content.svelte +35 -0
- package/dist/components/admin-bar/ui/popover/popover-content.svelte.d.ts +10 -0
- package/dist/components/admin-bar/ui/popover/popover-trigger.svelte +7 -0
- package/dist/components/admin-bar/ui/popover/popover-trigger.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/popover/popover.svelte +7 -0
- package/dist/components/admin-bar/ui/popover/popover.svelte.d.ts +3 -0
- package/dist/components/admin-bar/ui/sheet/index.d.ts +10 -0
- package/dist/components/admin-bar/ui/sheet/index.js +12 -0
- package/dist/components/admin-bar/ui/sheet/sheet-close.svelte +7 -0
- package/dist/components/admin-bar/ui/sheet/sheet-close.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/sheet/sheet-content.svelte +64 -0
- package/dist/components/admin-bar/ui/sheet/sheet-content.svelte.d.ts +17 -0
- package/dist/components/admin-bar/ui/sheet/sheet-description.svelte +17 -0
- package/dist/components/admin-bar/ui/sheet/sheet-description.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte +23 -0
- package/dist/components/admin-bar/ui/sheet/sheet-footer.svelte.d.ts +5 -0
- package/dist/components/admin-bar/ui/sheet/sheet-header.svelte +20 -0
- package/dist/components/admin-bar/ui/sheet/sheet-header.svelte.d.ts +5 -0
- package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte +20 -0
- package/dist/components/admin-bar/ui/sheet/sheet-overlay.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/sheet/sheet-title.svelte +17 -0
- package/dist/components/admin-bar/ui/sheet/sheet-title.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte +7 -0
- package/dist/components/admin-bar/ui/sheet/sheet-trigger.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/sheet/sheet.svelte +7 -0
- package/dist/components/admin-bar/ui/sheet/sheet.svelte.d.ts +3 -0
- package/dist/components/admin-bar/ui/switch/index.d.ts +2 -0
- package/dist/components/admin-bar/ui/switch/index.js +2 -0
- package/dist/components/admin-bar/ui/switch/switch.svelte +27 -0
- package/dist/components/admin-bar/ui/switch/switch.svelte.d.ts +5 -0
- package/dist/components/admin-bar/ui/tabs/index.d.ts +5 -0
- package/dist/components/admin-bar/ui/tabs/index.js +7 -0
- package/dist/components/admin-bar/ui/tabs/tabs-content.svelte +17 -0
- package/dist/components/admin-bar/ui/tabs/tabs-content.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/tabs/tabs-list.svelte +16 -0
- package/dist/components/admin-bar/ui/tabs/tabs-list.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte +20 -0
- package/dist/components/admin-bar/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
- package/dist/components/admin-bar/ui/tabs/tabs.svelte +19 -0
- package/dist/components/admin-bar/ui/tabs/tabs.svelte.d.ts +4 -0
- package/dist/components/cms/cms-boolean.svelte +9 -18
- package/dist/components/cms/cms-boolean.svelte.d.ts +1 -0
- package/dist/components/cms/cms-collection.svelte +46 -0
- package/dist/components/cms/cms-collection.svelte.d.ts +25 -0
- package/dist/components/cms/cms-contact.svelte +26 -0
- package/dist/components/cms/cms-contact.svelte.d.ts +14 -0
- package/dist/components/cms/cms-date-time-editable.svelte +3 -3
- package/dist/components/cms/cms-date-time.svelte +17 -17
- package/dist/components/cms/cms-date-time.svelte.d.ts +2 -1
- package/dist/components/cms/cms-faq.svelte +22 -0
- package/dist/components/cms/cms-faq.svelte.d.ts +15 -0
- package/dist/components/cms/cms-gallery.svelte +31 -0
- package/dist/components/cms/cms-gallery.svelte.d.ts +16 -0
- package/dist/components/cms/cms-hours-editable.svelte +160 -0
- package/dist/components/cms/cms-hours-editable.svelte.d.ts +17 -0
- package/dist/components/cms/cms-hours.svelte +42 -0
- package/dist/components/cms/cms-hours.svelte.d.ts +19 -0
- package/dist/components/cms/cms-image-editable.svelte +13 -4
- package/dist/components/cms/cms-image.svelte +19 -34
- package/dist/components/cms/cms-image.svelte.d.ts +7 -8
- package/dist/components/cms/cms-link-editable.svelte +4 -60
- package/dist/components/cms/cms-link.svelte +15 -28
- package/dist/components/cms/cms-link.svelte.d.ts +4 -9
- package/dist/components/cms/cms-logos.svelte +29 -0
- package/dist/components/cms/cms-logos.svelte.d.ts +16 -0
- package/dist/components/cms/cms-markdown.svelte +15 -16
- package/dist/components/cms/cms-markdown.svelte.d.ts +1 -0
- package/dist/components/cms/cms-nav.svelte +22 -0
- package/dist/components/cms/cms-nav.svelte.d.ts +15 -0
- package/dist/components/cms/cms-number-editable.svelte +4 -2
- package/dist/components/cms/cms-number.svelte +18 -17
- package/dist/components/cms/cms-number.svelte.d.ts +2 -1
- package/dist/components/cms/cms-pricing.svelte +36 -0
- package/dist/components/cms/cms-pricing.svelte.d.ts +14 -0
- package/dist/components/cms/cms-rich-text.svelte +17 -22
- package/dist/components/cms/cms-rich-text.svelte.d.ts +1 -0
- package/dist/components/cms/cms-schedule.svelte +35 -0
- package/dist/components/cms/cms-schedule.svelte.d.ts +16 -0
- package/dist/components/cms/cms-social-links.svelte +35 -0
- package/dist/components/cms/cms-social-links.svelte.d.ts +15 -0
- package/dist/components/cms/cms-stats.svelte +31 -0
- package/dist/components/cms/cms-stats.svelte.d.ts +16 -0
- package/dist/components/cms/cms-steps.svelte +31 -0
- package/dist/components/cms/cms-steps.svelte.d.ts +16 -0
- package/dist/components/cms/cms-store.svelte.d.ts +14 -0
- package/dist/components/cms/cms-store.svelte.js +39 -2
- package/dist/components/cms/cms-structured-editable.svelte +29 -0
- package/dist/components/cms/{cms-repeater-editable.svelte.d.ts → cms-structured-editable.svelte.d.ts} +12 -8
- package/dist/components/cms/cms-structured.svelte +50 -0
- package/dist/components/cms/cms-structured.svelte.d.ts +49 -0
- package/dist/components/cms/cms-team.svelte +22 -0
- package/dist/components/cms/cms-team.svelte.d.ts +15 -0
- package/dist/components/cms/cms-testimonials.svelte +36 -0
- package/dist/components/cms/cms-testimonials.svelte.d.ts +15 -0
- package/dist/components/cms/cms-text.svelte +23 -30
- package/dist/components/cms/cms-text.svelte.d.ts +3 -1
- package/dist/components/cms/cms-timeline.svelte +35 -0
- package/dist/components/cms/cms-timeline.svelte.d.ts +16 -0
- package/dist/components/cms/index.d.ts +15 -1
- package/dist/components/cms/index.js +15 -1
- package/dist/components/cms/scope.d.ts +25 -5
- package/dist/components/cms/use-cms-field.svelte.d.ts +54 -0
- package/dist/components/cms/use-cms-field.svelte.js +83 -0
- package/dist/core/metadata.d.ts +44 -0
- package/dist/core/metadata.js +59 -0
- package/dist/core/shapes/collection.d.ts +60 -0
- package/dist/core/shapes/collection.js +194 -0
- package/dist/core/shapes/contact.d.ts +70 -0
- package/dist/core/shapes/contact.js +214 -0
- package/dist/core/shapes/faq.d.ts +12 -0
- package/dist/core/shapes/faq.js +48 -0
- package/dist/core/shapes/form.d.ts +46 -0
- package/dist/core/shapes/form.js +7 -0
- package/dist/core/shapes/hours.d.ts +91 -0
- package/dist/core/shapes/hours.js +325 -0
- package/dist/core/shapes/image.d.ts +15 -0
- package/dist/core/shapes/image.js +27 -0
- package/dist/core/shapes/index.d.ts +31 -0
- package/dist/core/shapes/index.js +13 -0
- package/dist/core/shapes/link.d.ts +15 -0
- package/dist/core/shapes/link.js +42 -0
- package/dist/core/shapes/nav.d.ts +21 -0
- package/dist/core/shapes/nav.js +71 -0
- package/dist/core/shapes/presets.d.ts +65 -0
- package/dist/core/shapes/presets.js +152 -0
- package/dist/core/shapes/pricing.d.ts +40 -0
- package/dist/core/shapes/pricing.js +115 -0
- package/dist/core/shapes/social-links.d.ts +19 -0
- package/dist/core/shapes/social-links.js +83 -0
- package/dist/core/shapes/team.d.ts +22 -0
- package/dist/core/shapes/team.js +70 -0
- package/dist/core/shapes/testimonials.d.ts +24 -0
- package/dist/core/shapes/testimonials.js +86 -0
- package/dist/core/structured-registry.d.ts +10 -0
- package/dist/core/structured-registry.js +6 -0
- package/dist/core/structured.d.ts +131 -0
- package/dist/core/structured.js +235 -0
- package/dist/core/wire.d.ts +31 -1
- package/dist/editor.d.ts +26 -0
- package/dist/editor.js +25 -0
- package/dist/index.d.ts +26 -4
- package/dist/index.js +31 -2
- package/dist/server/load-cms.js +1 -0
- package/package.json +12 -3
- package/plugin/README.md +22 -2
- package/plugin/dist/build.d.ts +74 -0
- package/plugin/dist/build.js +210 -0
- package/plugin/dist/build.js.map +1 -0
- package/plugin/dist/index.d.ts +10 -1
- package/plugin/dist/index.js +63 -4
- package/plugin/dist/index.js.map +1 -1
- package/plugin/dist/manifest.d.ts +31 -0
- package/plugin/dist/manifest.js +146 -41
- package/plugin/dist/manifest.js.map +1 -1
- package/plugin/dist/parse-svelte.d.ts +15 -0
- package/plugin/dist/parse-svelte.js +26 -16
- package/plugin/dist/parse-svelte.js.map +1 -1
- package/dist/components/admin-bar/site-settings-panel.svelte +0 -225
- package/dist/components/admin-bar/site-settings-panel.svelte.d.ts +0 -7
- package/dist/components/cms/cms-repeater-editable.svelte +0 -137
- package/dist/components/cms/cms-repeater.svelte +0 -55
- 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
|
-
|
|
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
|
-
| `
|
|
210
|
-
| `
|
|
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
|
-
|
|
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
|
-
<
|
|
240
|
-
{
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
</
|
|
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
|
-
|
|
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.**
|
|
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;
|
|
336
|
-
- Call `
|
|
337
|
-
-
|
|
338
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/dist/backend/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
8
|
-
label: '
|
|
9
|
+
seo: {
|
|
10
|
+
label: 'Search & sharing',
|
|
9
11
|
fields: {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
label: '
|
|
20
|
+
locales: {
|
|
21
|
+
label: 'Languages',
|
|
17
22
|
fields: {
|
|
18
|
-
|
|
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-
|
|
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
|
|
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-
|
|
1666
|
-
<
|
|
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
|
|