@velastack/cms 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -47
- package/dist/backend/index.d.ts +7 -5
- package/dist/backend/index.js +7 -5
- package/dist/backend/types.d.ts +1 -1
- package/dist/components/admin-bar/admin-bar-internal.svelte +54 -59
- package/dist/components/admin-bar/admin-bar.css +1 -1
- package/dist/components/admin-bar/admin-bar.svelte +11 -7
- package/dist/components/admin-bar/field-input.svelte +20 -1
- package/dist/components/admin-bar/field-input.svelte.d.ts +2 -1
- 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/history-panel.svelte +4 -4
- 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 +1 -1
- 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/pages-panel.svelte +1 -1
- 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/structured-editor-popover.svelte +15 -6
- package/dist/components/admin-bar/theme.svelte.js +1 -1
- 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-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.svelte +3 -25
- package/dist/components/cms/cms-image.svelte.d.ts +3 -11
- package/dist/components/cms/cms-link-editable.svelte +4 -60
- package/dist/components/cms/cms-link.svelte +3 -23
- package/dist/components/cms/cms-link.svelte.d.ts +3 -10
- 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-nav.svelte +22 -0
- package/dist/components/cms/cms-nav.svelte.d.ts +15 -0
- 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-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.js +8 -5
- 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-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/core/cms-fetch.d.ts +16 -0
- package/dist/core/cms-fetch.js +24 -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/{components/cms → core}/structured-registry.d.ts +2 -2
- package/dist/core/structured.d.ts +33 -10
- package/dist/core/structured.js +98 -47
- package/dist/editor.d.ts +4 -0
- package/dist/editor.js +4 -0
- package/dist/index.d.ts +22 -8
- package/dist/index.js +24 -5
- package/dist/server/README.md +1 -1
- package/dist/server/api-adapter.js +1 -1
- package/dist/server/cms.d.ts +1 -1
- package/dist/server/cms.js +1 -1
- package/dist/server/load-cms.js +1 -1
- package/package.json +15 -11
- package/plugin/README.md +34 -4
- package/plugin/dist/adapter.d.ts +5 -5
- package/plugin/dist/adapter.js +21 -7
- package/plugin/dist/adapter.js.map +1 -1
- package/plugin/dist/build-state.d.ts +1 -0
- package/plugin/dist/build-state.js +10 -1
- package/plugin/dist/build-state.js.map +1 -1
- package/plugin/dist/build.d.ts +2 -2
- package/plugin/dist/build.js +8 -7
- package/plugin/dist/build.js.map +1 -1
- package/plugin/dist/index.js +5 -1
- package/plugin/dist/index.js.map +1 -1
- package/plugin/dist/manifest.d.ts +18 -0
- package/plugin/dist/manifest.js +78 -10
- package/plugin/dist/manifest.js.map +1 -1
- package/plugin/dist/transforms.d.ts +4 -4
- package/plugin/dist/transforms.js +4 -4
- package/dist/cms.d.ts +0 -6
- package/dist/cms.js +0 -27
- package/dist/components/cms/cms-repeater-editable.svelte +0 -137
- package/dist/components/cms/cms-repeater.svelte +0 -52
- package/dist/components/cms/cms-repeater.svelte.d.ts +0 -31
- /package/dist/{components/cms → core}/structured-registry.js +0 -0
package/README.md
CHANGED
|
@@ -42,6 +42,8 @@ CMS identity therefore can't be `(component file + field name)`. It has to be `(
|
|
|
42
42
|
npm install @velastack/cms
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
Requires SvelteKit 3, Svelte 5.57.1+, Vite 8.0.12+ and Node 22.17+. SvelteKit 2 projects stay on `@velastack/cms@^0.5`.
|
|
46
|
+
|
|
45
47
|
## Quick start
|
|
46
48
|
|
|
47
49
|
### 1. Register the Vite plugin
|
|
@@ -57,9 +59,20 @@ export default defineConfig({
|
|
|
57
59
|
});
|
|
58
60
|
```
|
|
59
61
|
|
|
60
|
-
The plugin reads `src/routes/` and `src/lib/` by default. Override with `cms({ routesDir, libDir })` if your project is laid out differently.
|
|
62
|
+
The plugin reads `src/routes/` and `src/lib/` by default. Override with `cms({ routesDir, libDir })` if your project is laid out differently. Imports through `#lib/…` (and any other `#` subpath import in your `package.json` `imports`) are followed, as are older `$lib/…` ones.
|
|
63
|
+
|
|
64
|
+
### 2. Create the CMS in `src/lib/cms.ts`
|
|
61
65
|
|
|
62
|
-
|
|
66
|
+
SvelteKit 3 projects import `src/lib` through the `#lib` subpath import, which `sv create` (and `sv migrate sveltekit-3`) declares in `package.json`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"imports": {
|
|
71
|
+
"#lib": "./src/lib/index.js",
|
|
72
|
+
"#lib/*": "./src/lib/*"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
63
76
|
|
|
64
77
|
```ts
|
|
65
78
|
// src/lib/cms.ts
|
|
@@ -84,7 +97,7 @@ Optionally include `svelte-meta-tags` for easy page metadata handling, but it is
|
|
|
84
97
|
// src/routes/+layout.server.ts
|
|
85
98
|
import { error, redirect, type ServerLoad } from '@sveltejs/kit';
|
|
86
99
|
import { defineBaseMetaTags } from 'svelte-meta-tags';
|
|
87
|
-
import { loadCms } from '
|
|
100
|
+
import { loadCms } from '#lib/cms.js';
|
|
88
101
|
|
|
89
102
|
export const load: ServerLoad = async (event) => {
|
|
90
103
|
const { baseMetaTags } = defineBaseMetaTags({
|
|
@@ -199,6 +212,27 @@ Only URLs that flow through `apiAdapter` responses are rewritten. If you build a
|
|
|
199
212
|
|
|
200
213
|
If `endpoint` is omitted (e.g. when using `mockAdapter` for tests/demos), the plugin's media steps are no-ops — builds proceed exactly as before.
|
|
201
214
|
|
|
215
|
+
### Static sites: `@velastack/cms/adapter`
|
|
216
|
+
|
|
217
|
+
A fully static site uses the bundled adapter, a wrapper around `@sveltejs/adapter-static` (same options). SvelteKit 3 takes the adapter in `sveltekit({...})`:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
// vite.config.ts
|
|
221
|
+
import { defineConfig } from 'vite';
|
|
222
|
+
import { sveltekit } from '@sveltejs/kit/vite';
|
|
223
|
+
import { cms } from '@velastack/cms/vite';
|
|
224
|
+
import cmsAdapter from '@velastack/cms/adapter';
|
|
225
|
+
|
|
226
|
+
export default defineConfig({
|
|
227
|
+
plugins: [
|
|
228
|
+
cms({ endpoint: 'https://cms.example.com/v1/projects/p1/cms' }),
|
|
229
|
+
sveltekit({ adapter: cmsAdapter({ fallback: '200.html' }) })
|
|
230
|
+
]
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
After adapter-static writes the site, it adds what the admin bar needs to preview and create pages that were not prerendered: a `__fallback.json` next to each parameterized route (`rooms/__fallback.json` for `/(marketing)/rooms/[slug]`), and a `__velastack_manifest` of the creatable routes' prerendered entries injected into the `fallback` page. The manifest needs `fallback` set and the `cms()` plugin in the same build; the adapter warns if the plugin never reported.
|
|
235
|
+
|
|
202
236
|
## CMS components
|
|
203
237
|
|
|
204
238
|
Every CMS component shares the same prop shape:
|
|
@@ -238,70 +272,93 @@ Rendered with `{@html}`. Edit mode swaps for a `<textarea>`.
|
|
|
238
272
|
|
|
239
273
|
Renders an `<img>`. Edit mode shows the current image plus a URL input.
|
|
240
274
|
|
|
241
|
-
|
|
275
|
+
## Structured components
|
|
276
|
+
|
|
277
|
+
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.
|
|
242
278
|
|
|
243
279
|
```svelte
|
|
244
|
-
<
|
|
245
|
-
{
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
280
|
+
<script lang="ts">
|
|
281
|
+
import { CmsHours, CmsNav, isActive } from '@velastack/cms';
|
|
282
|
+
</script>
|
|
283
|
+
|
|
284
|
+
<!-- In the root layout: site-wide by construction. -->
|
|
285
|
+
<CmsNav name="nav.primary">
|
|
286
|
+
{#snippet children(items)}
|
|
287
|
+
{#each items as item (item.id)}
|
|
288
|
+
<a href={item.href} aria-current={isActive(item, page.url) ? 'page' : undefined}
|
|
289
|
+
>{item.label}</a
|
|
290
|
+
>
|
|
291
|
+
{/each}
|
|
252
292
|
{/snippet}
|
|
253
|
-
</
|
|
293
|
+
</CmsNav>
|
|
294
|
+
|
|
295
|
+
<!-- On the contact page: the same value, read and written through the root scope. -->
|
|
296
|
+
<CmsHours name="hours" scope="root">
|
|
297
|
+
{#snippet children(h)}
|
|
298
|
+
<p>{h.today.label}: {h.today.text} · {h.status}</p>
|
|
299
|
+
{#each h.rows as row (row.id)}<dt>{row.days}</dt>
|
|
300
|
+
<dd>{row.text}</dd>{/each}
|
|
301
|
+
{/snippet}
|
|
302
|
+
</CmsHours>
|
|
254
303
|
```
|
|
255
304
|
|
|
256
|
-
|
|
305
|
+
| Component | Value | Snippet receives | Helpers |
|
|
306
|
+
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
307
|
+
| `CmsNav` | `{ items: [{ id, label, link, children }] }` | items with resolved `href` | `isActive(item, url)` |
|
|
308
|
+
| `CmsHours` | week grid, `exceptions`, `note`, `labels`, `timezone` | `rows`, `today`, `isOpenNow`, `status`, `exceptionRows` in the visitor's locale | `toOpeningHoursSpecification` |
|
|
309
|
+
| `CmsContact` | `name`, `address`, `phones`, `emails`, `whatsapp`, `directionsUrl`, `geo`, `labels` | plus `telHref`, `mailto`, `whatsappHref`, `mapsHref`, `formattedAddress` | `toPostalAddress` |
|
|
310
|
+
| `CmsSocialLinks` | `{ items: [{ id, platform, url, label }] }` | items with `name` (label or platform name) | `toSameAs`, `SOCIAL_PLATFORMS` |
|
|
311
|
+
| `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` |
|
|
312
|
+
| `CmsTeam` | name, role, rich bio, photo, email, phone, profile links | members | |
|
|
313
|
+
| `CmsTestimonials` | rich quote, author, role, company, photo, rating, source, date | testimonials | `toReview` |
|
|
314
|
+
| `CmsPricing` | `currency`, `labels.custom`, tiers with a feature checklist and a CTA | tiers with `priceText` and `href` | `formatPrice` |
|
|
315
|
+
| `CmsFaq` | question, rich answer | questions | `toFaqPage` |
|
|
316
|
+
| `CmsStats`, `CmsSteps`, `CmsTimeline`, `CmsGallery`, `CmsLogos`, `CmsSchedule` | fixed list presets | items | `logosView`, `scheduleByDay` |
|
|
257
317
|
|
|
258
|
-
|
|
318
|
+
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.
|
|
259
319
|
|
|
260
|
-
|
|
320
|
+
Every value type (`CmsHoursValue`, `NavItem`, …), schema (`cmsHours`, …) and JSON-LD helper is exported from the package root.
|
|
261
321
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
322
|
+
### Building one
|
|
323
|
+
|
|
324
|
+
The package ships the pieces a structured component is made of, and the components above are built from nothing else.
|
|
265
325
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
326
|
+
```ts
|
|
327
|
+
// The value's rules: a versioned shape, its translatable strings, and the form the popover draws.
|
|
328
|
+
import { defineForm, asItems, asString } from '@velastack/cms';
|
|
329
|
+
|
|
330
|
+
export const cmsFaq = defineForm<CmsFaqValue>({
|
|
331
|
+
component: 'CmsFaq', // canonical export name, recorded in the manifest
|
|
332
|
+
label: 'FAQ', // popover header
|
|
333
|
+
version: 1, // written as `v` on every value
|
|
334
|
+
translatable: [], // root fields that translate (dotted paths allowed: 'labels.closed')
|
|
335
|
+
items: { key: 'items', translatable: ['question', 'answer'] }, // lists and their translatable fields; nest `items` for lists inside items
|
|
336
|
+
normalize: (raw) => ({ v: 1, items: asItems(raw?.items, …) }),
|
|
337
|
+
empty: () => ({ v: 1, items: [] }),
|
|
338
|
+
fields: [{ key: 'items', type: 'list', label: 'Questions', itemLabel: 'question', titleKey: 'question', blank: () => ({ question: '', answer: '' }), fields: [
|
|
339
|
+
{ key: 'question', label: 'Question', type: 'text' },
|
|
340
|
+
{ key: 'answer', label: 'Answer', type: 'html' }
|
|
341
|
+
] }]
|
|
342
|
+
});
|
|
276
343
|
```
|
|
277
344
|
|
|
278
345
|
```svelte
|
|
279
|
-
<!--
|
|
346
|
+
<!-- The display component, inside the package: the generic one, given the schema and a view function. -->
|
|
280
347
|
<script lang="ts">
|
|
281
|
-
import
|
|
282
|
-
let { name, scope, fallback, children } = $props();
|
|
283
|
-
const field = useCmsField(
|
|
284
|
-
() => ({ name, scope }),
|
|
285
|
-
(raw) => hours.read(raw, fallback)
|
|
286
|
-
);
|
|
348
|
+
import Structured from './cms-structured.svelte'; // src/lib/components/cms, inside the package
|
|
349
|
+
let { name, scope, fallback, value, children } = $props();
|
|
287
350
|
</script>
|
|
288
351
|
|
|
289
|
-
{
|
|
290
|
-
{#await import('./cms-hours-editable.svelte') then { default: Editable }}
|
|
291
|
-
<Editable {field} {children} />
|
|
292
|
-
{/await}
|
|
293
|
-
{:else}
|
|
294
|
-
{@render children(field.current)}
|
|
295
|
-
{/if}
|
|
352
|
+
<Structured schema={cmsFaq} {name} {scope} {fallback} {value} view={faqView} {children} />
|
|
296
353
|
```
|
|
297
354
|
|
|
298
|
-
|
|
355
|
+
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.
|
|
299
356
|
|
|
300
357
|
Rules every structured value follows (`core/structured.ts`):
|
|
301
358
|
|
|
302
359
|
- **Whole-object writes, explicit `null` clears.** Components never patch a leaf; they write the full `{ v, … }` object. Cleared fields are `null`.
|
|
303
|
-
- **List items carry a stable `id`** (`newItemId()`); reorder and delete work because arrays replace wholesale on merge.
|
|
304
|
-
- **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). `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.
|
|
360
|
+
- **List items carry a stable `id`** (`newItemId()`) at every nesting level; reorder and delete work because arrays replace wholesale on merge.
|
|
361
|
+
- **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.
|
|
305
362
|
|
|
306
363
|
### `<CmsEntries />`
|
|
307
364
|
|
|
@@ -347,7 +404,7 @@ A component is treated as a CMS component if **any** of the following hold:
|
|
|
347
404
|
|
|
348
405
|
Use it from any route: `<CmsLink name="hero.cta" />`. The plugin picks up `hero.cta` as a field on the page's scope; no plugin config edit required.
|
|
349
406
|
|
|
350
|
-
2. **It's a named export from your local `src/lib/components/cms/index.{ts,js}` barrel
|
|
407
|
+
2. **It's a named export from your local `src/lib/components/cms/index.{ts,js}` barrel** (imported as `#lib/components/cms/index.js`). Re-export your component there if you prefer a single import path.
|
|
351
408
|
|
|
352
409
|
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.
|
|
353
410
|
|
package/dist/backend/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* export const cms = createCmsBackend();
|
|
8
8
|
*
|
|
9
9
|
* // src/routes/cms/[...path]/+server.ts
|
|
10
|
-
* import { cms } from '../server/cms';
|
|
10
|
+
* import { cms } from '../server/cms.js';
|
|
11
11
|
* export const prerender = false;
|
|
12
12
|
* export const fallback = cms.handler;
|
|
13
13
|
* ```
|
|
@@ -25,10 +25,12 @@
|
|
|
25
25
|
* default; enabling it with an `origin` predicate that returns `true`
|
|
26
26
|
* unconditionally lets any site drive the mutating endpoints with a signed-in
|
|
27
27
|
* editor's cookie, because SvelteKit's CSRF check only guards form content
|
|
28
|
-
* types and
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* types (and, since Kit 3, requests with no content type) and these are JSON.
|
|
29
|
+
* The admin bar sends every mutation with a JSON content type for that reason.
|
|
30
|
+
* SvelteKit *does* guard `multipart/form-data`, so a cross-origin
|
|
31
|
+
* `POST /media` needs the site's origin in the backend app's
|
|
32
|
+
* `sveltekit({ csrf: { trustedOrigins: ['https://site.example'] } })` — that
|
|
33
|
+
* check is production-only, so it will not show up in `vela dev`.
|
|
32
34
|
*
|
|
33
35
|
* The session cookie is scoped to the mount path (`/cms`, or
|
|
34
36
|
* `/v1/projects/<id>/cms`), so several projects on one origin hold independent
|
package/dist/backend/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* export const cms = createCmsBackend();
|
|
8
8
|
*
|
|
9
9
|
* // src/routes/cms/[...path]/+server.ts
|
|
10
|
-
* import { cms } from '../server/cms';
|
|
10
|
+
* import { cms } from '../server/cms.js';
|
|
11
11
|
* export const prerender = false;
|
|
12
12
|
* export const fallback = cms.handler;
|
|
13
13
|
* ```
|
|
@@ -25,10 +25,12 @@
|
|
|
25
25
|
* default; enabling it with an `origin` predicate that returns `true`
|
|
26
26
|
* unconditionally lets any site drive the mutating endpoints with a signed-in
|
|
27
27
|
* editor's cookie, because SvelteKit's CSRF check only guards form content
|
|
28
|
-
* types and
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* types (and, since Kit 3, requests with no content type) and these are JSON.
|
|
29
|
+
* The admin bar sends every mutation with a JSON content type for that reason.
|
|
30
|
+
* SvelteKit *does* guard `multipart/form-data`, so a cross-origin
|
|
31
|
+
* `POST /media` needs the site's origin in the backend app's
|
|
32
|
+
* `sveltekit({ csrf: { trustedOrigins: ['https://site.example'] } })` — that
|
|
33
|
+
* check is production-only, so it will not show up in `vela dev`.
|
|
32
34
|
*
|
|
33
35
|
* The session cookie is scoped to the mount path (`/cms`, or
|
|
34
36
|
* `/v1/projects/<id>/cms`), so several projects on one origin hold independent
|
package/dist/backend/types.d.ts
CHANGED
|
@@ -138,7 +138,7 @@ export type CmsBackendOptions = {
|
|
|
138
138
|
*
|
|
139
139
|
* Enabling this is a security decision, not a convenience one: the session
|
|
140
140
|
* cookie is credentialed and the JSON mutation endpoints are not covered by
|
|
141
|
-
* SvelteKit's CSRF check (it only guards form content types). An `origin`
|
|
141
|
+
* SvelteKit's CSRF check (it only guards form and missing content types). An `origin`
|
|
142
142
|
* predicate that returns `true` unconditionally lets any site on the
|
|
143
143
|
* internet drive those endpoints with a signed-in editor's cookie. Allowlist
|
|
144
144
|
* the origins you actually serve.
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
afterNavigate,
|
|
4
|
-
beforeNavigate,
|
|
5
|
-
goto,
|
|
6
|
-
invalidateAll,
|
|
7
|
-
replaceState
|
|
8
|
-
} from '$app/navigation';
|
|
2
|
+
import { afterNavigate, beforeNavigate, goto, refreshAll } from '$app/navigation';
|
|
9
3
|
import { page } from '$app/state';
|
|
10
4
|
import { pages } from 'virtual:vela-cms/pages';
|
|
11
5
|
import { cmsStore, type PageDeleteOutcome } from '../cms/cms-store.svelte.js';
|
|
@@ -14,6 +8,7 @@
|
|
|
14
8
|
import CssRoot from './css-root.svelte';
|
|
15
9
|
import { adminBarTheme, type AdminBarTheme } from './theme.svelte.js';
|
|
16
10
|
import type { CmsDeployState } from '../../core/wire.js';
|
|
11
|
+
import { cmsFetch } from '../../core/cms-fetch.js';
|
|
17
12
|
import DeletePageDialog, {
|
|
18
13
|
type DeletePageMode,
|
|
19
14
|
type RedirectTarget
|
|
@@ -186,7 +181,7 @@
|
|
|
186
181
|
|
|
187
182
|
const previewUrl = $derived.by(() => {
|
|
188
183
|
if (versionKey) {
|
|
189
|
-
const url = new URL(page.url);
|
|
184
|
+
const url = new URL(page.url.href);
|
|
190
185
|
url.searchParams.delete('edit');
|
|
191
186
|
url.searchParams.delete('preview');
|
|
192
187
|
url.searchParams.set('version', versionKey);
|
|
@@ -194,7 +189,7 @@
|
|
|
194
189
|
}
|
|
195
190
|
const key = previewKey;
|
|
196
191
|
if (!key) return '';
|
|
197
|
-
const url = new URL(page.url);
|
|
192
|
+
const url = new URL(page.url.href);
|
|
198
193
|
url.searchParams.delete('edit');
|
|
199
194
|
url.searchParams.set('preview', key);
|
|
200
195
|
return url.toString();
|
|
@@ -211,7 +206,7 @@
|
|
|
211
206
|
};
|
|
212
207
|
|
|
213
208
|
const setPreviewParam = async (key: string | null, opts: { replace?: boolean } = {}) => {
|
|
214
|
-
const url = new URL(page.url);
|
|
209
|
+
const url = new URL(page.url.href);
|
|
215
210
|
const current = url.searchParams.get('preview');
|
|
216
211
|
url.searchParams.delete('edit');
|
|
217
212
|
if (key) {
|
|
@@ -223,14 +218,14 @@
|
|
|
223
218
|
}
|
|
224
219
|
const path = url.pathname + url.search + url.hash;
|
|
225
220
|
if (opts.replace) {
|
|
226
|
-
|
|
221
|
+
await goto(path, { shallow: true, replace: true, state: page.state });
|
|
227
222
|
} else {
|
|
228
|
-
await goto(path, {
|
|
223
|
+
await goto(path, { reset: false });
|
|
229
224
|
}
|
|
230
225
|
};
|
|
231
226
|
|
|
232
227
|
const setVersionParam = async (key: string | null, opts: { replace?: boolean } = {}) => {
|
|
233
|
-
const url = new URL(page.url);
|
|
228
|
+
const url = new URL(page.url.href);
|
|
234
229
|
const current = url.searchParams.get('version');
|
|
235
230
|
url.searchParams.delete('edit');
|
|
236
231
|
url.searchParams.delete('preview');
|
|
@@ -243,14 +238,14 @@
|
|
|
243
238
|
}
|
|
244
239
|
const path = url.pathname + url.search + url.hash;
|
|
245
240
|
if (opts.replace) {
|
|
246
|
-
|
|
241
|
+
await goto(path, { shallow: true, replace: true, state: page.state });
|
|
247
242
|
} else {
|
|
248
|
-
await goto(path, {
|
|
243
|
+
await goto(path, { reset: false });
|
|
249
244
|
// Force the server load to re-fetch. SvelteKit's URL tracking
|
|
250
245
|
// doesn't always pick up our `?version=` access (it lives behind
|
|
251
246
|
// the `loadCms` helper) — visible on Exit, where we'd otherwise
|
|
252
247
|
// keep showing the past release's `page.data.cms.docs`.
|
|
253
|
-
await
|
|
248
|
+
await refreshAll();
|
|
254
249
|
}
|
|
255
250
|
};
|
|
256
251
|
|
|
@@ -264,7 +259,7 @@
|
|
|
264
259
|
// (and on static-export sites with no server load to re-run). Like the
|
|
265
260
|
// other gating params, we strip the param when `null`.
|
|
266
261
|
const setLocaleParam = async (locale: string | null, opts: { replace?: boolean } = {}) => {
|
|
267
|
-
const url = new URL(page.url);
|
|
262
|
+
const url = new URL(page.url.href);
|
|
268
263
|
const current = url.searchParams.get('locale');
|
|
269
264
|
if (locale) {
|
|
270
265
|
if (current === locale) return;
|
|
@@ -275,18 +270,18 @@
|
|
|
275
270
|
}
|
|
276
271
|
const path = url.pathname + url.search + url.hash;
|
|
277
272
|
if (opts.replace) {
|
|
278
|
-
|
|
273
|
+
await goto(path, { shallow: true, replace: true, state: page.state });
|
|
279
274
|
return;
|
|
280
275
|
}
|
|
281
276
|
if (!locale) {
|
|
282
277
|
clearingLocale = true;
|
|
283
278
|
try {
|
|
284
|
-
await goto(path, {
|
|
279
|
+
await goto(path, { reset: false });
|
|
285
280
|
} finally {
|
|
286
281
|
clearingLocale = false;
|
|
287
282
|
}
|
|
288
283
|
} else {
|
|
289
|
-
await goto(path, {
|
|
284
|
+
await goto(path, { reset: false });
|
|
290
285
|
}
|
|
291
286
|
};
|
|
292
287
|
|
|
@@ -333,7 +328,7 @@
|
|
|
333
328
|
// Until then, the overlay-sync effect must NOT strip `?preview=` from the
|
|
334
329
|
// URL — `cmsStore.openRelease` is null pre-fetch, but that's "unknown",
|
|
335
330
|
// not "no draft". Without this gate, a hard refresh on `?preview=<key>`
|
|
336
|
-
// would treat the param as stale and
|
|
331
|
+
// would treat the param as stale and shallow-replace it away.
|
|
337
332
|
let openReleaseFetched = $state(false);
|
|
338
333
|
|
|
339
334
|
// On mount and on subsequent URL/store changes: derive what to do (URL sync,
|
|
@@ -411,6 +406,7 @@
|
|
|
411
406
|
// appended; already-correct nav targets pass through unchanged. Full-page
|
|
412
407
|
// unloads (`willUnload`) and external/hash-only navs bypass.
|
|
413
408
|
beforeNavigate((nav) => {
|
|
409
|
+
if (nav.shallow) return;
|
|
414
410
|
if (nav.type === 'leave' || nav.willUnload) return;
|
|
415
411
|
if (!nav.to) return;
|
|
416
412
|
if (nav.to.url.origin !== location.origin) return;
|
|
@@ -434,10 +430,7 @@
|
|
|
434
430
|
nav.cancel();
|
|
435
431
|
const url = new URL(nav.to.url);
|
|
436
432
|
carryLocaleOnto(url);
|
|
437
|
-
void goto(url.pathname + url.search + url.hash, {
|
|
438
|
-
keepFocus: true,
|
|
439
|
-
noScroll: true
|
|
440
|
-
});
|
|
433
|
+
void goto(url.pathname + url.search + url.hash, { reset: false });
|
|
441
434
|
return;
|
|
442
435
|
}
|
|
443
436
|
// Target has no `?version=`. If we're exiting on purpose, let the nav
|
|
@@ -448,10 +441,7 @@
|
|
|
448
441
|
url.searchParams.delete('preview');
|
|
449
442
|
url.searchParams.set('version', versionKey);
|
|
450
443
|
carryLocaleOnto(url);
|
|
451
|
-
void goto(url.pathname + url.search + url.hash, {
|
|
452
|
-
keepFocus: true,
|
|
453
|
-
noScroll: true
|
|
454
|
-
});
|
|
444
|
+
void goto(url.pathname + url.search + url.hash, { reset: false });
|
|
455
445
|
return;
|
|
456
446
|
}
|
|
457
447
|
|
|
@@ -462,10 +452,7 @@
|
|
|
462
452
|
nav.cancel();
|
|
463
453
|
const url = new URL(nav.to.url);
|
|
464
454
|
carryLocaleOnto(url);
|
|
465
|
-
void goto(url.pathname + url.search + url.hash, {
|
|
466
|
-
keepFocus: true,
|
|
467
|
-
noScroll: true
|
|
468
|
-
});
|
|
455
|
+
void goto(url.pathname + url.search + url.hash, { reset: false });
|
|
469
456
|
return;
|
|
470
457
|
}
|
|
471
458
|
|
|
@@ -477,23 +464,26 @@
|
|
|
477
464
|
const url = new URL(nav.to.url);
|
|
478
465
|
if (key) url.searchParams.set('preview', key);
|
|
479
466
|
carryLocaleOnto(url);
|
|
480
|
-
void goto(url.pathname + url.search + url.hash, {
|
|
481
|
-
keepFocus: true,
|
|
482
|
-
noScroll: true
|
|
483
|
-
});
|
|
467
|
+
void goto(url.pathname + url.search + url.hash, { reset: false });
|
|
484
468
|
});
|
|
485
469
|
|
|
486
470
|
// Re-apply the overlay on every internal navigation. SvelteKit may strip
|
|
487
471
|
// the gating param when nav targets aren't preserve-params links, so we
|
|
488
|
-
// reapply it via
|
|
472
|
+
// reapply it via a shallow replace too. Merge (not reset) so unchanged-layout
|
|
489
473
|
// overlays keep showing while new-page-scope fetches resolve.
|
|
490
|
-
afterNavigate(() => {
|
|
474
|
+
afterNavigate(({ shallow }) => {
|
|
475
|
+
if (shallow) return;
|
|
476
|
+
|
|
491
477
|
if (versionKey) {
|
|
492
|
-
const url = new URL(page.url);
|
|
478
|
+
const url = new URL(page.url.href);
|
|
493
479
|
if (url.searchParams.get('version') !== versionKey) {
|
|
494
480
|
url.searchParams.delete('preview');
|
|
495
481
|
url.searchParams.set('version', versionKey);
|
|
496
|
-
|
|
482
|
+
void goto(url.pathname + url.search + url.hash, {
|
|
483
|
+
shallow: true,
|
|
484
|
+
replace: true,
|
|
485
|
+
state: page.state
|
|
486
|
+
});
|
|
497
487
|
}
|
|
498
488
|
// `reset` so a nav from one release to another wipes the prior
|
|
499
489
|
// snapshot's overlay before merging in the new one — otherwise
|
|
@@ -522,10 +512,14 @@
|
|
|
522
512
|
cmsStore.clearOverlay();
|
|
523
513
|
return;
|
|
524
514
|
}
|
|
525
|
-
const url = new URL(page.url);
|
|
515
|
+
const url = new URL(page.url.href);
|
|
526
516
|
if (url.searchParams.get('preview') !== key) {
|
|
527
517
|
url.searchParams.set('preview', key);
|
|
528
|
-
|
|
518
|
+
void goto(url.pathname + url.search + url.hash, {
|
|
519
|
+
shallow: true,
|
|
520
|
+
replace: true,
|
|
521
|
+
state: page.state
|
|
522
|
+
});
|
|
529
523
|
}
|
|
530
524
|
void cmsStore.loadAndApplyOverlay(endpoint, currentScopes(), key, { locale: currentLocale });
|
|
531
525
|
void cmsStore.loadAndApplyEntriesOverlay(endpoint, currentEntriesRouteIds(), key, {
|
|
@@ -684,7 +678,7 @@
|
|
|
684
678
|
deploying = true;
|
|
685
679
|
deployError = null;
|
|
686
680
|
try {
|
|
687
|
-
const res = await
|
|
681
|
+
const res = await cmsFetch(`${endpoint}/deploy`, { method: 'POST' });
|
|
688
682
|
if (!res.ok) {
|
|
689
683
|
deployError = (await res.text()) || 'Could not start the deploy.';
|
|
690
684
|
return;
|
|
@@ -925,9 +919,8 @@
|
|
|
925
919
|
|
|
926
920
|
const onRegeneratePreviewKey = async () => {
|
|
927
921
|
if (versionKey && versionRelease) {
|
|
928
|
-
const res = await
|
|
929
|
-
method: 'POST'
|
|
930
|
-
credentials: 'include'
|
|
922
|
+
const res = await cmsFetch(`${endpoint}/release/history/${versionRelease.id}/preview-key`, {
|
|
923
|
+
method: 'POST'
|
|
931
924
|
});
|
|
932
925
|
if (!res.ok) return;
|
|
933
926
|
const data = (await res.json()) as { preview_key: string };
|
|
@@ -948,9 +941,8 @@
|
|
|
948
941
|
return;
|
|
949
942
|
}
|
|
950
943
|
|
|
951
|
-
const res = await
|
|
952
|
-
method: 'POST'
|
|
953
|
-
credentials: 'include'
|
|
944
|
+
const res = await cmsFetch(`${endpoint}/release/preview-key`, {
|
|
945
|
+
method: 'POST'
|
|
954
946
|
});
|
|
955
947
|
if (!res.ok) return;
|
|
956
948
|
await cmsStore.fetchOpenRelease(endpoint);
|
|
@@ -959,9 +951,8 @@
|
|
|
959
951
|
|
|
960
952
|
const onDiscardAllChanges = async () => {
|
|
961
953
|
if (!confirm('Discard all unpublished changes in your working copy?')) return;
|
|
962
|
-
const res = await
|
|
963
|
-
method: 'POST'
|
|
964
|
-
credentials: 'include'
|
|
954
|
+
const res = await cmsFetch(`${endpoint}/release/discard`, {
|
|
955
|
+
method: 'POST'
|
|
965
956
|
});
|
|
966
957
|
if (!res.ok) return;
|
|
967
958
|
cmsStore.setOpenRelease(null);
|
|
@@ -1171,6 +1162,7 @@
|
|
|
1171
1162
|
const sourceContents: Record<string, unknown> = res.ok
|
|
1172
1163
|
? ((await res.json()) as { contents: Record<string, unknown> }).contents
|
|
1173
1164
|
: {};
|
|
1165
|
+
|
|
1174
1166
|
const { metadata: meta, ...rest } = sourceContents as { metadata?: unknown } & Record<
|
|
1175
1167
|
string,
|
|
1176
1168
|
unknown
|
|
@@ -1179,6 +1171,7 @@
|
|
|
1179
1171
|
meta && typeof meta === 'object' && !Array.isArray(meta)
|
|
1180
1172
|
? (meta as Record<string, unknown>)
|
|
1181
1173
|
: {};
|
|
1174
|
+
|
|
1182
1175
|
closeAllPanels();
|
|
1183
1176
|
duplicateSource = { tree: rest, metadata: sourceMetadata };
|
|
1184
1177
|
newDialog = config;
|
|
@@ -1261,7 +1254,7 @@
|
|
|
1261
1254
|
duplicateSource = null;
|
|
1262
1255
|
pagesOpen = false;
|
|
1263
1256
|
cmsStore.isEditing = true;
|
|
1264
|
-
await goto(url, {
|
|
1257
|
+
await goto(url, { reset: false });
|
|
1265
1258
|
} finally {
|
|
1266
1259
|
newPageCreating = false;
|
|
1267
1260
|
}
|
|
@@ -1320,7 +1313,7 @@
|
|
|
1320
1313
|
<KbdShortcut keys="⌘E" class={menuShortcutClass} />
|
|
1321
1314
|
</Menubar.Item>
|
|
1322
1315
|
<Menubar.Item class={menuItemClass} onSelect={onOpenSeo}>
|
|
1323
|
-
SEO &
|
|
1316
|
+
SEO & Metadata
|
|
1324
1317
|
<KbdShortcut keys="⌘I" class={menuShortcutClass} />
|
|
1325
1318
|
</Menubar.Item>
|
|
1326
1319
|
<Menubar.Separator class={menuSeparatorClass} />
|
|
@@ -1394,7 +1387,7 @@
|
|
|
1394
1387
|
onSelect={onOpenPublish}
|
|
1395
1388
|
disabled={counts.total === 0}
|
|
1396
1389
|
>
|
|
1397
|
-
Review &
|
|
1390
|
+
Review & Publish…
|
|
1398
1391
|
{#if counts.total > 0}
|
|
1399
1392
|
<span class="vela:ml-auto vela:flex vela:items-center vela:gap-2">
|
|
1400
1393
|
<span
|
|
@@ -1410,9 +1403,11 @@
|
|
|
1410
1403
|
<KbdShortcut keys="⌘P" class={menuShortcutClass} />
|
|
1411
1404
|
{/if}
|
|
1412
1405
|
</Menubar.Item>
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1406
|
+
|
|
1407
|
+
<Menubar.Item class={menuItemClass} onSelect={onShareLink} disabled={!previewUrl}
|
|
1408
|
+
>Share Preview Link…</Menubar.Item
|
|
1409
|
+
>
|
|
1410
|
+
|
|
1416
1411
|
<Menubar.Item
|
|
1417
1412
|
class={menuItemDestructiveClass}
|
|
1418
1413
|
onSelect={onDiscardAllChanges}
|