Next.js package for Prepr CMS preview functionality with advanced debugging and visual editing capabilities
A powerful TypeScript library that provides preview functionality, visual editing capabilities, and A/B testing for Prepr CMS integrated with Next.js applications.
``bash`Install the package
npm install @preprio/prepr-nextjsor
pnpm add @preprio/prepr-nextjs
Add environment variables to your .env:
`bash`
PREPR_GRAPHQL_URL=https://graphql.prepr.io/{YOUR_ACCESS_TOKEN}
PREPR_ENV=preview
Set up middleware in middleware.ts:
`typescript
import type { NextRequest } from 'next/server'
import createPreprMiddleware from '@preprio/prepr-nextjs/middleware'
export function middleware(request: NextRequest) {
return createPreprMiddleware(request, { preview: true })
}
`
Add toolbar and tracking to your layout:
`typescript
import { getToolbarProps, extractAccessToken } from '@preprio/prepr-nextjs/server'
import { PreprToolbar, PreprToolbarProvider, PreprTrackingPixel } from '@preprio/prepr-nextjs/react'
import '@preprio/prepr-nextjs/index.css'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const toolbarProps = await getToolbarProps(process.env.PREPR_GRAPHQL_URL!)
const accessToken = extractAccessToken(process.env.PREPR_GRAPHQL_URL!)
return (
📋 Prerequisites
Before installing, ensure you have:
- Next.js 13.0.0 or later (supports App Router)
- React 17.0.0 or later (React 18+ recommended)
- Node.js 16.0.0 or later
- A Prepr account
- Prepr GraphQL URL (found in Settings → Access tokens)
$3
1. Create a Prepr account at prepr.io
2. Get your GraphQL URL:
- Go to Settings → Access tokens
- Find your GraphQL Preview access token
!preview API URL
- Copy the full GraphQL URL (e.g.,
https://graphql.prepr.io/e6f7a0521f11e5149ce65b0e9f372ced2dfc923490890e7f225da1db84cxxxxx)
- The URL format is always https://graphql.prepr.io/{YOUR_ACCESS_TOKEN}
3. Enable edit mode (for toolbar):
- Open your GraphQL Preview access token
- Check "Enable edit mode"
- Save the token
!Preview access token🔧 Installation & Setup
$3
`bash
npm install @preprio/prepr-nextjs
or
pnpm add @preprio/prepr-nextjs
or
yarn add @preprio/prepr-nextjs
`$3
Create or update your
.env file:`bash
Required: Your Prepr GraphQL endpoint
PREPR_GRAPHQL_URL=https://graphql.prepr.io/{YOUR_ACCESS_TOKEN}Required: Environment mode
PREPR_ENV=preview # Use 'preview' for staging/development
PREPR_ENV=production # Use 'production' for live sites
Optional: Enable debug logging (development only)
PREPR_DEBUG=true
`> Important: Replace
{YOUR_ACCESS_TOKEN} with your actual Prepr access token from Settings → Access tokens.$3
The middleware handles personalization headers, customer ID tracking, and preview mode functionality.
#### TypeScript Setup
Create or update
middleware.ts in your project root:`typescript
import type { NextRequest } from 'next/server'
import createPreprMiddleware from '@preprio/prepr-nextjs/middleware'export function middleware(request: NextRequest) {
return createPreprMiddleware(request, {
preview: process.env.PREPR_ENV === 'preview'
})
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
`#### JavaScript Setup
Create or update
middleware.js in your project root:`javascript
import createPreprMiddleware from '@preprio/prepr-nextjs/middleware'export async function middleware(request) {
return createPreprMiddleware(request, {
preview: process.env.PREPR_ENV === 'preview'
})
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
`#### Chaining with Existing Middleware
##### Basic Chaining Pattern
`typescript
import type { NextRequest, NextResponse } from 'next/server'
import createPreprMiddleware from '@preprio/prepr-nextjs/middleware'export function middleware(request: NextRequest) {
// Start with your existing middleware
let response = NextResponse.next()
// Add your custom logic
if (request.nextUrl.pathname.startsWith('/admin')) {
response.headers.set('x-admin-route', 'true')
}
// Chain with Prepr middleware
return createPreprMiddleware(request, response, {
preview: process.env.PREPR_ENV === 'preview'
})
}
`##### With next-intl Integration
`typescript
import type { NextRequest } from 'next/server'
import createIntlMiddleware from 'next-intl/middleware'
import createPreprMiddleware from '@preprio/prepr-nextjs/middleware'const intlMiddleware = createIntlMiddleware({
locales: ['en', 'de', 'fr'],
defaultLocale: 'en'
})
export function middleware(request: NextRequest) {
// First run internationalization middleware
const intlResponse = intlMiddleware(request)
// If next-intl returns a redirect, return it immediately
if (intlResponse.status >= 300 && intlResponse.status < 400) {
return intlResponse
}
// Otherwise, chain with Prepr middleware
return createPreprMiddleware(request, intlResponse, {
preview: process.env.PREPR_ENV === 'preview'
})
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
`##### Advanced Chaining with Multiple Middlewares
`typescript
import type { NextRequest, NextResponse } from 'next/server'
import createPreprMiddleware from '@preprio/prepr-nextjs/middleware'
import { authMiddleware } from '@/lib/auth'
import { rateLimitMiddleware } from '@/lib/rate-limit'export function middleware(request: NextRequest) {
let response = NextResponse.next()
// 1. Rate limiting
response = rateLimitMiddleware(request, response)
// 2. Authentication (if needed)
if (request.nextUrl.pathname.startsWith('/dashboard')) {
response = authMiddleware(request, response)
}
// 3. Custom headers
response.headers.set('x-custom-header', 'my-value')
// 4. Finally, Prepr middleware
return createPreprMiddleware(request, response, {
preview: process.env.PREPR_ENV === 'preview'
})
}
`##### Conditional Chaining
`typescript
import type { NextRequest, NextResponse } from 'next/server'
import createPreprMiddleware from '@preprio/prepr-nextjs/middleware'export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Skip Prepr middleware for API routes
if (pathname.startsWith('/api/')) {
return NextResponse.next()
}
// Apply different logic based on route
let response = NextResponse.next()
if (pathname.startsWith('/blog')) {
response.headers.set('x-content-type', 'blog')
} else if (pathname.startsWith('/product')) {
response.headers.set('x-content-type', 'product')
}
// Always apply Prepr middleware for content routes
return createPreprMiddleware(request, response, {
preview: process.env.PREPR_ENV === 'preview'
})
}
`$3
#### App Router (Next.js 13+)
The toolbar should only be rendered in preview environments to avoid showing development tools in production. Here are several approaches for conditional rendering:
##### Basic Conditional Rendering
Update your
app/layout.tsx:`typescript
import { getToolbarProps } from '@preprio/prepr-nextjs/server'
import {
PreprToolbar,
PreprToolbarProvider
} from '@preprio/prepr-nextjs/react'
import '@preprio/prepr-nextjs/index.css'export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const isPreview = process.env.PREPR_ENV === 'preview'
const toolbarProps = isPreview ? await getToolbarProps(process.env.PREPR_GRAPHQL_URL!) : null
return (
{isPreview && toolbarProps ? (
{children}
) : (
children
)}
)
}
`##### Advanced Conditional Rendering with Error Handling
For production applications, you may want more robust error handling:
`typescript
import { getToolbarProps } from '@preprio/prepr-nextjs/server'
import {
PreprToolbar,
PreprToolbarProvider
} from '@preprio/prepr-nextjs/react'
import '@preprio/prepr-nextjs/index.css'export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const isPreview = process.env.PREPR_ENV === 'preview'
const graphqlUrl = process.env.PREPR_GRAPHQL_URL
// Only fetch toolbar props in preview mode with valid URL
let toolbarProps = null
if (isPreview && graphqlUrl) {
try {
toolbarProps = await getToolbarProps(graphqlUrl)
} catch (error) {
console.error('Failed to load toolbar:', error)
// Continue without toolbar instead of breaking the app
}
}
return (
{isPreview && toolbarProps ? (
{children}
) : (
children
)}
)
}
`##### Component-Level Conditional Rendering
For better separation of concerns, create a dedicated component:
`typescript
// components/PreviewWrapper.tsx
import { getToolbarProps } from '@preprio/prepr-nextjs/server'
import {
PreprToolbar,
PreprToolbarProvider
} from '@preprio/prepr-nextjs/react'interface PreviewWrapperProps {
children: React.ReactNode
}
export default async function PreviewWrapper({ children }: PreviewWrapperProps) {
const isPreview = process.env.PREPR_ENV === 'preview'
if (!isPreview) {
return <>{children}>
}
const toolbarProps = await getToolbarProps(process.env.PREPR_GRAPHQL_URL!)
return (
{children}
)
}
// app/layout.tsx
import PreviewWrapper from '@/components/PreviewWrapper'
import '@preprio/prepr-nextjs/index.css'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
{children}
)
}
`#### Why Conditional Rendering Matters
1. Performance: Prevents unnecessary API calls in production
2. Security: Avoids exposing preview functionality to end users
3. Bundle Size: Excludes toolbar code from production builds
4. User Experience: Ensures clean production UI without development tools
#### Best Practices for Conditional Rendering
- Environment Variables: Always use environment variables to control preview mode
- Error Boundaries: Wrap preview components in error boundaries to prevent crashes
- Fallback UI: Always provide a fallback when toolbar fails to load
- TypeScript Safety: Use proper type guards when checking conditions
- Bundle Optimization: Consider dynamic imports for preview-only code
`typescript
// Dynamic import example for advanced optimization
const ToolbarDynamic = dynamic(
() => import('@preprio/prepr-nextjs/react').then(mod => mod.PreprToolbar),
{
ssr: false,
loading: () => Loading preview tools...
}
)
`$3
The tracking pixel is essential for collecting user interaction data and enabling personalization features. It should be included in all environments (both preview and production).
#### Basic Setup
Add the tracking pixel to your layout's
section:`typescript
import { extractAccessToken } from '@preprio/prepr-nextjs/server'
import { PreprTrackingPixel } from '@preprio/prepr-nextjs/react'export default function RootLayout({ children }: { children: React.ReactNode }) {
const accessToken = extractAccessToken(process.env.PREPR_GRAPHQL_URL!)
return (
{accessToken && }
{children}
)
}
`#### Alternative: Body Placement
You can also place the tracking pixel in the body if needed:
`typescript
export default function RootLayout({ children }: { children: React.ReactNode }) {
const accessToken = extractAccessToken(process.env.PREPR_GRAPHQL_URL!)
return (
{accessToken && }
{children}
)
}
`$3
Use the
getPreprHeaders() helper function in your data fetching to enable personalization and A/B testing:#### With Apollo Client
`typescript
import { getClient } from '@/lib/client'
import { GetPageBySlugDocument } from '@/gql/graphql'
import { getPreprHeaders } from '@preprio/prepr-nextjs/server'const getData = async (slug: string) => {
const { data } = await getClient().query({
query: GetPageBySlugDocument,
variables: { slug },
context: {
headers: await getPreprHeaders(),
},
fetchPolicy: 'no-cache',
})
return data
}
`#### With Fetch API
`typescript
import { getPreprHeaders } from '@preprio/prepr-nextjs/server'const getData = async (slug: string) => {
const headers = await getPreprHeaders()
const response = await fetch(process.env.PREPR_GRAPHQL_URL!, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers,
},
body: JSON.stringify({
query:
,
variables: { slug },
}),
})
return response.json()
}
`🎛️ API Reference
$3
####
getPreprHeaders()
Returns all Prepr headers for API requests.`typescript
import { getPreprHeaders } from '@preprio/prepr-nextjs/server'const headers = await getPreprHeaders()
// Returns: { 'prepr-customer-id': 'uuid', 'Prepr-Segments': 'segment-id', ... }
`####
getPreprUUID()
Returns the current customer ID from headers.`typescript
import { getPreprUUID } from '@preprio/prepr-nextjs/server'const customerId = await getPreprUUID()
// Returns: 'uuid-string' or null
`####
getActiveSegment()
Returns the currently active segment.`typescript
import { getActiveSegment } from '@preprio/prepr-nextjs/server'const segment = await getActiveSegment()
// Returns: 'segment-id' or null
`####
getActiveVariant()
Returns the currently active A/B testing variant.`typescript
import { getActiveVariant } from '@preprio/prepr-nextjs/server'const variant = await getActiveVariant()
// Returns: 'A' | 'B' | null
`####
getToolbarProps()
Fetches all necessary props for the toolbar component.`typescript
import { getToolbarProps } from '@preprio/prepr-nextjs/server'const props = await getToolbarProps(process.env.PREPR_GRAPHQL_URL!)
// Returns: { activeSegment, activeVariant, data }
`####
validatePreprToken()
Validates a Prepr GraphQL URL format.`typescript
import { validatePreprToken } from '@preprio/prepr-nextjs/server'const result = validatePreprToken('https://graphql.prepr.io/YOUR_ACCESS_TOKEN')
// Returns: { valid: boolean, error?: string }
`####
isPreviewMode()
Checks if the current environment is in preview mode.`typescript
import { isPreviewMode } from '@preprio/prepr-nextjs/server'const isPreview = isPreviewMode()
// Returns: boolean
`####
extractAccessToken()
Extracts the access token from a Prepr GraphQL URL.`typescript
import { extractAccessToken } from '@preprio/prepr-nextjs/server'const token = extractAccessToken('https://graphql.prepr.io/abc123')
// Returns: 'abc123' or null
`$3
####
PreprToolbarProvider
Context provider that wraps your app with toolbar functionality.`typescript
import { PreprToolbarProvider } from '@preprio/prepr-nextjs/react'
{children}
`####
PreprToolbar
The main toolbar component.`typescript
import { PreprToolbar } from '@preprio/prepr-nextjs/react'
`####
PreprTrackingPixel
User tracking component that loads the Prepr tracking script.`typescript
import { PreprTrackingPixel } from '@preprio/prepr-nextjs/react'
`🔧 Configuration Options
$3
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
|
PREPR_GRAPHQL_URL | Yes | - | Your Prepr GraphQL endpoint URL |
| PREPR_ENV | Yes | - | Environment mode (preview or production) |$3
`typescript
// Simple usage (creates new NextResponse)
createPreprMiddleware(request, {
preview: boolean // Enable preview mode functionality
})// Chaining usage (uses existing NextResponse)
createPreprMiddleware(request, response, {
preview: boolean // Enable preview mode functionality
})
`$3
`typescript
props={toolbarProps}
options={{
debug: true, // Enable debug logging
locale: 'en', // Set UI locale (e.g., 'en', 'nl')
}}
>
`$3
This package includes simple, built-in i18n support with JSON dictionaries. You can:
- Pass a
locale through PreprToolbarProvider options
- Read the current locale from the store
- Translate UI strings via a useTranslations hook
- Omit locale to auto-detect from the browser (supports en and nl, defaults to en)Usage:
`tsx
import { PreprToolbarProvider, PreprToolbar, useTranslations } from '@preprio/prepr-nextjs/react'export default function Layout({ children }: { children: React.ReactNode }) {
const toolbarProps = / ... /
return (
{children}
)
}
function ExampleComponent() {
const { t, locale } = useTranslations()
return (
{t('common.viewingAs')}
Locale: {locale}
)
}
`To add or customize translations, edit the JSON files under
src/i18n/locales/ (e.g., en.json, nl.json).🚨 Troubleshooting
$3
#### Toolbar Not Showing
- Check environment: Ensure
PREPR_ENV=preview is set
- Verify GraphQL URL: Make sure PREPR_GRAPHQL_URL is correct and follows the format https://graphql.prepr.io/YOUR_ACCESS_TOKEN
- Check token permissions: Ensure "Enable edit mode" is checked in Prepr#### Headers Not Working
- Middleware setup: Verify middleware is properly configured
- API calls: Ensure you're using
getPreprHeaders() in your API calls
- Environment: Check that environment variables are loaded#### TypeScript Errors
- Version compatibility: Ensure you're using compatible versions of Next.js and React
- Type imports: Import types from
@preprio/prepr-nextjs/types#### Build Issues
- CSS imports: Make sure to import the CSS file in your layout
- Server components: Ensure server functions are only called in server components
$3
The package includes comprehensive error handling:
`typescript
import { PreprError } from '@preprio/prepr-nextjs/server'try {
const segments = await getPreprEnvironmentSegments(process.env.PREPR_GRAPHQL_URL!)
} catch (error) {
if (error instanceof PreprError) {
console.log('Error code:', error.code)
console.log('Context:', error.context)
}
}
`$3
Enable debug logging in development:
`typescript
props={toolbarProps}
options={{ debug: true }}
>
`$3
Note: This section only applies to versions 2.1.* and below. Version 2.2.0+ handles stega encoding automatically.
In older versions, stega-encoded text could cause layout shifts with
letter-spacing or ReactWrapBalancer. You needed to manually clean the text using vercelStegaSplit:`typescript
import { vercelStegaSplit } from '@vercel/stega'export default function Component({ encodedText }: { encodedText: string }) {
const { cleaned, encoded } = vercelStegaSplit(encodedText)
return (
{cleaned}
{encoded && (
{encoded}
)}
)
}
`Key points for manual handling (legacy approach):
- Place
data-prepr-edit-target on the root element (e.g., )
- Render the cleaned text normally
- Put encoded text in a hidden for edit mode detectionsuppressHydrationWarning
- Add to prevent React warnings
Upgrade to v2.2.0+ to eliminate this manual work and benefit from automatic cleaning!
The middleware automatically:
1. Generates customer IDs: Creates unique visitor identifiers
2. Tracks UTM parameters: Captures marketing campaign data
3. Manages segments: Handles audience segmentation
4. Processes A/B tests: Manages variant assignments
5. Sets headers: Adds necessary headers for API calls
The toolbar provides:
- Segment selection: Switch between different audience segments
- A/B testing: Toggle between variants A and B
- Edit mode: Visual content editing capabilities
- Reset functionality: Clear personalization settings
When edit mode is enabled, the package:
1. Auto-cleans stega encoding (v2.2.0+): Automatically removes invisible Unicode characters that cause layout shifts
2. Scans content: Identifies editable content using Stega encoding
3. Highlights elements: Shows proximity-based highlighting
4. Provides overlays: Click-to-edit functionality
5. Syncs with Prepr: Direct integration with Prepr's editing interface
#### Automatic Stega Cleaning (v2.2.0+)
Version 2.2.0 introduces automatic stega text cleaning in preview mode:
- Stega-encoded text is automatically cleaned after page load
- No layout shifts from invisible Unicode characters
- Works seamlessly with letter-spacing and ReactWrapBalancer`
- Data attributes persist for instant edit mode activation
- Backward compatible with manual split patterns
No manual stega handling is required in v2.2.0 and later!
If you're upgrading from v1, please follow the Upgrade Guide for detailed migration instructions.
MIT License - see the LICENSE file for details.
- Documentation: Prepr Documentation
- Issues: GitHub Issues
- Support: Prepr Support