@preprio/toolkit 0.5.0-rc.2 → 0.5.0-rc.4
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 +209 -10
- package/dist/astro/index.cjs +1194 -1
- package/dist/astro/index.d.cts +47 -1
- package/dist/astro/index.d.ts +47 -1
- package/dist/astro/index.js +1197 -1
- package/dist/chunk-46ISDRHT.js +26 -0
- package/dist/chunk-74KY5EAM.js +1219 -0
- package/dist/chunk-AHBN6FQP.js +184 -0
- package/dist/{chunk-FLPNN3CV.js → chunk-PDNCNTDB.js} +1370 -623
- package/dist/chunk-S3VT4UWM.js +75 -0
- package/dist/chunk-TG6N3C22.js +246 -0
- package/dist/chunk-URXSDJOU.js +356 -0
- package/dist/index.cjs +3166 -392
- package/dist/index.d.cts +238 -4
- package/dist/index.d.ts +238 -4
- package/dist/index.js +189 -149
- package/dist/live-preview-H4643QYF.js +18 -0
- package/dist/nextjs/components.cjs +1522 -359
- package/dist/nextjs/components.d.cts +1 -1
- package/dist/nextjs/components.d.ts +1 -1
- package/dist/nextjs/components.js +4 -1
- package/dist/nextjs/index.cjs +1085 -6
- package/dist/nextjs/index.d.cts +73 -1
- package/dist/nextjs/index.d.ts +73 -1
- package/dist/nextjs/index.js +1079 -7
- package/dist/nextjs/live-action.cjs +285 -0
- package/dist/nextjs/live-action.d.cts +12 -0
- package/dist/nextjs/live-action.d.ts +12 -0
- package/dist/nextjs/live-action.js +68 -0
- package/dist/nextjs/live.cjs +1896 -0
- package/dist/nextjs/live.d.cts +52 -0
- package/dist/nextjs/live.d.ts +52 -0
- package/dist/nextjs/live.js +265 -0
- package/dist/nuxt/index.cjs +1 -1
- package/dist/nuxt/index.js +1 -1
- package/dist/nuxt/live-preview-O2ZTJNW4.js +1520 -0
- package/dist/nuxt/live.cjs +1720 -0
- package/dist/nuxt/live.d.cts +43 -0
- package/dist/nuxt/live.d.ts +43 -0
- package/dist/nuxt/live.js +94 -0
- package/dist/{pixel-B3oQNbZ3.d.cts → pixel-CX422dCO.d.cts} +10 -1
- package/dist/{pixel-B3oQNbZ3.d.ts → pixel-CX422dCO.d.ts} +10 -1
- package/dist/react/components.cjs +1522 -359
- package/dist/react/components.d.cts +9 -0
- package/dist/react/components.d.ts +9 -0
- package/dist/react/components.js +1522 -359
- package/dist/sveltekit/index.cjs +1 -1
- package/dist/sveltekit/index.js +1 -1
- package/package.json +12 -1
- package/src/astro/PreprToolbar.astro +7 -1
package/README.md
CHANGED
|
@@ -250,6 +250,157 @@ The loader rewrites `w_` to the width Next asks for and scales `h_` alongside it
|
|
|
250
250
|
|
|
251
251
|
See the runnable example at `examples/nextjs` for the full source of truth.
|
|
252
252
|
|
|
253
|
+
### Live preview (beta)
|
|
254
|
+
|
|
255
|
+
True live preview inside the Prepr editor iframe: keystrokes patch your query
|
|
256
|
+
results instantly by item `_id`; saves refetch the affected queries.
|
|
257
|
+
|
|
258
|
+
**Requirements.** Live preview runs only where the Prepr middleware handles
|
|
259
|
+
the request with `preview: true` (`createPreprMiddleware(request, { preview })`
|
|
260
|
+
in `proxy.ts`). Make sure your middleware `matcher` covers every page you
|
|
261
|
+
preview: on a page it skips, live preview does nothing and shows no error.
|
|
262
|
+
Pass your preview flag as `enabled` to every live preview entry point below;
|
|
263
|
+
each one does nothing without it.
|
|
264
|
+
|
|
265
|
+
Live updates and click-to-edit find content by id, never by position, so
|
|
266
|
+
they stay correct while an editor adds, removes or reorders blocks before
|
|
267
|
+
saving:
|
|
268
|
+
|
|
269
|
+
- Select `_id` (and ideally `__typename`) on every model and component level
|
|
270
|
+
that should live-update. Items without `_id` silently stay static.
|
|
271
|
+
- In dynamic content fields, select `_id` on every entry type (`Text`,
|
|
272
|
+
`Quote`, …). `Assets` entries have no `_id` of their own: select
|
|
273
|
+
`items { _id }` instead.
|
|
274
|
+
- A `Text` entry's content updates whether the query selects `body`,
|
|
275
|
+
`html` or `text` (the latter with tags stripped).
|
|
276
|
+
- Render each dynamic content entry inside an element marked with
|
|
277
|
+
`data-prepr-edit-target="<field>.<index>"` and
|
|
278
|
+
`data-prepr-entry-id="<entry _id>"` (for `Assets`, the first asset's
|
|
279
|
+
`_id`). The index is the fallback when no id matches.
|
|
280
|
+
- Values without a hidden edit link (images, videos, other URLs) are made
|
|
281
|
+
click-to-edit with `previewAttrs({ id, field }, { enabled: isPreview })`
|
|
282
|
+
on the element that renders them.
|
|
283
|
+
- Where the page renders a linked item whose content comes from items nested
|
|
284
|
+
inside it (for example a personalization or a shared section referenced
|
|
285
|
+
from a stack), wrap it in an element with
|
|
286
|
+
`data-prepr-reference="<linked item _id>"`. The editor only knows the items
|
|
287
|
+
the open item links to directly; content inside the wrapper is outlined as
|
|
288
|
+
that linked item, and clicking it opens the link in the editor.
|
|
289
|
+
|
|
290
|
+
Configure once in a small client component mounted in your root layout:
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
'use client';
|
|
294
|
+
import { useEffect } from 'react';
|
|
295
|
+
import { initLivePreview } from '@preprio/toolkit/nextjs/live';
|
|
296
|
+
|
|
297
|
+
export default function LivePreviewInit({ preview }: { preview: boolean }) {
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
initLivePreview({
|
|
300
|
+
// Your preview flag. Without it nothing starts.
|
|
301
|
+
enabled: preview,
|
|
302
|
+
// Runs a registered query against Prepr GraphQL. `query` is whatever
|
|
303
|
+
// you registered (string or DocumentNode) — you own the transport.
|
|
304
|
+
fetcher: async (query, variables) => {
|
|
305
|
+
/* POST to your Prepr GraphQL endpoint, return json.data */
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
}, [preview]);
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Register query results in client components with `useLive`:
|
|
314
|
+
|
|
315
|
+
```tsx
|
|
316
|
+
'use client';
|
|
317
|
+
import { useLive } from '@preprio/toolkit/nextjs/live';
|
|
318
|
+
|
|
319
|
+
export default function PageContent({ data, slug }) {
|
|
320
|
+
const liveData = useLive(data, {
|
|
321
|
+
query: GetPageBySlugDocument,
|
|
322
|
+
variables: { slug },
|
|
323
|
+
});
|
|
324
|
+
// render from liveData — it re-renders on editor keystrokes and saves
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
In `'use client'` files always import from `@preprio/toolkit/nextjs/live` —
|
|
329
|
+
the `/nextjs` barrel also exports server-only helpers (`next/headers`), which
|
|
330
|
+
a client module graph must not include.
|
|
331
|
+
|
|
332
|
+
Omitting `query`/`variables` degrades that registration to keystroke pushes
|
|
333
|
+
only. For server-component pages, mount `<PreprRefresh enabled={preview} />`
|
|
334
|
+
instead — it calls `router.refresh()` on every save. Don't combine
|
|
335
|
+
`PreprRefresh` with `useLive` registrations that pass a `query`: a save would
|
|
336
|
+
both refetch and refresh. `useLive` without a `query` works alongside it.
|
|
337
|
+
|
|
338
|
+
Without `enabled`, and outside the Prepr editor iframe, all of this is a
|
|
339
|
+
passthrough with zero overhead. The CMS editor must emit `prepr:fieldChange` (per keystroke) and
|
|
340
|
+
`prepr:itemChanged` (on save, with the root content item id) into the preview
|
|
341
|
+
iframe — `examples/editor-emulator` emulates this for local development.
|
|
342
|
+
Unsaved structural changes (adding/reordering components) appear after save;
|
|
343
|
+
typed text is live immediately.
|
|
344
|
+
|
|
345
|
+
#### Unsaved changes in server components: `normalizeQuery` (experimental, Next.js 16+)
|
|
346
|
+
|
|
347
|
+
Server components show the editor's unsaved changes when you pass their query
|
|
348
|
+
results through `normalizeQuery`, with whatever client you fetch with:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { normalizeQuery } from '@preprio/toolkit/nextjs';
|
|
352
|
+
|
|
353
|
+
const preview = process.env.VERCEL_ENV !== 'production';
|
|
354
|
+
|
|
355
|
+
const data = await fetchFromPrepr(GetPageDocument, { slug }); // any client
|
|
356
|
+
const page = await normalizeQuery(GetPageDocument, data, { enabled: preview });
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Mount `<PreprRefresh enabled={preview} />` in the root layout. While an
|
|
360
|
+
editor types, it sends the unsaved changes to the server and the route
|
|
361
|
+
re-renders with them; on save it refreshes with the stored content.
|
|
362
|
+
|
|
363
|
+
- The query (source text, a `DocumentNode`, or a typed document string) tells
|
|
364
|
+
the toolkit which response key holds which field. Changes land under
|
|
365
|
+
aliases, inside single components, on dynamic content entries, and on every
|
|
366
|
+
place the same item appears. Fields the query did not select are left
|
|
367
|
+
alone. Select `_id` on every item and component that should update.
|
|
368
|
+
- Changed text keeps its hidden edit link, so it stays click-to-edit.
|
|
369
|
+
- `useLive` data keeps the typed values across these re-renders.
|
|
370
|
+
|
|
371
|
+
**Production safety.** Pass `enabled` from the same preview flag you give the
|
|
372
|
+
middleware, and never `true` on a production deployment:
|
|
373
|
+
|
|
374
|
+
- With `enabled: false` (the default) the result is returned untouched and
|
|
375
|
+
the request is never read, so static and ISR pages stay static.
|
|
376
|
+
`PreprRefresh`, `initLivePreview` and `useLive` don't listen to the editor
|
|
377
|
+
at all.
|
|
378
|
+
- Next.js registers the toolkit's server action in every build that imports
|
|
379
|
+
`@preprio/toolkit/nextjs/live`, so anyone can call it, browsers and scripts
|
|
380
|
+
alike. It does nothing (no parsing, no storage, no re-render) unless the
|
|
381
|
+
Prepr middleware handled the request with `preview: true`. The middleware
|
|
382
|
+
drops `Prepr-*` headers a client sends, so this can't be forged on a page
|
|
383
|
+
the middleware runs for. On a page its `matcher` skips, a forged header can
|
|
384
|
+
at most trigger an extra render, never change content, because
|
|
385
|
+
`normalizeQuery` there still needs `enabled`.
|
|
386
|
+
- Changes only reach the re-render of the editor's own server action call.
|
|
387
|
+
The action stores them in the server process under a random, single-use
|
|
388
|
+
token and sets only that token as a cookie that expires immediately. Any
|
|
389
|
+
other request carrying the cookie, such as a plain or cacheable GET that
|
|
390
|
+
replays a token, sees nothing.
|
|
391
|
+
- Changes are only accepted from the Prepr editor (`https://*.prepr.io`, or
|
|
392
|
+
your `allowedEditorOrigins` via `initLivePreview`), never from another page
|
|
393
|
+
that frames your site. They are validated as hostile input: link fields
|
|
394
|
+
never take `javascript:`, `vbscript:` or `data:` URLs. HTML text is passed
|
|
395
|
+
on as typed, like rich text from the API.
|
|
396
|
+
|
|
397
|
+
On Next.js 15 and older the action does nothing and pages update on save.
|
|
398
|
+
|
|
399
|
+
For Astro and Nuxt, see their sections. The framework-free core exports,
|
|
400
|
+
`normalizeQuery(query, result, { log })`, `startHtmlLivePreview()` and
|
|
401
|
+
`readLiveLogFromRequest(request)` from `@preprio/toolkit`, cover any other
|
|
402
|
+
server-rendered stack.
|
|
403
|
+
|
|
253
404
|
## Astro
|
|
254
405
|
|
|
255
406
|
**`src/middleware.ts`**
|
|
@@ -301,6 +452,34 @@ The `.astro` components ship as source, compiled by your own Astro/Vite pipeline
|
|
|
301
452
|
|
|
302
453
|
See the runnable example at `examples/astro` for the full source of truth.
|
|
303
454
|
|
|
455
|
+
### Live preview (experimental)
|
|
456
|
+
|
|
457
|
+
Show the editor's unsaved changes while they type. Pass `preview` to
|
|
458
|
+
`onPreprRequest` as above, and run each query result through
|
|
459
|
+
`normalizeQuery` with your preview flag:
|
|
460
|
+
|
|
461
|
+
```astro
|
|
462
|
+
---
|
|
463
|
+
import { normalizeQuery } from '@preprio/toolkit/astro';
|
|
464
|
+
|
|
465
|
+
const data = await fetchFromPrepr(GetPageDocument, { slug }); // any client
|
|
466
|
+
const page = normalizeQuery(GetPageDocument, data, {
|
|
467
|
+
enabled: preview,
|
|
468
|
+
locals: Astro.locals,
|
|
469
|
+
});
|
|
470
|
+
---
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
`PreprToolbar.astro` does the rest. Inside the Prepr editor it requests the
|
|
474
|
+
page again with the unsaved changes and updates only the parts of the DOM that
|
|
475
|
+
changed; on save it shows the stored content. Preview must render on demand
|
|
476
|
+
(`output: 'server'`).
|
|
477
|
+
|
|
478
|
+
Safety: the middleware only accepts changes in preview, from a same-origin
|
|
479
|
+
`POST` that carries the toolkit's header and a JSON body, and marks that
|
|
480
|
+
response `Cache-Control: private, no-store`. With `enabled: false` the result
|
|
481
|
+
is returned untouched. Changes are accepted only from the Prepr editor.
|
|
482
|
+
|
|
304
483
|
## SvelteKit
|
|
305
484
|
|
|
306
485
|
**`src/hooks.server.ts`**
|
|
@@ -412,6 +591,25 @@ The `.vue` components ship as source (compiled by your own Vite/Vue pipeline) an
|
|
|
412
591
|
|
|
413
592
|
See the runnable example at `examples/nuxt` for the full source of truth.
|
|
414
593
|
|
|
594
|
+
### Live preview (experimental)
|
|
595
|
+
|
|
596
|
+
Show the editor's unsaved changes while they type, with no server round trip:
|
|
597
|
+
|
|
598
|
+
```ts
|
|
599
|
+
import { useNormalizedQuery } from '@preprio/toolkit/nuxt/live';
|
|
600
|
+
|
|
601
|
+
const { data, refresh } = await useAsyncData('page', () => fetchPage(slug));
|
|
602
|
+
const page = useNormalizedQuery(GetPageDocument, data, {
|
|
603
|
+
enabled: preview, // your preview flag; nothing starts without it
|
|
604
|
+
onSave: refresh,
|
|
605
|
+
});
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
Render from `page`. Nuxt hydrates the whole page, so every component that
|
|
609
|
+
shows a changed field updates. Without `enabled`, on the server and outside
|
|
610
|
+
the Prepr editor's preview iframe it returns the data unchanged. Changes are accepted only from
|
|
611
|
+
the Prepr editor.
|
|
612
|
+
|
|
415
613
|
## React (no framework)
|
|
416
614
|
|
|
417
615
|
`@preprio/toolkit/react` carries the two components that need nothing but React — for Vite + React Router, TanStack Router, Remix SPA mode, or Create React App. Next.js users want the [Next.js section](#nextjs) instead; its `PreprToolbar` binds the App Router for you.
|
|
@@ -636,7 +834,7 @@ controller.destroy();
|
|
|
636
834
|
| Option | Type | Description |
|
|
637
835
|
| ------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
638
836
|
| `props` | `PreprToolbarProps` | From `getToolbarProps`. Optional — a headless preview has no segment list to pass. |
|
|
639
|
-
| `options` | `PreprPreviewOptions` | `{ debug?, locale?, features?, ui?, allowedEditorOrigins?, autoClean? }`. See [Preview options](#preview-options).
|
|
837
|
+
| `options` | `PreprPreviewOptions` | `{ debug?, locale?, features?, ui?, allowedEditorOrigins?, strictMode?, autoClean? }`. See [Preview options](#preview-options). |
|
|
640
838
|
| `navigation` | `PreprNavigationAdapter` | How segment/variant switches navigate. Optional — omitted, the toolbar uses `window.location.assign` (a full page load). `reload` is optional too, runs after a preview-mode toggle, and defaults to `window.location.reload()`. The Next.js and SvelteKit wrappers wire all of this up for you. |
|
|
641
839
|
|
|
642
840
|
`createPreprPreview` is a no-op outside a browser (no `window`/`document`) and returns a controller whose `destroy()` does nothing, so it is safe to call during SSR. Call it **once per page** — two calls start two bridges and announce the preview twice.
|
|
@@ -694,14 +892,15 @@ createPreprMiddleware(request, response, { preview });
|
|
|
694
892
|
createPreprPreview({ props, options: { debug: true, locale: 'nl' } });
|
|
695
893
|
```
|
|
696
894
|
|
|
697
|
-
| Option | Type
|
|
698
|
-
| ---------------------- |
|
|
699
|
-
| `debug` | `boolean`
|
|
700
|
-
| `locale` | `'en' \| 'nl'`
|
|
701
|
-
| `features` | `PreprFeatures`
|
|
702
|
-
| `ui` | `boolean`
|
|
703
|
-
| `allowedEditorOrigins` | `string[]`
|
|
704
|
-
| `
|
|
895
|
+
| Option | Type | Default | Description |
|
|
896
|
+
| ---------------------- | ------------------------------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
897
|
+
| `debug` | `boolean` | `false` | Enable debug logging. |
|
|
898
|
+
| `locale` | `'en' \| 'nl' \| 'fr' \| 'de'` | auto-detected | UI language. Falls back to the first supported browser language, then `en`. Inside the Prepr editor, the editor's interface language wins. |
|
|
899
|
+
| `features` | `PreprFeatures` | all enabled | Which features run. See [Feature flags](#feature-flags). |
|
|
900
|
+
| `ui` | `boolean` | `true` | Mount the visible toolbar. See [Headless preview](#headless-preview-no-toolbar-ui). |
|
|
901
|
+
| `allowedEditorOrigins` | `string[]` | `*.prepr.io` | Exact editor origins allowed to drive this preview, for self-hosted or local editors. Replaces the wildcard. `['*']` accepts any origin — development only. |
|
|
902
|
+
| `strictMode` | `boolean` | `false` | Your preview token uses GraphQL strict mode (required fields never null). The editor then holds back new entries until their required fields are filled and never sends a cleared required field as null. Also on `PreprRefresh`, `initLivePreview` and `useNormalizedQuery`. |
|
|
903
|
+
| `autoClean` | `boolean` | `true` | Strip the invisible stega characters from visible text. See [Stega auto-cleaning](#stega-auto-cleaning). |
|
|
705
904
|
|
|
706
905
|
`features` and `ui` are independent: `features` decides _what runs_, `ui` decides _whether the toolbar is visible_.
|
|
707
906
|
|
|
@@ -809,7 +1008,7 @@ Because the toolbar renders inside a shadow root, none of your own page CSS —
|
|
|
809
1008
|
|
|
810
1009
|
### Internationalization
|
|
811
1010
|
|
|
812
|
-
The toolbar UI ships with English and
|
|
1011
|
+
The toolbar UI ships with English, Dutch, French and German. Pass `locale` explicitly, or omit it to auto-detect from the browser and fall back to `en`. Inside the Prepr editor's preview, the visual editor's labels follow the language the editor user has set for the CMS, falling back to `locale` when the editor sends a language the toolkit does not support:
|
|
813
1012
|
|
|
814
1013
|
```typescript
|
|
815
1014
|
createPreprPreview({ props, options: { locale: 'nl' } });
|