Next.js Integration
Complete guide for integrating CSP Kit with Next.js applications, covering both App Router and Pages Router with the new TypeScript API.
๐ Quick Startโ
Installationโ
npm install @csp-kit/generator @csp-kit/data
# or
yarn add @csp-kit/generator @csp-kit/data
# or
pnpm add @csp-kit/generator @csp-kit/data
Basic Setupโ
// lib/csp.ts
import { generateCSP } from '@csp-kit/generator';
import { GoogleAnalytics, VercelAnalytics, GoogleFonts } from '@csp-kit/data';
export const cspConfig = generateCSP({
services: [GoogleAnalytics, VercelAnalytics, GoogleFonts],
});
๐ฑ App Router (Next.js 13+)โ
Method 1: Middleware (Recommended)โ
Create middleware.ts
in your project root:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { generateCSPHeader } from '@csp-kit/generator';
import { GoogleAnalytics, VercelAnalytics, GoogleFonts } from '@csp-kit/data';
export function middleware(request: NextRequest) {
// Generate CSP header
const csp = generateCSPHeader({
services: [GoogleAnalytics, VercelAnalytics, GoogleFonts],
});
// Create response
const response = NextResponse.next();
// Add CSP header
response.headers.set('Content-Security-Policy', csp);
return response;
}
export const config = {
matcher: [
/*
* Match all request paths except:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
Method 2: Route Headers with Nonceโ
// app/layout.tsx
import { headers } from 'next/headers';
import { generateCSP } from '@csp-kit/generator';
import { GoogleAnalytics, VercelAnalytics } from '@csp-kit/data';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const nonce = headers().get('x-nonce') || '';
return (
<html lang="en">
<head>
<CSPMeta nonce={nonce} />
</head>
<body>{children}</body>
</html>
);
}
function CSPMeta({ nonce }: { nonce: string }) {
const result = generateCSP({
services: [GoogleAnalytics, VercelAnalytics],
nonce
});
return (
<meta
httpEquiv="Content-Security-Policy"
content={result.header}
/>
);
}
Method 3: Dynamic CSP by Routeโ
// middleware.ts - Dynamic CSP based on route
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { generateCSPHeader, defineService } from '@csp-kit/generator';
import { GoogleAnalytics, Stripe, Typeform, Intercom } from '@csp-kit/data';
// Custom service for your API
const MyAPI = defineService({
directives: {
'connect-src': ['https://api.myapp.com'],
},
});
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Base services for all routes
const baseServices = [GoogleAnalytics, MyAPI];
// Add services based on route
const services = [...baseServices];
if (pathname.startsWith('/checkout')) {
services.push(Stripe);
}
if (pathname.startsWith('/contact')) {
services.push(Typeform);
}
if (pathname.startsWith('/support')) {
services.push(Intercom);
}
const csp = generateCSPHeader({ services });
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp);
return response;
}
๐ Pages Router (Next.js 12 and below)โ
Method 1: Custom Documentโ
// pages/_document.tsx
import Document, { Html, Head, Main, NextScript } from 'next/document';
import type { DocumentContext, DocumentInitialProps } from 'next/document';
import { generateCSP } from '@csp-kit/generator';
import { GoogleAnalytics, GoogleFonts, VercelAnalytics } from '@csp-kit/data';
class MyDocument extends Document {
static async getInitialProps(
ctx: DocumentContext
): Promise<DocumentInitialProps & { csp: string }> {
const initialProps = await Document.getInitialProps(ctx);
// Generate CSP
const cspResult = generateCSP({
services: [GoogleAnalytics, GoogleFonts, VercelAnalytics]
});
// Add CSP header if we have access to res
if (ctx.res) {
ctx.res.setHeader('Content-Security-Policy', cspResult.header);
}
return {
...initialProps,
csp: cspResult.header
};
}
render() {
const { csp } = this.props as any;
return (
<Html>
<Head>
{csp && (
<meta
httpEquiv="Content-Security-Policy"
content={csp}
/>
)}
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
Method 2: Custom App with getInitialPropsโ
// pages/_app.tsx
import App from 'next/app';
import type { AppContext, AppInitialProps } from 'next/app';
import { generateCSP } from '@csp-kit/generator';
import { GoogleAnalytics, GoogleFonts } from '@csp-kit/data';
class MyApp extends App {
static async getInitialProps({
Component,
ctx,
}: AppContext): Promise<AppInitialProps> {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
// Set CSP header on server side
if (ctx.res) {
const cspResult = generateCSP({
services: [GoogleAnalytics, GoogleFonts]
});
ctx.res.setHeader('Content-Security-Policy', cspResult.header);
}
return { pageProps };
}
render() {
const { Component, pageProps } = this.props;
return <Component {...pageProps} />;
}
}
export default MyApp;
๐ง Advanced Configurationsโ
Environment-Based CSPโ
// lib/csp.ts
import { generateCSP, defineService } from '@csp-kit/generator';
import { GoogleFonts } from '@csp-kit/data';
// Development tools service
const NextDevTools = defineService({
directives: {
'script-src': ["'unsafe-eval'"], // Required for hot reload
'connect-src': [
'http://localhost:3000',
'ws://localhost:3000', // Hot reload websocket
'http://localhost:3001', // API routes in dev
],
'style-src': ["'unsafe-inline'"], // For styled-jsx in dev
},
});
export function getCSPForEnvironment() {
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
return generateCSP({
services: [GoogleFonts, ...(isDevelopment ? [NextDevTools] : [])],
development: {
// More permissive in development
unsafeEval: true,
unsafeInline: true,
},
production: {
// Strict in production
reportUri: process.env.CSP_REPORT_URI || '/api/csp-report',
},
});
}
CSP with Nonce for Inline Scriptsโ
// app/layout.tsx
import { headers } from 'next/headers';
import { generateCSP, generateNonce } from '@csp-kit/generator';
import { GoogleAnalytics } from '@csp-kit/data';
import Script from 'next/script';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const nonce = generateNonce();
const cspResult = generateCSP({
services: [GoogleAnalytics],
nonce
});
return (
<html lang="en">
<head>
<meta
httpEquiv="Content-Security-Policy"
content={cspResult.header}
/>
</head>
<body>
{children}
{/* Use nonce for inline scripts */}
<Script
id="google-analytics"
strategy="afterInteractive"
nonce={nonce}
>
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${process.env.NEXT_PUBLIC_GA_ID}');
`}
</Script>
</body>
</html>
);
}
CSP for Specific Featuresโ
// middleware.ts - Feature-specific CSP
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { generateCSPHeader, defineService } from '@csp-kit/generator';
import { GoogleAnalytics, Stripe, Youtube, GoogleMaps } from '@csp-kit/data';
// Custom services for your features
const ImageCDN = defineService({
directives: {
'img-src': [
'https://images.myapp.com',
'data:', // For placeholder images
'blob:', // For dynamic images
],
},
});
const WebSocketAPI = defineService({
directives: {
'connect-src': ['wss://realtime.myapp.com'],
},
});
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Always include these services
const services = [GoogleAnalytics, ImageCDN];
// Add services based on features used
if (pathname.includes('/videos')) {
services.push(Youtube);
}
if (pathname.includes('/maps')) {
services.push(GoogleMaps);
}
if (pathname.includes('/checkout')) {
services.push(Stripe);
}
if (pathname.includes('/chat')) {
services.push(WebSocketAPI);
}
const csp = generateCSPHeader({ services });
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp);
return response;
}
๐ฏ Common Use Casesโ
E-commerce Siteโ
// middleware.ts - E-commerce CSP
import { generateCSPHeader } from '@csp-kit/generator';
import {
GoogleAnalytics,
FacebookPixel,
Stripe,
Paypal,
GoogleFonts,
Intercom,
Hotjar,
Mailchimp,
GoogleAds,
} from '@csp-kit/data';
export function middleware(request: NextRequest) {
const csp = generateCSPHeader({
services: [
// Analytics & Marketing
GoogleAnalytics,
FacebookPixel,
GoogleAds,
Hotjar,
// Payments
Stripe,
Paypal,
// Customer Support
Intercom,
// Email Marketing
Mailchimp,
// UI
GoogleFonts,
],
});
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp);
return response;
}
SaaS Applicationโ
// lib/csp-saas.ts
import { generateCSP, defineService } from '@csp-kit/generator';
import {
GoogleAnalytics,
Mixpanel,
Sentry,
Intercom,
Stripe,
Auth0,
GoogleFonts,
} from '@csp-kit/data';
// Custom services for SaaS
const GraphQLAPI = defineService({
directives: {
'connect-src': [
'https://api.yoursaas.com',
'wss://subscriptions.yoursaas.com', // GraphQL subscriptions
],
},
});
const FileStorage = defineService({
directives: {
'img-src': ['https://files.yoursaas.com'],
'media-src': ['https://files.yoursaas.com'],
'connect-src': ['https://files.yoursaas.com'], // For uploads
},
});
export const saasCsp = generateCSP({
services: [
// Analytics
GoogleAnalytics,
Mixpanel,
// Error Monitoring
Sentry,
// Customer Support
Intercom,
// Billing
Stripe,
// Authentication
Auth0,
// UI
GoogleFonts,
// Custom
GraphQLAPI,
FileStorage,
],
reportUri: 'https://api.yoursaas.com/csp-violations',
});
Blog/Content Siteโ
// Blog CSP configuration
import { generateCSPHeader } from '@csp-kit/generator';
import {
GoogleAnalytics,
GoogleFonts,
Youtube,
Twitter,
Instagram,
GoogleAds,
Disqus,
} from '@csp-kit/data';
const blogCsp = generateCSPHeader({
services: [
// Analytics
GoogleAnalytics,
// Typography
GoogleFonts,
// Embeds
Youtube,
Twitter,
Instagram,
// Monetization
GoogleAds,
// Comments (if Disqus is supported in the future)
// Disqus
],
});
๐ Security Best Practicesโ
Report-Only Mode for Testingโ
// middleware.ts - Test CSP without breaking site
import { generateReportOnlyCSP } from '@csp-kit/generator';
import { GoogleAnalytics, NewServiceToTest } from '@csp-kit/data';
export function middleware(request: NextRequest) {
const reportOnlyCsp = generateReportOnlyCSP({
services: [GoogleAnalytics, NewServiceToTest],
reportUri: '/api/csp-report',
});
const response = NextResponse.next();
response.headers.set('Content-Security-Policy-Report-Only', reportOnlyCsp);
return response;
}
CSP Violation Reportingโ
// app/api/csp-report/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const report = await request.json();
// Log CSP violations
console.error('CSP Violation:', {
documentUri: report['document-uri'],
violatedDirective: report['violated-directive'],
blockedUri: report['blocked-uri'],
sourceFile: report['source-file'],
lineNumber: report['line-number'],
columnNumber: report['column-number'],
});
// Optionally send to monitoring service
// await sendToMonitoring(report);
return new NextResponse(null, { status: 204 });
} catch (error) {
console.error('Error processing CSP report:', error);
return new NextResponse(null, { status: 500 });
}
}
Gradual CSP Rolloutโ
// middleware.ts - Gradually enable strict CSP
import { generateCSP } from '@csp-kit/generator';
import { GoogleAnalytics } from '@csp-kit/data';
import { cookies } from 'next/headers';
export function middleware(request: NextRequest) {
const cookieStore = cookies();
const isInStrictCSPGroup = cookieStore.get('strict-csp')?.value === 'true';
// 10% of users get strict CSP
if (!cookieStore.has('strict-csp')) {
const enableStrict = Math.random() < 0.1;
cookieStore.set('strict-csp', enableStrict ? 'true' : 'false');
}
const csp = isInStrictCSPGroup
? generateCSP({ services: [GoogleAnalytics] }) // Strict
: generateCSP({
services: [GoogleAnalytics],
unsafeInline: true, // Less strict for gradual rollout
});
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp.header);
return response;
}
๐ Performance Optimizationโ
Static CSP Generationโ
// next.config.js - Generate CSP at build time
const { generateCSPHeader } = require('@csp-kit/generator');
const { GoogleAnalytics, VercelAnalytics } = require('@csp-kit/data');
const csp = generateCSPHeader({
services: [GoogleAnalytics, VercelAnalytics],
});
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: csp,
},
],
},
];
},
};
Edge Runtime Compatibleโ
// middleware.ts - Works with Edge Runtime
import { generateCSPHeader } from '@csp-kit/generator';
import { GoogleAnalytics, VercelAnalytics } from '@csp-kit/data';
export const config = {
runtime: 'edge',
};
export function middleware(request: Request) {
// CSP Kit works in Vercel Edge Functions
const csp = generateCSPHeader({
services: [GoogleAnalytics, VercelAnalytics],
});
return new Response(null, {
headers: {
'Content-Security-Policy': csp,
},
});
}
๐งช Testingโ
Test CSP Configurationโ
// __tests__/csp.test.ts
import { describe, it, expect } from 'vitest';
import { generateCSP } from '@csp-kit/generator';
import { GoogleAnalytics, Stripe, GoogleFonts } from '@csp-kit/data';
describe('CSP Configuration', () => {
it('should generate valid CSP for production', () => {
const result = generateCSP({
services: [GoogleAnalytics, Stripe, GoogleFonts],
});
expect(result.header).toContain("script-src 'self'");
expect(result.includedServices).toHaveLength(3);
expect(result.warnings).toHaveLength(0);
});
it('should include nonce when requested', () => {
const result = generateCSP({
services: [GoogleAnalytics],
nonce: true,
});
expect(result.nonce).toBeDefined();
expect(result.header).toContain(`'nonce-${result.nonce}'`);
});
});
E2E Testing with CSPโ
// cypress/e2e/csp.cy.ts
describe('CSP Compliance', () => {
it('should not have CSP violations', () => {
cy.visit('/');
// Check for CSP violations in console
cy.window().then(win => {
cy.spy(win.console, 'error').as('consoleError');
});
// Interact with features that use external scripts
cy.get('[data-testid="analytics-button"]').click();
// Should not have CSP violation errors
cy.get('@consoleError').should(
'not.have.been.calledWith',
Cypress.sinon.match(/Content Security Policy/)
);
});
});
๐จ Troubleshootingโ
Common Issuesโ
1. CSP blocking Next.js development features
// Use environment-specific configuration
import { defineService } from '@csp-kit/generator';
const NextJSDev = defineService({
directives: {
'script-src': ["'unsafe-eval'"], // For Fast Refresh
'connect-src': [
'http://localhost:3000',
'ws://localhost:3000', // Hot reload
'http://localhost:3001', // API routes
],
'style-src': ["'unsafe-inline'"], // For styled-jsx
},
});
2. Next.js Image Optimization
// Add image domains for next/image
const NextImages = defineService({
directives: {
'img-src': [
'https://your-domain.com',
'https://images.your-domain.com',
'data:', // For placeholder images
'blob:', // For dynamic images
],
},
});
3. Styled Components / Emotion
// Handle CSS-in-JS libraries
import { generateCSP } from '@csp-kit/generator';
import { GoogleAnalytics } from '@csp-kit/data';
const result = generateCSP({
services: [GoogleAnalytics],
nonce: true // Use nonce for styled-components
});
// In your app
import { ServerStyleSheet } from 'styled-components';
const sheet = new ServerStyleSheet();
const html = renderToString(
sheet.collectStyles(<App nonce={result.nonce} />)
);
๐ Next.js Specific Resourcesโ
๐ More Examplesโ
More framework integration examples coming soon! For now, you can adapt the patterns shown above for other frameworks by following similar middleware patterns.