@preprio/toolkit 0.4.3 → 0.5.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +416 -23
  2. package/dist/astro/index.cjs +1290 -1
  3. package/dist/astro/index.d.cts +47 -1
  4. package/dist/astro/index.d.ts +47 -1
  5. package/dist/astro/index.js +1297 -1
  6. package/dist/chunk-F46GJS6Y.js +26 -0
  7. package/dist/{chunk-H7VA7NGY.js → chunk-IDEC77LZ.js} +1480 -621
  8. package/dist/chunk-IPMB32AZ.js +246 -0
  9. package/dist/chunk-KD3GK6TO.js +1221 -0
  10. package/dist/chunk-S3VT4UWM.js +75 -0
  11. package/dist/chunk-TBBU3LDD.js +184 -0
  12. package/dist/chunk-URXSDJOU.js +356 -0
  13. package/dist/codegen/index.cjs +82 -0
  14. package/dist/codegen/index.d.cts +70 -0
  15. package/dist/codegen/index.d.ts +70 -0
  16. package/dist/codegen/index.js +64 -0
  17. package/dist/index.cjs +3412 -409
  18. package/dist/index.d.cts +287 -5
  19. package/dist/index.d.ts +287 -5
  20. package/dist/index.js +307 -144
  21. package/dist/live-preview-DLXGXRE5.js +18 -0
  22. package/dist/nextjs/components.cjs +1662 -388
  23. package/dist/nextjs/components.d.cts +1 -1
  24. package/dist/nextjs/components.d.ts +1 -1
  25. package/dist/nextjs/components.js +4 -1
  26. package/dist/nextjs/index.cjs +1181 -6
  27. package/dist/nextjs/index.d.cts +83 -3
  28. package/dist/nextjs/index.d.ts +83 -3
  29. package/dist/nextjs/index.js +1179 -7
  30. package/dist/nextjs/live-action.cjs +285 -0
  31. package/dist/nextjs/live-action.d.cts +12 -0
  32. package/dist/nextjs/live-action.d.ts +12 -0
  33. package/dist/nextjs/live-action.js +68 -0
  34. package/dist/nextjs/live.cjs +1899 -0
  35. package/dist/nextjs/live.d.cts +54 -0
  36. package/dist/nextjs/live.d.ts +54 -0
  37. package/dist/nextjs/live.js +268 -0
  38. package/dist/nuxt/chunk-BWPGO25E.js +340 -0
  39. package/dist/nuxt/index.cjs +1 -1
  40. package/dist/nuxt/index.js +1 -1
  41. package/dist/nuxt/live-preview-2RCU35W5.js +1189 -0
  42. package/dist/nuxt/live.cjs +1816 -0
  43. package/dist/nuxt/live.d.cts +43 -0
  44. package/dist/nuxt/live.d.ts +43 -0
  45. package/dist/nuxt/live.js +198 -0
  46. package/dist/{pixel-e1fUWl2Y.d.cts → pixel-CX422dCO.d.cts} +18 -3
  47. package/dist/{pixel-e1fUWl2Y.d.ts → pixel-CX422dCO.d.ts} +18 -3
  48. package/dist/react/components.cjs +1662 -388
  49. package/dist/react/components.d.cts +17 -2
  50. package/dist/react/components.d.ts +17 -2
  51. package/dist/react/components.js +1662 -388
  52. package/dist/sveltekit/chunk-BWPGO25E.js +340 -0
  53. package/dist/sveltekit/index.cjs +1 -1
  54. package/dist/sveltekit/index.js +1 -1
  55. package/dist/sveltekit/live-preview-2RCU35W5.js +1189 -0
  56. package/dist/sveltekit/live.cjs +1830 -0
  57. package/dist/sveltekit/live.d.cts +59 -0
  58. package/dist/sveltekit/live.d.ts +59 -0
  59. package/dist/sveltekit/live.js +204 -0
  60. package/package.json +37 -11
  61. package/src/astro/PreprToolbar.astro +7 -1
package/README.md CHANGED
@@ -17,8 +17,9 @@ Every integration is the same three steps:
17
17
  1. **Middleware** — resolves the visitor's customer ID, segment, and A/B variant into request headers.
18
18
  2. **Layout** — mounts the preview toolbar and the tracking pixel.
19
19
  3. **Data fetching** — forwards the Prepr headers on your GraphQL requests, so Prepr returns personalized content.
20
+ 4. **Live preview** (optional) — shows the editor's unsaved changes in Prepr's side-by-side preview while they type. See [Live preview](#live-preview).
20
21
 
21
- Jump to your framework: [Next.js](#nextjs) · [Astro](#astro) · [SvelteKit](#sveltekit) · [Nuxt](#nuxt) · [React without a framework](#react-no-framework) · [anything else](#any-other-framework).
22
+ Jump to your framework: [Next.js](#nextjs) · [Astro](#astro) · [SvelteKit](#sveltekit) · [Nuxt](#nuxt) · [React without a framework](#react-no-framework) · [anything else](#any-other-framework) · [Live preview](#live-preview).
22
23
 
23
24
  Coming from `@preprio/prepr-nextjs`? See the [migration guide](https://github.com/preprio/prepr-toolkit/blob/main/MIGRATION.md).
24
25
 
@@ -122,9 +123,13 @@ Per-framework peer dependencies — all optional; installing without them is fin
122
123
  | `@preprio/toolkit` (vanilla core) | none |
123
124
  | `@preprio/toolkit/react` | `react` >= 17, `react-dom` >= 17 |
124
125
  | `@preprio/toolkit/nextjs` | `next` >= 13, `react` >= 17, `react-dom` >= 17 |
126
+ | `@preprio/toolkit/nextjs/live` | `next` >= 13, `react` >= 18 (live preview) |
125
127
  | `@preprio/toolkit/astro` | none (`.astro` components compile in your own pipeline) |
126
128
  | `@preprio/toolkit/sveltekit` | `svelte` (only for the `.svelte` components) |
129
+ | `@preprio/toolkit/sveltekit/live` | none (returns a plain Svelte store) |
130
+ | `@preprio/toolkit/codegen` | `graphql` >= 16 (build time, from your Codegen setup) |
127
131
  | `@preprio/toolkit/nuxt` | `vue` (only for the `.vue` components) |
132
+ | `@preprio/toolkit/nuxt/live` | `vue` >= 3.3 |
128
133
 
129
134
  ## The preview gate
130
135
 
@@ -250,6 +255,84 @@ The loader rewrites `w_` to the width Next asks for and scales `h_` alongside it
250
255
 
251
256
  See the runnable example at `examples/nextjs` for the full source of truth.
252
257
 
258
+ ### Live preview in Next.js
259
+
260
+ Two additions turn on [live preview](#live-preview) for an App Router site.
261
+ Both take your preview flag, the same one you pass the middleware.
262
+
263
+ **1. Run each query result through `normalizeQuery`**, with whatever client
264
+ you fetch with:
265
+
266
+ ```ts
267
+ import { normalizeQuery } from '@preprio/toolkit/nextjs';
268
+
269
+ const preview = process.env.VERCEL_ENV !== 'production';
270
+
271
+ const data = await fetchFromPrepr(GetPageDocument, { slug }); // any client
272
+ const page = await normalizeQuery(GetPageDocument, data, { enabled: preview });
273
+ ```
274
+
275
+ **2. Mount `PreprRefresh` once in the root layout:**
276
+
277
+ ```tsx
278
+ import { PreprRefresh } from '@preprio/toolkit/nextjs/live';
279
+
280
+ {
281
+ preview && <PreprRefresh enabled />;
282
+ }
283
+ ```
284
+
285
+ While an editor types, `PreprRefresh` sends the unsaved changes to a server
286
+ action and the route re-renders with them; on save it calls
287
+ `router.refresh()` for the stored content. Server components need nothing
288
+ else. Requires Next.js 16 or later; on Next.js 15 and older pages update on
289
+ save only.
290
+
291
+ **Client components** can skip the server round trip: wrap the data they
292
+ render in `useLive`, and keystrokes patch it in the browser. Import it from
293
+ `@preprio/toolkit/nextjs/live` — the `/nextjs` barrel also exports
294
+ server-only helpers (`next/headers`), which a client module graph must not
295
+ include.
296
+
297
+ ```tsx
298
+ 'use client';
299
+ import { useLive } from '@preprio/toolkit/nextjs/live';
300
+
301
+ export default function Hero({ hero }: { hero: HeroFragment }) {
302
+ const live = useLive(hero);
303
+ return <h1>{live.title}</h1>;
304
+ }
305
+ ```
306
+
307
+ **Client-fetched data** (no server components): instead of `PreprRefresh`,
308
+ call `initLivePreview({ enabled: preview, fetcher })` once in a client
309
+ component, and pass `{ query, variables }` to `useLive`. On save the toolkit
310
+ runs the query again through your `fetcher`:
311
+
312
+ ```tsx
313
+ 'use client';
314
+ import { useEffect } from 'react';
315
+ import { initLivePreview, useLive } from '@preprio/toolkit/nextjs/live';
316
+
317
+ export function LivePreviewInit({ preview }: { preview: boolean }) {
318
+ useEffect(() => {
319
+ initLivePreview({
320
+ enabled: preview,
321
+ // Runs a registered query against Prepr GraphQL; return `json.data`.
322
+ fetcher: (query, variables) => fetchFromPrepr(query, variables),
323
+ });
324
+ }, [preview]);
325
+ return null;
326
+ }
327
+
328
+ // In a client component:
329
+ const liveData = useLive(data, { query: GetPageDocument, variables: { slug } });
330
+ ```
331
+
332
+ Don't combine `PreprRefresh` with `useLive` registrations that pass a
333
+ `query`: a save would both refetch and refresh. `useLive` without a `query`
334
+ works alongside it.
335
+
253
336
  ## Astro
254
337
 
255
338
  **`src/middleware.ts`**
@@ -301,6 +384,29 @@ The `.astro` components ship as source, compiled by your own Astro/Vite pipeline
301
384
 
302
385
  See the runnable example at `examples/astro` for the full source of truth.
303
386
 
387
+ ### Live preview in Astro
388
+
389
+ To turn on [live preview](#live-preview), pass `preview` to `onPreprRequest`
390
+ as above, and run each query result through `normalizeQuery` with the same
391
+ flag:
392
+
393
+ ```astro
394
+ ---
395
+ import { normalizeQuery } from '@preprio/toolkit/astro';
396
+
397
+ const data = await fetchFromPrepr(GetPageDocument, { slug }); // any client
398
+ const page = normalizeQuery(GetPageDocument, data, {
399
+ enabled: preview,
400
+ locals: Astro.locals,
401
+ });
402
+ ---
403
+ ```
404
+
405
+ `PreprToolbar.astro` does the rest. Inside the Prepr editor it requests the
406
+ page again with the unsaved changes and updates only the parts of the DOM that
407
+ changed; on save it shows the stored content. Preview pages must render on
408
+ demand (`output: 'server'`, or `export const prerender = false`).
409
+
304
410
  ## SvelteKit
305
411
 
306
412
  **`src/hooks.server.ts`**
@@ -361,6 +467,35 @@ The `.svelte` components ship as source (compiled by your own Vite/Svelte pipeli
361
467
 
362
468
  See the runnable example at `examples/sveltekit` for the full source of truth.
363
469
 
470
+ ### Live preview in SvelteKit
471
+
472
+ To turn on [live preview](#live-preview), return your preview flag from
473
+ `load`, then run the query result through `normalizedQuery`. It applies the
474
+ editor's changes in the browser, with no server round trip:
475
+
476
+ ```svelte
477
+ <script lang="ts">
478
+ import { invalidateAll } from '$app/navigation';
479
+ import { normalizedQuery } from '@preprio/toolkit/sveltekit/live';
480
+
481
+ let { data } = $props();
482
+
483
+ const result = $derived(
484
+ normalizedQuery(GetPageDocument, data.result, {
485
+ enabled: data.preview,
486
+ onSave: invalidateAll,
487
+ }),
488
+ );
489
+ </script>
490
+
491
+ <h1>{$result.Page.title}</h1>
492
+ ```
493
+
494
+ `normalizedQuery` returns a Svelte store: render from `$result`. Pass the
495
+ result in the shape the query returns (`{ Page: … }`, not just the page), so
496
+ the toolkit can tell which response key holds which field. On save,
497
+ `invalidateAll` reruns your `load` functions for the stored content.
498
+
364
499
  ## Nuxt
365
500
 
366
501
  Requires Nitro's Node.js preset (the Nuxt default).
@@ -412,6 +547,26 @@ The `.vue` components ship as source (compiled by your own Vite/Vue pipeline) an
412
547
 
413
548
  See the runnable example at `examples/nuxt` for the full source of truth.
414
549
 
550
+ ### Live preview in Nuxt
551
+
552
+ To turn on [live preview](#live-preview), run the `useAsyncData` result
553
+ through `useNormalizedQuery` with your preview flag. It applies the editor's
554
+ changes in the browser, with no server round trip:
555
+
556
+ ```ts
557
+ import { useNormalizedQuery } from '@preprio/toolkit/nuxt/live';
558
+
559
+ const { data, refresh } = await useAsyncData('page', () => fetchPage(slug));
560
+ const page = useNormalizedQuery(GetPageDocument, data, {
561
+ enabled: preview,
562
+ onSave: refresh,
563
+ });
564
+ ```
565
+
566
+ Render from `page`. Nuxt hydrates the whole page, so every component that
567
+ shows a changed field updates. On save, `refresh` refetches the stored
568
+ content.
569
+
415
570
  ## React (no framework)
416
571
 
417
572
  `@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.
@@ -467,23 +622,22 @@ const location = useLocation();
467
622
 
468
623
  ### Tracking pageviews
469
624
 
470
- `loadTrackingPixel` queues one `pageload` event when it installs. Client-side route changes emit nothing on their own, so fire them yourself:
625
+ The pixel tracks pageviews by itself, client-side route changes included. It counts a view for the Prepr content item named by a `prepr:id` meta tag in `<head>`, and watches that tag: when a route change points it at another item, the pixel records a new view. Render the tag on every page that shows a Prepr item:
471
626
 
472
627
  ```tsx
473
- import { useEffect } from 'react';
474
- import { useLocation } from 'react-router';
475
- import { trackEvent } from '@preprio/toolkit';
476
-
477
- function usePreprPageviews() {
478
- const { pathname } = useLocation();
479
-
480
- useEffect(() => {
481
- trackEvent('pageload');
482
- }, [pathname]);
628
+ function Page({ page }: { page: { _id: string } }) {
629
+ return (
630
+ <>
631
+ <meta property="prepr:id" content={page._id} />
632
+ {/* … */}
633
+ </>
634
+ );
483
635
  }
484
636
  ```
485
637
 
486
- This double-counts the first pageview, since the pixel's own install event fires too. Skip the initial run with a ref if the exact count matters.
638
+ React 19 hoists a `<meta>` rendered anywhere in the tree into `<head>`. On React 18, set it with your head manager (`react-helmet-async` or similar) instead.
639
+
640
+ A page without the tag records no view. Calling `trackEvent('pageload')` yourself does not help: the pixel takes one `pageload` per page load and ignores the rest. Use `trackEvent` for your own events, such as `trackEvent('add_to_cart', { productId })`.
487
641
 
488
642
  The `id` is your access token, and in a client-rendered app it is public — bundled into JS and visible in devtools. Use a tracking-only token; do not derive it from a browser-exposed GraphQL URL, which would also expose content read access.
489
643
 
@@ -533,6 +687,236 @@ const controller = createPreprPreview({ props: toolbarProps });
533
687
 
534
688
  Serialize the server-computed props into the page — a `<script type="application/json">` tag works well — and read them back on the client. See the runnable example at `examples/express` for the full source of truth.
535
689
 
690
+ ## Live preview
691
+
692
+ Inside Prepr's side-by-side preview, live preview shows what an editor types
693
+ before they save: text, links and other fields update on the page as they
694
+ type, and a save shows the stored content. Outside the editor, and when it is
695
+ not enabled, it does nothing and costs nothing: the runtime loads only inside
696
+ the editor iframe.
697
+
698
+ | Framework | Entry point | Applies changes | Setup |
699
+ | --------- | --------------------------------- | ------------------------------------- | --------------------------------------- |
700
+ | Next.js | `@preprio/toolkit/nextjs/live` | On the server (`normalizeQuery`) | [Next.js](#live-preview-in-nextjs) |
701
+ | Astro | `@preprio/toolkit/astro` | On the server (`normalizeQuery`) | [Astro](#live-preview-in-astro) |
702
+ | SvelteKit | `@preprio/toolkit/sveltekit/live` | In the browser (`normalizedQuery`) | [SvelteKit](#live-preview-in-sveltekit) |
703
+ | Nuxt | `@preprio/toolkit/nuxt/live` | In the browser (`useNormalizedQuery`) | [Nuxt](#live-preview-in-nuxt) |
704
+
705
+ Other server-rendered stacks can use the framework-free core exports from
706
+ `@preprio/toolkit`: `readLiveLogFromRequest(request)` in the server handler,
707
+ `normalizeQuery(query, result, { log })` on each result, and
708
+ `startHtmlLivePreview()` in the browser. The Astro wrapper is built from
709
+ exactly these.
710
+
711
+ ### Before you start
712
+
713
+ 1. **Set up the toolbar for your framework first.** Live preview runs only
714
+ where the Prepr middleware handles the request with `preview: true`. Make
715
+ sure the middleware covers every page you preview: on a page it skips,
716
+ live preview does nothing and shows no error.
717
+ 2. **Pass your preview flag as `enabled`** to every live preview entry point.
718
+ Each one does nothing without it. Never pass `true` on a production
719
+ deployment.
720
+ 3. **Point Prepr at your preview deployment.** In Prepr, set the preview URL
721
+ to a deployment that uses the **GraphQL Preview** access token, with
722
+ **Enable edit mode** checked.
723
+ 4. **Allow Prepr to frame your site.** If your server sends a
724
+ `Content-Security-Policy`, add your Prepr environments to
725
+ `frame-ancestors`, for example
726
+ `frame-ancestors https://acme.prepr.io https://dev-acme.prepr.io`.
727
+
728
+ ### Preparing your queries
729
+
730
+ Live updates and click-to-edit find content by id, never by position, so
731
+ they stay correct while an editor adds, removes or reorders blocks before
732
+ saving. Every model, component and dynamic content entry that should
733
+ live-update needs `_id` (and ideally `__typename`) in the query result; items
734
+ without `_id` stay static.
735
+
736
+ There are three ways to get them there. Pick the first that fits your setup.
737
+
738
+ #### Option 1: GraphQL Codegen (recommended)
739
+
740
+ Add the `preprIds` document transform to your Codegen output. `_id` is
741
+ added wherever your schema has one: on models and components, and inside
742
+ each member of a dynamic content field. Collections like `Pages` and unions
743
+ themselves never get an `_id`, so the queries stay valid. Your `.graphql`
744
+ files stay as they are; the generated documents and types include the field,
745
+ at no runtime cost. Adding a field never breaks code that compiles today, so
746
+ this is safe to turn on in an existing project.
747
+
748
+ ```ts
749
+ // codegen.ts
750
+ import type { CodegenConfig } from '@graphql-codegen/cli';
751
+ import { preprIds } from '@preprio/toolkit/codegen';
752
+
753
+ const config: CodegenConfig = {
754
+ schema: process.env.PREPR_GRAPHQL_URL,
755
+ documents: ['src/queries/**/*.graphql'],
756
+ generates: {
757
+ 'src/gql/': {
758
+ preset: 'client',
759
+ documentTransforms: [preprIds],
760
+ },
761
+ },
762
+ };
763
+
764
+ export default config;
765
+ ```
766
+
767
+ Run Codegen again after adding it. Needs `graphql` 16 or later, which a
768
+ Codegen setup already has.
769
+
770
+ To add `__typename` everywhere too, use `createPreprIds({ typename: true })`
771
+ instead. It is off by default because a selected `__typename` changes the
772
+ generated types: each member of a union is typed on its own, so code that
773
+ reads a union's fields without checking `__typename` first stops compiling.
774
+ Live preview only needs it on dynamic content entries, where you select it
775
+ already to render them.
776
+
777
+ #### Option 2: at runtime, without Codegen
778
+
779
+ `addPreprIds(document, schema)` does the same to a parsed query. It needs
780
+ your schema, which Prepr serves through introspection on the same GraphQL
781
+ URL. Load it once on the server, and transform each query once:
782
+
783
+ ```ts
784
+ import { addPreprIds } from '@preprio/toolkit/codegen';
785
+ import {
786
+ buildClientSchema,
787
+ getIntrospectionQuery,
788
+ parse,
789
+ print,
790
+ type GraphQLSchema,
791
+ } from 'graphql';
792
+
793
+ const url = process.env.PREPR_GRAPHQL_URL!;
794
+
795
+ let schema: Promise<GraphQLSchema> | undefined;
796
+ function preprSchema(): Promise<GraphQLSchema> {
797
+ schema ??= fetch(url, {
798
+ method: 'POST',
799
+ headers: { 'Content-Type': 'application/json' },
800
+ body: JSON.stringify({ query: getIntrospectionQuery() }),
801
+ })
802
+ .then((response) => response.json())
803
+ .then(({ data }) => buildClientSchema(data));
804
+ return schema;
805
+ }
806
+
807
+ const prepared = new Map<string, string>();
808
+ export async function withPreprIds(query: string): Promise<string> {
809
+ let result = prepared.get(query);
810
+ if (!result) {
811
+ result = print(addPreprIds(parse(query), await preprSchema()));
812
+ prepared.set(query, result);
813
+ }
814
+ return result;
815
+ }
816
+ ```
817
+
818
+ Then send `await withPreprIds(query)` instead of `query` in your fetch
819
+ helper. With a client that takes a `DocumentNode`, pass
820
+ `addPreprIds(document, schema)` instead. Keep this on the server: the
821
+ GraphQL URL carries your access token.
822
+
823
+ #### Option 3: by hand
824
+
825
+ Select `_id` (and `__typename`) on every model and component level, and on
826
+ every entry type in dynamic content fields (`Text`, `Quote`, …). `Assets`
827
+ entries have no `_id` of their own: select `items { _id }` instead.
828
+
829
+ The rest is about rendering:
830
+
831
+ - A `Text` entry's content updates whether the query selects `body`,
832
+ `html` or `text` (the latter with tags stripped).
833
+ - **Dynamic content entries are found automatically.** In preview,
834
+ `normalizeQuery`, `normalizedQuery` and `useNormalizedQuery` add each
835
+ entry's `_id` (and, for `Text` entries, the field path) to the hidden edit
836
+ link of every text inside it. Click-to-edit and the editor's "jump to
837
+ field" then find the entry by id, also after an editor reorders entries.
838
+ Render a `Text` entry's HTML straight into one wrapper element, so its
839
+ edit link lands on that wrapper and the whole entry is outlined.
840
+ - Prepr images are click-to-edit inside the editor without any markup: the
841
+ toolkit matches image URLs (also through image optimizers like
842
+ `/_next/image`) to the open item's assets.
843
+ - **Set the attributes yourself only where that does not reach:**
844
+ - Data you fetch in the browser without those helpers: run it through
845
+ `annotateEditLinks(result, query)` from `@preprio/toolkit`, or mark the
846
+ element as below.
847
+ - An entry with no text and no Prepr image (an embed, a video URL): wrap
848
+ it in an element with `data-prepr-edit-target="<field>.<index>"` and
849
+ `data-prepr-entry-id="<entry _id>"` (for `Assets`, the first asset's
850
+ `_id`). Attributes you set always win over the automatic ones.
851
+ - Other values without a hidden edit link (video and other URLs): make
852
+ them click-to-edit with
853
+ `previewAttrs({ id, field }, { enabled: preview })` on the element that
854
+ renders them. Encoded text inside that element does not override it.
855
+ - Fields Prepr returns without an edit link, such as remote content, next
856
+ to a text field that has one: `stegaTarget(text, { field })` reads the
857
+ item and path from the text and points at the sibling field, ready for
858
+ `previewAttrs`. Use `products.items.2` to point at one entry.
859
+
860
+ ```tsx
861
+ // Each product card opens its row of the component's `products` field.
862
+ <article
863
+ {...previewAttrs(
864
+ stegaTarget(block.title, { field: `products.items.${index}` }),
865
+ { enabled: preview },
866
+ )}
867
+ >
868
+ ```
869
+ - Where the page renders a linked item whose content comes from items nested
870
+ inside it (for example a personalization or a shared section referenced
871
+ from a stack), wrap it in an element with
872
+ `data-prepr-reference="<linked item _id>"`. The editor only knows the items
873
+ the open item links to directly; content inside the wrapper is outlined as
874
+ that linked item, and clicking it opens the link in the editor.
875
+
876
+ Typed text and changed fields show immediately. Structural changes, such as
877
+ adding or reordering components, appear after save.
878
+
879
+ ### Options
880
+
881
+ `PreprRefresh`, `initLivePreview`, `normalizedQuery` and `useNormalizedQuery`
882
+ take the same two options as the toolbar (in Astro, set them on the
883
+ toolbar's `options`):
884
+
885
+ - **`allowedEditorOrigins`** — exact editor origins, replacing the
886
+ `https://*.prepr.io` default. Only needed when Prepr runs on your own domain.
887
+ - **`strictMode`** — set it when your preview token uses GraphQL strict mode
888
+ (required fields never null). The editor then holds back new entries until
889
+ their required fields are filled, and never sends a cleared required field
890
+ as null.
891
+
892
+ ### Security
893
+
894
+ - **Disabled means untouched.** With `enabled: false` (the default) results
895
+ are returned as they are and the request is never read, so static and ISR
896
+ pages stay static.
897
+ - **Only the Prepr editor can send changes.** Changes are accepted only from
898
+ `https://*.prepr.io` (or your `allowedEditorOrigins`), never from another
899
+ page that frames your site. They are validated as hostile input: link
900
+ fields never take `javascript:`, `vbscript:` or `data:` URLs. HTML text is
901
+ passed on as typed, like rich text from the API.
902
+ - **One editor's changes never reach anyone else.** On the server-side
903
+ frameworks, changes are read only in preview. Astro accepts them only from
904
+ a same-origin `POST` carrying the toolkit's header and marks that response
905
+ `Cache-Control: private, no-store`. Next.js stores them in the server
906
+ process under a random, single-use token, so a replayed or cached request
907
+ sees nothing.
908
+ - **The Next.js server action is public but inert.** Next.js registers the
909
+ toolkit's server action in every build that imports
910
+ `@preprio/toolkit/nextjs/live`, so anyone can call it. It does nothing
911
+ unless the Prepr middleware handled the request with `preview: true`, and
912
+ the middleware drops `Prepr-*` headers a client sends.
913
+
914
+ ### Trying it locally
915
+
916
+ The Next.js, Astro, SvelteKit and Nuxt examples all have live preview wired
917
+ up. Open them from a Prepr environment whose preview URL points at your local
918
+ server.
919
+
536
920
  ## API Reference
537
921
 
538
922
  ### Framework server helpers
@@ -636,7 +1020,7 @@ controller.destroy();
636
1020
  | Option | Type | Description |
637
1021
  | ------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
638
1022
  | `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). |
1023
+ | `options` | `PreprPreviewOptions` | `{ debug?, locale?, features?, ui?, allowedEditorOrigins?, strictMode?, autoClean? }`. See [Preview options](#preview-options). |
640
1024
  | `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
1025
 
642
1026
  `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 +1078,15 @@ createPreprMiddleware(request, response, { preview });
694
1078
  createPreprPreview({ props, options: { debug: true, locale: 'nl' } });
695
1079
  ```
696
1080
 
697
- | Option | Type | Default | Description |
698
- | ---------------------- | --------------- | ------------- | -------------------------------------------------------------------------------------------------------- |
699
- | `debug` | `boolean` | `false` | Enable debug logging. |
700
- | `locale` | `'en' \| 'nl'` | auto-detected | UI language. Falls back to the first supported browser language, then `en`. |
701
- | `features` | `PreprFeatures` | all enabled | Which features run. See [Feature flags](#feature-flags). |
702
- | `ui` | `boolean` | `true` | Mount the visible toolbar. See [Headless preview](#headless-preview-no-toolbar-ui). |
703
- | `allowedEditorOrigins` | `string[]` | `*.prepr.io` | Exact editor origins allowed to drive this preview, for self-hosted editors. Replaces the wildcard. |
704
- | `autoClean` | `boolean` | `true` | Strip the invisible stega characters from visible text. See [Stega auto-cleaning](#stega-auto-cleaning). |
1081
+ | Option | Type | Default | Description |
1082
+ | ---------------------- | ------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1083
+ | `debug` | `boolean` | `false` | Enable debug logging. |
1084
+ | `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. |
1085
+ | `features` | `PreprFeatures` | all enabled | Which features run. See [Feature flags](#feature-flags). |
1086
+ | `ui` | `boolean` | `true` | Mount the visible toolbar. See [Headless preview](#headless-preview-no-toolbar-ui). |
1087
+ | `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. |
1088
+ | `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`, `normalizedQuery` and `useNormalizedQuery`. |
1089
+ | `autoClean` | `boolean` | `true` | Strip the invisible stega characters from visible text. See [Stega auto-cleaning](#stega-auto-cleaning). |
705
1090
 
706
1091
  `features` and `ui` are independent: `features` decides _what runs_, `ui` decides _whether the toolbar is visible_.
707
1092
 
@@ -809,7 +1194,7 @@ Because the toolbar renders inside a shadow root, none of your own page CSS —
809
1194
 
810
1195
  ### Internationalization
811
1196
 
812
- The toolbar UI ships with English and Dutch. Pass `locale` explicitly, or omit it to auto-detect from the browser and fall back to `en`:
1197
+ 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
1198
 
814
1199
  ```typescript
815
1200
  createPreprPreview({ props, options: { locale: 'nl' } });
@@ -837,6 +1222,14 @@ createPreprPreview({ props, options: { locale: 'nl' } });
837
1222
  - **API calls**: personalization only works if you spread `getPreprHeaders()` into your GraphQL fetch. Missing headers is the most common reason segments appear to have no effect.
838
1223
  - **Chained middleware**: when chaining, pass the existing `NextResponse` as the second argument. Creating a fresh `NextResponse.next()` afterwards discards Prepr's request headers.
839
1224
 
1225
+ ### Live preview does not update
1226
+
1227
+ - **Check `enabled`**: every live preview entry point needs your preview flag, and the middleware must run with `preview: true` for that page.
1228
+ - **Check the preview URL in Prepr**: it must point at a deployment that uses the GraphQL Preview token. A production build with preview off shows saved content only.
1229
+ - **Check `_id`**: an item or component whose query does not select `_id` stays static. Add the `preprIds` Codegen transform, or see [Preparing your queries](#preparing-your-queries).
1230
+ - **Check the frame**: a `Refused to frame` error in the browser console means your `Content-Security-Policy` `frame-ancestors` does not list your Prepr environment.
1231
+ - **Next.js only shows changes on save**: `normalizeQuery` with `PreprRefresh` needs Next.js 16 or later.
1232
+
840
1233
  ### Segments switch but content does not change
841
1234
 
842
1235
  The toolbar sets the segment; your data fetch decides what to do with it. Check that the fetch forwards `getPreprHeaders()` and is not being served from cache — use `cache: 'no-store'`, or Apollo's `fetchPolicy: 'no-cache'`, on personalized queries.