Next.js Pages Router - Complete Setup Guide
Complete guide for setting up Meticulous with Next.js applications using the Pages Router (Next.js 12 and earlier, or Next.js 13+ without App Router).
Overview
The Pages Router is the traditional Next.js routing system. This guide covers:
- Recorder installation in
_document.tsx - CI/CD configuration with companion assets
- Authentication handling
- Common patterns and troubleshooting
Prerequisites:
- Next.js application using Pages Router
- Basic familiarity with Meticulous concepts
Quick Start
Step 1: Install Recorder in _document.tsx
Add the Meticulous recorder script to your custom Document component before any other scripts.
File: pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Head>
{/* Meticulous recorder - MUST be first script */}
{/* Replace YOUR_PROJECT_ID with your project ID from the dashboard */}
<script
data-project-id="YOUR_PROJECT_ID"
src="https://snippet.meticulous.ai/v1/meticulous.js"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
Important: The recorder must load before Next.js client-side JavaScript to capture all events.
If you don't have _document.tsx: Create it in pages/_document.tsx with the code above.
Step 2: Configure GitHub Actions Workflow
Create a workflow that builds your app and uses companion assets for optimal performance.
File: .github/workflows/meticulous.yml
name: Meticulous Tests
on:
push:
branches: [main]
pull_request: {}
workflow_dispatch: {}
permissions:
actions: write
contents: read
issues: write
pull-requests: write
statuses: read
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build Docker image
uses: docker/build-push-action@v6
with:
context: .
tags: my-app:${{ github.sha }}
platforms: linux/amd64
push: false
load: true
- name: Run Meticulous tests
uses: alwaysmeticulous/report-diffs-action/upload-container@v1
with:
api-token: ${{ secrets.METICULOUS_API_TOKEN }}
image-tag: my-app:${{ github.sha }}
# Optional: set if your container does not respect the PORT env var
container-port: 3000
# Optional: extra runtime env vars for the container
container-env: |
NODE_ENV=production
The recommended approach is to build a Docker image of your Next.js app and have Meticulous host it via the upload-container action. The container only needs to live for the duration of the upload step — Meticulous runs it on its own infrastructure for the actual test run.
Your Dockerfile should:
- Build for
linux/amd64 - Run
next start(or equivalent) in the foreground - Listen on the
PORTenv var (or setcontainer-portto match) - Respond
2xxto a health-check endpoint (defaults toGET /; override withcontainer-health-check-endpoint)
If you don't already have a Dockerfile, the Next.js with Docker example is a good starting point.
Alternative: Tunnel a locally-served app (cloud-compute)
If your CI environment can't build a Docker image, you can fall back to the tunnel-based cloud-compute action. This keeps a server alive in the runner and uses companion assets to make Next.js static asset serving fast.
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build Next.js app
run: npm run build
env:
NODE_ENV: production
- name: Prepare companion assets
run: |
mkdir -p companion-assets/_next
cp -r .next/static companion-assets/_next/
- name: Start app
run: |
npm start &
npx wait-on http://localhost:3000 --timeout 60000
env:
PORT: 3000
- name: Run Meticulous tests
uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
with:
api-token: ${{ secrets.METICULOUS_API_TOKEN }}
app-url: "http://localhost:3000"
companion-assets-folder: "companion-assets"
companion-assets-regex: "^/_next/static/"
Step 3: Add API Token Secret
- Get your API token from Meticulous dashboard → Project Settings
- Go to GitHub repo → Settings → Secrets and variables → Actions
- Create secret named
METICULOUS_API_TOKENwith your token
Companion Assets
The companion-assets pattern only applies to the cloud-compute (tunnel) workflow. If you're using the recommended upload-container action, Meticulous serves your app directly from the uploaded image and there's nothing additional to configure.
Why Use Companion Assets?
Next.js static assets (/_next/static/) are large and numerous. Serving them through the tunnel is slow.
Performance improvement:
- Without companion assets: ~5 minutes test duration
- With companion assets: ~2 minutes test duration (60% faster)
Setup
The workflow above already includes companion assets setup:
- name: Prepare companion assets
run: |
mkdir -p companion-assets/_next
cp -r .next/static companion-assets/_next/
- name: Run Meticulous tests
with:
companion-assets-folder: "companion-assets"
companion-assets-regex: "^/_next/static/"
How it works:
- Build creates
.next/static/with all static assets - Copy
.next/static/tocompanion-assets/_next/static/ - Meticulous serves these files directly, bypassing the tunnel
Learn more: Companion Assets Guide
Common Patterns
Pattern 1: Detect Test Mode
Use window.Meticulous.isRunningAsTest to detect when running as a test:
// In any component
function MyComponent() {
const isTest = window.Meticulous?.isRunningAsTest
if (isTest) {
// Skip animations, use test data, etc.
}
return <div>...</div>
}
Pattern 2: Bypass Authentication
// pages/_app.tsx
import { useEffect } from 'react'
import { useRouter } from 'next/router'
function MyApp({ Component, pageProps }) {
const router = useRouter()
useEffect(() => {
if (window.Meticulous?.isRunningAsTest) {
// Mock authentication for tests
localStorage.setItem('auth-token', 'test-token')
localStorage.setItem('user', JSON.stringify({
id: 'test-user',
name: 'Test User',
email: 'test@example.com'
}))
}
}, [])
return <Component {...pageProps} />
}
Pattern 3: Server-Side Detection
Check for meticulous-is-test header in getServerSideProps:
export const getServerSideProps = async (context) => {
const { req } = context
const isTest = req.headers['meticulous-is-test'] === '1'
if (isTest) {
// Skip auth redirect, use test data, etc.
return {
props: {
user: { id: 'test-user', name: 'Test User' }
}
}
}
// Normal server-side logic
const session = await getSession(context)
if (!session) {
return {
redirect: {
destination: '/login',
permanent: false,
}
}
}
return {
props: {
user: session.user
}
}
}
Pattern 4: Handle Dynamic Timestamps
Ignore elements with frequently changing content:
// Add meticulous-ignore class
<div className="meticulous-ignore">
Posted {formatDistanceToNow(post.createdAt)} ago
</div>
Or configure in project settings to ignore CSS selectors globally.
Complete Example
File Structure
your-app/
├── pages/
│ ├── _app.tsx # App wrapper
│ ├── _document.tsx # Recorder installation
│ ├── index.tsx # Home page
│ └── dashboard.tsx # Protected page
├── lib/
│ └── auth.ts # Auth utilities
├── .github/
│ └── workflows/
│ └── meticulous.yml # CI/CD
└── package.json
Example: Protected Page
File: pages/dashboard.tsx
import { GetServerSideProps } from 'next'
import { getSession } from 'next-auth/react'
interface DashboardProps {
user: {
id: string
name: string
email: string
}
}
export const getServerSideProps: GetServerSideProps<DashboardProps> = async (context) => {
const isTest = context.req.headers['meticulous-is-test'] === '1'
if (isTest) {
// Bypass auth during tests
return {
props: {
user: {
id: 'test-user-123',
name: 'Test User',
email: 'test@example.com'
}
}
}
}
// Normal auth flow
const session = await getSession(context)
if (!session) {
return {
redirect: {
destination: '/login?redirect=/dashboard',
permanent: false,
}
}
}
return {
props: {
user: session.user
}
}
}
export default function Dashboard({ user }: DashboardProps) {
return (
<div>
<h1>Welcome, {user.name}!</h1>
<p>Email: {user.email}</p>
</div>
)
}
CI/CD Configuration Details
Environment Variables
Build-time variables (prefixed with NEXT_PUBLIC_):
- name: Build Next.js app
run: npm run build
env:
NEXT_PUBLIC_API_URL: "http://localhost:3000/api"
NODE_ENV: production
Runtime variables (server-side only):
- name: Start app
run: npm start &
env:
DATABASE_URL: "postgresql://..."
API_SECRET: ${{ secrets.API_SECRET }}
Custom Build Scripts
If you have a custom build process:
- name: Build app
run: |
npm run build:custom
npm run postbuild:assets
- name: Prepare companion assets
run: |
mkdir -p companion-assets/_next
cp -r .next/static companion-assets/_next/
# Copy any additional static assets
cp -r public/static companion-assets/static
Troubleshooting
Issue: Recorder Not Loading
Symptom: window.Meticulous is undefined
Checks:
- Verify
_document.tsxhas recorder script in<Head> - Check project ID is correct
- Check for CSP blocking (console errors)
- Verify script loads before other scripts
Fix: Ensure recorder is in <Head>, not <body>:
<Head>
<script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js" />
{/* Other head elements */}
</Head>
Issue: App Doesn't Start in CI
Symptom: "ECONNREFUSED" or "Failed to connect to http://localhost:3000"
Common causes:
- Build failed silently
- Port already in use
- Missing environment variables
Debug:
- name: Start app with logging
run: |
npm start > app.log 2>&1 &
sleep 5
cat app.log
- name: Verify app is running
run: |
curl http://localhost:3000 || echo "App not responding"
npx wait-on http://localhost:3000 --timeout 60000
Issue: Authentication Blocks Tests
Symptom: Tests fail because pages redirect to login
Solution 1: Bypass auth in getServerSideProps
export const getServerSideProps = (context) => {
const isTest = context.req.headers['meticulous-is-test'] === '1'
if (isTest) {
return { props: { user: mockUser } }
}
// Normal auth flow
}
Solution 2: Mock auth in _app.tsx
useEffect(() => {
if (window.Meticulous?.isRunningAsTest) {
// Set mock auth data
localStorage.setItem('token', 'test-token')
}
}, [])
See full guide: Troubleshoot Authentication
Issue: Companion Assets Not Loading
Symptom: Console errors for /_next/static/ files
Checks:
- Verify folder exists:
ls -la companion-assets/_next/static - Check files were copied after build
- Verify regex pattern:
^/_next/static/
Fix: Ensure build completes before copying:
- name: Build Next.js app
run: npm run build
- name: Verify build output
run: ls -la .next/static
- name: Prepare companion assets
run: |
mkdir -p companion-assets/_next
cp -r .next/static companion-assets/_next/
ls -la companion-assets/_next/static
Issue: False Positive Diffs
Symptom: Tests show diffs for content that hasn't changed
Common causes:
- Timestamps: "Posted 5 min ago" vs "Posted 6 min ago"
- Random IDs or UUIDs
- Animations not completing
Fixes:
Timestamps: Add meticulous-ignore class
<span className="meticulous-ignore">
Posted {timeAgo} ago
</span>
Random IDs: Use deterministic IDs in tests
const generateId = () => {
if (window.Meticulous?.isRunningAsTest) {
return 'test-id-12345'
}
return crypto.randomUUID()
}
Animations: Disable in tests
const animationDuration = window.Meticulous?.isRunningAsTest ? 0 : 300
Learn more: Fix False Positive Diffs
Migration from App Router
If you're migrating to Pages Router from App Router:
- Move recorder: From
app/layout.tsxtopages/_document.tsx - Update auth: Change from
headers()togetServerSideProps - Test thoroughly: Server-side logic differs between routers
Testing Best Practices
1. Record Real User Sessions
Record sessions in staging or production (with appropriate privacy controls):
// Only load recorder in specific environments
const shouldLoadRecorder =
process.env.NEXT_PUBLIC_ENV === 'staging' ||
process.env.NEXT_PUBLIC_ENV === 'production'
2. Handle Dynamic Content
For frequently changing content:
<div className="meticulous-ignore">
{/* Content that changes frequently */}
</div>
3. Test Locally
Run tests locally before CI:
# Start your app
npm run dev
# In another terminal
npx @alwaysmeticulous/cli simulate \
--sessionId="YOUR_SESSION_ID" \
--appUrl="http://localhost:3000"
Advanced Configuration
Monorepo Setup
If your Next.js app is in a subdirectory:
- name: Install dependencies
working-directory: ./apps/frontend
run: npm ci
- name: Build app
working-directory: ./apps/frontend
run: npm run build
- name: Prepare companion assets
working-directory: ./apps/frontend
run: |
mkdir -p companion-assets/_next
cp -r .next/static companion-assets/_next/
- name: Start app
working-directory: ./apps/frontend
run: npm start &
- name: Run Meticulous tests
uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
with:
api-token: ${{ secrets.METICULOUS_API_TOKEN }}
app-url: "http://localhost:3000"
companion-assets-folder: "./apps/frontend/companion-assets"
companion-assets-regex: "^/_next/static/"
See Also
- Onboarding Guide - General Meticulous setup
- Troubleshoot Authentication - Auth patterns and solutions
- Companion Assets Guide - Deep dive into static asset optimization
- Fix False Positives - Handle non-deterministic content