Methodology ·
Leaderboard ·
Report a bug ·
Skills for agents
The only tool purpose-built for **programmatic SEO compliance**. It shifts the unit of analysis from URL to template: point it at a 10,000-URL pSEO directory and pseolint identifies the template clusters (e.g. `/listing/:slug`, `/category/:slug`), samples K pages from each, and produces a per-template verdict + variance metric. **Fix one template, fix N pages.**
```bash
npx pseolint http://localhost:3000
```
Table of contents
- [Why this exists](#why-this-exists)
- [How pseolint differs](#how-pseolint-differs)
- [Quick Start](#quick-start)
- [What It Checks](#what-it-checks): the 65 rules
- [CLI Options](#cli-options)
- [GitHub Action](#github-action)
- [Fix rail: from audit to pull request](#fix-rail--from-audit-to-pull-request)
- [Skills for Claude & coding agents](#skills-for-claude--coding-agents-new)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
## Skills for Claude & coding agents (new)
Design pages that pass *before* you crawl them. The [`skills/`](skills/) suite gives
Claude (and any agent that supports the `skills` / Claude Code plugin format)
programmatic-SEO and **answer-engine optimization (AEO / GEO)** guidance where
every recommendation is bound to a runnable pseolint rule:
```bash
npx skills add ouranos-labs/pseolint --skill pseolint aeo
```
- **`pseolint`**: full-lifecycle programmatic SEO: design → build → audit → fix → gate.
- **`aeo`**: get cited in AI Overviews / ChatGPT / Perplexity, not just ranked.
Unlike prose checklists, these have teeth: the design-time advice ends in
`npx pseolint` pass/fail. See [`skills/README.md`](skills/README.md).
## Why this exists
Programmatic SEO works, when it works. The gap between "1,000 indexed pages" and "1,000 pages that survive a SpamBrain pass" is where most pSEO sites die. The Helpful Content Update made that gap permanent.
Existing SEO tools (Screaming Frog, Sitebulb, Ahrefs Site Audit) were built for editorially-curated sites. They check pages one at a time. But the SpamBrain risks of pSEO are *between* pages: doorway clusters, near-duplicates, entity-swap templates, thin-content propagation. You can't catch them with per-page rules.
pseolint audits the graph: it groups results by template before surfacing them. Run it before you publish, gate it in CI, fix the broken template before SpamBrain does.
### How it compares
| | pseolint | Screaming Frog | Ahrefs Site Audit | Sitebulb |
|---|:---:|:---:|:---:|:---:|
| Unit of analysis | **template cluster** | URL | URL | URL |
| Near-duplicate / doorway / entity-swap detection | ✅ | partial | - | - |
| SpamBrain-policy risk verdict | ✅ | - | - | - |
| AEO / AI-Overview citability checks | ✅ | - | - | - |
| AI fix → pull request | ✅ | - | - | - |
| CLI · GitHub Action · MCP server | ✅ | desktop | SaaS | desktop |
| Open source | ✅ MIT | - | - | - |
The general-purpose crawlers do plenty pseolint doesn't (JS rendering at scale, backlink data, log-file analysis). pseolint is the specialist for the one thing they weren't built for: **programmatic-SEO compliance at the template level.**
## How pseolint differs
- **Graph-level, not page-level.** Detects near-duplicate clusters, doorway patterns, and entity-swap doorways across thousands of pages. Per-page tools can't see these.
- **SpamBrain + AI Overview.** 65 rules across 8 categories: SpamBrain-policy mapping (penalty risk) plus `aeo/*` (AI Overview citability: `llms.txt`, AI-crawler access, citable facts, answer-first, summary-bait).
- **Developer workflow, not SaaS UI.** CLI, GitHub Action, JSON/HTML reports, MCP server, browser extension (SERP competitive recon). Lives in your repo and your PRs.
- **Actionable, not advisory.** Every finding has a fix, an effort tag (`quick fix` / `moderate` / `structural`), and a Google docs reference.
- **Safe for hosted use.** SSRF guard (DNS-validated), robots.txt honoured for our own crawler, analytics-blocking in render mode, `AbortSignal` cancellation, `safeMode: "saas"` preset for embedding in services.
- **Calibrated against reputable pSEO.** Engine verdicts are calibrated against a curated corpus of in-production pSEO sites that demonstrably win in search. Doorway-pattern findings cluster (no more per-pair noise); verdicts are reproducible at a fixed `sampleSeed`. Dated snapshot results, the open-source corpus, and the trade-offs we accepted live at [pseolint.dev/methodology](https://pseolint.dev/methodology). Spec: [docs/superpowers/specs/2026-05-03-calibration-against-reputable-pseo.md](./docs/superpowers/specs/2026-05-03-calibration-against-reputable-pseo.md).
- **Authority-blind by design, with a manual override.** pseolint analyses static content + the link graph it can see. It does NOT measure backlinks, brand mentions, domain age, or any external trust signal: there is no Moz/Ahrefs/Semrush dependency. This means the engine itself is calibrated for the authority tier of the calibration corpus (established brands). It exposes `authorityScore` (0-100, via the `--authority-score` CLI flag, the core API, or the MCP param) so callers can adjust the verdict ladder for their tier: `>= 80` shifts one tier lenient (established brand can absorb shapes a newer site can't); `<= 30` shifts one tier stricter. Raw `risk` number unchanged so CI gates stay stable. Without the flag, treat verdicts as a directional minimum.
- **Honest about blind spots.** Beyond domain authority, pseolint does not currently detect: image SEO dimensions, schema-content drift (e.g. JSON-LD price ≠ rendered price), outbound-link health, search-intent alignment, parameter-URL crawl-budget waste, and a handful of specialty gaps (mobile-friendliness, cookie-banner detection, AMP/News/Video schema). The complete blind-spot audit lives at [docs/superpowers/specs/2026-05-03-pseolint-blind-spots.md](./docs/superpowers/specs/2026-05-03-pseolint-blind-spots.md): every gap categorized by impact tier with the roadmap fix.
Full version history (calibration rounds, per-rule changes, safety hardening) is in [CHANGELOG.md](./CHANGELOG.md).
## Grow: make the ignored pages rank (no backlinks)
A pSEO site rarely has a penalty problem. It has a **Google-ignores-most-of-it** problem: thousands of sitemap URLs, a few percent earning impressions. Backlinks are not the only lever. Search Console already records which of your pages Google chose to show. `pseolint grow` reads that, crawls the site, and learns what separates shown pages from ignored ones **within the same template**. Each site is its own control group, so domain authority cancels out. Then it writes the plan.
```bash
# Search Console → Performance → Pages → Export (last 3 months) → Pages.csv
npx pseolint grow https://yoursite.com --gsc Pages.csv --out growth/
```
What you get:
- **Show rate per template.** The pSEO number that matters: `/city/:slug 4,200 pages, 3% shown`.
- **What Google rewards on this site.** Shown vs ignored pages per template, compared with a Mann-Whitney U test and Benjamini-Hochberg false-discovery control. Example: *"Shown pages have MORE internal PageRank: 0.51 vs 0.38 (AUC 0.85)"*, or *"LESS similarity to sibling pages: 70% vs 85%"*. Only differences that survive the correction are printed.
- **A link plan (`links.csv`).** Every link goes `from → to` with its anchor. Links start on pages Google already shows, because those get recrawled, and point to topically related pages that sit at positions 8-30 or get no impressions at all. Existing links are never re-suggested.
- **Title rewrites (`retitle.csv`).** Pages ranking in the top 15 whose CTR is under half the expected CTR for that position.
- **Self-inflicted gaps.** Sitemap URLs that are noindex, canonicalised away or erroring (`unblock.csv`), and pages earning impressions that the sitemap forgot (`sitemap-additions.txt`).
- **Differentiate / prune.** Ignored near-clones of their siblings, and templates Google rejects wholesale (under 5% shown).
Then tell the engines that index on request:
```bash
# Serve https://yoursite.com/.txt containing the key, then:
npx pseolint indexnow https://yoursite.com --key --since 7
```
IndexNow reaches Bing, Yandex, Seznam and Naver, and Bing's index also feeds ChatGPT search and DuckDuckGo. Google ignores IndexNow, and its Indexing API only accepts job and livestream pages. For Google, the levers are the link plan and an accurate sitemap `lastmod`.
Re-run `grow` monthly with a fresh export. The template show rates are your scoreboard.
## Quick Start
```bash
# Point it at your local dev server: that's it
npx pseolint http://localhost:3000
```
Automatically discovers all pages by following internal links. No sitemap, no config, no build step needed.
```bash
# Save a visual report
npx pseolint http://localhost:3000 --format html --output report.html
# Audit a live site (per-template output is the default)
npx pseolint https://yoursite.com
# CI gate on build output
npx pseolint ./out --ci-threshold concerning --format json
```
### Per-template output (v0.6 default)
```
Verdict: CONCERNING
Integrity C · Discoverability B · Citation C · Data A
Per-template breakdown (3 templates):
/listing/:slug CONCERNING C
10/8201 URLs (0.1%) uniformity 85%
8/10 samples fail `spam/thin-content`
/category/:slug READY A
10/312 URLs (3.2%) uniformity 94%
/help/:slug CAUTION B
10/47 URLs (21.3%) uniformity 78%
3/10 samples fail `content/missing-author`
```
`--format json` includes the `templates` array alongside the existing `findings` list:
```json
{
"verdict": "concerning",
"risk": 60,
"templates": [
{
"signature": "/listing/:slug",
"totalUrls": 8201,
"auditedUrls": ["https://example.com/listing/foo", "..."],
"verdict": "concerning",
"risk": 60,
"variance": {
"uniformityScore": 0.85,
"topDriver": { "ruleId": "spam/thin-content", "fireRate": 0.8 }
}
},
{ "signature": "/category/:slug", "verdict": "ready", "risk": 12 }
],
"findings": [...]
}
```
Use `--legacy-flat` to suppress the template cards and get the v0.5-style flat findings list.
### Partial coverage (`truncated`)
If the crawl is interrupted (e.g. the backpressure watchdog aborts because the origin is degrading), pseolint still emits whatever it collected, flagged as partial:
```json
{
"verdict": "ready",
"risk": 12,
"truncated": true,
"truncatedReason": "Origin degraded mid-crawl (p95 latency exceeded threshold)",
"pageCount": 42
}
```
When `truncated` is `true`, **treat `pageCount`, `risk`, and `verdict` as lower bounds**: a partial pass is not a full pass. The CLI prints a `PARTIAL REPORT` banner and exits non-zero; the GitHub Action warns (and can fail with `fail-on-truncated: true`); the MCP tools and web report surface the same flag. Programmatic consumers should branch on it. The full output contract is published as a JSON Schema (`packages/core/schemas/audit-summary.schema.json`, `$id` carries the `schemaVersion`).
## Audit Modes
| Mode | Command | What you get |
|------|---------|-------------|
| **Local dev server** | `npx pseolint http://localhost:3000` | Full rendered pages, HTTP headers, redirect detection, crawl discovery. **Best results.** |
| **Live site** | `npx pseolint https://yoursite.com` | Same as above against production. Slower (network latency). |
| **Build directory** | `npx pseolint ./out` | Static HTML files only. No HTTP headers, no redirect detection, no soft-404 detection, no sitemap comparison. Use for CI gates. |
> **Why localhost is recommended:** Build directories contain framework artifacts (Next.js `[slug].html` shells, empty client-rendered pages) that produce false positives. Your dev server renders the actual pages Google will see: with canonicals, meta tags, and full content.
## What It Checks
**65 rules** across **8 categories** (all 8 scored), producing a weighted **SpamBrain Risk Score** (0-100) and an independent **AEO sub-score** for AI Overview citability. Every rule is backed by a primary source (Google Search Central, sitemaps.org, ogp.me, Lighthouse); the checks we deliberately *refuse* to run (folklore the primary sources contradict, like title/description character limits) live in [docs/folklore.md](./docs/folklore.md):
### SpamBrain Risk Detection
| Rule | What It Checks | Severity |
|------|---------------|----------|
| `spam/near-duplicate` | SimHash similarity between all page pairs (>85%) | Critical |
| `spam/entity-swap` | Doorway pages where only a proper noun changes | Critical |
| `spam/doorway-pattern` | Composite: entity-swap + thin + identical structure + same meta | Critical |
| `spam/thin-content` | Pages below 300 words (excluding nav/header/footer) | Error |
| `spam/boilerplate-ratio` | Pages with >70% shared template content | Error |
| `spam/template-diversity` | Identical DOM structure across all pages | Warning |
| `spam/publication-velocity` | >100 pages sharing the same publish date | Warning |
| `spam/template-coverage` | Template dimension coverage (e.g. 87 of 960 possible combinations) | Info |
### Content Quality
| Rule | What It Checks | Severity |
|------|---------------|----------|
| `content/unique-value` | Each page must have 100+ words not found on any other page | Error |
| `content/meta-uniqueness` | Meta descriptions identical after entity masking | Error |
| `content/title-uniqueness` | Empty/missing title, very short or excessively long title, or two pages sharing the exact title (raw, not entity-masked: catalog templates with per-record entity values pass) | Error / Warning / Info |
| `content/heading-structure` | No `