@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 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)