# Audit

> SEO audit checklist and automated testing for Next.js sites. This skill should be used when the user asks to "audit SEO", "check SEO", "SEO checklist", "Lighthouse SEO", "SEO test", "automated SEO testing", "SEO report", or wants to systematically evaluate their site's SEO health and identify issues.

- Skill: `agents-store/audit` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add agents-store/audit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/agents-store/audit/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Marketing & Growth
- Author: agents-store (https://skillmd.com/u/agents-store)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/agents-store/audit

---


# SEO Audit

Systematic checklist and automated testing patterns for evaluating a Next.js site's SEO health.

## Quick Audit Commands

Run these to get an instant snapshot of SEO status:

```bash
# Check all pages have metadata exports
grep -r "export const metadata\|export async function generateMetadata" src/app --include="*.tsx" --include="*.ts" -l

# Find pages WITHOUT metadata
find src/app -name "page.tsx" | while read f; do
  grep -qE "metadata|generateMetadata" "$f" || echo "MISSING metadata: $f"
done

# Check for sitemap and robots
ls src/app/sitemap.ts src/app/robots.ts 2>/dev/null || echo "Missing sitemap.ts or robots.ts"

# Check metadataBase
grep -r "metadataBase" src/app/layout.tsx || echo "NO metadataBase set"

# Find images without alt text
grep -rn "<img\|<Image" src/ --include="*.tsx" | grep -v "alt=" | head -20

# Check for H1 tags in pages
find src/app -name "page.tsx" | while read f; do
  grep -qE "<h1|<H1" "$f" || echo "NO H1: $f"
done

# Check structured data
grep -r "application/ld+json" src/ --include="*.tsx" -l
```

## Full SEO Audit Checklist

### 1. Crawlability & Indexability

| Check | How to Verify | Priority |
|-------|---------------|----------|
| `robots.ts` exists and is correct | Visit `/robots.txt` | Critical |
| `sitemap.ts` exists with all pages | Visit `/sitemap.xml`, count URLs | Critical |
| Sitemap submitted to Google Search Console | GSC → Sitemaps | Critical |
| No broken internal links (404s) | Run crawler or Lighthouse | High |
| No redirect chains (A→B→C) | Check `next.config.ts` redirects | High |
| No orphan pages | Verify every page has inbound links | Medium |
| Clean URL structure (lowercase, hyphens) | Review route structure | Medium |
| Staging blocked from indexing | Check `robots.ts` non-production rules | Critical |

### 2. Metadata & Social

| Check | How to Verify | Priority |
|-------|---------------|----------|
| `metadataBase` set in root layout | Grep for `metadataBase` | Critical |
| Title template configured | Check root layout `title.template` | Critical |
| Every page has unique `title` | Grep for metadata exports | Critical |
| Every page has unique `description` | Check metadata exports | Critical |
| `description` length 150-160 chars | Measure each page | High |
| `title` length 50-60 chars | Measure each page | High |
| Canonical URL on every page | Check `alternates.canonical` | Critical |
| Open Graph tags present | View source, check `og:` tags | High |
| OG image configured (1200x630) | Test with Facebook Debugger | High |
| Twitter Card configured | Test with Twitter Card Validator | Medium |

### 3. Structured Data

| Check | How to Verify | Priority |
|-------|---------------|----------|
| Organization schema on root layout | Check for JSON-LD in layout | High |
| BreadcrumbList on inner pages | Check for breadcrumb JSON-LD | Medium |
| Article schema on blog posts | Check blog page JSON-LD | High (if blog) |
| Product schema on product pages | Check product page JSON-LD | High (if e-commerce) |
| No FAQPage on non-gov/health site | Grep for FAQPage schema | High |
| Schema validates in Rich Results Test | Test live URLs | Critical |
| `schema-dts` installed for type safety | Check `package.json` | Medium |

### 4. Performance

| Check | How to Verify | Priority |
|-------|---------------|----------|
| LCP < 2.5s | PageSpeed Insights | Critical |
| INP < 200ms | PageSpeed Insights (field data) | Critical |
| CLS < 0.1 | PageSpeed Insights | Critical |
| LCP image has `priority` prop | Check hero/banner Image component | High |
| All images have `sizes` prop | Grep for `<Image` without sizes | High |
| All images have `width`/`height` or `fill` | Grep for missing dimensions | High |
| `next/font` used (no external font requests) | Check network tab, layout.tsx | High |
| AVIF/WebP enabled | Check `next.config.ts` images.formats | Medium |
| No render-blocking third-party scripts | Check `<Script>` strategy props | Medium |

### 5. Content

| Check | How to Verify | Priority |
|-------|---------------|----------|
| Single H1 per page | Grep for `<h1` in pages | Critical |
| Logical heading hierarchy (no skips) | Audit heading levels per page | High |
| All images have alt text | Grep for `<Image` without `alt` | Critical |
| Alt text is descriptive (not "image") | Manual review | High |
| Descriptive internal link text | No "click here" anchors | Medium |
| Breadcrumb navigation present | Check inner pages | Medium |

### 6. Security & Mobile

| Check | How to Verify | Priority |
|-------|---------------|----------|
| HTTPS everywhere | Check production URL | Critical |
| Security headers configured | Check response headers | Medium |
| Mobile responsive | Test with Chrome DevTools | Critical |
| Touch targets >= 48px | Lighthouse accessibility audit | High |
| No horizontal scroll on mobile | Manual check | High |
| Viewport meta tag present | View source | Critical |

## Automated Testing with Playwright

Create an SEO test file that validates critical SEO elements:

```tsx
// tests/seo.spec.ts
import { test, expect } from '@playwright/test'

const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'

test.describe('SEO Fundamentals', () => {
  test('homepage has correct meta tags', async ({ page }) => {
    await page.goto(BASE_URL)

    // Title
    const title = await page.title()
    expect(title).toBeTruthy()
    expect(title.length).toBeLessThanOrEqual(60)

    // Meta description
    const description = page.locator('meta[name="description"]')
    await expect(description).toHaveAttribute('content', /.{50,160}/)

    // Canonical
    const canonical = page.locator('link[rel="canonical"]')
    await expect(canonical).toHaveAttribute('href', /https?:\/\//)

    // OG tags
    await expect(page.locator('meta[property="og:title"]')).toHaveAttribute('content', /.+/)
    await expect(page.locator('meta[property="og:description"]')).toHaveAttribute('content', /.+/)
    await expect(page.locator('meta[property="og:image"]')).toHaveAttribute('content', /https?:\/\//)
  })

  test('all pages have single H1', async ({ page }) => {
    const pages = ['/', '/about', '/blog']
    for (const path of pages) {
      await page.goto(`${BASE_URL}${path}`)
      const h1Count = await page.locator('h1').count()
      expect(h1Count, `${path} should have exactly 1 H1`).toBe(1)
    }
  })

  test('all images have alt text', async ({ page }) => {
    await page.goto(BASE_URL)
    const images = page.locator('img')
    const count = await images.count()
    for (let i = 0; i < count; i++) {
      const alt = await images.nth(i).getAttribute('alt')
      // alt can be empty string (decorative) but must exist
      expect(alt, `Image ${i} missing alt attribute`).not.toBeNull()
    }
  })

  test('sitemap.xml is valid', async ({ request }) => {
    const response = await request.get(`${BASE_URL}/sitemap.xml`)
    expect(response.status()).toBe(200)
    const body = await response.text()
    expect(body).toContain('<urlset')
    expect(body).toContain('<url>')
    expect(body).toContain('<loc>')
  })

  test('robots.txt is valid', async ({ request }) => {
    const response = await request.get(`${BASE_URL}/robots.txt`)
    expect(response.status()).toBe(200)
    const body = await response.text()
    expect(body).toContain('User-agent')
    expect(body).toContain('Sitemap')
  })

  test('structured data is valid JSON', async ({ page }) => {
    await page.goto(BASE_URL)
    const scripts = page.locator('script[type="application/ld+json"]')
    const count = await scripts.count()
    expect(count).toBeGreaterThan(0)

    for (let i = 0; i < count; i++) {
      const content = await scripts.nth(i).textContent()
      expect(() => JSON.parse(content!), `JSON-LD block ${i} is invalid JSON`).not.toThrow()
      const parsed = JSON.parse(content!)
      expect(parsed['@context']).toBe('https://schema.org')
    }
  })
})
```

## Lighthouse CI Integration

Add Lighthouse CI to your GitHub Actions:

```yaml
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci && npm run build
      - uses: treosh/lighthouse-ci-action@v12
        with:
          urls: |
            http://localhost:3000/
            http://localhost:3000/blog
          budgetPath: ./lighthouse-budget.json
          uploadArtifacts: true
```

Lighthouse budget file:
```json
[{
  "path": "/*",
  "timings": [
    { "metric": "largest-contentful-paint", "budget": 2500 },
    { "metric": "cumulative-layout-shift", "budget": 0.1 },
    { "metric": "total-blocking-time", "budget": 200 }
  ],
  "resourceCounts": [
    { "resourceType": "script", "budget": 10 },
    { "resourceType": "total", "budget": 50 }
  ]
}]
```

## Full-Site Scan with Unlighthouse

For scanning every page on your site at once:

```bash
npx unlighthouse --site https://yourdomain.com
```

Unlighthouse crawls your entire site and runs Lighthouse on each page, producing a dashboard with aggregated results.

## Audit Report Template

After running the audit, present results in this format:

```
## SEO Audit Report — [Site Name] — [Date]

### Critical Issues (fix immediately)
- [ ] Issue description — affected pages — recommended fix

### High Priority (fix this sprint)
- [ ] Issue description — affected pages — recommended fix

### Medium Priority (plan for next sprint)
- [ ] Issue description — affected pages — recommended fix

### Passing
- [x] Item that passed verification
```

