routelint
RouteLint
RouteLint is a CLI for finding technical SEO problems in your deployed site. It catches broken links, redirects that land in the wrong place, empty server-rendered pages, and conflicts between your canonicals, sitemaps, and indexing rules.
Use it after a deploy, during a URL migration, or in CI to catch regressions. Start with a URL:
npx routelint check https://example.com
Checks start with the HTML and headers your server returns. Add --rendered to compare them with the DOM after JavaScript runs, using Playwright. Reports give you findings tied to URLs and the evidence behind them.
What it checks
| Area | What RouteLint looks for |
|---|---|
| Route discovery | URLs from seeds, files or stdin, sitemaps and sitemap indexes, robots.txt, internal links, and optional Next.js build manifests |
| HTTP delivery | Errors, redirect chains, final destinations, content types, response sizes, timeouts, and different responses for different bots |
| Redirect contracts | Whether a redirect uses the expected first-hop status, reaches the right destination within the hop limit, and lands on a healthy target |
| Server-rendered HTML | Missing or misplaced titles, descriptions, canonicals, robots directives, H1s, and language; empty shells, soft 404s, and duplicate body content |
| Rendered comparison | Content and SEO tags that only appear after JavaScript runs, plus differences from the server response |
| Link graph | Broken links, links through redirects or to noindex pages, orphan pages, dead ends, and pages buried too deep |
| Indexing consistency | Sitemap URLs that redirect, fail, or use noindex; missing sitemap coverage; robots conflicts; unhealthy or shared canonical targets |
| International SEO | Invalid or duplicate hreflang values, missing or noindex targets, and missing return links |
| Next.js | App Router and Pages Router paths, prerendered and ISR routes, redirects, dynamic route samples, basePath, and routes missing after deployment |
| Regression checks | Differences between saved reports, or output limited to new and worsened findings |
HTML checks ignore unused <template> contents. If your page inserts that content with JavaScript, use --rendered to check the resulting DOM.
You control the crawl's page count, depth, response size, redirects, timeout, and concurrency. The crawl graph stays on the origin you provide.
RouteLint respects robots.txt by default. A missing file (404/410) allows crawling. If the file is unavailable, invalid, too large, or times out, RouteLint skips requests and records why. Use --ignore-robots when you intend to bypass that policy.
Quick start
RouteLint requires Node.js 22.12.0 or newer.
Check a deployed site
npx routelint check https://example.com
For a report you can open in a browser or save as a CI artifact:
npx routelint check https://example.com \
--format html \
--output routelint-report.html
The HTML report is a single file with no external assets. The graph and tables work without JavaScript; enable it for search and filtering.
Add Next.js build routes
Build your app and start its production server, then run:
npx routelint next http://localhost:3000 --root .
RouteLint reads supported .next manifests and checks those routes against the running site. It does not import or execute your application code. For a dynamic route such as /blog/[slug], provide real URLs through config, a sitemap, or internal links:
baseUrl: http://localhost:3000
next:
root: .
buildDirectory: .next
samples:
/blog/[slug]:
- /blog/hello-world
/shop/[...path]:
- /shop/shoes/running
Next.js manifests are internal build files, so their formats can change. RouteLint warns about unsupported data and marks ambiguous rendering modes as unknown, including partially prerendered routes it cannot classify.
Nuxt, Astro, and other SSR sites
Use check with your site's URL. Sitemaps and internal links supply the routes:
npx routelint check https://example.com \
--sitemap https://example.com/sitemap-index.xml
No framework adapter is needed: these checks work with the HTTP responses and HTML your site serves.
Check an explicit route inventory
Already have a list of routes? Put one URL or path on each line. Blank lines and # comments are ignored. Invalid entries fail the run with a line number; URLs on other origins are skipped and reported.
# routes.txt
/
/pricing
/docs/getting-started
https://example.com/legal
npx routelint check https://example.com --urls routes.txt
cat routes.txt | npx routelint check https://example.com --urls -
Verify intentional redirects
For a migration or URL cleanup, tell RouteLint where each old path should go:
baseUrl: https://example.com
redirects:
- from: /old-pricing
to: /pricing
status: 301
maxHops: 1
This expects /old-pricing to return a 301 and reach /pricing in one hop. RouteLint adds both URLs to the crawl and reports a missing redirect, wrong status, wrong destination, or unhealthy target as an error. Extra hops produce a warning. Redirects that match are counted as verified.
Contracts support exact sources, query strings, path patterns such as /old/*, and destinations on other origins. Pattern samples add concrete sources to check. routelint next also checks concrete, unconditional redirects from supported build manifests. See the redirect guide for the supported cases and finding codes.
Compare SSR with the rendered DOM
If content or metadata depends on JavaScript, enable browser comparison. Install Playwright and Chromium alongside RouteLint:
npm install --save-dev routelint playwright
npx playwright install chromium
npx routelint check https://example.com --rendered
Playwright is optional; a normal RouteLint install does not download a browser. During rendered checks, preview headers are sent only to the audited origin, and service workers are blocked.
Commands
routelint check [base-url] Crawl and lint any HTTP(S) site
routelint next [base-url] Add Next.js build routes to the live crawl
routelint diff <old> <new> Compare two JSON reports
routelint init [file] Write a documented starter config
Run routelint <command> --help for the full option list.
Useful examples:
# Compare Googlebot with a browser-like request
npx routelint check https://example.com --agent googlebot --agent browser
# Keep meaningful query-string routes
npx routelint check https://example.com --query keep
# Limit the crawl to public product and article paths
npx routelint check https://example.com \
--include '/products/**' \
--include '/articles/**' \
--exclude '/articles/preview/**'
# Save a baseline before a deployment
npx routelint check https://example.com --format json -o baseline.json
# After deploying, capture a new report and compare
npx routelint check https://example.com --format json -o current.json
npx routelint diff baseline.json current.json --fail-on warning
# On later CI runs, emit and fail only on new or worsened findings
npx routelint check https://example.com \
--changed-only baseline.json \
--format sarif \
--output routelint.sarif
Configuration
Save your usual options in routelint.config.yml. Generate a starter file with:
npx routelint init
Here's an example with a redirect contract, crawl limits, and a rule override for /docs/**:
baseUrl: https://example.com
seeds:
- /
urls: []
sitemaps: auto
agents:
- routelint
respectRobots: true
queryPolicy: drop
redirects:
- from: /old-pricing
to: /pricing
status: 301
maxHops: 1
include: []
exclude:
- /account/**
limits:
maxPages: 250
maxDepth: 8
concurrency: 6
timeoutMs: 15000
maxBytes: 2000000
maxRedirects: 5
delayMs: 0
honorCrawlDelay: true
audit:
requireTitle: true
requireDescription: true
requireCanonical: true
requireH1: true
requireSitemapCoverage: true
maxDepth: 4
# Optional response-time budgets, in milliseconds:
# maxResponseMs: 1000
# maxRedirectHopMs: 500
severities:
noindex: info
paths:
- include:
- /docs/**
requireDescription: false
severities:
missing-h1: info
rendered:
enabled: false
concurrency: 2
timeoutMs: 20000
settleMs: 250
Command-line options override the config file. JSON config works too.
Raw page requests, including redirect hops and agent comparisons, honor the matching robots.txt Crawl-delay or Request-rate, capped at 10 seconds. Set limits.delayMs or --delay 250ms for explicit minimum spacing; explicit delays are not capped. --ignore-crawl-delay ignores robots pacing while keeping access rules. Discovery requests for robots.txt and sitemaps, and optional browser rendering, use their own request policies.
Response-time budgets inspect the primary agent's complete capture and its redirect hops. Captures include network and body-processing time, excluding deliberate pacing waits. Path scopes can override both budgets; each redirect hop uses its own URL's scope.
Preview credentials
For a protected preview, read credentials from an environment variable:
headers:
Authorization: ${ROUTELINT_AUTHORIZATION}
export ROUTELINT_AUTHORIZATION='Bearer replace-me'
npx routelint check https://preview.example.com
You can also repeat --header 'Name: value' on the command line. Configured header values stay out of reports, sensitive response headers are redacted, and configured headers are stripped when a redirect crosses an origin.
Reports can still contain private URLs. Keep them and real credentials out of Git, and only check systems you have permission to access.
Reports and CI
Choose terminal for a quick read, html for a shareable report, json for saved baselines or tooling, and sarif for code-scanning integrations. markdown produces a GitHub summary, csv exports findings for spreadsheets, and junit produces CI test results. JUnit maps errors and warnings to failures, independently of the CLI's --fail-on exit threshold; informational findings pass.
npx routelint check https://example.com \
--format sarif \
--output routelint.sarif \
--fail-on error
Set --fail-on to error, warning, info, or none to choose which findings fail the command.
- Exit
0: the command completed and no finding met the threshold. - Exit
1: at least one finding met the threshold. - Exit
2: configuration, arguments, or the command itself failed.
To check your own site in CI, start with examples/github-actions.yml.
This repository's CI verifies RouteLint itself: lint, types, tests, builds, and package installation, with jobs on Linux, macOS, and Windows. Releases go through a PR; npm publishing happens interactively from a maintainer's terminal. See PUBLISHING.md.
Reading the result honestly
A route that failed and a route that could not be checked need different follow-up. Reports tell you when robots rules, crawl limits, timeouts, or oversized responses prevented a check. If the crawl is cut short, RouteLint avoids treating every unseen target as missing.
RouteLint checks delivery and technical SEO. It does not:
- calculate an SEO score;
- claim browser equivalence unless
--renderedwas enabled and the capture completed; - diagnose hydration errors or user interactions;
- calculate Core Web Vitals;
- submit URLs to search engines;
- crawl external links by default;
- emulate framework-specific dynamic or conditional redirect rules;
- claim that an unobserved dynamic route exists;
- replace a full authenticated browser test or a search-engine index report.
For details, read the finding reference, redirect guide, rendered checks, URL-list guide, or Next.js discovery notes.
Programmatic API
import { loadConfig, renderJsonReport, runRouteLint } from "routelint";
const config = await loadConfig({
overrides: {
baseUrl: "https://example.com",
maxPages: 100,
},
});
const report = await runRouteLint(config);
process.stdout.write(renderJsonReport(report));
Reports carry a schema version, which RouteLint validates when reading a saved baseline. Redirect helpers and their TypeScript types are also exported; report.redirectContracts gives you the results from a normal run.
Development
npm ci
npm run check
npm run check runs Biome, strict TypeScript, the full test suite, a clean build, and an installed-tarball smoke test.
License
MIT. Built by Niko M..
Related Packages
Deterministic technical-SEO audit from your terminal. Crawls a site, measures re...
SEO essentials for Next.js and Nuxt with Contentful or Contentstack: per-entry m...
Version History
| Version | Released | License |
|---|---|---|
| 0.4.0 | MIT | |
| 0.3.1 | MIT | |
| 0.3.0 | MIT | |
| 0.2.0 | MIT | |
| 0.1.0 | MIT |