@preprio/toolkit 0.5.0-rc.4 → 0.5.0-rc.6
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 +337 -165
- package/dist/astro/index.cjs +126 -30
- package/dist/astro/index.js +130 -30
- package/dist/{chunk-TG6N3C22.js → chunk-IPMB32AZ.js} +1 -1
- package/dist/{chunk-74KY5EAM.js → chunk-KD3GK6TO.js} +2 -0
- package/dist/{chunk-AHBN6FQP.js → chunk-KZIRM54F.js} +1 -1
- package/dist/{chunk-PDNCNTDB.js → chunk-L3SDL6MP.js} +11 -3
- package/dist/{chunk-46ISDRHT.js → chunk-WHZNCYUS.js} +1 -1
- package/dist/codegen/index.cjs +79 -0
- package/dist/codegen/index.d.cts +46 -0
- package/dist/codegen/index.d.ts +46 -0
- package/dist/codegen/index.js +62 -0
- package/dist/index.cjs +133 -28
- package/dist/index.d.cts +14 -2
- package/dist/index.d.ts +14 -2
- package/dist/index.js +108 -6
- package/dist/{live-preview-H4643QYF.js → live-preview-DLXGXRE5.js} +2 -2
- package/dist/nextjs/components.cjs +11 -3
- package/dist/nextjs/components.js +2 -2
- package/dist/nextjs/index.cjs +124 -28
- package/dist/nextjs/index.d.cts +2 -0
- package/dist/nextjs/index.d.ts +2 -0
- package/dist/nextjs/index.js +128 -28
- package/dist/nextjs/live-action.js +3 -3
- package/dist/nextjs/live.cjs +4 -1
- package/dist/nextjs/live.d.cts +3 -1
- package/dist/nextjs/live.d.ts +3 -1
- package/dist/nextjs/live.js +5 -2
- package/dist/nuxt/chunk-BWPGO25E.js +340 -0
- package/dist/nuxt/index.cjs +1 -1
- package/dist/nuxt/index.js +1 -1
- package/dist/nuxt/{live-preview-O2ZTJNW4.js → live-preview-2RCU35W5.js} +6 -337
- package/dist/nuxt/live.cjs +1007 -911
- package/dist/nuxt/live.js +109 -5
- package/dist/react/components.cjs +11 -3
- package/dist/react/components.js +11 -3
- package/dist/sveltekit/chunk-BWPGO25E.js +340 -0
- package/dist/sveltekit/index.cjs +1 -1
- package/dist/sveltekit/index.js +1 -1
- package/dist/sveltekit/live-preview-2RCU35W5.js +1189 -0
- package/dist/sveltekit/live.cjs +1830 -0
- package/dist/sveltekit/live.d.cts +59 -0
- package/dist/sveltekit/live.d.ts +59 -0
- package/dist/sveltekit/live.js +204 -0
- package/package.json +16 -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,156 +255,83 @@ 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
|
|
|
253
|
-
### Live preview
|
|
258
|
+
### Live preview in Next.js
|
|
254
259
|
|
|
255
|
-
|
|
256
|
-
|
|
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.
|
|
257
262
|
|
|
258
|
-
**
|
|
259
|
-
|
|
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.
|
|
263
|
+
**1. Run each query result through `normalizeQuery`**, with whatever client
|
|
264
|
+
you fetch with:
|
|
264
265
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
saving:
|
|
266
|
+
```ts
|
|
267
|
+
import { normalizeQuery } from '@preprio/toolkit/nextjs';
|
|
268
268
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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.
|
|
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
|
+
```
|
|
289
274
|
|
|
290
|
-
|
|
275
|
+
**2. Mount `PreprRefresh` once in the root layout:**
|
|
291
276
|
|
|
292
277
|
```tsx
|
|
293
|
-
|
|
294
|
-
import { useEffect } from 'react';
|
|
295
|
-
import { initLivePreview } from '@preprio/toolkit/nextjs/live';
|
|
278
|
+
import { PreprRefresh } from '@preprio/toolkit/nextjs/live';
|
|
296
279
|
|
|
297
|
-
|
|
298
|
-
|
|
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;
|
|
280
|
+
{
|
|
281
|
+
preview && <PreprRefresh enabled />;
|
|
310
282
|
}
|
|
311
283
|
```
|
|
312
284
|
|
|
313
|
-
|
|
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.
|
|
314
296
|
|
|
315
297
|
```tsx
|
|
316
298
|
'use client';
|
|
317
299
|
import { useLive } from '@preprio/toolkit/nextjs/live';
|
|
318
300
|
|
|
319
|
-
export default function
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
variables: { slug },
|
|
323
|
-
});
|
|
324
|
-
// render from liveData — it re-renders on editor keystrokes and saves
|
|
301
|
+
export default function Hero({ hero }: { hero: HeroFragment }) {
|
|
302
|
+
const live = useLive(hero);
|
|
303
|
+
return <h1>{live.title}</h1>;
|
|
325
304
|
}
|
|
326
305
|
```
|
|
327
306
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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.
|
|
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`:
|
|
344
311
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
```ts
|
|
351
|
-
import { normalizeQuery } from '@preprio/toolkit/nextjs';
|
|
312
|
+
```tsx
|
|
313
|
+
'use client';
|
|
314
|
+
import { useEffect } from 'react';
|
|
315
|
+
import { initLivePreview, useLive } from '@preprio/toolkit/nextjs/live';
|
|
352
316
|
|
|
353
|
-
|
|
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
|
+
}
|
|
354
327
|
|
|
355
|
-
|
|
356
|
-
const
|
|
328
|
+
// In a client component:
|
|
329
|
+
const liveData = useLive(data, { query: GetPageDocument, variables: { slug } });
|
|
357
330
|
```
|
|
358
331
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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.
|
|
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.
|
|
403
335
|
|
|
404
336
|
## Astro
|
|
405
337
|
|
|
@@ -452,11 +384,11 @@ The `.astro` components ship as source, compiled by your own Astro/Vite pipeline
|
|
|
452
384
|
|
|
453
385
|
See the runnable example at `examples/astro` for the full source of truth.
|
|
454
386
|
|
|
455
|
-
### Live preview
|
|
387
|
+
### Live preview in Astro
|
|
456
388
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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:
|
|
460
392
|
|
|
461
393
|
```astro
|
|
462
394
|
---
|
|
@@ -472,13 +404,8 @@ const page = normalizeQuery(GetPageDocument, data, {
|
|
|
472
404
|
|
|
473
405
|
`PreprToolbar.astro` does the rest. Inside the Prepr editor it requests the
|
|
474
406
|
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
|
|
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.
|
|
407
|
+
changed; on save it shows the stored content. Preview pages must render on
|
|
408
|
+
demand (`output: 'server'`, or `export const prerender = false`).
|
|
482
409
|
|
|
483
410
|
## SvelteKit
|
|
484
411
|
|
|
@@ -540,6 +467,35 @@ The `.svelte` components ship as source (compiled by your own Vite/Svelte pipeli
|
|
|
540
467
|
|
|
541
468
|
See the runnable example at `examples/sveltekit` for the full source of truth.
|
|
542
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
|
+
|
|
543
499
|
## Nuxt
|
|
544
500
|
|
|
545
501
|
Requires Nitro's Node.js preset (the Nuxt default).
|
|
@@ -591,24 +547,25 @@ The `.vue` components ship as source (compiled by your own Vite/Vue pipeline) an
|
|
|
591
547
|
|
|
592
548
|
See the runnable example at `examples/nuxt` for the full source of truth.
|
|
593
549
|
|
|
594
|
-
### Live preview
|
|
550
|
+
### Live preview in Nuxt
|
|
595
551
|
|
|
596
|
-
|
|
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:
|
|
597
555
|
|
|
598
556
|
```ts
|
|
599
557
|
import { useNormalizedQuery } from '@preprio/toolkit/nuxt/live';
|
|
600
558
|
|
|
601
559
|
const { data, refresh } = await useAsyncData('page', () => fetchPage(slug));
|
|
602
560
|
const page = useNormalizedQuery(GetPageDocument, data, {
|
|
603
|
-
enabled: preview,
|
|
561
|
+
enabled: preview,
|
|
604
562
|
onSave: refresh,
|
|
605
563
|
});
|
|
606
564
|
```
|
|
607
565
|
|
|
608
566
|
Render from `page`. Nuxt hydrates the whole page, so every component that
|
|
609
|
-
shows a changed field updates.
|
|
610
|
-
|
|
611
|
-
the Prepr editor.
|
|
567
|
+
shows a changed field updates. On save, `refresh` refetches the stored
|
|
568
|
+
content.
|
|
612
569
|
|
|
613
570
|
## React (no framework)
|
|
614
571
|
|
|
@@ -665,23 +622,22 @@ const location = useLocation();
|
|
|
665
622
|
|
|
666
623
|
### Tracking pageviews
|
|
667
624
|
|
|
668
|
-
|
|
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:
|
|
669
626
|
|
|
670
627
|
```tsx
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
useEffect(() => {
|
|
679
|
-
trackEvent('pageload');
|
|
680
|
-
}, [pathname]);
|
|
628
|
+
function Page({ page }: { page: { _id: string } }) {
|
|
629
|
+
return (
|
|
630
|
+
<>
|
|
631
|
+
<meta property="prepr:id" content={page._id} />
|
|
632
|
+
{/* … */}
|
|
633
|
+
</>
|
|
634
|
+
);
|
|
681
635
|
}
|
|
682
636
|
```
|
|
683
637
|
|
|
684
|
-
|
|
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 })`.
|
|
685
641
|
|
|
686
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.
|
|
687
643
|
|
|
@@ -731,6 +687,214 @@ const controller = createPreprPreview({ props: toolbarProps });
|
|
|
731
687
|
|
|
732
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.
|
|
733
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` and
|
|
741
|
+
`__typename` are added wherever your schema has them: on models and
|
|
742
|
+
components, and inside each member of a dynamic content field. Collections
|
|
743
|
+
like `Pages` and unions themselves never get an `_id`, so the queries stay
|
|
744
|
+
valid. Your `.graphql` files stay as they are; the generated documents and
|
|
745
|
+
types include the fields, at no runtime cost.
|
|
746
|
+
|
|
747
|
+
```ts
|
|
748
|
+
// codegen.ts
|
|
749
|
+
import type { CodegenConfig } from '@graphql-codegen/cli';
|
|
750
|
+
import { preprIds } from '@preprio/toolkit/codegen';
|
|
751
|
+
|
|
752
|
+
const config: CodegenConfig = {
|
|
753
|
+
schema: process.env.PREPR_GRAPHQL_URL,
|
|
754
|
+
documents: ['src/queries/**/*.graphql'],
|
|
755
|
+
generates: {
|
|
756
|
+
'src/gql/': {
|
|
757
|
+
preset: 'client',
|
|
758
|
+
documentTransforms: [preprIds],
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
export default config;
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
Run Codegen again after adding it. Needs `graphql` 16 or later, which a
|
|
767
|
+
Codegen setup already has.
|
|
768
|
+
|
|
769
|
+
#### Option 2: at runtime, without Codegen
|
|
770
|
+
|
|
771
|
+
`addPreprIds(document, schema)` does the same to a parsed query. It needs
|
|
772
|
+
your schema, which Prepr serves through introspection on the same GraphQL
|
|
773
|
+
URL. Load it once on the server, and transform each query once:
|
|
774
|
+
|
|
775
|
+
```ts
|
|
776
|
+
import { addPreprIds } from '@preprio/toolkit/codegen';
|
|
777
|
+
import {
|
|
778
|
+
buildClientSchema,
|
|
779
|
+
getIntrospectionQuery,
|
|
780
|
+
parse,
|
|
781
|
+
print,
|
|
782
|
+
type GraphQLSchema,
|
|
783
|
+
} from 'graphql';
|
|
784
|
+
|
|
785
|
+
const url = process.env.PREPR_GRAPHQL_URL!;
|
|
786
|
+
|
|
787
|
+
let schema: Promise<GraphQLSchema> | undefined;
|
|
788
|
+
function preprSchema(): Promise<GraphQLSchema> {
|
|
789
|
+
schema ??= fetch(url, {
|
|
790
|
+
method: 'POST',
|
|
791
|
+
headers: { 'Content-Type': 'application/json' },
|
|
792
|
+
body: JSON.stringify({ query: getIntrospectionQuery() }),
|
|
793
|
+
})
|
|
794
|
+
.then((response) => response.json())
|
|
795
|
+
.then(({ data }) => buildClientSchema(data));
|
|
796
|
+
return schema;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const prepared = new Map<string, string>();
|
|
800
|
+
export async function withPreprIds(query: string): Promise<string> {
|
|
801
|
+
let result = prepared.get(query);
|
|
802
|
+
if (!result) {
|
|
803
|
+
result = print(addPreprIds(parse(query), await preprSchema()));
|
|
804
|
+
prepared.set(query, result);
|
|
805
|
+
}
|
|
806
|
+
return result;
|
|
807
|
+
}
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
Then send `await withPreprIds(query)` instead of `query` in your fetch
|
|
811
|
+
helper. With a client that takes a `DocumentNode`, pass
|
|
812
|
+
`addPreprIds(document, schema)` instead. Keep this on the server: the
|
|
813
|
+
GraphQL URL carries your access token.
|
|
814
|
+
|
|
815
|
+
#### Option 3: by hand
|
|
816
|
+
|
|
817
|
+
Select `_id` (and `__typename`) on every model and component level, and on
|
|
818
|
+
every entry type in dynamic content fields (`Text`, `Quote`, …). `Assets`
|
|
819
|
+
entries have no `_id` of their own: select `items { _id }` instead.
|
|
820
|
+
|
|
821
|
+
The rest is about rendering:
|
|
822
|
+
|
|
823
|
+
- A `Text` entry's content updates whether the query selects `body`,
|
|
824
|
+
`html` or `text` (the latter with tags stripped).
|
|
825
|
+
- **Dynamic content entries are found automatically.** In preview,
|
|
826
|
+
`normalizeQuery`, `normalizedQuery` and `useNormalizedQuery` add each
|
|
827
|
+
entry's `_id` (and, for `Text` entries, the field path) to the hidden edit
|
|
828
|
+
link of every text inside it. Click-to-edit and the editor's "jump to
|
|
829
|
+
field" then find the entry by id, also after an editor reorders entries.
|
|
830
|
+
Render a `Text` entry's HTML straight into one wrapper element, so its
|
|
831
|
+
edit link lands on that wrapper and the whole entry is outlined.
|
|
832
|
+
- Prepr images are click-to-edit inside the editor without any markup: the
|
|
833
|
+
toolkit matches image URLs (also through image optimizers like
|
|
834
|
+
`/_next/image`) to the open item's assets.
|
|
835
|
+
- **Set the attributes yourself only where that does not reach:**
|
|
836
|
+
- Data you fetch in the browser without those helpers: run it through
|
|
837
|
+
`annotateEditLinks(result, query)` from `@preprio/toolkit`, or mark the
|
|
838
|
+
element as below.
|
|
839
|
+
- An entry with no text and no Prepr image (an embed, a video URL): wrap
|
|
840
|
+
it in an element with `data-prepr-edit-target="<field>.<index>"` and
|
|
841
|
+
`data-prepr-entry-id="<entry _id>"` (for `Assets`, the first asset's
|
|
842
|
+
`_id`). Attributes you set always win over the automatic ones.
|
|
843
|
+
- Other values without a hidden edit link (video and other URLs): make
|
|
844
|
+
them click-to-edit with
|
|
845
|
+
`previewAttrs({ id, field }, { enabled: preview })` on the element that
|
|
846
|
+
renders them.
|
|
847
|
+
- Where the page renders a linked item whose content comes from items nested
|
|
848
|
+
inside it (for example a personalization or a shared section referenced
|
|
849
|
+
from a stack), wrap it in an element with
|
|
850
|
+
`data-prepr-reference="<linked item _id>"`. The editor only knows the items
|
|
851
|
+
the open item links to directly; content inside the wrapper is outlined as
|
|
852
|
+
that linked item, and clicking it opens the link in the editor.
|
|
853
|
+
|
|
854
|
+
Typed text and changed fields show immediately. Structural changes, such as
|
|
855
|
+
adding or reordering components, appear after save.
|
|
856
|
+
|
|
857
|
+
### Options
|
|
858
|
+
|
|
859
|
+
`PreprRefresh`, `initLivePreview`, `normalizedQuery` and `useNormalizedQuery`
|
|
860
|
+
take the same two options as the toolbar (in Astro, set them on the
|
|
861
|
+
toolbar's `options`):
|
|
862
|
+
|
|
863
|
+
- **`allowedEditorOrigins`** — exact editor origins, replacing the
|
|
864
|
+
`https://*.prepr.io` default. Only needed when Prepr runs on your own domain.
|
|
865
|
+
- **`strictMode`** — set it when your preview token uses GraphQL strict mode
|
|
866
|
+
(required fields never null). The editor then holds back new entries until
|
|
867
|
+
their required fields are filled, and never sends a cleared required field
|
|
868
|
+
as null.
|
|
869
|
+
|
|
870
|
+
### Security
|
|
871
|
+
|
|
872
|
+
- **Disabled means untouched.** With `enabled: false` (the default) results
|
|
873
|
+
are returned as they are and the request is never read, so static and ISR
|
|
874
|
+
pages stay static.
|
|
875
|
+
- **Only the Prepr editor can send changes.** Changes are accepted only from
|
|
876
|
+
`https://*.prepr.io` (or your `allowedEditorOrigins`), never from another
|
|
877
|
+
page that frames your site. They are validated as hostile input: link
|
|
878
|
+
fields never take `javascript:`, `vbscript:` or `data:` URLs. HTML text is
|
|
879
|
+
passed on as typed, like rich text from the API.
|
|
880
|
+
- **One editor's changes never reach anyone else.** On the server-side
|
|
881
|
+
frameworks, changes are read only in preview. Astro accepts them only from
|
|
882
|
+
a same-origin `POST` carrying the toolkit's header and marks that response
|
|
883
|
+
`Cache-Control: private, no-store`. Next.js stores them in the server
|
|
884
|
+
process under a random, single-use token, so a replayed or cached request
|
|
885
|
+
sees nothing.
|
|
886
|
+
- **The Next.js server action is public but inert.** Next.js registers the
|
|
887
|
+
toolkit's server action in every build that imports
|
|
888
|
+
`@preprio/toolkit/nextjs/live`, so anyone can call it. It does nothing
|
|
889
|
+
unless the Prepr middleware handled the request with `preview: true`, and
|
|
890
|
+
the middleware drops `Prepr-*` headers a client sends.
|
|
891
|
+
|
|
892
|
+
### Trying it locally
|
|
893
|
+
|
|
894
|
+
The Next.js, Astro, SvelteKit and Nuxt examples all have live preview wired
|
|
895
|
+
up. Open them from a Prepr environment whose preview URL points at your local
|
|
896
|
+
server.
|
|
897
|
+
|
|
734
898
|
## API Reference
|
|
735
899
|
|
|
736
900
|
### Framework server helpers
|
|
@@ -892,15 +1056,15 @@ createPreprMiddleware(request, response, { preview });
|
|
|
892
1056
|
createPreprPreview({ props, options: { debug: true, locale: 'nl' } });
|
|
893
1057
|
```
|
|
894
1058
|
|
|
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).
|
|
1059
|
+
| Option | Type | Default | Description |
|
|
1060
|
+
| ---------------------- | ------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1061
|
+
| `debug` | `boolean` | `false` | Enable debug logging. |
|
|
1062
|
+
| `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. |
|
|
1063
|
+
| `features` | `PreprFeatures` | all enabled | Which features run. See [Feature flags](#feature-flags). |
|
|
1064
|
+
| `ui` | `boolean` | `true` | Mount the visible toolbar. See [Headless preview](#headless-preview-no-toolbar-ui). |
|
|
1065
|
+
| `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. |
|
|
1066
|
+
| `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`. |
|
|
1067
|
+
| `autoClean` | `boolean` | `true` | Strip the invisible stega characters from visible text. See [Stega auto-cleaning](#stega-auto-cleaning). |
|
|
904
1068
|
|
|
905
1069
|
`features` and `ui` are independent: `features` decides _what runs_, `ui` decides _whether the toolbar is visible_.
|
|
906
1070
|
|
|
@@ -1036,6 +1200,14 @@ createPreprPreview({ props, options: { locale: 'nl' } });
|
|
|
1036
1200
|
- **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.
|
|
1037
1201
|
- **Chained middleware**: when chaining, pass the existing `NextResponse` as the second argument. Creating a fresh `NextResponse.next()` afterwards discards Prepr's request headers.
|
|
1038
1202
|
|
|
1203
|
+
### Live preview does not update
|
|
1204
|
+
|
|
1205
|
+
- **Check `enabled`**: every live preview entry point needs your preview flag, and the middleware must run with `preview: true` for that page.
|
|
1206
|
+
- **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.
|
|
1207
|
+
- **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).
|
|
1208
|
+
- **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.
|
|
1209
|
+
- **Next.js only shows changes on save**: `normalizeQuery` with `PreprRefresh` needs Next.js 16 or later.
|
|
1210
|
+
|
|
1039
1211
|
### Segments switch but content does not change
|
|
1040
1212
|
|
|
1041
1213
|
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.
|