> Meticulous records real user sessions from a web app and replays them against each commit to catch visual regressions.
>
> Full AI-readable docs index (complete project setup journey + every page as markdown): https://app.meticulous.ai/llms.txt

# 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](/docs/onboarding-guide)

---

## 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`

```typescript
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`

```yaml
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 `PORT` env var (or set `container-port` to match)
- Respond `2xx` to a health-check endpoint (defaults to `GET /`; override with `container-health-check-endpoint`)

If you don't already have a Dockerfile, the [Next.js with Docker example](https://github.com/vercel/next.js/tree/canary/examples/with-docker) is a good starting point.

---

### Step 3: Add API Token Secret

1. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
2. Go to GitHub repo → Settings → Secrets and variables → Actions
3. Create secret named `METICULOUS_API_TOKEN` with your token

---

## Companion Assets

> **info: Only relevant if you're using cloud-compute**
> 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

Add these steps to your `cloud-compute` workflow:

```yaml
- 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**:
1. Build creates `.next/static/` with all static assets
2. Copy `.next/static/` to `companion-assets/_next/static/`
3. Meticulous serves these files directly, bypassing the tunnel

Learn more: [Companion Assets Guide](/docs/how-to/companion-assets-advanced)

---

## Common Patterns

### Pattern 1: Detect Test Mode

Use `window.Meticulous.isRunningAsTest` to detect when running as a test:

```typescript
// 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

```typescript
// 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`:

```typescript
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:

```typescript
// 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`

```typescript
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_`):

```yaml
- 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):

```yaml
- 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:

```yaml
- 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**:
1. Verify `_document.tsx` has recorder script in `<Head>`
2. Check project ID is correct
3. Check for CSP blocking (console errors)
4. Verify script loads before other scripts

**Fix**: Ensure recorder is in `<Head>`, not `<body>`:

```typescript
<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**:
1. Build failed silently
2. Port already in use
3. Missing environment variables

**Debug**:

```yaml
- 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`**

```typescript
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`**

```typescript
useEffect(() => {
  if (window.Meticulous?.isRunningAsTest) {
    // Set mock auth data
    localStorage.setItem('token', 'test-token')
  }
}, [])
```

See full guide: [Troubleshoot Authentication](/docs/how-to/troubleshoot-auth)

---

### Issue: Companion Assets Not Loading

**Symptom**: Console errors for `/_next/static/` files

**Checks**:
1. Verify folder exists: `ls -la companion-assets/_next/static`
2. Check files were copied after build
3. Verify regex pattern: `^/_next/static/`

**Fix**: Ensure build completes before copying:

```yaml
- 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**:
1. Timestamps: "Posted 5 min ago" vs "Posted 6 min ago"
2. Random IDs or UUIDs
3. Animations not completing

**Fixes**:

**Timestamps**: Add `meticulous-ignore` class
```typescript
<span className="meticulous-ignore">
  Posted {timeAgo} ago
</span>
```

**Random IDs**: Use deterministic IDs in tests
```typescript
const generateId = () => {
  if (window.Meticulous?.isRunningAsTest) {
    return 'test-id-12345'
  }
  return crypto.randomUUID()
}
```

**Animations**: Disable in tests
```typescript
const animationDuration = window.Meticulous?.isRunningAsTest ? 0 : 300
```

Learn more: [Fix False Positive Diffs](/docs/how-to/fix-false-positive-diffs)

---

## Migration from App Router

If you're migrating to Pages Router from App Router:

1. **Move recorder**: From `app/layout.tsx` to `pages/_document.tsx`
2. **Update auth**: Change from `headers()` to `getServerSideProps`
3. **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):

```typescript
// 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:

```typescript
<div className="meticulous-ignore">
  {/* Content that changes frequently */}
</div>
```

### 3. Test Locally

Run tests locally before CI:

```bash
# 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:

```yaml
- 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](/docs/onboarding-guide) - General Meticulous setup
- [Troubleshoot Authentication](/docs/how-to/troubleshoot-auth) - Auth patterns and solutions
- [Companion Assets Guide](/docs/how-to/companion-assets-advanced) - Deep dive into static asset optimization
- [Fix False Positives](/docs/how-to/fix-false-positive-diffs) - Handle non-deterministic content
