playwright-seo
v0.1.8
Published
SEO checks for Playwright — simples, configurável e agnóstico.
Maintainers
Readme
playwright-seo
SEO checks for Playwright — simple, configurable, and framework-agnostic.
Run lightweight SEO audits automatically after each test or on-demand, with clean Playwright-style output.
Table of Contents
- Features
- Prerequisites
- Installation
- Quick Start
- Targeted Usage
- Configuration Reference
- API Reference
- Output & Logging
- Best Practices
- Troubleshooting
- FAQ
- Contributing
- License
Features
- ✅ Drop-in wrapper to run audits automatically after each test
- ✅ On-demand API (
runSeoChecks) for focused checks - ✅ Config file with on/off switches for every rule
- ✅ Skip
noindex(meta orX-Robots-Tag) - ✅ Exclude URLs via glob or RegExp
- ✅ Pretty, actionable output: URL + rule + HTML snippets
- ✅ Per-worker dedupe to reduce noise on large suites
- ✅ Severity: choose
error(fail tests) orwarning(log only, don’t fail) - ✅ Peer dep on
@playwright/test— no vendor lock-in
Prerequisites
- Node.js ≥ 16
- Playwright Test ≥ 1.41
- Tests in TypeScript or JavaScript
- Ability to install dev dependencies
Tip: If you use TypeScript, ensure the tsconfig used by your tests is the one Playwright loads at runtime.
Installation
npm
npm i -D playwright-seo
# (optional) if your editor complains about Node APIs used by the CLI:
npm i -D @types/nodeYarn
yarn add -D playwright-seo
# (optional)
yarn add -D @types/nodeThe CLI prints a friendly update notice using
simple-update-notifier. Disable viaPLAYWRIGHT_SEO_UPDATE_NOTIFIER=false(CI is silent by default).
Quick Start
1) Generate config
Create playwright-seo.config.ts at your project root.
npm
npx playwright-seo-config
# or
npm run playwright-seo-configYarn
yarn playwright-seo-config
# or
yarn run playwright-seo-configThis creates playwright-seo.config.ts (safe to edit):
// playwright-seo.config.ts
import { defineSeoConfig } from 'playwright-seo';
export default defineSeoConfig({
// Rules (on/off)
enforceHtmlLang: true,
enforceViewport: true,
enforceSingleH1: true,
enforceTitle: true,
title: { min: 10, max: 70 }, // Size
enforceMetaDescription: true,
metaDescription: { min: 50, max: 160 }, // Size
enforceCanonical: true,
enforceImgAlt: true,
forbidNoindexOnProd: true,
checkMainResponseStatus: true,
// Behavior
skipIfNoindex: true,
maxNodesPerIssue: 5,
excludeUrls: [], // e.g. ['/', '/admin/*', /\/api\//]
waitFor: 'domcontentloaded', // 'load' | 'domcontentloaded' | 'networkidle'
// Runner (how the audit is executed)
runner: {
// Avoid running the same URL more than once per worker
dedupePerWorker: true,
// Severity:
// - 'error' => fail test on violations (default)
// - 'warning' => log only, don't fail
severity: 'error'
}
});2) Config SEO tests per project/environment (via options fixtures) -> Recommended
Create tests/support/seo.auto.ts file or add in your fixtures file:
// tests/support/seo.auto.ts
import { test as base } from '@playwright/test';
import { seoAuto } from 'playwright-seo/fixture';
import type { SeoAutoFixtures } from 'playwright-seo/fixture';
export const test = base.extend<SeoAutoFixtures>({
// your fixtures...
// plug the SEO auto-fixture (runs after each test)
...seoAuto(), // minimal: uses project config if available, else defaults
});
export { expect } from '@playwright/test';
If you prefer to be explicit, you can still pass defaults:
...seoAuto({ defaults: { config: toRuleConfig(seoUser) } })
You can pass seoAudit/seoOptions through playwright.config.ts using option fixtures:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
import type { SeoAutoFixtures } from 'playwright-seo/fixture';
import seoUser from './playwright-seo.config';
import { toRuleConfig } from 'playwright-seo';
export default defineConfig<SeoAutoFixtures>({
projects: [
{
name: 'e2e-seo',
use: {
/**
* Toggle SEO audit by environment // optional
* seoAudit: process.env.APP_ENV !== 'development',
**/
seoAudit: true,
seoOptions: {
config: toRuleConfig(seoUser),
severity: toRunnerOptions(seoUser).severity
},
}
}
]
});This way you can turn things on/off and parameterize them per project without touching the specs, following Playwright's official fixture best practices.
*Other configuration methods available
3) Automatic SEO tests
// example.spec.ts
import { test, expect } from '../support/seo.auto';
test('SEO Tests in playwright-seo - npm', async ({ page }) => {
await page.goto('https://www.npmjs.com/package/playwright-seo');
await expect(page).toHaveTitle(/playwright-seo - npm/);
// ✅ SEO audit runs automatically after the test
});*Other ways of use available
*) Other ways -> Alternatives
Alternative 1 - Wrapper
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
import seoUser from './playwright-seo.config';
import { toRuleConfig } from 'playwright-seo';
export default defineConfig({
projects: [
{
name: 'e2e',
use: {
...devices['Desktop Chrome'],
// Toggle SEO audit by environment:
seoAudit: process.env.APP_ENV !== 'development',
// Apply your generated config:
seoOptions: { config: toRuleConfig(seoUser) }
} as any
}
]
});
toRuleConfigconverts your user config into the internal format used by the engine.
Prefer importing from the root (playwright-seo). The legacy subpath (playwright-seo/config) also works.
Enable audit globally (one-liner)
Create a wrapper once and use it in all specs:
// tests/support/withSeo.ts
import { createSeoTest } from 'playwright-seo';
import seoUser from '../../playwright-seo.config';
import { toRuleConfig, toRunnerOptions } from 'playwright-seo';
export const { test, expect } = createSeoTest({
// Feed the rules/thresholds
defaults: { config: toRuleConfig(seoUser) },
// Runner behavior
dedupePerWorker: toRunnerOptions(seoUser).dedupePerWorker,
severity: toRunnerOptions(seoUser).severity, // 'error' | 'warning'
});Use the wrapper in your tests (swap one import):
// before: import { test, expect } from '@playwright/test'
import { test, expect } from '../support/withSeo';
test('SEO Tests in playwright-seo - npm', async ({ page }) => {
await page.goto('https://www.npmjs.com/package/playwright-seo');
await expect(page).toHaveTitle(/playwright-seo - npm/);
// ✅ SEO audit runs automatically after the test
});Optional alias via tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": { "@tests": ["tests/support/withSeo"] }
}
}Then in specs:
import { test, expect } from '@tests';Targeted Usage
Per project (e.g., staging)
// playwright.config.ts
projects: [
{
name: 'staging',
use: {
seoAudit: true,
seoOptions: {
config: toRuleConfig(seoUser),
severity: toRunnerOptions(seoUser).severity,
excludeUrls: ['/preview/*', /\/internal\//]
},
}
},
{ name: 'local', use: { seoAudit: false } } // turn off locally
]Per spec / describe
Spec - disable for this entire file
// tests/support/seo.auto.ts
import { test } from '../support/seo.auto.ts';
// disable for this entire file
test.use({ seoAudit: false });
test.describe('Test without SEO', () => {
test('loads', async ({ page }) => { /* ... */ });
});describe block - disable for this entire block
// tests/support/seo.auto.ts
import { test } from '../support/seo.auto.ts';
test.describe('Test without SEO', () => {
// disable for this entire block
test.use({ seoAudit: false });
test('loads', async ({ page }) => { /* ... */ });
});Configuration Reference
Rules (on/off)
Key options you can toggle:
enforceHtmlLang: require<html lang="...">enforceViewport: require<meta name="viewport">enforceSingleH1: require exactly one<h1>enforceTitle+title.min/max: check<title>lengthenforceMetaDescription+metaDescription.min/max: check meta description lengthenforceCanonical: require exactly one absoluterel="canonical"enforceImgAlt: require usefulalt(allowsalt=""only when not inside<a>)forbidNoindexOnProd: forbidnoindexwhenAPP_ENV === 'production'checkMainResponseStatus: unexpected status codes on the main responseskipIfNoindex: skip audit ifmeta noindexorX-Robots-Tag: noindexis presentexcludeUrls: patterns to ignore (glob or RegExp)waitFor:'load' | 'domcontentloaded' | 'networkidle'maxNodesPerIssue: how many sample nodes to show per rule
Runner (execution behavior)
runner.dedupePerWorker(default true): Audit each normalized URL once per worker to avoid duplicated logs.runner.severity('error'|'warning', default'error'):error→ violations fail the test (current default behavior)warning→ violations are logged and annotated, but don’t fail
Use them in your wrapper via:
import { toRunnerOptions } from 'playwright-seo';
createSeoTest({
dedupePerWorker: toRunnerOptions(seoUser).dedupePerWorker,
severity: toRunnerOptions(seoUser).severity,
});API Reference
defineSeoConfig
Helper for config IntelliSense and validation in playwright-seo.config.ts.
toRuleConfig
Converts user config → internal rule config consumed by runSeoChecks.
toRunnerOptions
Extracts runner options (e.g., dedupePerWorker, severity) from the user config.
createSeoTest
Creates a Playwright test wrapper that runs the SEO audit after each test.
createSeoTest(opts?: {
defaults?: RunOptions;
dedupePerWorker?: boolean;
severity?: 'error' | 'warning';
})runSeoChecks
Runs the audit once on the current page and returns a report.
const result = await runSeoChecks(page, { config, formatter? });Output & Logging
Failure output follows Playwright’s style and includes the audited URL, the failing rule, and pretty-printed HTML snippets for problematic elements, e.g.:
SEO violations at: https://www.npmjs.com/package/playwright-seo
────────────────────────────────────────
[img-alt] Images without useful alt: 2
• Element 1:
```html
<img src="/x.webp" alt="">
**Severity behavior**:
- **`error`**: prints the violations and **fails** the test (`expect(...).toBeTruthy()`).
- **`warning`**: prints the violations via `console.warn` and adds a `seo-warning` **annotation** to the test (visible in the HTML report), but **does not fail**.
---
## Best Practices
- Keep `skipIfNoindex: true` to avoid false alarms on intentionally hidden pages.
- Use `excludeUrls` for admin, health, preview, and internal routes.
- Leave `runner.dedupePerWorker: true` to reduce log noise.
- Run the audit on **stable environments** (staging/prod) for reliable results.
- Treat failures as **actionable tasks**: missing `alt`, duplicate canonicals, etc.
---
## Troubleshooting
**“Cannot find name 'document' / 'HTMLElement'”**
Add DOM libs to the tsconfig compiling the package/project that uses the lib:
```json
{ "compilerOptions": { "lib": ["ES2021", "DOM", "DOM.Iterable"] } }“Cannot find module 'node:fs'” in the CLI
Install Node types and ensure they’re enabled:
{ "compilerOptions": { "types": ["node"] } }Module resolution (TS)
You can import from the root (playwright-seo) in any project.
The legacy subpath import (playwright-seo/config) is also supported (via exports + typesVersions) for projects using moduleResolution: node.
Update notifier in the CLI
Silence with:
PLAYWRIGHT_SEO_UPDATE_NOTIFIER=falseCI is silent by default (CI=true).
FAQ
Do I have to change all my imports?
Just once. Point tests to the wrapper (../support/withSeo or @tests).
Prefer a one-off code mod if you have many files.
Alternatively, call runSeoChecks only where you want it.
Does it work with plain JS projects?
Yes. The wrapper and CLI work in JS. If you use TS, you’ll get IntelliSense in playwright-seo.config.ts.
Monorepo?
Add a playwright-seo.config.ts per package that has tests, and import it from that package’s playwright.config.ts.
Where do I configure “dedupe per worker” and severity?
In your config under runner, and pass it to the wrapper via toRunnerOptions:
import { toRunnerOptions } from 'playwright-seo';
createSeoTest({
dedupePerWorker: toRunnerOptions(seoUser).dedupePerWorker,
severity: toRunnerOptions(seoUser).severity
});Contributing
PRs are welcome! Please:
- Run
npm run typecheckandnpm run build. - Add tests or examples for new rules/behaviors (e.g., severity).
- Keep the README and config template in sync.
License
MIT © Contributors
Happy testing!
