Skip to main content

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.

Source facts

Repository
Agents-Store/codex-plugins
Last source activity
November 1, 2026 at 02:46
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
3 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
audit
description
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.
# 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 | | `robots.ts` does not disallow `/_next/` | Read `robots.ts` — blocked JS/CSS stops Google rendering pages | Critical | | Generated sitemaps (`generateSitemaps`) are all discoverable | Next.js builds no sitemap index; check `robots.ts` lists them or a route handler serves an index | High | ### 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 | `curl -sA "facebookexternalhit/1.1" <url> \| grep -i og:` (bots get `<head>` metadata; on request-time rendered pages browsers and Googlebot get streamed metadata, while prerendered pages keep it in the initial `<head>`) | High | | OG image configured (1200x630) | Test with Facebook Debugger | High | | Twitter Card configured | Check `twitter:card` tags; preview in the X post composer (the Card Validator was retired) | 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) | | Search appearance not planned around FAQPage or HowTo | Grep for FAQPage / HowTo: both rich results are removed from Google Search; existing markup is harmless, but do not count it as SEO work | Low | | WebSite markup on the home page with `name` / `alternateName` | Check home page JSON-LD | Medium | | Structured data matches visible page content | Compare JSON-LD with the rendered page | High | | Schema validates (Rich Results Test for types Google renders; Schema.org Validator for the rest) | 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 `loading="eager"` + `fetchPriority="high"` (`preload` in Next.js 16 only if one LCP image for all viewports; if the LCP image differs per viewport, `fetchPriority="high"` only — no `loading="eager"` or `preload`) | 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 | Inspect `<head>` in DevTools; customize via `export const viewport` | Critical | | HTML document well under 2 MB uncompressed (Googlebot crawls the first 2 MB) | Check document size; keep metadata and JSON-LD near the top | Medium | ## 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 }) => { // Playwright reads the rendered DOM, so streamed metadata (request-time rendered pages) is found here too. // For HTML-limited social bots, also run: curl -sA "facebookexternalhit/1.1" <url> 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@v7 - uses: actions/setup-node@v7 with: { node-version: '22' } - 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 ```
View on GitHub