@preprio/toolkit 0.2.0-beta.6 → 0.2.0-beta.9
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 +237 -471
- package/dist/astro/index.cjs +20 -6
- package/dist/astro/index.d.cts +1 -1
- package/dist/astro/index.d.ts +1 -1
- package/dist/astro/index.js +20 -6
- package/dist/{chunk-C6YEXA5G.js → chunk-JVUG6S4O.js} +42 -47
- package/dist/index.cjs +58 -52
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -6
- package/dist/nextjs/components.cjs +45 -48
- package/dist/nextjs/components.js +5 -3
- package/dist/nextjs/image-loader.cjs +7 -2
- package/dist/nextjs/image-loader.d.cts +1 -1
- package/dist/nextjs/image-loader.d.ts +1 -1
- package/dist/nextjs/image-loader.js +7 -2
- package/dist/nextjs/index.cjs +31 -9
- package/dist/nextjs/index.d.cts +99 -2
- package/dist/nextjs/index.d.ts +99 -2
- package/dist/nextjs/index.js +31 -9
- package/dist/nuxt/index.cjs +20 -6
- package/dist/nuxt/index.js +20 -6
- package/dist/react/components.cjs +42 -47
- package/dist/react/components.js +42 -47
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.d.cts +1 -1
- package/dist/sveltekit/index.cjs +20 -6
- package/dist/sveltekit/index.js +20 -6
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
# @preprio/toolkit
|
|
2
2
|
|
|
3
|
-
A framework-agnostic TypeScript library that provides preview functionality, visual editing
|
|
3
|
+
A framework-agnostic TypeScript library that provides preview functionality, visual editing, and A/B testing for [Prepr CMS](https://prepr.io). Ships thin wrappers for Next.js, Nuxt, Astro, and SvelteKit on top of a vanilla core that runs anywhere.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Every integration is the same three steps:
|
|
6
|
+
|
|
7
|
+
1. **Middleware** — resolves the visitor's customer ID, segment, and A/B variant into request headers.
|
|
8
|
+
2. **Layout** — mounts the preview toolbar and the tracking pixel.
|
|
9
|
+
3. **Data fetching** — forwards the Prepr headers on your GraphQL requests, so Prepr returns personalized content.
|
|
10
|
+
|
|
11
|
+
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).
|
|
12
|
+
|
|
13
|
+
## Quick Start (Next.js)
|
|
6
14
|
|
|
7
15
|
```bash
|
|
8
|
-
# Install the package
|
|
9
16
|
npm install @preprio/toolkit
|
|
10
17
|
# or
|
|
11
18
|
pnpm add @preprio/toolkit
|
|
12
19
|
```
|
|
13
20
|
|
|
14
|
-
Add your Prepr GraphQL URL to your `.env
|
|
21
|
+
Add your Prepr GraphQL URL (Settings → Access tokens in Prepr) to your `.env`. The toolkit reads **no environment variables itself** — the name is yours; you pass the value in explicitly:
|
|
15
22
|
|
|
16
23
|
```bash
|
|
17
24
|
PREPR_GRAPHQL_URL=https://graphql.prepr.io/{YOUR_ACCESS_TOKEN}
|
|
18
25
|
```
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
**`middleware.ts`**
|
|
21
28
|
|
|
22
29
|
```typescript
|
|
23
30
|
import type { NextRequest } from 'next/server';
|
|
@@ -35,7 +42,7 @@ export const config = {
|
|
|
35
42
|
};
|
|
36
43
|
```
|
|
37
44
|
|
|
38
|
-
|
|
45
|
+
**`app/layout.tsx`**
|
|
39
46
|
|
|
40
47
|
```tsx
|
|
41
48
|
import type { ReactNode } from 'react';
|
|
@@ -46,7 +53,11 @@ import {
|
|
|
46
53
|
PreprTrackingPixel,
|
|
47
54
|
} from '@preprio/toolkit/nextjs';
|
|
48
55
|
|
|
49
|
-
export default async function RootLayout({
|
|
56
|
+
export default async function RootLayout({
|
|
57
|
+
children,
|
|
58
|
+
}: {
|
|
59
|
+
children: ReactNode;
|
|
60
|
+
}) {
|
|
50
61
|
const preview = process.env.VERCEL_ENV !== 'production';
|
|
51
62
|
const toolbarProps = preview
|
|
52
63
|
? await getToolbarProps(process.env.PREPR_GRAPHQL_URL!)
|
|
@@ -65,118 +76,67 @@ export default async function RootLayout({ children }: { children: ReactNode })
|
|
|
65
76
|
}
|
|
66
77
|
```
|
|
67
78
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
## Prerequisites
|
|
71
|
-
|
|
72
|
-
Before installing, ensure you have:
|
|
73
|
-
|
|
74
|
-
- **Node.js 18.0.0 or later**
|
|
75
|
-
- **A Prepr account**
|
|
76
|
-
- **A Prepr GraphQL URL** (found in Settings → Access tokens)
|
|
79
|
+
**Data fetching** — spread `getPreprHeaders()` into every Prepr GraphQL request:
|
|
77
80
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
| Entry point | Peer dependencies |
|
|
81
|
-
| --- | --- |
|
|
82
|
-
| `@preprio/toolkit` (vanilla core) | none |
|
|
83
|
-
| `@preprio/toolkit/react` | `react` >= 17, `react-dom` >= 17 |
|
|
84
|
-
| `@preprio/toolkit/nextjs` | `next` >= 13, `react` >= 17, `react-dom` >= 17 |
|
|
85
|
-
| `@preprio/toolkit/astro` | none (`.astro` components compile in your own pipeline) |
|
|
86
|
-
| `@preprio/toolkit/sveltekit` | `svelte` (only for the `.svelte` components) |
|
|
87
|
-
| `@preprio/toolkit/nuxt` | `vue` (only for the `.vue` components) |
|
|
88
|
-
|
|
89
|
-
All peer dependencies are optional — installing without React or Next.js is fine as long as you only import the entry points you have dependencies for.
|
|
90
|
-
|
|
91
|
-
### Prepr Account Setup
|
|
92
|
-
|
|
93
|
-
1. **Create a Prepr account** at [prepr.io](https://prepr.io)
|
|
94
|
-
2. **Get your GraphQL URL**:
|
|
95
|
-
- Go to Settings → Access tokens
|
|
96
|
-
- Find your GraphQL Preview access token
|
|
97
|
-
- Copy the full GraphQL URL (e.g. `https://graphql.prepr.io/e6f7a0521f11e5149ce65b0e9f372ced2dfc923490890e7f225da1db84cxxxxx`)
|
|
98
|
-
- The URL format is always `https://graphql.prepr.io/{YOUR_ACCESS_TOKEN}`
|
|
99
|
-
3. **Enable edit mode** (required for click-to-edit in the toolbar):
|
|
100
|
-
- Open your GraphQL Preview access token
|
|
101
|
-
- Check "Enable edit mode"
|
|
102
|
-
- Save the token
|
|
103
|
-
|
|
104
|
-
## Installation & Setup
|
|
105
|
-
|
|
106
|
-
### 1. Install the Package
|
|
81
|
+
```typescript
|
|
82
|
+
import { getPreprHeaders } from '@preprio/toolkit/nextjs';
|
|
107
83
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
84
|
+
const response = await fetch(process.env.PREPR_GRAPHQL_URL!, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: {
|
|
87
|
+
'Content-Type': 'application/json',
|
|
88
|
+
...(await getPreprHeaders()),
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({ query, variables }),
|
|
91
|
+
cache: 'no-store',
|
|
92
|
+
});
|
|
114
93
|
```
|
|
115
94
|
|
|
116
|
-
|
|
95
|
+
No CSS import is needed. The toolbar renders into a shadow-DOM custom element (`<prepr-toolbar>`) with its styles inlined, so it can neither leak into nor inherit from your page's stylesheets.
|
|
117
96
|
|
|
118
|
-
|
|
97
|
+
## Prerequisites
|
|
119
98
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
99
|
+
- **Node.js 18.17 or later**
|
|
100
|
+
- **A Prepr account** — [prepr.io](https://prepr.io)
|
|
101
|
+
- **A Prepr GraphQL URL**:
|
|
102
|
+
1. Go to Settings → Access tokens and open your **GraphQL Preview** access token.
|
|
103
|
+
2. Copy the full GraphQL URL — always `https://graphql.prepr.io/{YOUR_ACCESS_TOKEN}`.
|
|
104
|
+
3. Check **"Enable edit mode"** on the token and save (required for click-to-edit).
|
|
124
105
|
|
|
125
|
-
|
|
106
|
+
Per-framework peer dependencies — all optional; installing without them is fine as long as you only import the entry points you have dependencies for:
|
|
126
107
|
|
|
127
|
-
|
|
108
|
+
| Entry point | Peer dependencies |
|
|
109
|
+
| --------------------------------- | ------------------------------------------------------- |
|
|
110
|
+
| `@preprio/toolkit` (vanilla core) | none |
|
|
111
|
+
| `@preprio/toolkit/react` | `react` >= 17, `react-dom` >= 17 |
|
|
112
|
+
| `@preprio/toolkit/nextjs` | `next` >= 13, `react` >= 17, `react-dom` >= 17 |
|
|
113
|
+
| `@preprio/toolkit/astro` | none (`.astro` components compile in your own pipeline) |
|
|
114
|
+
| `@preprio/toolkit/sveltekit` | `svelte` (only for the `.svelte` components) |
|
|
115
|
+
| `@preprio/toolkit/nuxt` | `vue` (only for the `.vue` components) |
|
|
128
116
|
|
|
129
|
-
|
|
117
|
+
## The preview gate
|
|
130
118
|
|
|
131
|
-
|
|
119
|
+
Three rules hold across every framework:
|
|
132
120
|
|
|
133
|
-
|
|
134
|
-
createPreprMiddleware(request, { preview: process.env.VERCEL_ENV !== 'production' });
|
|
135
|
-
```
|
|
121
|
+
1. **The toolkit reads no environment variables.** Every value reaches it as an explicit argument, so behavior is identical across frameworks and bundlers. Because the GraphQL URL embeds an access token, keep it server-side unless you intend it to be public.
|
|
136
122
|
|
|
137
|
-
|
|
123
|
+
2. **Preview mode is one boolean: the `preview` option you pass to the middleware.** Resolve it from whatever already distinguishes your environments — `VERCEL_ENV`, `NODE_ENV`, SvelteKit's `dev`, Astro's `import.meta.env.DEV`, a branch check, your own feature flag. A hardcoded `{ preview: true }` enables preview everywhere, **production included**.
|
|
138
124
|
|
|
139
|
-
|
|
125
|
+
3. **`getToolbarProps` is ungated — it fetches whenever you call it.** Gate the call on the same flag you pass the middleware:
|
|
140
126
|
|
|
141
|
-
```typescript
|
|
142
|
-
const toolbarProps = preview ? await getToolbarProps(token) : null;
|
|
143
|
-
```
|
|
127
|
+
```typescript
|
|
128
|
+
const toolbarProps = preview ? await getToolbarProps(token) : null;
|
|
129
|
+
```
|
|
144
130
|
|
|
145
|
-
|
|
131
|
+
It never throws: a failed segment fetch degrades to an empty segment list, so no `try`/`catch` or error boundary is required.
|
|
146
132
|
|
|
147
133
|
## Next.js
|
|
148
134
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
The middleware assigns the visitor's customer ID, captures UTM parameters, and resolves the active segment and A/B variant into request headers.
|
|
152
|
-
|
|
153
|
-
Create or update `middleware.ts` in your project root:
|
|
154
|
-
|
|
155
|
-
```typescript
|
|
156
|
-
import type { NextRequest } from 'next/server';
|
|
157
|
-
import { createPreprMiddleware } from '@preprio/toolkit/nextjs';
|
|
158
|
-
|
|
159
|
-
export function middleware(request: NextRequest) {
|
|
160
|
-
return createPreprMiddleware(request, { preview: true });
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export const config = {
|
|
164
|
-
matcher: [
|
|
165
|
-
/*
|
|
166
|
-
* Match all request paths except for the ones starting with:
|
|
167
|
-
* - api (API routes)
|
|
168
|
-
* - _next/static (static files)
|
|
169
|
-
* - _next/image (image optimization files)
|
|
170
|
-
* - favicon.ico (favicon file)
|
|
171
|
-
*/
|
|
172
|
-
'/((?!api|_next/static|_next/image|favicon.ico).*)',
|
|
173
|
-
],
|
|
174
|
-
};
|
|
175
|
-
```
|
|
135
|
+
The [Quick Start](#quick-start-nextjs) above is the complete basic setup. This section covers the extras.
|
|
176
136
|
|
|
177
137
|
Despite the `create` prefix, `createPreprMiddleware` runs per request — it does not return a handler.
|
|
178
138
|
|
|
179
|
-
|
|
139
|
+
### Chaining with existing middleware
|
|
180
140
|
|
|
181
141
|
Pass an existing `NextResponse` as the second argument to fold Prepr's headers and cookies onto a response another middleware already produced:
|
|
182
142
|
|
|
@@ -184,6 +144,8 @@ Pass an existing `NextResponse` as the second argument to fold Prepr's headers a
|
|
|
184
144
|
import { NextResponse, type NextRequest } from 'next/server';
|
|
185
145
|
import { createPreprMiddleware } from '@preprio/toolkit/nextjs';
|
|
186
146
|
|
|
147
|
+
const preview = process.env.VERCEL_ENV !== 'production';
|
|
148
|
+
|
|
187
149
|
export function middleware(request: NextRequest) {
|
|
188
150
|
const response = NextResponse.next();
|
|
189
151
|
|
|
@@ -191,11 +153,11 @@ export function middleware(request: NextRequest) {
|
|
|
191
153
|
response.headers.set('x-admin-route', 'true');
|
|
192
154
|
}
|
|
193
155
|
|
|
194
|
-
return createPreprMiddleware(request, response, { preview
|
|
156
|
+
return createPreprMiddleware(request, response, { preview });
|
|
195
157
|
}
|
|
196
158
|
```
|
|
197
159
|
|
|
198
|
-
|
|
160
|
+
With next-intl (return its redirects before Prepr runs):
|
|
199
161
|
|
|
200
162
|
```typescript
|
|
201
163
|
import type { NextRequest } from 'next/server';
|
|
@@ -207,129 +169,26 @@ const intlMiddleware = createIntlMiddleware({
|
|
|
207
169
|
defaultLocale: 'en',
|
|
208
170
|
});
|
|
209
171
|
|
|
172
|
+
const preview = process.env.VERCEL_ENV !== 'production';
|
|
173
|
+
|
|
210
174
|
export function middleware(request: NextRequest) {
|
|
211
175
|
const intlResponse = intlMiddleware(request);
|
|
212
176
|
|
|
213
|
-
// If next-intl returns a redirect, return it immediately.
|
|
214
177
|
if (intlResponse.status >= 300 && intlResponse.status < 400) {
|
|
215
178
|
return intlResponse;
|
|
216
179
|
}
|
|
217
180
|
|
|
218
|
-
return createPreprMiddleware(request, intlResponse, { preview
|
|
181
|
+
return createPreprMiddleware(request, intlResponse, { preview });
|
|
219
182
|
}
|
|
220
|
-
|
|
221
|
-
export const config = {
|
|
222
|
-
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
|
223
|
-
};
|
|
224
183
|
```
|
|
225
184
|
|
|
226
|
-
|
|
185
|
+
To skip Prepr for some routes entirely, return early with `NextResponse.next()` before calling `createPreprMiddleware` — or exclude them in `config.matcher`.
|
|
227
186
|
|
|
228
|
-
|
|
229
|
-
import { NextResponse, type NextRequest } from 'next/server';
|
|
230
|
-
import { createPreprMiddleware } from '@preprio/toolkit/nextjs';
|
|
187
|
+
### Tracking pixel
|
|
231
188
|
|
|
232
|
-
|
|
233
|
-
// Skip Prepr entirely for API routes.
|
|
234
|
-
if (request.nextUrl.pathname.startsWith('/api/')) {
|
|
235
|
-
return NextResponse.next();
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return createPreprMiddleware(request, { preview: process.env.VERCEL_ENV !== 'production' });
|
|
239
|
-
}
|
|
240
|
-
```
|
|
189
|
+
The pixel collects the interaction data that powers personalization and A/B testing. Include it in **all** environments, preview and production alike — the Quick Start layout already does. `PreprTrackingPixel` renders nothing and loads the CDN script on mount, so its position in the tree does not matter.
|
|
241
190
|
|
|
242
|
-
###
|
|
243
|
-
|
|
244
|
-
`getToolbarProps` is ungated, so gate the call on the same flag you pass to the middleware:
|
|
245
|
-
|
|
246
|
-
```tsx
|
|
247
|
-
import type { ReactNode } from 'react';
|
|
248
|
-
import { getToolbarProps, PreprToolbar } from '@preprio/toolkit/nextjs';
|
|
249
|
-
|
|
250
|
-
export default async function RootLayout({ children }: { children: ReactNode }) {
|
|
251
|
-
const preview = process.env.VERCEL_ENV !== 'production';
|
|
252
|
-
const toolbarProps = preview
|
|
253
|
-
? await getToolbarProps(process.env.PREPR_GRAPHQL_URL!)
|
|
254
|
-
: null;
|
|
255
|
-
|
|
256
|
-
return (
|
|
257
|
-
<html lang="en">
|
|
258
|
-
<body>
|
|
259
|
-
{children}
|
|
260
|
-
{toolbarProps && <PreprToolbar {...toolbarProps} />}
|
|
261
|
-
</body>
|
|
262
|
-
</html>
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
Skipping the call outside preview means no API call and no toolbar. `getToolbarProps` also never throws — a failed segment fetch degrades to an empty segment list rather than crashing the host app — so no `try`/`catch` or error boundary is required.
|
|
268
|
-
|
|
269
|
-
To keep the toolbar out of the production bundle entirely, gate the *import* rather than the props, with a `next/dynamic` import behind the same flag.
|
|
270
|
-
|
|
271
|
-
### Tracking Pixel
|
|
272
|
-
|
|
273
|
-
The tracking pixel collects the interaction data that powers personalization and A/B testing. Include it in **all** environments, preview and production alike.
|
|
274
|
-
|
|
275
|
-
```tsx
|
|
276
|
-
import { extractAccessToken, PreprTrackingPixel } from '@preprio/toolkit/nextjs';
|
|
277
|
-
|
|
278
|
-
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
279
|
-
const accessToken = extractAccessToken(process.env.PREPR_GRAPHQL_URL!);
|
|
280
|
-
|
|
281
|
-
return (
|
|
282
|
-
<html lang="en">
|
|
283
|
-
<body>
|
|
284
|
-
{children}
|
|
285
|
-
{accessToken && <PreprTrackingPixel id={accessToken} />}
|
|
286
|
-
</body>
|
|
287
|
-
</html>
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
`PreprTrackingPixel` renders nothing and loads the CDN script on mount, so its position in the tree does not matter.
|
|
293
|
-
|
|
294
|
-
### API Integration
|
|
295
|
-
|
|
296
|
-
Use `getPreprHeaders()` to forward the request's Prepr personalization context to your Prepr GraphQL fetches. Prepr resolves segments and A/B variants from these headers, so a query sent without them always returns the default, unpersonalized content.
|
|
297
|
-
|
|
298
|
-
The headers are set by the middleware, so they are only present on routes its matcher covers — off-matcher requests yield an empty object.
|
|
299
|
-
|
|
300
|
-
The signature differs per framework: `getPreprHeaders()` is async and zero-argument on Next.js (it reads `headers()` for you), while the Astro, SvelteKit, and Nuxt builds are synchronous and take the request's `Headers`.
|
|
301
|
-
|
|
302
|
-
#### With the Fetch API
|
|
303
|
-
|
|
304
|
-
```typescript
|
|
305
|
-
import { getPreprHeaders } from '@preprio/toolkit/nextjs';
|
|
306
|
-
|
|
307
|
-
async function getPage(slug: string) {
|
|
308
|
-
const response = await fetch(process.env.PREPR_GRAPHQL_URL!, {
|
|
309
|
-
method: 'POST',
|
|
310
|
-
headers: {
|
|
311
|
-
'Content-Type': 'application/json',
|
|
312
|
-
...(await getPreprHeaders()),
|
|
313
|
-
},
|
|
314
|
-
body: JSON.stringify({
|
|
315
|
-
query: `
|
|
316
|
-
query GetPageBySlug($slug: String!) {
|
|
317
|
-
Page(slug: $slug) {
|
|
318
|
-
title
|
|
319
|
-
content
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
`,
|
|
323
|
-
variables: { slug },
|
|
324
|
-
}),
|
|
325
|
-
cache: 'no-store',
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
return response.json();
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
#### With Apollo Client
|
|
191
|
+
### With Apollo Client
|
|
333
192
|
|
|
334
193
|
```typescript
|
|
335
194
|
import { getClient } from '@/lib/client';
|
|
@@ -348,7 +207,7 @@ async function getPage(slug: string) {
|
|
|
348
207
|
}
|
|
349
208
|
```
|
|
350
209
|
|
|
351
|
-
### Image
|
|
210
|
+
### Image loader
|
|
352
211
|
|
|
353
212
|
Prepr's stream CDN resizes at the edge. Routing `next/image` through the bundled loader makes Next request the exact width it needs, instead of pulling the full-resolution original and re-encoding it through `/_next/image`.
|
|
354
213
|
|
|
@@ -375,16 +234,12 @@ The API emits each asset's real dimensions, so a Prepr URL renders as-is:
|
|
|
375
234
|
https://393qibegr87z.b-cdn.net/w_800,h_600,fx_75,fy_67/b7ijwafd586-photo.jpg
|
|
376
235
|
```
|
|
377
236
|
|
|
378
|
-
The loader rewrites `w_` to the width Next asks for and scales `h_` alongside it, holding the emitted ratio — for a cropped asset that ratio is the authored crop box, so the crop survives the resize. Focal points (`fx`/`fy`) are percentages and carry through unchanged, as does any other option in the segment. Quality is not a CDN option and is ignored.
|
|
379
|
-
|
|
380
|
-
URLs with no transform segment pass through untouched — set `unoptimized` on those `<Image>`s.
|
|
237
|
+
The loader rewrites `w_` to the width Next asks for and scales `h_` alongside it, holding the emitted ratio — for a cropped asset that ratio is the authored crop box, so the crop survives the resize. Focal points (`fx`/`fy`) are percentages and carry through unchanged, as does any other option in the segment. Quality is not a CDN option and is ignored. URLs with no transform segment pass through untouched — set `unoptimized` on those `<Image>`s.
|
|
381
238
|
|
|
382
239
|
See the runnable example at `examples/nextjs` for the full source of truth.
|
|
383
240
|
|
|
384
241
|
## Astro
|
|
385
242
|
|
|
386
|
-
### Middleware Setup
|
|
387
|
-
|
|
388
243
|
**`src/middleware.ts`**
|
|
389
244
|
|
|
390
245
|
```typescript
|
|
@@ -392,11 +247,11 @@ import { onPreprRequest } from '@preprio/toolkit/astro';
|
|
|
392
247
|
import { defineMiddleware } from 'astro:middleware';
|
|
393
248
|
|
|
394
249
|
export const onRequest = defineMiddleware((context, next) =>
|
|
395
|
-
onPreprRequest(context, next, { preview: import.meta.env.DEV })
|
|
250
|
+
onPreprRequest(context, next, { preview: import.meta.env.DEV }),
|
|
396
251
|
);
|
|
397
252
|
```
|
|
398
253
|
|
|
399
|
-
|
|
254
|
+
**Layout** — the same preview flag gates `getToolbarProps`:
|
|
400
255
|
|
|
401
256
|
```astro
|
|
402
257
|
---
|
|
@@ -404,8 +259,8 @@ import PreprToolbar from '@preprio/toolkit/astro/components/PreprToolbar';
|
|
|
404
259
|
import PreprTrackingPixel from '@preprio/toolkit/astro/components/PreprTrackingPixel';
|
|
405
260
|
import { extractAccessToken, getToolbarProps } from '@preprio/toolkit/astro';
|
|
406
261
|
|
|
407
|
-
const
|
|
408
|
-
const toolbarProps =
|
|
262
|
+
const preview = import.meta.env.DEV;
|
|
263
|
+
const toolbarProps = preview
|
|
409
264
|
? await getToolbarProps(Astro.request.headers, import.meta.env.PREPR_GRAPHQL_URL)
|
|
410
265
|
: null;
|
|
411
266
|
const accessToken = extractAccessToken(import.meta.env.PREPR_GRAPHQL_URL);
|
|
@@ -420,16 +275,22 @@ const accessToken = extractAccessToken(import.meta.env.PREPR_GRAPHQL_URL);
|
|
|
420
275
|
</html>
|
|
421
276
|
```
|
|
422
277
|
|
|
423
|
-
|
|
278
|
+
**Data fetching** — the Astro helpers are synchronous and take the request's `Headers`:
|
|
424
279
|
|
|
425
|
-
|
|
280
|
+
```typescript
|
|
281
|
+
import { getPreprHeaders } from '@preprio/toolkit/astro';
|
|
282
|
+
|
|
283
|
+
const headers = getPreprHeaders(Astro.request.headers);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`import.meta.env.DEV` is Astro's own dev flag; swap in whatever signal marks preview in your deployment.
|
|
287
|
+
|
|
288
|
+
The `.astro` components ship as source, compiled by your own Astro/Vite pipeline. They use full-page navigation by default, matching Astro's MPA model — switching segment or variant re-runs the server-rendered output.
|
|
426
289
|
|
|
427
290
|
See the runnable example at `examples/astro` for the full source of truth.
|
|
428
291
|
|
|
429
292
|
## SvelteKit
|
|
430
293
|
|
|
431
|
-
`svelte` is an optional peer dependency, needed only if you import the `.svelte` components.
|
|
432
|
-
|
|
433
294
|
**`src/hooks.server.ts`**
|
|
434
295
|
|
|
435
296
|
```typescript
|
|
@@ -439,7 +300,7 @@ import { preprHandle } from '@preprio/toolkit/sveltekit';
|
|
|
439
300
|
export const handle = preprHandle({ preview: dev });
|
|
440
301
|
```
|
|
441
302
|
|
|
442
|
-
**`src/routes/+layout.server.ts`** —
|
|
303
|
+
**`src/routes/+layout.server.ts`** — the same flag gates `getToolbarProps`:
|
|
443
304
|
|
|
444
305
|
```typescript
|
|
445
306
|
import { dev } from '$app/environment';
|
|
@@ -450,7 +311,10 @@ import type { LayoutServerLoad } from './$types';
|
|
|
450
311
|
export const load: LayoutServerLoad = async ({ request }) => {
|
|
451
312
|
if (!dev) return { toolbarProps: null };
|
|
452
313
|
|
|
453
|
-
const toolbarProps = await getToolbarProps(
|
|
314
|
+
const toolbarProps = await getToolbarProps(
|
|
315
|
+
request.headers,
|
|
316
|
+
env.PREPR_GRAPHQL_URL!,
|
|
317
|
+
);
|
|
454
318
|
return { toolbarProps };
|
|
455
319
|
};
|
|
456
320
|
```
|
|
@@ -473,9 +337,7 @@ export const load: LayoutServerLoad = async ({ request }) => {
|
|
|
473
337
|
{/if}
|
|
474
338
|
```
|
|
475
339
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
Inside `load` functions and endpoints you can also read the headers the hook already computed straight off `event.locals`:
|
|
340
|
+
**Data fetching** — in `load` functions and endpoints, read the headers the hook already computed off `event.locals`:
|
|
479
341
|
|
|
480
342
|
```typescript
|
|
481
343
|
import { getPreprHeadersFromLocals } from '@preprio/toolkit/sveltekit';
|
|
@@ -483,18 +345,20 @@ import { getPreprHeadersFromLocals } from '@preprio/toolkit/sveltekit';
|
|
|
483
345
|
const preprHeaders = getPreprHeadersFromLocals(event.locals);
|
|
484
346
|
```
|
|
485
347
|
|
|
348
|
+
The `.svelte` components ship as source (compiled by your own Vite/Svelte pipeline) and mount client-side via `onMount`, so they never run during SSR.
|
|
349
|
+
|
|
486
350
|
See the runnable example at `examples/sveltekit` for the full source of truth.
|
|
487
351
|
|
|
488
352
|
## Nuxt
|
|
489
353
|
|
|
490
|
-
|
|
354
|
+
Requires Nitro's Node.js preset (the Nuxt default).
|
|
491
355
|
|
|
492
356
|
**`server/middleware/prepr.ts`**
|
|
493
357
|
|
|
494
358
|
```typescript
|
|
495
359
|
import { handlePreprRequest } from '@preprio/toolkit/nuxt';
|
|
496
360
|
|
|
497
|
-
export default defineEventHandler(event => {
|
|
361
|
+
export default defineEventHandler((event) => {
|
|
498
362
|
handlePreprRequest(event, { preview: import.meta.dev });
|
|
499
363
|
});
|
|
500
364
|
```
|
|
@@ -512,7 +376,7 @@ const { data: toolbarProps } = await useAsyncData('prepr-toolbar', async () => {
|
|
|
512
376
|
if (!import.meta.dev) return null;
|
|
513
377
|
return getToolbarProps(
|
|
514
378
|
new Headers(requestHeaders as Record<string, string>),
|
|
515
|
-
useRuntimeConfig().public.preprGraphqlUrl
|
|
379
|
+
useRuntimeConfig().public.preprGraphqlUrl,
|
|
516
380
|
);
|
|
517
381
|
});
|
|
518
382
|
</script>
|
|
@@ -524,9 +388,7 @@ const { data: toolbarProps } = await useAsyncData('prepr-toolbar', async () => {
|
|
|
524
388
|
</template>
|
|
525
389
|
```
|
|
526
390
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
The middleware folds the computed Prepr headers back onto the incoming request — read them via `useRequestHeaders()` in composables, or straight off the h3 event in server routes:
|
|
391
|
+
**Data fetching** — the middleware folds the computed Prepr headers back onto the incoming request. Read them via `useRequestHeaders()` in composables, or straight off the h3 event in server routes:
|
|
530
392
|
|
|
531
393
|
```typescript
|
|
532
394
|
import { getPreprHeadersFromEvent } from '@preprio/toolkit/nuxt';
|
|
@@ -534,6 +396,8 @@ import { getPreprHeadersFromEvent } from '@preprio/toolkit/nuxt';
|
|
|
534
396
|
const preprHeaders = getPreprHeadersFromEvent(event);
|
|
535
397
|
```
|
|
536
398
|
|
|
399
|
+
The `.vue` components ship as source (compiled by your own Vite/Vue pipeline) and mount client-side via `onMounted`, so they never run during SSR.
|
|
400
|
+
|
|
537
401
|
See the runnable example at `examples/nuxt` for the full source of truth.
|
|
538
402
|
|
|
539
403
|
## React (no framework)
|
|
@@ -558,17 +422,17 @@ Mount `PreprPreview` once per page — two copies start two editor bridges.
|
|
|
558
422
|
|
|
559
423
|
### Without personalization
|
|
560
424
|
|
|
561
|
-
A client-rendered app with no segments and no A/B testing needs **no middleware and no server helpers at all**. The middleware exists to resolve segment and variant cookies into request headers; with both features off there are no headers to resolve. What remains
|
|
425
|
+
A client-rendered app with no segments and no A/B testing needs **no middleware and no server helpers at all**. The middleware exists to resolve segment and variant cookies into request headers; with both features off there are no headers to resolve. What remains — click-to-edit and the editor's scroll restore — is purely client-side:
|
|
562
426
|
|
|
563
427
|
```tsx
|
|
564
428
|
<PreprPreview options={{ features: { segments: false, abTesting: false } }} />
|
|
565
429
|
```
|
|
566
430
|
|
|
567
|
-
No `navigation` prop
|
|
431
|
+
No `navigation` prop or router context is needed in this configuration; the component can mount anywhere in the tree.
|
|
568
432
|
|
|
569
433
|
### With personalization
|
|
570
434
|
|
|
571
|
-
Segments and A/B testing need request headers, which need a server
|
|
435
|
+
Segments and A/B testing need request headers, which need a server — either a framework with one (React Router in framework mode, Next.js) or your own backend using the [vanilla core](#any-other-framework).
|
|
572
436
|
|
|
573
437
|
Once there is a server, pass a `navigation` adapter so segment and variant switches route softly instead of reloading:
|
|
574
438
|
|
|
@@ -581,7 +445,7 @@ const location = useLocation();
|
|
|
581
445
|
<PreprPreview
|
|
582
446
|
{...toolbarProps}
|
|
583
447
|
navigation={{
|
|
584
|
-
navigate: url => navigate(url),
|
|
448
|
+
navigate: (url) => navigate(url),
|
|
585
449
|
currentPath: () => location.pathname + location.search,
|
|
586
450
|
}}
|
|
587
451
|
/>;
|
|
@@ -591,7 +455,7 @@ const location = useLocation();
|
|
|
591
455
|
|
|
592
456
|
### Tracking pageviews
|
|
593
457
|
|
|
594
|
-
`loadTrackingPixel` queues one `pageload` event when it installs. Client-side route changes emit nothing on their own, so
|
|
458
|
+
`loadTrackingPixel` queues one `pageload` event when it installs. Client-side route changes emit nothing on their own, so fire them yourself:
|
|
595
459
|
|
|
596
460
|
```tsx
|
|
597
461
|
import { useEffect } from 'react';
|
|
@@ -622,9 +486,12 @@ import { processPreprRequest } from '@preprio/toolkit';
|
|
|
622
486
|
|
|
623
487
|
export function preprMiddleware({ preview } = {}) {
|
|
624
488
|
return (req, res, next) => {
|
|
625
|
-
const { requestHeaders, responseCookies } = processPreprRequest(
|
|
626
|
-
|
|
627
|
-
|
|
489
|
+
const { requestHeaders, responseCookies } = processPreprRequest(
|
|
490
|
+
toWebRequest(req),
|
|
491
|
+
{
|
|
492
|
+
preview,
|
|
493
|
+
},
|
|
494
|
+
);
|
|
628
495
|
|
|
629
496
|
for (const cookie of responseCookies) {
|
|
630
497
|
res.cookie(cookie.name, cookie.value, {
|
|
@@ -639,8 +506,6 @@ export function preprMiddleware({ preview } = {}) {
|
|
|
639
506
|
}
|
|
640
507
|
```
|
|
641
508
|
|
|
642
|
-
`processPreprRequest` reads no environment variables — no part of the toolkit does. Resolve the preview flag yourself before passing it in.
|
|
643
|
-
|
|
644
509
|
Forward the headers on your GraphQL fetch with `getPreprHeadersFromHeaders(requestHeaders)`, and resolve toolbar props with `getToolbarPropsFromHeaders(requestHeaders, graphqlUrl)`.
|
|
645
510
|
|
|
646
511
|
**Client** — mount the toolbar imperatively:
|
|
@@ -660,115 +525,49 @@ Serialize the server-computed props into the page — a `<script type="applicati
|
|
|
660
525
|
|
|
661
526
|
### Framework server helpers
|
|
662
527
|
|
|
663
|
-
Each wrapper exposes the same five helpers, differing only in how they reach the request headers.
|
|
664
|
-
|
|
665
|
-
| Helper | Next.js | Astro / SvelteKit / Nuxt |
|
|
666
|
-
| --- | --- | --- |
|
|
667
|
-
| `getPreprUUID` | `await getPreprUUID()` | `getPreprUUID(headers)` |
|
|
668
|
-
| `getActiveSegment` | `await getActiveSegment()` | `getActiveSegment(headers)` |
|
|
669
|
-
| `getActiveVariant` | `await getActiveVariant()` | `getActiveVariant(headers)` |
|
|
670
|
-
| `getPreprHeaders` | `await getPreprHeaders()` | `getPreprHeaders(headers)` |
|
|
671
|
-
| `getToolbarProps` | `await getToolbarProps(token)` | `await getToolbarProps(headers, token)` |
|
|
672
|
-
|
|
673
|
-
The Next.js versions read via `next/headers` and are async. The Astro, SvelteKit and Nuxt versions take a standard `Headers` (`Astro.request.headers`, `event.request.headers`, or one built from `useRequestHeaders()`) and are sync, except `getToolbarProps`.
|
|
674
|
-
|
|
675
|
-
#### `getPreprUUID()`
|
|
676
|
-
|
|
677
|
-
Returns the current visitor's Prepr Customer ID.
|
|
678
|
-
|
|
679
|
-
```typescript
|
|
680
|
-
const customerId = await getPreprUUID();
|
|
681
|
-
// 'uuid-string' or null
|
|
682
|
-
```
|
|
528
|
+
Each wrapper exposes the same five helpers, differing only in how they reach the request headers. The Next.js versions read `next/headers` and are async; the Astro, SvelteKit, and Nuxt versions take a standard `Headers` (`Astro.request.headers`, `event.request.headers`, or one built from `useRequestHeaders()`) and are sync, except `getToolbarProps`.
|
|
683
529
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const segment = await getActiveSegment();
|
|
692
|
-
// 'segment-id' or null
|
|
693
|
-
```
|
|
694
|
-
|
|
695
|
-
#### `getActiveVariant()`
|
|
696
|
-
|
|
697
|
-
Returns the active A/B testing variant.
|
|
698
|
-
|
|
699
|
-
```typescript
|
|
700
|
-
const variant = await getActiveVariant();
|
|
701
|
-
// 'A' | 'B' | null
|
|
702
|
-
```
|
|
703
|
-
|
|
704
|
-
#### `getPreprHeaders()`
|
|
705
|
-
|
|
706
|
-
Returns all Prepr headers for the request, ready to spread into a fetch.
|
|
707
|
-
|
|
708
|
-
```typescript
|
|
709
|
-
const headers = await getPreprHeaders();
|
|
710
|
-
// { 'prepr-customer-id': 'uuid', 'Prepr-Segments': 'segment-id', ... }
|
|
711
|
-
```
|
|
530
|
+
| Helper | Next.js | Astro / SvelteKit / Nuxt | Returns |
|
|
531
|
+
| ------------------ | ------------------------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
532
|
+
| `getPreprUUID` | `await getPreprUUID()` | `getPreprUUID(headers)` | Visitor's Prepr Customer ID, or `null` when the middleware did not run for this request. |
|
|
533
|
+
| `getActiveSegment` | `await getActiveSegment()` | `getActiveSegment(headers)` | Active segment ID, or `null`. |
|
|
534
|
+
| `getActiveVariant` | `await getActiveVariant()` | `getActiveVariant(headers)` | `'A'`, `'B'`, or `null`. |
|
|
535
|
+
| `getPreprHeaders` | `await getPreprHeaders()` | `getPreprHeaders(headers)` | All Prepr headers as an object, ready to spread into a fetch. |
|
|
536
|
+
| `getToolbarProps` | `await getToolbarProps(token)` | `await getToolbarProps(headers, token)` | `{ activeSegment, activeVariant, segments }`. Never throws — failures degrade to an empty segment list. |
|
|
712
537
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
Fetches the props needed to mount the toolbar. Never throws.
|
|
716
|
-
|
|
717
|
-
```typescript
|
|
718
|
-
const props = await getToolbarProps(process.env.PREPR_GRAPHQL_URL!);
|
|
719
|
-
// { activeSegment, activeVariant, segments }
|
|
720
|
-
```
|
|
538
|
+
`getPreprUUID()` returning `null` is a useful check when headers seem to be missing — it means the middleware did not cover the route.
|
|
721
539
|
|
|
722
540
|
### Token helpers
|
|
723
541
|
|
|
724
542
|
Exported from every entry point, including the core.
|
|
725
543
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
Asserts that a Prepr GraphQL URL is well formed. Returns nothing and **throws** `PreprError` on a bad value — code `MISSING_TOKEN` when empty, `INVALID_TOKEN` when it is not an HTTPS URL.
|
|
729
|
-
|
|
730
|
-
```typescript
|
|
731
|
-
validatePreprToken(process.env.PREPR_GRAPHQL_URL!); // throws PreprError if invalid
|
|
732
|
-
```
|
|
733
|
-
|
|
734
|
-
#### `extractAccessToken(url)`
|
|
735
|
-
|
|
736
|
-
Extracts the access token from a Prepr GraphQL URL — the value `PreprTrackingPixel` needs as its `id`.
|
|
737
|
-
|
|
738
|
-
Returns the token as a `string`. It never returns `null` — a URL that is malformed, not on `graphql.prepr.io`, or missing a token segment throws a `PreprError` with code `INVALID_TOKEN`.
|
|
544
|
+
- **`validatePreprToken(token)`** — asserts a Prepr GraphQL URL is well formed. Returns nothing; **throws** `PreprError` on a bad value (`MISSING_TOKEN` when empty, `INVALID_TOKEN` when not an HTTPS URL).
|
|
545
|
+
- **`extractAccessToken(url)`** — extracts the access token from a Prepr GraphQL URL — the value `PreprTrackingPixel` needs as its `id`. Always returns a `string`; a URL that is malformed, not on `graphql.prepr.io`, or missing a token segment throws `PreprError(INVALID_TOKEN)`.
|
|
739
546
|
|
|
740
547
|
```typescript
|
|
741
548
|
const token = extractAccessToken('https://graphql.prepr.io/abc123');
|
|
742
549
|
// 'abc123'
|
|
743
|
-
|
|
744
|
-
extractAccessToken('not-a-url'); // throws PreprError(INVALID_TOKEN)
|
|
745
550
|
```
|
|
746
551
|
|
|
747
552
|
### Components
|
|
748
553
|
|
|
749
554
|
#### `PreprToolbar`
|
|
750
555
|
|
|
751
|
-
The toolbar. Takes the props returned by `getToolbarProps`, and renders nothing itself — the UI is a shadow-DOM custom element mounted imperatively.
|
|
556
|
+
The toolbar. Takes the props returned by `getToolbarProps`, and renders nothing itself — the UI is a shadow-DOM custom element mounted imperatively. There is no provider to wrap your tree in; state lives in the toolbar's own store.
|
|
752
557
|
|
|
753
558
|
```tsx
|
|
754
559
|
<PreprToolbar {...toolbarProps} />
|
|
755
560
|
```
|
|
756
561
|
|
|
757
|
-
There is no provider to wrap your tree in; state lives in the toolbar's own store.
|
|
758
|
-
|
|
759
562
|
#### `PreprPreview`
|
|
760
563
|
|
|
761
564
|
From `@preprio/toolkit/react`. The framework-free equivalent of `PreprToolbar`: same preview runtime, with the router binding left to you. Renders nothing.
|
|
762
565
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
|
768
|
-
| --- | --- | --- |
|
|
769
|
-
| `activeSegment` / `activeVariant` / `segments` | — | Toolbar data, as returned by `getToolbarProps`. Optional for a headless preview. |
|
|
770
|
-
| `options` | `PreprPreviewOptions` | Feature flags, `ui`, locale, debug (see [Preview Options](#preview-options)). |
|
|
771
|
-
| `navigation` | `PreprNavigationAdapter` | Optional router binding. Defaults to `window.location`, which is correct for any router that keeps the URL bar in sync. |
|
|
566
|
+
| Prop | Type | Description |
|
|
567
|
+
| ---------------------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
|
568
|
+
| `activeSegment` / `activeVariant` / `segments` | — | Toolbar data, as returned by `getToolbarProps`. Optional for a headless preview. |
|
|
569
|
+
| `options` | `PreprPreviewOptions` | Feature flags, `ui`, locale, debug (see [Preview options](#preview-options)). |
|
|
570
|
+
| `navigation` | `PreprNavigationAdapter` | Optional router binding. Defaults to `window.location`, which is correct for any router that keeps the URL bar in sync. |
|
|
772
571
|
|
|
773
572
|
Every prop is read once on mount; changing one afterwards has no effect, since a live update would have to tear down the editor bridge and re-announce the preview. Remount to change configuration.
|
|
774
573
|
|
|
@@ -777,24 +576,29 @@ Every prop is read once on mount; changing one afterwards has no effect, since a
|
|
|
777
576
|
Loads Prepr's CDN tracking pixel on mount. Renders nothing. Exported from both `@preprio/toolkit/react` and `@preprio/toolkit/nextjs` — the same component, with nothing Next-specific about it.
|
|
778
577
|
|
|
779
578
|
```tsx
|
|
780
|
-
<PreprTrackingPixel
|
|
579
|
+
<PreprTrackingPixel
|
|
580
|
+
id={accessToken}
|
|
581
|
+
config={{ destinations: { googleTagManager: true } }}
|
|
582
|
+
/>
|
|
781
583
|
```
|
|
782
584
|
|
|
783
|
-
| Prop
|
|
784
|
-
|
|
|
785
|
-
| `id`
|
|
786
|
-
| `config` | `PreprPixelConfig` | Optional pixel configuration (see [Pixel
|
|
585
|
+
| Prop | Type | Description |
|
|
586
|
+
| -------- | ------------------ | ------------------------------------------------------------------- |
|
|
587
|
+
| `id` | `string` | Prepr tracking/access token. Required. |
|
|
588
|
+
| `config` | `PreprPixelConfig` | Optional pixel configuration (see [Pixel options](#pixel-options)). |
|
|
787
589
|
|
|
788
590
|
### Core exports
|
|
789
591
|
|
|
790
|
-
Available from `@preprio/toolkit` for apps outside
|
|
592
|
+
Available from `@preprio/toolkit` for apps outside the supported frameworks.
|
|
791
593
|
|
|
792
594
|
#### `processPreprRequest(request, options?)`
|
|
793
595
|
|
|
794
596
|
Computes the Prepr headers and cookies for a WHATWG `Request`.
|
|
795
597
|
|
|
796
598
|
```typescript
|
|
797
|
-
const { requestHeaders, responseCookies } = processPreprRequest(request, {
|
|
599
|
+
const { requestHeaders, responseCookies } = processPreprRequest(request, {
|
|
600
|
+
preview: true,
|
|
601
|
+
});
|
|
798
602
|
// requestHeaders: Headers — forward downstream and on to Prepr
|
|
799
603
|
// responseCookies: CookieSpec[] — { name, value, maxAge, path }
|
|
800
604
|
```
|
|
@@ -808,7 +612,7 @@ const controller = createPreprPreview({
|
|
|
808
612
|
props: toolbarProps,
|
|
809
613
|
options: { debug: true, locale: 'nl' },
|
|
810
614
|
navigation: {
|
|
811
|
-
navigate: url => router.push(url),
|
|
615
|
+
navigate: (url) => router.push(url),
|
|
812
616
|
currentPath: () => window.location.pathname + window.location.search,
|
|
813
617
|
reload: () => router.refresh(),
|
|
814
618
|
},
|
|
@@ -817,52 +621,25 @@ const controller = createPreprPreview({
|
|
|
817
621
|
controller.destroy();
|
|
818
622
|
```
|
|
819
623
|
|
|
820
|
-
| Option
|
|
821
|
-
|
|
|
822
|
-
| `props`
|
|
823
|
-
| `options`
|
|
824
|
-
| `navigation` | `PreprNavigationAdapter` | How segment/variant switches navigate. |
|
|
825
|
-
|
|
826
|
-
The navigation adapter is optional. Omit it and the toolbar uses `window.location.assign` — a full page load. Supply one to integrate with a client-side router; its `reload` member is optional too, and runs after a preview-mode toggle (defaulting to `window.location.reload()`). The Next.js and SvelteKit wrappers wire all of this up for you.
|
|
827
|
-
|
|
828
|
-
`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.
|
|
829
|
-
|
|
830
|
-
Chrome is skipped — while every side effect stays wired — when any of these hold: `ui: false`, `?prepr_hide_bar=true` (how the Prepr live-preview iframe suppresses the bar), or running inside the editor's iframe. See [Headless preview](#headless-preview-no-toolbar-ui).
|
|
831
|
-
|
|
832
|
-
#### `loadTrackingPixel(id, config?)`
|
|
833
|
-
|
|
834
|
-
Installs the CDN tracking pixel. Idempotent, and a no-op outside a browser.
|
|
835
|
-
|
|
836
|
-
```typescript
|
|
837
|
-
loadTrackingPixel('YOUR_ACCESS_TOKEN', {
|
|
838
|
-
destinations: { googleTagManager: true },
|
|
839
|
-
variantImpressionThreshold: 0.5,
|
|
840
|
-
});
|
|
841
|
-
```
|
|
842
|
-
|
|
843
|
-
This is a typed facade over Prepr's existing CDN pixel (`https://cdn.tracking.prepr.io/js/prepr-v2.min.js`). It reproduces the legacy `<script>` snippet's queue-stub semantics, so calls made before the CDN script finishes loading are queued and flushed once it is ready.
|
|
844
|
-
|
|
845
|
-
#### `trackEvent(name, data?)`
|
|
624
|
+
| Option | Type | Description |
|
|
625
|
+
| ------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
626
|
+
| `props` | `PreprToolbarProps` | From `getToolbarProps`. Optional — a headless preview has no segment list to pass. |
|
|
627
|
+
| `options` | `PreprPreviewOptions` | `{ debug?, locale?, features?, ui?, allowedEditorOrigins? }`. See [Preview options](#preview-options). |
|
|
628
|
+
| `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. |
|
|
846
629
|
|
|
847
|
-
|
|
630
|
+
`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.
|
|
848
631
|
|
|
849
|
-
|
|
850
|
-
trackEvent('add_to_cart', { productId: 'abc123' });
|
|
851
|
-
```
|
|
852
|
-
|
|
853
|
-
#### `setTrackingParam(key, value)`
|
|
854
|
-
|
|
855
|
-
Sets a persistent tracking parameter on the pixel.
|
|
632
|
+
#### Tracking
|
|
856
633
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
634
|
+
- **`loadTrackingPixel(id, config?)`** — installs the CDN tracking pixel. Idempotent, and a no-op outside a browser. A typed facade over Prepr's existing CDN pixel (`https://cdn.tracking.prepr.io/js/prepr-v2.min.js`), reproducing the legacy `<script>` snippet's queue-stub semantics: calls made before the CDN script loads are queued and flushed once it is ready.
|
|
635
|
+
- **`trackEvent(name, data?)`** — sends a custom tracking event: `trackEvent('add_to_cart', { productId: 'abc123' })`.
|
|
636
|
+
- **`setTrackingParam(key, value)`** — sets a persistent tracking parameter: `setTrackingParam('user_type', 'returning')`.
|
|
860
637
|
|
|
861
638
|
`trackEvent` and `setTrackingParam` are independent of `loadTrackingPixel` — they work against `window.prepr` however it got installed, including a legacy HTML snippet already on the page. If no pixel is installed at all they warn once in the console and return without throwing.
|
|
862
639
|
|
|
863
640
|
#### `stegaClean(value)`
|
|
864
641
|
|
|
865
|
-
Strips stega-encoded characters from a string. Rarely needed, since cleaning is automatic (see [Visual
|
|
642
|
+
Strips stega-encoded characters from a string. Rarely needed, since cleaning is automatic (see [Visual editing](#visual-editing)), but available when you need a clean value for comparison, sorting, or a non-DOM API.
|
|
866
643
|
|
|
867
644
|
#### Header-based server helpers
|
|
868
645
|
|
|
@@ -879,75 +656,41 @@ import {
|
|
|
879
656
|
} from '@preprio/toolkit';
|
|
880
657
|
```
|
|
881
658
|
|
|
882
|
-
Unlike `getToolbarProps`, `getPreprEnvironmentSegments(token)` throws on failure — see [Error
|
|
883
|
-
|
|
884
|
-
## Configuration Options
|
|
885
|
-
|
|
886
|
-
### Environment Variables
|
|
659
|
+
Unlike `getToolbarProps`, `getPreprEnvironmentSegments(token)` throws on failure — see [Error handling](#error-handling).
|
|
887
660
|
|
|
888
|
-
|
|
661
|
+
## Configuration
|
|
889
662
|
|
|
890
|
-
|
|
891
|
-
| --- | --- | --- | --- |
|
|
892
|
-
| `PREPR_GRAPHQL_URL` | Yes | — | Your Prepr GraphQL endpoint URL. The name is yours to choose; it is passed to `getToolbarProps` as a plain string. |
|
|
893
|
-
|
|
894
|
-
### Middleware Options
|
|
663
|
+
### Middleware options
|
|
895
664
|
|
|
896
665
|
```typescript
|
|
897
666
|
// Simple usage (creates a new NextResponse)
|
|
898
|
-
createPreprMiddleware(request, { preview
|
|
667
|
+
createPreprMiddleware(request, { preview });
|
|
899
668
|
|
|
900
669
|
// Chaining usage (folds onto an existing NextResponse)
|
|
901
|
-
createPreprMiddleware(request, response, { preview
|
|
670
|
+
createPreprMiddleware(request, response, { preview });
|
|
902
671
|
```
|
|
903
672
|
|
|
904
|
-
| Option
|
|
905
|
-
|
|
|
906
|
-
| `preview`
|
|
907
|
-
| `features` | `PreprFeatures` | Disable features app-wide. See [Feature flags](#feature-flags).
|
|
908
|
-
| `version`
|
|
673
|
+
| Option | Type | Description |
|
|
674
|
+
| ---------- | --------------- | ---------------------------------------------------------------------------------- |
|
|
675
|
+
| `preview` | `boolean` | Enables preview mode. The only gate — resolve it from your own environment signal. |
|
|
676
|
+
| `features` | `PreprFeatures` | Disable features app-wide. See [Feature flags](#feature-flags). |
|
|
677
|
+
| `version` | `string` | Override the version reported in the `Prepr-Package` header. Mainly for tests. |
|
|
909
678
|
|
|
910
|
-
### Preview
|
|
679
|
+
### Preview options
|
|
911
680
|
|
|
912
681
|
```typescript
|
|
913
682
|
createPreprPreview({ props, options: { debug: true, locale: 'nl' } });
|
|
914
683
|
```
|
|
915
684
|
|
|
916
|
-
| Option
|
|
917
|
-
|
|
|
918
|
-
| `debug`
|
|
919
|
-
| `locale`
|
|
920
|
-
| `features`
|
|
921
|
-
| `ui`
|
|
922
|
-
| `allowedEditorOrigins` | `string[]`
|
|
923
|
-
|
|
924
|
-
`features` and `ui` are independent: `features` decides *what runs*, `ui` decides *whether the toolbar is visible*.
|
|
925
|
-
|
|
926
|
-
### Headless preview (no toolbar UI)
|
|
685
|
+
| Option | Type | Default | Description |
|
|
686
|
+
| ---------------------- | --------------- | ------------- | --------------------------------------------------------------------------------------------------- |
|
|
687
|
+
| `debug` | `boolean` | `false` | Enable debug logging. |
|
|
688
|
+
| `locale` | `'en' \| 'nl'` | auto-detected | UI language. Falls back to the first supported browser language, then `en`. |
|
|
689
|
+
| `features` | `PreprFeatures` | all enabled | Which features run. See [Feature flags](#feature-flags). |
|
|
690
|
+
| `ui` | `boolean` | `true` | Mount the visible toolbar. See [Headless preview](#headless-preview-no-toolbar-ui). |
|
|
691
|
+
| `allowedEditorOrigins` | `string[]` | `*.prepr.io` | Exact editor origins allowed to drive this preview, for self-hosted editors. Replaces the wildcard. |
|
|
927
692
|
|
|
928
|
-
`
|
|
929
|
-
|
|
930
|
-
```typescript
|
|
931
|
-
// Live editing, no bar:
|
|
932
|
-
const preview = createPreprPreview({
|
|
933
|
-
options: { ui: false, features: { editMode: true } },
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
preview.destroy();
|
|
937
|
-
```
|
|
938
|
-
|
|
939
|
-
`props` is optional here — a headless preview that only wants click-to-edit or scroll restore has no segment list to pass. For scroll restore and nothing else:
|
|
940
|
-
|
|
941
|
-
```typescript
|
|
942
|
-
createPreprPreview({
|
|
943
|
-
options: {
|
|
944
|
-
ui: false,
|
|
945
|
-
features: { segments: false, abTesting: false, editMode: false },
|
|
946
|
-
},
|
|
947
|
-
});
|
|
948
|
-
```
|
|
949
|
-
|
|
950
|
-
`?prepr_hide_bar=true` and the editor's own iframe both imply `ui: false`. Outside an iframe the bridge is a no-op, so mounting unconditionally is safe. Call `createPreprPreview` **once per page** — two calls start two bridges and announce the preview twice.
|
|
693
|
+
`features` and `ui` are independent: `features` decides _what runs_, `ui` decides _whether the toolbar is visible_.
|
|
951
694
|
|
|
952
695
|
### Feature flags
|
|
953
696
|
|
|
@@ -957,38 +700,65 @@ Preview mode is the master switch. Within it, each feature can be turned off wit
|
|
|
957
700
|
import type { PreprFeatures } from '@preprio/toolkit';
|
|
958
701
|
|
|
959
702
|
export const preprFeatures: PreprFeatures = {
|
|
960
|
-
segments: false,
|
|
703
|
+
segments: false, // or { enabled: false }
|
|
961
704
|
abTesting: true,
|
|
962
705
|
editMode: { enabled: false },
|
|
963
706
|
};
|
|
964
707
|
```
|
|
965
708
|
|
|
966
|
-
| Feature
|
|
967
|
-
|
|
|
968
|
-
| `segments`
|
|
969
|
-
| `abTesting` | No A/B control or variant badge. No `Prepr-ABtesting` header, from cookie **or** `?prepr_preview_ab`. No variant cookie written.
|
|
970
|
-
| `editMode`
|
|
709
|
+
| Feature | Off means |
|
|
710
|
+
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
711
|
+
| `segments` | No segment picker. No `Prepr-Segments` header, from cookie **or** `?prepr_preview_segment`. No segment cookie written. The `_Segments` API call is skipped, saving a round-trip per page. |
|
|
712
|
+
| `abTesting` | No A/B control or variant badge. No `Prepr-ABtesting` header, from cookie **or** `?prepr_preview_ab`. No variant cookie written. |
|
|
713
|
+
| `editMode` | No Edit mode control, no click-to-edit overlay, no close-edit pill. |
|
|
971
714
|
|
|
972
715
|
Pass the **same object to both sides**. Each enforces its own half, so a feature disabled in only one place is still half-on — the UI would hide a control the middleware still honours:
|
|
973
716
|
|
|
974
717
|
```typescript
|
|
975
718
|
// server
|
|
976
|
-
createPreprMiddleware(request, { preview
|
|
719
|
+
createPreprMiddleware(request, { preview, features: preprFeatures });
|
|
977
720
|
const props = await getToolbarProps(graphqlUrl, preprFeatures);
|
|
978
721
|
|
|
979
722
|
// client
|
|
980
723
|
<PreprToolbar {...props} options={{ features: preprFeatures }} />
|
|
981
724
|
```
|
|
982
725
|
|
|
983
|
-
Every framework's `getToolbarProps` takes `features` as its last argument, and every `<PreprToolbar>` accepts an `options` prop. Keeping the object in one module both sides import is the simplest way to stay in sync.
|
|
726
|
+
Every framework's `getToolbarProps` takes `features` as its last argument, and every `<PreprToolbar>` accepts an `options` prop. Keeping the object in one module both sides import is the simplest way to stay in sync — the examples use `src/prepr-features.ts` (`shared/prepr-features.ts` in Nuxt).
|
|
984
727
|
|
|
985
728
|
Disabling a feature also stops its `segment_changed` / `variant_changed` events, and the Reset button ignores it — state the user cannot see is never rewritten.
|
|
986
729
|
|
|
987
730
|
**`editMode` does not disable the Prepr visual editor.** It governs your site's own click-to-edit affordance. When the Prepr editor frames your site it drives edit mode over its own `postMessage` handshake, which keeps working regardless — that is the CMS operating inside its own iframe, not an affordance offered to your visitors. Treat `editMode: false` as a UI choice, never as a security control.
|
|
988
731
|
|
|
989
|
-
|
|
732
|
+
### Headless preview (no toolbar UI)
|
|
733
|
+
|
|
734
|
+
`ui: false` keeps every non-visual side effect wired — click-to-edit, the editor bridge, scroll restore, cookies and headers — while rendering no chrome of its own. That is how a site gets live editing, or editor scroll restore, alongside its own UI instead of the Prepr bar.
|
|
735
|
+
|
|
736
|
+
```typescript
|
|
737
|
+
// Live editing, no bar:
|
|
738
|
+
const preview = createPreprPreview({
|
|
739
|
+
options: { ui: false, features: { editMode: true } },
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
// Later, on teardown (SPA route change, component unmount):
|
|
743
|
+
preview.destroy();
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
`props` is optional here — a headless preview that only wants click-to-edit or scroll restore has no segment list to pass. For scroll restore and nothing else:
|
|
990
747
|
|
|
991
|
-
|
|
748
|
+
```typescript
|
|
749
|
+
createPreprPreview({
|
|
750
|
+
options: {
|
|
751
|
+
ui: false,
|
|
752
|
+
features: { segments: false, abTesting: false, editMode: false },
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
Scroll restore comes free with any preview, headless or not: inside the live-preview iframe the editor saves and restores the reader's position over the `postMessage` bridge, with no extra call and no configuration.
|
|
758
|
+
|
|
759
|
+
`?prepr_hide_bar=true` and the editor's own iframe both imply `ui: false`; the bridge stays connected in each case. Outside an iframe the bridge is a no-op, so mounting unconditionally is safe. The handshake is only ever accepted from `https://<tenant>.prepr.io`, or from `allowedEditorOrigins` when set.
|
|
760
|
+
|
|
761
|
+
### Pixel options
|
|
992
762
|
|
|
993
763
|
```typescript
|
|
994
764
|
loadTrackingPixel(id, {
|
|
@@ -997,23 +767,23 @@ loadTrackingPixel(id, {
|
|
|
997
767
|
});
|
|
998
768
|
```
|
|
999
769
|
|
|
1000
|
-
| Option
|
|
1001
|
-
|
|
|
1002
|
-
| `destinations.googleTagManager` | `boolean` | Forward events to Google Tag Manager.
|
|
1003
|
-
| `variantImpressionThreshold`
|
|
770
|
+
| Option | Type | Description |
|
|
771
|
+
| ------------------------------- | --------- | ---------------------------------------------------------------------- |
|
|
772
|
+
| `destinations.googleTagManager` | `boolean` | Forward events to Google Tag Manager. |
|
|
773
|
+
| `variantImpressionThreshold` | `number` | Visibility ratio required before an A/B variant impression is counted. |
|
|
1004
774
|
|
|
1005
775
|
### Theming
|
|
1006
776
|
|
|
1007
777
|
The toolbar's shadow DOM reads its look from CSS custom properties set on the `prepr-toolbar` host element, or any ancestor — custom properties inherit through the shadow boundary. All have fallbacks, so theming is entirely optional.
|
|
1008
778
|
|
|
1009
|
-
| Custom property
|
|
1010
|
-
|
|
|
1011
|
-
| `--prepr-primary` | Toggle button, badges, status pill color
|
|
1012
|
-
| `--prepr-bg`
|
|
1013
|
-
| `--prepr-text`
|
|
1014
|
-
| `--prepr-radius`
|
|
1015
|
-
| `--prepr-shadow`
|
|
1016
|
-
| `--prepr-z-index` | Base stacking order (backdrop/panel/pills) | `10000`
|
|
779
|
+
| Custom property | Purpose | Default |
|
|
780
|
+
| ----------------- | ------------------------------------------ | ----------------------------------------- |
|
|
781
|
+
| `--prepr-primary` | Toggle button, badges, status pill color | `#4338ca` |
|
|
782
|
+
| `--prepr-bg` | Panel background | `#eef2ff` |
|
|
783
|
+
| `--prepr-text` | Reserved for future text theming | `#1f2937` |
|
|
784
|
+
| `--prepr-radius` | Corner radius (panel, radio group, inputs) | `8px` |
|
|
785
|
+
| `--prepr-shadow` | Panel drop shadow | `0px 0px 40px 0px rgba(31, 41, 55, 0.24)` |
|
|
786
|
+
| `--prepr-z-index` | Base stacking order (backdrop/panel/pills) | `10000` |
|
|
1017
787
|
|
|
1018
788
|
```css
|
|
1019
789
|
prepr-toolbar {
|
|
@@ -1057,7 +827,7 @@ The toolbar sets the segment; your data fetch decides what to do with it. Check
|
|
|
1057
827
|
- **Server helpers in client components**: the Next.js helpers read `next/headers` and are server-only. Calling them from a `'use client'` component fails at build time.
|
|
1058
828
|
- **Missing peer dependencies**: importing `/nextjs` without `next`, `react`, and `react-dom` installed will not resolve. The core and `/astro` entry points have no framework peers.
|
|
1059
829
|
|
|
1060
|
-
### Error
|
|
830
|
+
### Error handling
|
|
1061
831
|
|
|
1062
832
|
Fetch and token failures throw `PreprError`, which carries a machine-readable code:
|
|
1063
833
|
|
|
@@ -1065,7 +835,9 @@ Fetch and token failures throw `PreprError`, which carries a machine-readable co
|
|
|
1065
835
|
import { PreprError, getPreprEnvironmentSegments } from '@preprio/toolkit';
|
|
1066
836
|
|
|
1067
837
|
try {
|
|
1068
|
-
const segments = await getPreprEnvironmentSegments(
|
|
838
|
+
const segments = await getPreprEnvironmentSegments(
|
|
839
|
+
process.env.PREPR_GRAPHQL_URL!,
|
|
840
|
+
);
|
|
1069
841
|
} catch (error) {
|
|
1070
842
|
if (error instanceof PreprError) {
|
|
1071
843
|
console.log('Error code:', error.code);
|
|
@@ -1078,7 +850,7 @@ Codes: `INVALID_TOKEN`, `MISSING_TOKEN`, `HTTP_ERROR`, `FETCH_ERROR`, `INVALID_R
|
|
|
1078
850
|
|
|
1079
851
|
`getToolbarProps` is the exception — it swallows these and returns an empty segment list, so a bad token cannot crash your app.
|
|
1080
852
|
|
|
1081
|
-
### Debug
|
|
853
|
+
### Debug mode
|
|
1082
854
|
|
|
1083
855
|
```typescript
|
|
1084
856
|
createPreprPreview({ props, options: { debug: true } });
|
|
@@ -1086,7 +858,7 @@ createPreprPreview({ props, options: { debug: true } });
|
|
|
1086
858
|
|
|
1087
859
|
## How It Works
|
|
1088
860
|
|
|
1089
|
-
### Middleware
|
|
861
|
+
### Middleware
|
|
1090
862
|
|
|
1091
863
|
On each request the middleware:
|
|
1092
864
|
|
|
@@ -1094,28 +866,22 @@ On each request the middleware:
|
|
|
1094
866
|
2. **Tracks UTM parameters** — lifts `utm_*` query params into `Prepr-Context-*` headers.
|
|
1095
867
|
3. **Manages segments** — resolves the active segment from cookie or preview query param.
|
|
1096
868
|
4. **Processes A/B tests** — resolves the active variant the same way.
|
|
1097
|
-
5. **Sets headers** — makes all of the above visible to
|
|
869
|
+
5. **Sets headers** — makes all of the above visible to your server-side code.
|
|
1098
870
|
|
|
1099
871
|
Cookie and query-parameter names are a frozen wire protocol shared with the Prepr editor.
|
|
1100
872
|
|
|
1101
|
-
### Toolbar
|
|
873
|
+
### Toolbar
|
|
1102
874
|
|
|
1103
875
|
- **Segment selection** — preview the site as any configured audience segment.
|
|
1104
876
|
- **A/B testing** — toggle between variants A and B.
|
|
1105
877
|
- **Edit mode** — click-to-edit, jumping straight to the field in Prepr.
|
|
1106
878
|
- **Reset** — clear all personalization overrides.
|
|
1107
879
|
|
|
1108
|
-
The toolbar renders with Preact into a shadow-DOM custom element (`<prepr-toolbar>`), which
|
|
1109
|
-
|
|
1110
|
-
### Visual Editing
|
|
880
|
+
The toolbar renders with Preact into a shadow-DOM custom element (`<prepr-toolbar>`), which keeps its styles isolated from the host page in both directions. Segment/variant selection and edit mode are wired up for you; you only mount the toolbar and supply its props.
|
|
1111
881
|
|
|
1112
|
-
|
|
882
|
+
### Visual editing
|
|
1113
883
|
|
|
1114
|
-
|
|
1115
|
-
2. **Auto-cleans stega** — strips the invisible Unicode characters after load, so they cannot cause layout shifts with `letter-spacing` or text balancing.
|
|
1116
|
-
3. **Highlights elements** — proximity-based highlighting as the cursor moves.
|
|
1117
|
-
4. **Provides overlays** — click-to-edit affordances on editable fields.
|
|
1118
|
-
5. **Syncs with Prepr** — talks to the editor over a `postMessage` bridge when running inside the live-preview iframe.
|
|
884
|
+
With edit mode enabled, the toolkit scans for stega-encoded content, strips the invisible Unicode characters after load so they cannot cause layout shifts, highlights editable elements by cursor proximity, and talks to the Prepr editor over a `postMessage` bridge when running inside the live-preview iframe.
|
|
1119
885
|
|
|
1120
886
|
Stega cleaning is automatic. There is no need to call `vercelStegaSplit` or hand-manage hidden spans — the data attributes the toolkit relies on survive the clean, so edit mode still activates instantly.
|
|
1121
887
|
|
|
@@ -1123,13 +889,13 @@ Stega cleaning is automatic. There is no need to call `vercelStegaSplit` or hand
|
|
|
1123
889
|
|
|
1124
890
|
Runnable examples live in the repository root:
|
|
1125
891
|
|
|
1126
|
-
| Example
|
|
1127
|
-
|
|
|
1128
|
-
| `examples/nextjs`
|
|
1129
|
-
| `examples/astro`
|
|
1130
|
-
| `examples/sveltekit` | `hooks.server.ts`, `+layout.server.ts`, `.svelte` components
|
|
1131
|
-
| `examples/nuxt`
|
|
1132
|
-
| `examples/express`
|
|
892
|
+
| Example | What it shows |
|
|
893
|
+
| -------------------- | ------------------------------------------------------------------ |
|
|
894
|
+
| `examples/nextjs` | App Router, middleware, Apollo, custom image loader |
|
|
895
|
+
| `examples/astro` | Astro middleware and `.astro` components |
|
|
896
|
+
| `examples/sveltekit` | `hooks.server.ts`, `+layout.server.ts`, `.svelte` components |
|
|
897
|
+
| `examples/nuxt` | `server/middleware`, `useAsyncData`, `.vue` components |
|
|
898
|
+
| `examples/express` | Vanilla core — a hand-written adapter for an unsupported framework |
|
|
1133
899
|
|
|
1134
900
|
## Support
|
|
1135
901
|
|