Skip to content
An SEO linter for CI

Catch SEO bugs before Google does

A Nuxt module, a DevTools tab and a CI crawler that fails the pipeline on SEO regressions. Any stack.

$npx nuxi module add @ranklint/nuxt
ranklint diff, demo app in this repo
$ranklint audit --url http://localhost:3000 --reporter json --output report.json

24 pages · playwright chromium · 7.1s

$ranklint diff --base main --current report.json --reporter markdown
errormeta:title-required/bugs/title-missing — Page has no <title>
errormeta:description-required/bugs/description-missing — Page has no meta description
errorheadings:single-h1/bugs/h1-multiple — Page has 2 <h1> elements, expected exactly 1
warnimages:alt-required/bugs/images-bad — Image has no alt attribute
warnimages:dimensions-required/bugs/images-bad — Image has no explicit width/height
warnimages:no-lazy-above-fold/bugs/images-bad — Image in the viewport uses loading="lazy"
6 new issues, 3 of them errorsexit code 1 · pipeline stopped
43
built-in rules
< 1 KB
client runtime, gzipped
6
report formats
MIT
license, open source
01 — what it is

A linter, not a score out of 100

Ranklint reads the HTML your server actually returns to a crawler and names the exact problem: an empty title, a broken canonical, a heading hierarchy that skips a level, a one-way hreflang pair. Three surfaces, one set of rules.

Nuxt module

sitemap.xml, robots.txt and typed JSON-LD out of the box. One command to install, no config required.

useJsonLduseRanklintIgnore

DevTools tab

Issues, heading structure and meta tags for the current route — right on the dev server, while you write code.

live checkslight and dark

CI crawler

Crawls your deployed site through Chromium. Works with any stack — Nuxt not required.

auditdiffwatch
02 — try it

Try it on your HTML

The same rules, running in this tab. Paste the markup of a page and the linter answers right here, with no server and no install.

Page HTMLlevel 1
<!doctype html>
<html lang="en">
<head>
  <title>Pricing</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta property="og:title" content="Pricing">
  <meta property="og:description" content="Plans and prices for teams of every size.">
  <meta property="og:image" content="https://example.com/og/pricing.png">
</head>
<body>
  <h1>Pricing plans for growing teams</h1>
  <h3>What the team plan includes</h3>
  <img src="/plans.png">
  <h1>Frequently asked questions</h1>
</body>
</html>
03 — in the editor

Visible on the dev server, not a month later

The SEO tab in Nuxt DevTools shows the current route the way a crawler sees it: the list of issues, the heading tree and every meta tag. It refreshes when the host page navigates and when the DOM changes, and there is a Rescan button.

  • Every finding carries a selector and a suggestion on how to fix it
  • 21 of the 43 rules need no network, with the same ids and levels as in CI
  • JSON-LD is validated in dev: warnings land in the console and in the panel
How to set it up
SEO PagesComponentsPayload/bugs/heading-jump
1warn
headings:hierarchyHeading level jumps from h1 to h3
heading structure
h1Heading jump fixture main heading here
h3This level jumps over h2 on purpose← h2 skipped
05 — in CI

The exit code decides, not the meeting

A quarterly crawl turns into a backlog everyone forgets. Ranklint checks every pull request: audit produces a JSON report, diff compares it against the base branch and fails the pipeline only on new errors. Existing debt never blocks a merge.

New findings compared to main

--reporter markdown
levelruleURLwhat happened
errormeta:title-required/bugs/title-missingPage has no <title>
errormeta:description-required/bugs/description-missingPage has no meta description
errorheadings:single-h1/bugs/h1-multiplePage has 2 <h1> elements, expected exactly 1
warnimages:alt-required/bugs/images-badImage has no alt attribute
warnimages:dimensions-required/bugs/images-badImage has no explicit width/height
warnimages:no-lazy-above-fold/bugs/images-badImage in the viewport uses loading="lazy"
Pre-existing findings are flagged in the report but never fail the build. exit 1

Report formats

markdownjsonjunithtmlgitlabgithub

junit goes into your runner's reports, gitlab is a Code Quality report, and github turns every finding into a pull request annotation.

Two steps in the pipeline

$ ranklint audit --url https://preview --reporter json --output report.json
$ ranklint diff --base main --current report.json --reporter markdown

Code review misses SEO regressions — in a browser everything looks fine. The exit code doesn't.

zones on one domain
  • /marketing — Nuxt 4owner: self
  • /docsdocumentation — staticowner: self
  • /appdashboard — SPA behind authowner: external

Each zone has its own list of checks and an owner: a zone you own is crawled in full, an external one only gets a HEAD check on the links that point at it.

06 — any stack

Nuxt is optional

The crawler speaks HTTP and renders pages in Chromium through Playwright. It does not care what serves them — Next, Django, WordPress or static files on a CDN.

  • Profiles gate the slow work: Lighthouse on release, a plain audit on every pull request
  • The Lighthouse profile plugs in as a separate check
  • Custom rules are plain functions that live in your repository
  • An HTML report with grouped findings — for people who live outside the terminal
07 — production

Checking does not stop at deploy

The ranklint audit --url https://prod --mode monitor run crawls production on a schedule, keeps every report in history and tells you when something breaks.

CrUX field data

Real-user LCP, CLS and INP for the whole origin land next to the linter's findings in the same report.

LCPCLSINP

Search Console

URL Inspection reports Google's indexing verdict, the coverage state and rich-results issues for the first ten pages of the crawl.

Alerts and history

A Slack or Telegram message on every new error; the report history and the dashboard live on GitHub or GitLab Pages.

08 — dashboard

Every monitor run publishes a dashboard

Add one step after the monitor and every scheduled run publishes a static site to GitHub or GitLab Pages. The team sees errors by page, trends and history without opening a terminal.

Dashboard of the last runexample.com
ranklint dashboard for example.com: 38 errors and 11 warnings in the last run, new and fixed issues since the previous run, the error trend and Lighthouse scores by page

What the team sees

  • A checklist for every page: what to fix, how, and a link to the rule docs
  • Fix once: issues that repeat across a section or the whole site, usually one template
  • Error trends, Lighthouse scores by page and CrUX once the key is set
  • Every run archived with its full report, new and fixed issues marked

One command, your brand

$ ranklint dashboard \
  --dir .ranklint/reports --out public
--logo--brand-name--accent--run-url

Your logo, name and accent color instead of ours. --run-url adds a Run audit button that opens your CI pipeline.

How to set up the dashboard
09 — in depth

What is an SEO linter

An SEO linter reads the HTML your site actually returns and reports the things that cost you traffic: a missing title, a canonical pointing at a redirect, a heading level that jumps from h1 to h3, a hreflang pair that only links one way. It is the same idea as ESLint, applied to the markup search engines read instead of the JavaScript you ship.

SEO audit in CI, not in a spreadsheet

Most SEO work dies in a spreadsheet. Someone runs a crawler once a quarter, exports a list of 400 issues, and the list rots while the site keeps shipping. A linter inverts that: the crawl runs on every pull request, the report is a diff, and the only thing you have to look at is what changed since the last green build. ranklint stores the JSON report as a build artifact, compares the current run against it and posts the difference as a comment.

See how audit and diff fit together

SEO regression testing on every pull request

Regressions are rarely dramatic. A refactor drops the canonical from one template, a CMS migration leaves 40 pages without a description, an experiment adds noindex and nobody removes it. Those changes pass code review because nothing looks broken in the browser. A crawl that runs with your tests catches them in the same hour they were introduced. Severity is per rule, so title length can stay a warning while a missing canonical stops the merge.

Configure rules and severities
The exit code makes the decision instead of a person.

Nuxt SEO module with zero config

In a Nuxt 4 app the module covers the parts that are pure plumbing. It serves sitemap.xml from your routes, robots.txt that knows the difference between staging and production, and JSON-LD that is validated against Schema.org while you develop, so a broken Product or FAQPage block shows up as a console warning instead of a silent loss of rich results. Each block is opt out, and a disabled block is never registered, so it costs nothing at runtime.

Read the module overview

Any stack: crawl a deployed site

The crawler talks HTTP, not framework internals. Point it at a URL and it renders each page in Chromium, which means client rendered content is judged the way a search engine judges it, and pages that only exist after hydration are reported as an indexing problem. One domain can be split into zones when different apps live under different paths, and profiles keep the slow checks off the fast pipeline.

Start with the CLI
10 — questions

What people usually ask

How is this different from Lighthouse?
Lighthouse gives a page a score. Ranklint crawls the whole site and returns a list of concrete rules with ids and levels — something you can fail a build on and diff between branches.
And from Nuxt SEO?
Nuxt SEO helps you generate tags. Ranklint checks what was actually served — and does it in CI as well. They don't conflict.
We're not on Nuxt. Will it work?
Yes. The CLI crawler works over HTTP against any deployed site. Without Nuxt you only lose the module and the DevTools tab.
Won't this start failing every build?
That is what ranklint diff is for: it fails a build only on errors that are new relative to the base branch, while a bare audit exits 1 on any error at all. Run diff in the pipeline and accumulated debt stays in the report.
Can I disable a rule on a single page?
In Nuxt, yes — useRanklintIgnore inside the page. There is no per-URL override anywhere else: set the rule to off in rules, or drop the address from the crawl with crawl.ignore.
Is Playwright required?
The crawler needs it to render pages in Chromium. The Nuxt module and the DevTools tab don't.
What does it cost?
Nothing. Ranklint is entirely free — open source under the MIT license, with no paid tiers and no limits.
Open on GitHub

The next SEO regression won't reach production

Install the module, add two steps to your pipeline — from there the exit code decides.