@preprio/toolkit 0.1.0-beta.1
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 +903 -0
- package/dist/astro/index.cjs +319 -0
- package/dist/astro/index.d.cts +76 -0
- package/dist/astro/index.d.ts +76 -0
- package/dist/astro/index.js +287 -0
- package/dist/chunk-H7HVQSSF.js +2081 -0
- package/dist/index.cjs +2393 -0
- package/dist/index.d.cts +179 -0
- package/dist/index.d.ts +179 -0
- package/dist/index.js +306 -0
- package/dist/nextjs/components.cjs +2087 -0
- package/dist/nextjs/components.d.cts +17 -0
- package/dist/nextjs/components.d.ts +17 -0
- package/dist/nextjs/components.js +34 -0
- package/dist/nextjs/image-loader.cjs +122 -0
- package/dist/nextjs/image-loader.d.cts +10 -0
- package/dist/nextjs/image-loader.d.ts +10 -0
- package/dist/nextjs/image-loader.js +101 -0
- package/dist/nextjs/index.cjs +386 -0
- package/dist/nextjs/index.d.cts +90 -0
- package/dist/nextjs/index.d.ts +90 -0
- package/dist/nextjs/index.js +352 -0
- package/dist/pixel-DVSJQC0K.d.cts +94 -0
- package/dist/pixel-DVSJQC0K.d.ts +94 -0
- package/dist/sveltekit/index.cjs +371 -0
- package/dist/sveltekit/index.d.cts +117 -0
- package/dist/sveltekit/index.d.ts +117 -0
- package/dist/sveltekit/index.js +334 -0
- package/package.json +93 -0
- package/src/astro/PreprToolbar.astro +53 -0
- package/src/astro/PreprTrackingPixel.astro +48 -0
- package/src/sveltekit/PreprToolbar.svelte +21 -0
- package/src/sveltekit/PreprTrackingPixel.svelte +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
# @preprio/toolkit
|
|
2
|
+
|
|
3
|
+
A framework-agnostic TypeScript library that provides preview functionality, visual editing capabilities, and A/B testing for [Prepr CMS](https://prepr.io). Ships thin wrappers for Next.js, Astro, and SvelteKit on top of a vanilla core that runs anywhere.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install the package
|
|
9
|
+
npm install @preprio/toolkit
|
|
10
|
+
# or
|
|
11
|
+
pnpm add @preprio/toolkit
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Add your Prepr GraphQL URL to your `.env`:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
PREPR_GRAPHQL_URL=https://graphql.prepr.io/{YOUR_ACCESS_TOKEN}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Set up middleware in `middleware.ts`:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import type { NextRequest } from 'next/server';
|
|
24
|
+
import { createPreprMiddleware } from '@preprio/toolkit/nextjs';
|
|
25
|
+
|
|
26
|
+
export function middleware(request: NextRequest) {
|
|
27
|
+
// `preview` is the only gate. Resolve it however your deployment already
|
|
28
|
+
// does — VERCEL_ENV, NODE_ENV, a branch check, your own feature flag.
|
|
29
|
+
const preview = process.env.VERCEL_ENV !== 'production';
|
|
30
|
+
return createPreprMiddleware(request, { preview });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const config = {
|
|
34
|
+
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
|
35
|
+
};
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Add the toolbar and tracking pixel to your layout:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import type { ReactNode } from 'react';
|
|
42
|
+
import {
|
|
43
|
+
extractAccessToken,
|
|
44
|
+
getToolbarProps,
|
|
45
|
+
PreprToolbar,
|
|
46
|
+
PreprTrackingPixel,
|
|
47
|
+
} from '@preprio/toolkit/nextjs';
|
|
48
|
+
|
|
49
|
+
export default async function RootLayout({ children }: { children: ReactNode }) {
|
|
50
|
+
const preview = process.env.VERCEL_ENV !== 'production';
|
|
51
|
+
const toolbarProps = preview
|
|
52
|
+
? await getToolbarProps(process.env.PREPR_GRAPHQL_URL!)
|
|
53
|
+
: null;
|
|
54
|
+
const accessToken = extractAccessToken(process.env.PREPR_GRAPHQL_URL!);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<html lang="en">
|
|
58
|
+
<body>
|
|
59
|
+
{children}
|
|
60
|
+
{toolbarProps && <PreprToolbar {...toolbarProps} />}
|
|
61
|
+
{accessToken && <PreprTrackingPixel id={accessToken} />}
|
|
62
|
+
</body>
|
|
63
|
+
</html>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
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.
|
|
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)
|
|
77
|
+
|
|
78
|
+
Per-framework requirements:
|
|
79
|
+
|
|
80
|
+
| Entry point | Peer dependencies |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `@preprio/toolkit` (vanilla core) | none |
|
|
83
|
+
| `@preprio/toolkit/nextjs` | `next` >= 13, `react` >= 17, `react-dom` >= 17 |
|
|
84
|
+
| `@preprio/toolkit/astro` | none (`.astro` components compile in your own pipeline) |
|
|
85
|
+
| `@preprio/toolkit/sveltekit` | `svelte` (only for the `.svelte` components) |
|
|
86
|
+
|
|
87
|
+
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.
|
|
88
|
+
|
|
89
|
+
### Prepr Account Setup
|
|
90
|
+
|
|
91
|
+
1. **Create a Prepr account** at [prepr.io](https://prepr.io)
|
|
92
|
+
2. **Get your GraphQL URL**:
|
|
93
|
+
- Go to Settings → Access tokens
|
|
94
|
+
- Find your GraphQL Preview access token
|
|
95
|
+
- Copy the full GraphQL URL (e.g. `https://graphql.prepr.io/e6f7a0521f11e5149ce65b0e9f372ced2dfc923490890e7f225da1db84cxxxxx`)
|
|
96
|
+
- The URL format is always `https://graphql.prepr.io/{YOUR_ACCESS_TOKEN}`
|
|
97
|
+
3. **Enable edit mode** (required for click-to-edit in the toolbar):
|
|
98
|
+
- Open your GraphQL Preview access token
|
|
99
|
+
- Check "Enable edit mode"
|
|
100
|
+
- Save the token
|
|
101
|
+
|
|
102
|
+
## Installation & Setup
|
|
103
|
+
|
|
104
|
+
### 1. Install the Package
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm install @preprio/toolkit
|
|
108
|
+
# or
|
|
109
|
+
pnpm add @preprio/toolkit
|
|
110
|
+
# or
|
|
111
|
+
yarn add @preprio/toolkit
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 2. Environment Configuration
|
|
115
|
+
|
|
116
|
+
Create or update your `.env` file:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# Required: your Prepr GraphQL endpoint
|
|
120
|
+
PREPR_GRAPHQL_URL=https://graphql.prepr.io/{YOUR_ACCESS_TOKEN}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
> **Important**: Replace `{YOUR_ACCESS_TOKEN}` with your actual Prepr access token from Settings → Access tokens.
|
|
124
|
+
|
|
125
|
+
The toolkit reads **no environment variables at all**. It has no opinion on the *name* of the URL variable either — `getToolbarProps(token)` takes the URL as a plain string, so store it wherever you like.
|
|
126
|
+
|
|
127
|
+
### 3. The preview gate
|
|
128
|
+
|
|
129
|
+
Preview mode is controlled by exactly one thing: the `preview` option you pass to the middleware.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
createPreprMiddleware(request, { preview: process.env.VERCEL_ENV !== 'production' });
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Resolve that boolean however your deployment already distinguishes environments — `VERCEL_ENV`, `NODE_ENV`, SvelteKit's `dev`, Astro's `import.meta.env.DEV`, a branch check, or your own feature flag. Because the toolkit reads nothing from the environment itself, the behaviour is identical across every framework and every bundler.
|
|
136
|
+
|
|
137
|
+
`getToolbarProps` is ungated in all frameworks — it fetches whenever you call it. Call it only when your preview flag is on:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const toolbarProps = preview ? await getToolbarProps(token) : null;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
> **Note:** a hardcoded `{ preview: true }` enables preview everywhere, production included. Always resolve it from an environment signal.
|
|
144
|
+
|
|
145
|
+
## Next.js
|
|
146
|
+
|
|
147
|
+
### Middleware Setup
|
|
148
|
+
|
|
149
|
+
The middleware assigns the visitor's customer ID, captures UTM parameters, and resolves the active segment and A/B variant into request headers.
|
|
150
|
+
|
|
151
|
+
Create or update `middleware.ts` in your project root:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import type { NextRequest } from 'next/server';
|
|
155
|
+
import { createPreprMiddleware } from '@preprio/toolkit/nextjs';
|
|
156
|
+
|
|
157
|
+
export function middleware(request: NextRequest) {
|
|
158
|
+
return createPreprMiddleware(request, { preview: true });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const config = {
|
|
162
|
+
matcher: [
|
|
163
|
+
/*
|
|
164
|
+
* Match all request paths except for the ones starting with:
|
|
165
|
+
* - api (API routes)
|
|
166
|
+
* - _next/static (static files)
|
|
167
|
+
* - _next/image (image optimization files)
|
|
168
|
+
* - favicon.ico (favicon file)
|
|
169
|
+
*/
|
|
170
|
+
'/((?!api|_next/static|_next/image|favicon.ico).*)',
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Despite the `create` prefix, `createPreprMiddleware` runs per request — it does not return a handler.
|
|
176
|
+
|
|
177
|
+
#### Chaining with Existing Middleware
|
|
178
|
+
|
|
179
|
+
Pass an existing `NextResponse` as the second argument to fold Prepr's headers and cookies onto a response another middleware already produced:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
183
|
+
import { createPreprMiddleware } from '@preprio/toolkit/nextjs';
|
|
184
|
+
|
|
185
|
+
export function middleware(request: NextRequest) {
|
|
186
|
+
const response = NextResponse.next();
|
|
187
|
+
|
|
188
|
+
if (request.nextUrl.pathname.startsWith('/admin')) {
|
|
189
|
+
response.headers.set('x-admin-route', 'true');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return createPreprMiddleware(request, response, { preview: true });
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
##### With next-intl
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import type { NextRequest } from 'next/server';
|
|
200
|
+
import createIntlMiddleware from 'next-intl/middleware';
|
|
201
|
+
import { createPreprMiddleware } from '@preprio/toolkit/nextjs';
|
|
202
|
+
|
|
203
|
+
const intlMiddleware = createIntlMiddleware({
|
|
204
|
+
locales: ['en', 'de', 'fr'],
|
|
205
|
+
defaultLocale: 'en',
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
export function middleware(request: NextRequest) {
|
|
209
|
+
const intlResponse = intlMiddleware(request);
|
|
210
|
+
|
|
211
|
+
// If next-intl returns a redirect, return it immediately.
|
|
212
|
+
if (intlResponse.status >= 300 && intlResponse.status < 400) {
|
|
213
|
+
return intlResponse;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return createPreprMiddleware(request, intlResponse, { preview: true });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export const config = {
|
|
220
|
+
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
|
221
|
+
};
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
##### Conditional chaining
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
228
|
+
import { createPreprMiddleware } from '@preprio/toolkit/nextjs';
|
|
229
|
+
|
|
230
|
+
export function middleware(request: NextRequest) {
|
|
231
|
+
// Skip Prepr entirely for API routes.
|
|
232
|
+
if (request.nextUrl.pathname.startsWith('/api/')) {
|
|
233
|
+
return NextResponse.next();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return createPreprMiddleware(request, { preview: process.env.VERCEL_ENV !== 'production' });
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Layout Integration
|
|
241
|
+
|
|
242
|
+
`getToolbarProps` is ungated, so gate the call on the same flag you pass to the middleware:
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
import type { ReactNode } from 'react';
|
|
246
|
+
import { getToolbarProps, PreprToolbar } from '@preprio/toolkit/nextjs';
|
|
247
|
+
|
|
248
|
+
export default async function RootLayout({ children }: { children: ReactNode }) {
|
|
249
|
+
const preview = process.env.VERCEL_ENV !== 'production';
|
|
250
|
+
const toolbarProps = preview
|
|
251
|
+
? await getToolbarProps(process.env.PREPR_GRAPHQL_URL!)
|
|
252
|
+
: null;
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<html lang="en">
|
|
256
|
+
<body>
|
|
257
|
+
{children}
|
|
258
|
+
{toolbarProps && <PreprToolbar {...toolbarProps} />}
|
|
259
|
+
</body>
|
|
260
|
+
</html>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
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.
|
|
266
|
+
|
|
267
|
+
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.
|
|
268
|
+
|
|
269
|
+
### Tracking Pixel
|
|
270
|
+
|
|
271
|
+
The tracking pixel collects the interaction data that powers personalization and A/B testing. Include it in **all** environments, preview and production alike.
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
import { extractAccessToken, PreprTrackingPixel } from '@preprio/toolkit/nextjs';
|
|
275
|
+
|
|
276
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
277
|
+
const accessToken = extractAccessToken(process.env.PREPR_GRAPHQL_URL!);
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<html lang="en">
|
|
281
|
+
<body>
|
|
282
|
+
{children}
|
|
283
|
+
{accessToken && <PreprTrackingPixel id={accessToken} />}
|
|
284
|
+
</body>
|
|
285
|
+
</html>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`PreprTrackingPixel` renders nothing and loads the CDN script on mount, so its position in the tree does not matter.
|
|
291
|
+
|
|
292
|
+
### API Integration
|
|
293
|
+
|
|
294
|
+
Use `getPreprHeaders()` to forward Prepr's personalization context to your own CMS or GraphQL fetches. Without these headers Prepr cannot resolve segments or A/B variants for the request.
|
|
295
|
+
|
|
296
|
+
#### With the Fetch API
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { getPreprHeaders } from '@preprio/toolkit/nextjs';
|
|
300
|
+
|
|
301
|
+
async function getPage(slug: string) {
|
|
302
|
+
const response = await fetch(process.env.PREPR_GRAPHQL_URL!, {
|
|
303
|
+
method: 'POST',
|
|
304
|
+
headers: {
|
|
305
|
+
'Content-Type': 'application/json',
|
|
306
|
+
...(await getPreprHeaders()),
|
|
307
|
+
},
|
|
308
|
+
body: JSON.stringify({
|
|
309
|
+
query: `
|
|
310
|
+
query GetPageBySlug($slug: String!) {
|
|
311
|
+
Page(slug: $slug) {
|
|
312
|
+
title
|
|
313
|
+
content
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
`,
|
|
317
|
+
variables: { slug },
|
|
318
|
+
}),
|
|
319
|
+
cache: 'no-store',
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return response.json();
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### With Apollo Client
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import { getClient } from '@/lib/client';
|
|
330
|
+
import { GetPageBySlugDocument } from '@/gql/graphql';
|
|
331
|
+
import { getPreprHeaders } from '@preprio/toolkit/nextjs';
|
|
332
|
+
|
|
333
|
+
async function getPage(slug: string) {
|
|
334
|
+
const { data } = await getClient().query({
|
|
335
|
+
query: GetPageBySlugDocument,
|
|
336
|
+
variables: { slug },
|
|
337
|
+
context: { headers: await getPreprHeaders() },
|
|
338
|
+
fetchPolicy: 'no-cache',
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return data;
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Image Loader
|
|
346
|
+
|
|
347
|
+
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`.
|
|
348
|
+
|
|
349
|
+
Next joins `loaderFile` against the app root and checks it with `existsSync`, so a bare package specifier will not resolve. Add a one-line wrapper file in your app root:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
// prepr-image-loader.ts
|
|
353
|
+
export { default } from '@preprio/toolkit/nextjs/image-loader';
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// next.config.ts
|
|
358
|
+
export default {
|
|
359
|
+
images: {
|
|
360
|
+
loader: 'custom',
|
|
361
|
+
loaderFile: './prepr-image-loader.ts',
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
The loader only rewrites URLs on `*.stream.prepr.io` and `*.b-cdn.net`; anything else passes through untouched. Existing crop and quality transforms in the URL survive the resize.
|
|
367
|
+
|
|
368
|
+
See the runnable example at `examples/nextjs` for the full source of truth.
|
|
369
|
+
|
|
370
|
+
## Astro
|
|
371
|
+
|
|
372
|
+
### Middleware Setup
|
|
373
|
+
|
|
374
|
+
**`src/middleware.ts`**
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
import { onPreprRequest } from '@preprio/toolkit/astro';
|
|
378
|
+
import { defineMiddleware } from 'astro:middleware';
|
|
379
|
+
|
|
380
|
+
export const onRequest = defineMiddleware((context, next) =>
|
|
381
|
+
onPreprRequest(context, next, { preview: import.meta.env.DEV })
|
|
382
|
+
);
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Layout Integration
|
|
386
|
+
|
|
387
|
+
```astro
|
|
388
|
+
---
|
|
389
|
+
import PreprToolbar from '@preprio/toolkit/astro/components/PreprToolbar';
|
|
390
|
+
import PreprTrackingPixel from '@preprio/toolkit/astro/components/PreprTrackingPixel';
|
|
391
|
+
import { extractAccessToken, getToolbarProps } from '@preprio/toolkit/astro';
|
|
392
|
+
|
|
393
|
+
const isPreview = import.meta.env.DEV;
|
|
394
|
+
const toolbarProps = isPreview
|
|
395
|
+
? await getToolbarProps(Astro.request.headers, import.meta.env.PREPR_GRAPHQL_URL)
|
|
396
|
+
: null;
|
|
397
|
+
const accessToken = extractAccessToken(import.meta.env.PREPR_GRAPHQL_URL);
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
<html lang="en">
|
|
401
|
+
<body>
|
|
402
|
+
<slot />
|
|
403
|
+
{toolbarProps && <PreprToolbar {...toolbarProps} />}
|
|
404
|
+
{accessToken && <PreprTrackingPixel id={accessToken} />}
|
|
405
|
+
</body>
|
|
406
|
+
</html>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
`import.meta.env.DEV` is Astro's own dev flag; swap in whatever signal marks preview in your deployment. The same value should feed both the middleware and `getToolbarProps`.
|
|
410
|
+
|
|
411
|
+
The `.astro` components ship as source, compiled by your own Astro/Vite pipeline rather than this package's build. They use full-page navigation by default, matching Astro's MPA model — so switching segment or variant re-runs the server-rendered output.
|
|
412
|
+
|
|
413
|
+
See the runnable example at `examples/astro` for the full source of truth.
|
|
414
|
+
|
|
415
|
+
## SvelteKit
|
|
416
|
+
|
|
417
|
+
`svelte` is an optional peer dependency, needed only if you import the `.svelte` components.
|
|
418
|
+
|
|
419
|
+
**`src/hooks.server.ts`**
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
import { dev } from '$app/environment';
|
|
423
|
+
import { preprHandle } from '@preprio/toolkit/sveltekit';
|
|
424
|
+
|
|
425
|
+
export const handle = preprHandle({ preview: dev });
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**`src/routes/+layout.server.ts`** — gate `getToolbarProps` on the same flag:
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
import { dev } from '$app/environment';
|
|
432
|
+
import { env } from '$env/dynamic/private';
|
|
433
|
+
import { getToolbarProps } from '@preprio/toolkit/sveltekit';
|
|
434
|
+
import type { LayoutServerLoad } from './$types';
|
|
435
|
+
|
|
436
|
+
export const load: LayoutServerLoad = async ({ request }) => {
|
|
437
|
+
if (!dev) return { toolbarProps: null };
|
|
438
|
+
|
|
439
|
+
const toolbarProps = await getToolbarProps(request.headers, env.PREPR_GRAPHQL_URL!);
|
|
440
|
+
return { toolbarProps };
|
|
441
|
+
};
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
**`src/routes/+layout.svelte`**
|
|
445
|
+
|
|
446
|
+
```svelte
|
|
447
|
+
<script lang="ts">
|
|
448
|
+
import PreprToolbar from '@preprio/toolkit/sveltekit/components/PreprToolbar';
|
|
449
|
+
import PreprTrackingPixel from '@preprio/toolkit/sveltekit/components/PreprTrackingPixel';
|
|
450
|
+
import type { LayoutData } from './$types';
|
|
451
|
+
|
|
452
|
+
let { data, children }: { data: LayoutData; children: import('svelte').Snippet } = $props();
|
|
453
|
+
</script>
|
|
454
|
+
|
|
455
|
+
{@render children()}
|
|
456
|
+
<PreprTrackingPixel id="YOUR_ACCESS_TOKEN" />
|
|
457
|
+
{#if data.toolbarProps}
|
|
458
|
+
<PreprToolbar {...data.toolbarProps} />
|
|
459
|
+
{/if}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
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.
|
|
463
|
+
|
|
464
|
+
Inside `load` functions and endpoints you can also read the headers the hook already computed straight off `event.locals`:
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
import { getPreprHeadersFromLocals } from '@preprio/toolkit/sveltekit';
|
|
468
|
+
|
|
469
|
+
const preprHeaders = getPreprHeadersFromLocals(event.locals);
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
See the runnable example at `examples/sveltekit` for the full source of truth.
|
|
473
|
+
|
|
474
|
+
## Any other framework
|
|
475
|
+
|
|
476
|
+
The core is runtime-neutral: it works off a WHATWG `Request` and hands back the headers to forward to Prepr plus the cookies to persist. Adapting it to Express, Hono, Fastify, or a plain server is roughly twenty lines.
|
|
477
|
+
|
|
478
|
+
**Server** — translate your framework's request and response to and from those neutral shapes:
|
|
479
|
+
|
|
480
|
+
```javascript
|
|
481
|
+
import { processPreprRequest } from '@preprio/toolkit';
|
|
482
|
+
|
|
483
|
+
export function preprMiddleware({ preview } = {}) {
|
|
484
|
+
return (req, res, next) => {
|
|
485
|
+
const { requestHeaders, responseCookies } = processPreprRequest(toWebRequest(req), {
|
|
486
|
+
preview,
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
for (const cookie of responseCookies) {
|
|
490
|
+
res.cookie(cookie.name, cookie.value, {
|
|
491
|
+
maxAge: cookie.maxAge * 1000,
|
|
492
|
+
path: cookie.path,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
res.locals.preprHeaders = requestHeaders;
|
|
497
|
+
next();
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
`processPreprRequest` reads no environment variables — no part of the toolkit does. Resolve the preview flag yourself before passing it in.
|
|
503
|
+
|
|
504
|
+
Forward the headers on your GraphQL fetch with `getPreprHeadersFromHeaders(requestHeaders)`, and resolve toolbar props with `getToolbarPropsFromHeaders(requestHeaders, graphqlUrl)`.
|
|
505
|
+
|
|
506
|
+
**Client** — mount the toolbar imperatively:
|
|
507
|
+
|
|
508
|
+
```javascript
|
|
509
|
+
import { createPreprToolbar, loadTrackingPixel } from '@preprio/toolkit';
|
|
510
|
+
|
|
511
|
+
loadTrackingPixel('YOUR_ACCESS_TOKEN');
|
|
512
|
+
|
|
513
|
+
const controller = createPreprToolbar({ props: toolbarProps });
|
|
514
|
+
// later: controller.destroy()
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
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.
|
|
518
|
+
|
|
519
|
+
## API Reference
|
|
520
|
+
|
|
521
|
+
### Framework server helpers
|
|
522
|
+
|
|
523
|
+
Each wrapper exposes the same five helpers, differing only in how they reach the request headers.
|
|
524
|
+
|
|
525
|
+
| Helper | Next.js | Astro / SvelteKit |
|
|
526
|
+
| --- | --- | --- |
|
|
527
|
+
| `getPreprUUID` | `await getPreprUUID()` | `getPreprUUID(headers)` |
|
|
528
|
+
| `getActiveSegment` | `await getActiveSegment()` | `getActiveSegment(headers)` |
|
|
529
|
+
| `getActiveVariant` | `await getActiveVariant()` | `getActiveVariant(headers)` |
|
|
530
|
+
| `getPreprHeaders` | `await getPreprHeaders()` | `getPreprHeaders(headers)` |
|
|
531
|
+
| `getToolbarProps` | `await getToolbarProps(token)` | `await getToolbarProps(headers, token)` |
|
|
532
|
+
|
|
533
|
+
The Next.js versions read via `next/headers` and are async. The Astro and SvelteKit versions take a standard `Headers` (`Astro.request.headers` or `event.request.headers`) and are sync, except `getToolbarProps`.
|
|
534
|
+
|
|
535
|
+
#### `getPreprUUID()`
|
|
536
|
+
|
|
537
|
+
Returns the current visitor's Prepr Customer ID.
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
const customerId = await getPreprUUID();
|
|
541
|
+
// 'uuid-string' or null
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Returns `null` when the middleware did not run for this request — a useful check when headers seem to be missing.
|
|
545
|
+
|
|
546
|
+
#### `getActiveSegment()`
|
|
547
|
+
|
|
548
|
+
Returns the currently active segment.
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
const segment = await getActiveSegment();
|
|
552
|
+
// 'segment-id' or null
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
#### `getActiveVariant()`
|
|
556
|
+
|
|
557
|
+
Returns the active A/B testing variant.
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
const variant = await getActiveVariant();
|
|
561
|
+
// 'A' | 'B' | null
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
#### `getPreprHeaders()`
|
|
565
|
+
|
|
566
|
+
Returns all Prepr headers for the request, ready to spread into a fetch.
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
const headers = await getPreprHeaders();
|
|
570
|
+
// { 'prepr-customer-id': 'uuid', 'Prepr-Segments': 'segment-id', ... }
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
#### `getToolbarProps(token)`
|
|
574
|
+
|
|
575
|
+
Fetches the props needed to mount the toolbar. Never throws.
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
const props = await getToolbarProps(process.env.PREPR_GRAPHQL_URL!);
|
|
579
|
+
// { activeSegment, activeVariant, segments }
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Token helpers
|
|
583
|
+
|
|
584
|
+
Exported from every entry point, including the core.
|
|
585
|
+
|
|
586
|
+
#### `validatePreprToken(token)`
|
|
587
|
+
|
|
588
|
+
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.
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
validatePreprToken(process.env.PREPR_GRAPHQL_URL!); // throws PreprError if invalid
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
#### `extractAccessToken(url)`
|
|
595
|
+
|
|
596
|
+
Extracts the access token from a Prepr GraphQL URL — the value `PreprTrackingPixel` needs as its `id`.
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
const token = extractAccessToken('https://graphql.prepr.io/abc123');
|
|
600
|
+
// 'abc123' or null
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Components
|
|
604
|
+
|
|
605
|
+
#### `PreprToolbar`
|
|
606
|
+
|
|
607
|
+
The toolbar. Takes the props returned by `getToolbarProps`, and renders nothing itself — the UI is a shadow-DOM custom element mounted imperatively.
|
|
608
|
+
|
|
609
|
+
```tsx
|
|
610
|
+
<PreprToolbar {...toolbarProps} />
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
There is no provider to wrap your tree in; state lives in the toolbar's own store.
|
|
614
|
+
|
|
615
|
+
#### `PreprTrackingPixel`
|
|
616
|
+
|
|
617
|
+
Loads Prepr's CDN tracking pixel on mount. Renders nothing.
|
|
618
|
+
|
|
619
|
+
```tsx
|
|
620
|
+
<PreprTrackingPixel id={accessToken} config={{ destinations: { googleTagManager: true } }} />
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
| Prop | Type | Description |
|
|
624
|
+
| --- | --- | --- |
|
|
625
|
+
| `id` | `string` | Prepr tracking/access token. Required. |
|
|
626
|
+
| `config` | `PreprPixelConfig` | Optional pixel configuration (see [Pixel Options](#pixel-options)). |
|
|
627
|
+
|
|
628
|
+
### Core exports
|
|
629
|
+
|
|
630
|
+
Available from `@preprio/toolkit` for apps outside Next.js, Astro, and SvelteKit.
|
|
631
|
+
|
|
632
|
+
#### `processPreprRequest(request, options?)`
|
|
633
|
+
|
|
634
|
+
Computes the Prepr headers and cookies for a WHATWG `Request`.
|
|
635
|
+
|
|
636
|
+
```typescript
|
|
637
|
+
const { requestHeaders, responseCookies } = processPreprRequest(request, { preview: true });
|
|
638
|
+
// requestHeaders: Headers — forward downstream and on to Prepr
|
|
639
|
+
// responseCookies: CookieSpec[] — { name, value, maxAge, path }
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
#### `createPreprToolbar(options)`
|
|
643
|
+
|
|
644
|
+
Mounts the toolbar and returns a controller.
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
const controller = createPreprToolbar({
|
|
648
|
+
props: toolbarProps,
|
|
649
|
+
options: { debug: true, locale: 'nl' },
|
|
650
|
+
navigation: {
|
|
651
|
+
navigate: url => router.push(url),
|
|
652
|
+
currentPath: () => window.location.pathname + window.location.search,
|
|
653
|
+
reload: () => router.refresh(),
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
controller.destroy();
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
| Option | Type | Description |
|
|
661
|
+
| --- | --- | --- |
|
|
662
|
+
| `props` | `PreprToolbarProps` | Required. From `getToolbarProps`. |
|
|
663
|
+
| `options` | `PreprToolbarOptions` | `{ debug?, locale? }`. |
|
|
664
|
+
| `navigation` | `PreprNavigationAdapter` | How segment/variant switches navigate. |
|
|
665
|
+
|
|
666
|
+
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.
|
|
667
|
+
|
|
668
|
+
`createPreprToolbar` 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. It also skips mounting when `prepr_hide_bar=true` is present, which is how the Prepr live-preview iframe suppresses the bar.
|
|
669
|
+
|
|
670
|
+
#### `loadTrackingPixel(id, config?)`
|
|
671
|
+
|
|
672
|
+
Installs the CDN tracking pixel. Idempotent, and a no-op outside a browser.
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
loadTrackingPixel('YOUR_ACCESS_TOKEN', {
|
|
676
|
+
destinations: { googleTagManager: true },
|
|
677
|
+
variantImpressionThreshold: 0.5,
|
|
678
|
+
});
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
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.
|
|
682
|
+
|
|
683
|
+
#### `trackEvent(name, data?)`
|
|
684
|
+
|
|
685
|
+
Sends a custom tracking event.
|
|
686
|
+
|
|
687
|
+
```typescript
|
|
688
|
+
trackEvent('add_to_cart', { productId: 'abc123' });
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
#### `setTrackingParam(key, value)`
|
|
692
|
+
|
|
693
|
+
Sets a persistent tracking parameter on the pixel.
|
|
694
|
+
|
|
695
|
+
```typescript
|
|
696
|
+
setTrackingParam('user_type', 'returning');
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
`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.
|
|
700
|
+
|
|
701
|
+
#### `stegaClean(value)`
|
|
702
|
+
|
|
703
|
+
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.
|
|
704
|
+
|
|
705
|
+
#### Header-based server helpers
|
|
706
|
+
|
|
707
|
+
The framework wrappers are thin renames of these. Use them directly when writing your own adapter.
|
|
708
|
+
|
|
709
|
+
```typescript
|
|
710
|
+
import {
|
|
711
|
+
getPreprUUIDFromHeaders,
|
|
712
|
+
getActiveSegmentFromHeaders,
|
|
713
|
+
getActiveVariantFromHeaders,
|
|
714
|
+
getPreprHeadersFromHeaders,
|
|
715
|
+
getToolbarPropsFromHeaders,
|
|
716
|
+
getPreprEnvironmentSegments,
|
|
717
|
+
} from '@preprio/toolkit';
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
Unlike `getToolbarProps`, `getPreprEnvironmentSegments(token)` throws on failure — see [Error Handling](#error-handling).
|
|
721
|
+
|
|
722
|
+
## Configuration Options
|
|
723
|
+
|
|
724
|
+
### Environment Variables
|
|
725
|
+
|
|
726
|
+
The toolkit reads no environment variables itself. The one value you need to store is the GraphQL endpoint, under any name you like:
|
|
727
|
+
|
|
728
|
+
| Variable | Required | Default | Description |
|
|
729
|
+
| --- | --- | --- | --- |
|
|
730
|
+
| `PREPR_GRAPHQL_URL` | Yes | — | Your Prepr GraphQL endpoint URL. The name is yours to choose; it is passed to `getToolbarProps` as a plain string. |
|
|
731
|
+
|
|
732
|
+
### Middleware Options
|
|
733
|
+
|
|
734
|
+
```typescript
|
|
735
|
+
// Simple usage (creates a new NextResponse)
|
|
736
|
+
createPreprMiddleware(request, { preview: true });
|
|
737
|
+
|
|
738
|
+
// Chaining usage (folds onto an existing NextResponse)
|
|
739
|
+
createPreprMiddleware(request, response, { preview: true });
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
| Option | Type | Description |
|
|
743
|
+
| --- | --- | --- |
|
|
744
|
+
| `preview` | `boolean` | Enables preview mode. The only gate — resolve it from your own environment signal. |
|
|
745
|
+
| `version` | `string` | Override the version reported in the `Prepr-Package` header. Mainly for tests. |
|
|
746
|
+
|
|
747
|
+
### Toolbar Options
|
|
748
|
+
|
|
749
|
+
```typescript
|
|
750
|
+
createPreprToolbar({ props, options: { debug: true, locale: 'nl' } });
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
| Option | Type | Default | Description |
|
|
754
|
+
| --- | --- | --- | --- |
|
|
755
|
+
| `debug` | `boolean` | `false` | Enable debug logging. |
|
|
756
|
+
| `locale` | `'en' \| 'nl'` | auto-detected | UI language. Falls back to the first supported browser language, then `en`. |
|
|
757
|
+
|
|
758
|
+
### Pixel Options
|
|
759
|
+
|
|
760
|
+
```typescript
|
|
761
|
+
loadTrackingPixel(id, {
|
|
762
|
+
destinations: { googleTagManager: true },
|
|
763
|
+
variantImpressionThreshold: 0.5,
|
|
764
|
+
});
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
| Option | Type | Description |
|
|
768
|
+
| --- | --- | --- |
|
|
769
|
+
| `destinations.googleTagManager` | `boolean` | Forward events to Google Tag Manager. |
|
|
770
|
+
| `variantImpressionThreshold` | `number` | Visibility ratio required before an A/B variant impression is counted. |
|
|
771
|
+
|
|
772
|
+
### Theming
|
|
773
|
+
|
|
774
|
+
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.
|
|
775
|
+
|
|
776
|
+
| Custom property | Purpose | Default |
|
|
777
|
+
| --- | --- | --- |
|
|
778
|
+
| `--prepr-primary` | Toggle button, badges, status pill color | `#4338ca` |
|
|
779
|
+
| `--prepr-bg` | Panel background | `#eef2ff` |
|
|
780
|
+
| `--prepr-text` | Reserved for future text theming | `#1f2937` |
|
|
781
|
+
| `--prepr-radius` | Corner radius (panel, radio group, inputs) | `8px` |
|
|
782
|
+
| `--prepr-shadow` | Panel drop shadow | `0px 0px 40px 0px rgba(31, 41, 55, 0.24)` |
|
|
783
|
+
| `--prepr-z-index` | Base stacking order (backdrop/panel/pills) | `10000` |
|
|
784
|
+
|
|
785
|
+
```css
|
|
786
|
+
prepr-toolbar {
|
|
787
|
+
--prepr-primary: #16a34a;
|
|
788
|
+
--prepr-radius: 4px;
|
|
789
|
+
}
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
Because the toolbar renders inside a shadow root, none of your own page CSS — Tailwind resets, global styles — can leak in or affect it. These custom properties are the only supported customization surface.
|
|
793
|
+
|
|
794
|
+
### Internationalization
|
|
795
|
+
|
|
796
|
+
The toolbar UI ships with English and Dutch. Pass `locale` explicitly, or omit it to auto-detect from the browser and fall back to `en`:
|
|
797
|
+
|
|
798
|
+
```typescript
|
|
799
|
+
createPreprToolbar({ props, options: { locale: 'nl' } });
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
## Troubleshooting
|
|
803
|
+
|
|
804
|
+
### Toolbar not showing
|
|
805
|
+
|
|
806
|
+
- **Check the preview flag**: `{ preview: true }` must reach the middleware, and `getToolbarProps` must actually be called. Log the boolean you are passing — an environment check that resolves to `false` is the most common cause.
|
|
807
|
+
- **Verify the GraphQL URL**: it must match `https://graphql.prepr.io/YOUR_ACCESS_TOKEN`.
|
|
808
|
+
- **Check the token permissions**: "Enable edit mode" must be checked on the token in Prepr.
|
|
809
|
+
- **Confirm the middleware matcher**: if `config.matcher` excludes the current path, no Prepr headers were set for it. `getPreprUUID()` returning `null` confirms this.
|
|
810
|
+
- **Check for `prepr_hide_bar=true`** in the URL, which suppresses the bar by design.
|
|
811
|
+
|
|
812
|
+
### Headers not working
|
|
813
|
+
|
|
814
|
+
- **Middleware setup**: confirm `middleware.ts` sits in the project root (or `src/`) and its matcher covers the route.
|
|
815
|
+
- **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.
|
|
816
|
+
- **Chained middleware**: when chaining, pass the existing `NextResponse` as the second argument. Creating a fresh `NextResponse.next()` afterwards discards Prepr's request headers.
|
|
817
|
+
|
|
818
|
+
### Segments switch but content does not change
|
|
819
|
+
|
|
820
|
+
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.
|
|
821
|
+
|
|
822
|
+
### Build issues
|
|
823
|
+
|
|
824
|
+
- **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.
|
|
825
|
+
- **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.
|
|
826
|
+
|
|
827
|
+
### Error Handling
|
|
828
|
+
|
|
829
|
+
Fetch and token failures throw `PreprError`, which carries a machine-readable code:
|
|
830
|
+
|
|
831
|
+
```typescript
|
|
832
|
+
import { PreprError, getPreprEnvironmentSegments } from '@preprio/toolkit';
|
|
833
|
+
|
|
834
|
+
try {
|
|
835
|
+
const segments = await getPreprEnvironmentSegments(process.env.PREPR_GRAPHQL_URL!);
|
|
836
|
+
} catch (error) {
|
|
837
|
+
if (error instanceof PreprError) {
|
|
838
|
+
console.log('Error code:', error.code);
|
|
839
|
+
console.log('Context:', error.context);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
Codes: `INVALID_TOKEN`, `MISSING_TOKEN`, `HTTP_ERROR`, `FETCH_ERROR`, `INVALID_RESPONSE`, `CONTEXT_ERROR`.
|
|
845
|
+
|
|
846
|
+
`getToolbarProps` is the exception — it swallows these and returns an empty segment list, so a bad token cannot crash your app.
|
|
847
|
+
|
|
848
|
+
### Debug Mode
|
|
849
|
+
|
|
850
|
+
```typescript
|
|
851
|
+
createPreprToolbar({ props, options: { debug: true } });
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
## How It Works
|
|
855
|
+
|
|
856
|
+
### Middleware Functionality
|
|
857
|
+
|
|
858
|
+
On each request the middleware:
|
|
859
|
+
|
|
860
|
+
1. **Generates customer IDs** — assigns a unique visitor identifier, persisted in the `__prepr_uid` cookie.
|
|
861
|
+
2. **Tracks UTM parameters** — lifts `utm_*` query params into `Prepr-Context-*` headers.
|
|
862
|
+
3. **Manages segments** — resolves the active segment from cookie or preview query param.
|
|
863
|
+
4. **Processes A/B tests** — resolves the active variant the same way.
|
|
864
|
+
5. **Sets headers** — makes all of the above visible to Server Components and route handlers via `headers()`.
|
|
865
|
+
|
|
866
|
+
Cookie and query-parameter names are a frozen wire protocol shared with the Prepr editor.
|
|
867
|
+
|
|
868
|
+
### Toolbar Features
|
|
869
|
+
|
|
870
|
+
- **Segment selection** — preview the site as any configured audience segment.
|
|
871
|
+
- **A/B testing** — toggle between variants A and B.
|
|
872
|
+
- **Edit mode** — click-to-edit, jumping straight to the field in Prepr.
|
|
873
|
+
- **Reset** — clear all personalization overrides.
|
|
874
|
+
|
|
875
|
+
The toolbar renders with Preact into a shadow-DOM custom element (`<prepr-toolbar>`), which is what 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.
|
|
876
|
+
|
|
877
|
+
### Visual Editing
|
|
878
|
+
|
|
879
|
+
With edit mode enabled, the toolkit:
|
|
880
|
+
|
|
881
|
+
1. **Scans content** — finds editable text via its stega encoding.
|
|
882
|
+
2. **Auto-cleans stega** — strips the invisible Unicode characters after load, so they cannot cause layout shifts with `letter-spacing` or text balancing.
|
|
883
|
+
3. **Highlights elements** — proximity-based highlighting as the cursor moves.
|
|
884
|
+
4. **Provides overlays** — click-to-edit affordances on editable fields.
|
|
885
|
+
5. **Syncs with Prepr** — talks to the editor over a `postMessage` bridge when running inside the live-preview iframe.
|
|
886
|
+
|
|
887
|
+
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.
|
|
888
|
+
|
|
889
|
+
## Examples
|
|
890
|
+
|
|
891
|
+
Runnable examples live in the repository root:
|
|
892
|
+
|
|
893
|
+
| Example | What it shows |
|
|
894
|
+
| --- | --- |
|
|
895
|
+
| `examples/nextjs` | App Router, middleware, Apollo, custom image loader |
|
|
896
|
+
| `examples/astro` | Astro middleware and `.astro` components |
|
|
897
|
+
| `examples/sveltekit` | `hooks.server.ts`, `+layout.server.ts`, `.svelte` components |
|
|
898
|
+
| `examples/express` | Vanilla core — a hand-written adapter for an unsupported framework |
|
|
899
|
+
|
|
900
|
+
## Support
|
|
901
|
+
|
|
902
|
+
- **Documentation**: [Prepr Documentation](https://docs.prepr.io)
|
|
903
|
+
- **Support**: [Prepr Support](https://prepr.io/support)
|