UNPKG

@preprio/prepr-nextjs

Version:

Next.js package for Prepr CMS preview functionality with advanced debugging and visual editing capabilities

796 lines (607 loc) 20.9 kB
# Prepr Next.js Package A powerful TypeScript library that provides preview functionality, visual editing capabilities, and A/B testing for [Prepr CMS](https://prepr.io) integrated with Next.js applications. ## ⚡ Quick Start ```bash # Install the package npm install @preprio/prepr-nextjs # or 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 ( <html> <head> {accessToken && <PreprTrackingPixel accessToken={accessToken!} />} </head> <body> <PreprToolbarProvider props={toolbarProps}> <PreprToolbar /> {children} </PreprToolbarProvider> </body> </html> ) } ``` ## 📋 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) ### Prepr Account Setup 1. **Create a Prepr account** at [prepr.io](https://prepr.io) 2. **Get your GraphQL URL**: - Go to Settings → Access tokens - Find your GraphQL Preview access token ![preview API URL](https://assets-site.prepr.io//35k5a4g45wuy-preview-access-token.png) - 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](https://assets-site.prepr.io/229kaekn7m96//preview-access-token-enable-edit-mode.png) ## 🔧 Installation & Setup ### 1. Install the Package ```bash npm install @preprio/prepr-nextjs # or pnpm add @preprio/prepr-nextjs # or yarn add @preprio/prepr-nextjs ``` ### 2. Environment Configuration 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. Middleware Setup 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' }) } ``` ### 4. Layout Integration #### 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 ( <html lang="en"> <body> {isPreview && toolbarProps ? ( <PreprToolbarProvider props={toolbarProps}> <PreprToolbar /> {children} </PreprToolbarProvider> ) : ( children )} </body> </html> ) } ``` ##### 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 ( <html lang="en"> <body> {isPreview && toolbarProps ? ( <PreprToolbarProvider props={toolbarProps}> <PreprToolbar /> {children} </PreprToolbarProvider> ) : ( children )} </body> </html> ) } ``` ##### 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 ( <PreprToolbarProvider props={toolbarProps}> <PreprToolbar /> {children} </PreprToolbarProvider> ) } // app/layout.tsx import PreviewWrapper from '@/components/PreviewWrapper' import '@preprio/prepr-nextjs/index.css' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <PreviewWrapper> {children} </PreviewWrapper> </body> </html> ) } ``` #### 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: () => <div>Loading preview tools...</div> } ) ``` ### 5. User Tracking Setup 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 `<head>` 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 ( <html> <head> {accessToken && <PreprTrackingPixel accessToken={accessToken!} />} </head> <body>{children}</body> </html> ) } ``` #### 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 ( <html> <body> {accessToken && <PreprTrackingPixel accessToken={accessToken!} />} {children} </body> </html> ) } ``` ### 6. API Integration 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: ` query GetPageBySlug($slug: String!) { Page(slug: $slug) { title content } } `, variables: { slug }, }), }) return response.json() } ``` ## 🎛️ API Reference ### Server Functions #### `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 ``` ### React Components #### `PreprToolbarProvider` Context provider that wraps your app with toolbar functionality. ```typescript import { PreprToolbarProvider } from '@preprio/prepr-nextjs/react' <PreprToolbarProvider props={toolbarProps}> {children} </PreprToolbarProvider> ``` #### `PreprToolbar` The main toolbar component. ```typescript import { PreprToolbar } from '@preprio/prepr-nextjs/react' <PreprToolbar /> ``` #### `PreprTrackingPixel` User tracking component that loads the Prepr tracking script. ```typescript import { PreprTrackingPixel } from '@preprio/prepr-nextjs/react' <PreprTrackingPixel accessToken="your-access-token" /> ``` ## 🔧 Configuration Options ### Environment Variables | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `PREPR_GRAPHQL_URL` | Yes | - | Your Prepr GraphQL endpoint URL | | `PREPR_ENV` | Yes | - | Environment mode (`preview` or `production`) | ### Middleware Options ```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 }) ``` ### Toolbar Options ```typescript <PreprToolbarProvider props={toolbarProps} options={{ debug: true // Enable debug logging }} > ``` ## 🚨 Troubleshooting ### Common Issues #### 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 ### Error Handling 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) } } ``` ### Debug Mode Enable debug logging in development: ```typescript <PreprToolbarProvider props={toolbarProps} options={{ debug: true }} > ``` ## 📊 How It Works ### Middleware Functionality 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 ### Toolbar Features 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 ### Visual Editing When edit mode is enabled, the package: 1. **Scans content**: Identifies editable content using Stega encoding 2. **Highlights elements**: Shows proximity-based highlighting 3. **Provides overlays**: Click-to-edit functionality 4. **Syncs with Prepr**: Direct integration with Prepr's editing interface ## 🔄 Upgrading from v1 to v2 If you're upgrading from v1, please follow the [Upgrade Guide](./UPGRADE_GUIDE.md) for detailed migration instructions. ## 📜 License MIT License - see the [LICENSE](./LICENSE) file for details. ## 🆘 Support - **Documentation**: [Prepr Documentation](https://docs.prepr.io) - **Issues**: [GitHub Issues](https://github.com/preprio/prepr-nextjs/issues) - **Support**: [Prepr Support](https://prepr.io/support)