ranklint — complete reference

SEO toolkit for Nuxt 4: sitemap/robots/JSON-LD generation out of the box, a live SEO linter in DevTools, and regression control in CI. Everything it does, every setting, every nuance.

v0.1 · 7 packages · 42 rules · Nuxt ^4.0.0 · Node ≥ 20 · MIT

Packages & architecture

Dependencies point strictly downward: core knows nothing about Nuxt or Playwright — page loading is described by the PageFetcher interface (implementations: HttpFetcher — bare fetch, PlaywrightFetcher — real Chrome). Everything revolves around four types: PageSnapshot (html after hydration + ssrHtml before JS, status, headers, ttfb, links, above-fold images) → CheckIssue (checkId, severity, message, url, selector, suggestion, docs) → Report (formatVersion: 1).

Every feature block is disableable — disabled code is not registered at all. The module's client bundle contribution is 485 bytes gzip (a 5 KB limit is guarded by a CI test).

Nuxt module

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@ranklint/nuxt'],
  ranklint: {
    site: { url: 'https://example.com', name: 'Example' },
    sitemap: {
      enabled: true,              // false or sitemap: false — the route is not registered
      path: '/sitemap.xml',
      sources: [ /* three kinds of sources, see below */ ],
      cacheTtl: 3600,             // in-memory response cache, seconds
    },
    robots: { mode: 'owner' },    // false — leave robots.txt alone entirely
    jsonLd: true,                 // false — useJsonLd is not registered
    devtools: true,               // false — the SEO tab is not registered
  },
})

Works with zero configuration: /sitemap.xml and /robots.txt are available right after npx nuxi module add @ranklint/nuxt. Requires Nuxt 4 — throws a clear error when compatibilityVersion < 4.

Sitemap: four sources of entries

Nuances: a failing source → console.warn + serve what was collected (a partial sitemap beats a 500). loc is absolutized against site.url, entries are deduplicated, XML is escaped. The path is added to nitro prerender — with nuxt generate sources are queried at build time.

Robots: environment policy

The route is registered only with mode: 'owner'. Environment: NUXT_RANKLINT_ENV → otherwise autodetect (dev, known CI variables). Non-prod → Disallow: / (UAT never leaks into the index), prod → Allow: / + a Sitemap: directive. An explicit override always beats autodetect. For a team that does not own the root robots.txt — ranklint generate robots-fragment.

runtimeConfig

Options live in runtimeConfig.ranklint — overridable per environment via env variables without a rebuild: NUXT_RANKLINT_SITE_URL and friends.

Composables

useJsonLd(type, data)

useJsonLd('Product', {
  name: 'Widget',
  offers: { price: 9.99, priceCurrency: 'USD' },
})

Renders <script type="application/ld+json"> via useHead with @context/@type. In dev the data is validated by Zod schemas (lazily imported — the validator never reaches the prod bundle) with console warnings. Schemas: Product, Article, BreadcrumbList, Organization, WebSite, FAQPage, Event, LocalBusiness, JobPosting; @graph graphs are supported. The CLI rule jsonld:valid-schema uses the same schema set — no duplication.

useRanklintIgnore([...ruleIds])

Emits <meta name="ranklint:ignore" content="...">; the audit subtracts these rules for that specific page. Works for custom rules too.

The DevTools "SEO" tab

An iframe Vue app, self-contained ~166 KB bundle (Vue runtime, level-1 checks and zod are inlined). Works live: re-checks on navigation and DOM mutations (MutationObserver, 400 ms debounce / 2 s max-wait). The DevTools panel's own DOM is stripped before analysis.

What the panel does not do (use the CLI): site-scope rules (duplicates, orphans, sitemap, robots), indexability:ssr-content, redirect chains, TTFB, Lighthouse. The tab only works with a built @ranklint/devtools — otherwise the module warns and skips registration.

ranklint.config — every field

The file ranklint.config.{ts,js,mjs,json,jsonc} is loaded by c12. TS gets autocompletion via defineRanklintConfig; JSON gets a JSON schema. The whole config is Zod-validated at startup: an unknown rule id is an error with did-you-mean (Levenshtein ≤ 3).

import { defineRanklintConfig } from '@ranklint/core'

export default defineRanklintConfig({
  extends: ['./agency-preset.ts', '@ranklint/preset-default'],

  site: { url: 'https://example.com', name: 'Example' },

  apps: {                                  // multi-app domain zones
    self: { paths: ['/en/market/**'] },    // the zone named self is crawled
    main: { paths: ['/**'], owner: 'external', checks: ['...'] },
  },

  rules: {
    'meta:title-length': ['error', { min: 30, max: 60 }],
    'meta:description-length': 'off',      // 'error' | 'warn' | 'info' | 'off' | [severity, options]
  },

  crawl: {
    entry: ['/en/market'],                 // crawl seed paths (default: the audited URL itself)
    concurrency: 5,                        // parallel tabs
    delay: 0,                              // ms between pages
    maxPages: 1000,                        // stop + truncated: true in the report
    ignore: ['/admin/**'],                 // globs — skip
    strategy: 'full',                      // 'full' | 'sitemap+sample'
    userAgent: 'custom UA',
    insecureTls: true,                     // self-signed certs on dev/staging
    viewport: { width: 375, height: 812 },
    auth: {                                // staging behind a wall
      headers: { 'X-Auth': '...' },
      basic: { username: '...', password: '...' },
      cookies: [{ name: '...', value: '...' }],
    },
  },

  robots: {                                // expectations for an EXTERNAL robots.txt
    mode: 'owner',                         // 'owner' | 'external'
    expect: {
      allow: ['/'], disallow: ['/admin'],
      sitemaps: ['https://example.com/sitemap.xml'],
      indexable: true,
    },
  },

  lighthouse: {
    enabled: true,
    runs: 5,                               // runs per URL
    aggregation: 'median',                 // 'median' | 'p75' | 'best'
    formFactor: 'mobile',                  // 'mobile' | 'desktop'
    maxUrls: 5,
    thresholds: { '/listing/**': { performance: 90, lcp: 2500 } },
  },

  customChecks: [ /* defineCheck({...}) */ ],

  monitor: {
    storage: 'fs',                         // 'fs' | 's3'
    dir: '.ranklint/reports',              // for fs
    keep: 60,                              // rotation
    bucket: '...', prefix: '...', endpoint: '...', region: '...',  // for s3
  },

  profiles: {                              // defu-patched over the base: --profile uat
    uat: { site: { url: 'https://uat.example.com' } },
  },
})

Layer precedence: the config itself > earlier extends entries > later ones. A profile is patched on top of the base. Local paths in extends require a file extension (./preset.ts).

insecureTls belongs only in a dev/local profile — it must never reach a prod audit.

The 42 rules

Each: 'error' | 'warn' | 'off' or [severity, options]; per-page suppression via useRanklintIgnore(). Scope page — per page, site — once for all pages.

meta
RuleScopeDefaultChecks / options
meta:title-requiredpageerror<title> exists
meta:title-lengthpagewarnlength; { min, max }
meta:description-requiredpageerrormeta description exists
meta:description-lengthpagewarnlength; { min, max }
meta:og-requiredpagewarnog:title, og:description, og:image
meta:twitter-cardpagewarntwitter:card is valid
meta:no-duplicate-titlesiteerroridentical titles across pages
meta:no-duplicate-descriptionsiteerroridentical descriptions
canonical:requiredpageerrorrel=canonical exists
canonical:validpageerrorcanonical responds 200 (network)
canonical:no-chainsitewarncanonical doesn't point to a page with a different canonical
headings
RuleScopeDefaultChecks / options
headings:single-h1pageerrorexactly one h1
headings:no-emptypagewarnno empty headings
headings:hierarchypagewarnno level skips (h2 → h4)
headings:h1-lengthpagewarnh1 length; { min, max }
headings:unique-h1sitewarnh1s are unique across pages
links
RuleScopeDefaultChecks / options
links:no-brokenpageerrorinternal links aren't 4xx/5xx (HEAD, cached per run)
links:no-redirect-chainpagewarnredirect chains; { maxHops }
links:permanent-redirectspagewarn302 where a 301 belongs
links:trailing-slash-consistentsitewarntrailing-slash consistency
links:no-orphanssitewarnsitemap pages nobody links to
i18n
RuleScopeDefaultChecks
hreflang:valid-targetspageerrorhreflang targets respond 200
hreflang:symmetricsiteerrorhreflang links are reciprocal
i18n:no-locale-leakpageerrorcontent language matches URL locale and html lang
structured-data · images
RuleScopeDefaultChecks / options
jsonld:parseablepageerrorJSON-LD parses
jsonld:valid-schemapageerrorvalidity against Schema.org schemas; { schemas } — your own
images:alt-requiredpagewarncontent imgs have alt
images:dimensions-requiredpagewarnwidth/height against CLS
images:no-lazy-above-foldpagewarnno loading=lazy in the viewport; { firstImages }
robots
RuleScopeDefaultChecks
robots:reachablesiteerrorrobots.txt responds 200
robots:zone-not-blockedsiteerrorown zone isn't Disallow'ed
robots:sitemap-declaredsitewarnSitemap directive declared
robots:env-policysiteerrorprod open / non-prod closed
robots:expected-disallowsitewarnrobots.expect expectations hold
indexability · http
RuleScopeDefaultChecks / options
indexability:ssr-contentpageerrorcontent exists in SSR, not only after hydration; { minRatio }
sitemap:reachablesiteerrorsitemap.xml responds 200 and parses
sitemap:no-noindexsiteerrorno noindex pages in the sitemap
http:no-mixed-contentpageerrorno http resources on an https page
http:no-soft-404siteerror404s aren't masked as 200
http:x-robots-consistentpageerrorX-Robots-Tag doesn't contradict meta robots
http:ttfb-budgetsitewarnp75 TTFB per group; { p75, budgets }
mobile:viewportpageerrorviewport meta exists

Special rules outside the registry

Smart heuristics

CLI — all commands

ranklint audit

FlagDescription
--urladdress of a live site
--startpath to .output/server/index.mjs — starts Nitro itself and stops it after; the server origin overrides site.url, so zones match localhost
--profileprofile from ranklint.config
--reportermarkdown (default) | json | junit | gitlab | html | github
--outputreport to a file instead of stdout
--json-outputadditionally the raw json (input for diff)
--cwdwhere to look for ranklint.config
--mode monitormonitoring mode (below)

Exit code 1 on error issues. Without a ranklint.config it runs with a minimal config from the URL origin. Flow: config → custom checks → seeds (crawl.entry paths or the audited URL; with strategy: 'sitemap+sample' — a sitemap sample: group by route pattern, up to 5 random URLs per group) → BFS crawl → checks → crawl-budget analysis (parametric URL groups: how many, how many with canonical/noindex — shows where crawl budget leaks) → Lighthouse → report.

ranklint diff

FlagDescription
--basepath to report.json or a git ref — then the report is pulled from CI artifacts
--currentcurrent report.json
--reportermarkdown | json
--outputto a file

The CI adapter picks by environment: GITHUB_ACTIONS=true → GitHub (artifact by name, zip unpacked by a built-in reader), otherwise GitLab (branch job artifact). Error policy: no CI context → warn + full "first run" report (a missing base is not an error); a download error (401/403/network) → hard fail, so an expired token can't hide behind an eternal first run. Exit 1 only on new error issues.

The rest

CommandFlagsWhat it does
lighthouse--url (req.), --runs, --cwd, --outputstandalone LH run with aggregation and config thresholds; exit 1 on violation
outline--url (req.), --output, --cwdmarkdown tree of h1–h6 for every crawled page — structure review
watch--url (default localhost:3000), --pages, --applive checks on file edits (see Watch)
history--dir, --limit (20), --csv, --cwdtrend of stored monitor reports as a table or CSV
generate robots-fragment--output, --cwda root robots.txt fragment from robots.expect for the owning team

Zones (multi-app domains)

When several apps from different teams share one domain:

apps: {
  self: { paths: ['/en/market/**', '/ar/market/**'] },  // the zone named self is crawled
  main: { paths: ['/**'], owner: 'external' },          // the rest is foreign
}

Every URL is classified by the most specific pattern (globs */**): own zone → full crawl; a foreign zone of the same domain → HEAD only, into the reachability queue → the links:reachable rule ("is our link into the other section alive?"); crawl.ignore → skip; external domain → counted, never visited. The same route-pattern matcher powers lighthouse thresholds, ttfb budgets and crawl-budget grouping. The DevTools panel understands zones too (via the dev endpoint). A broken link into a foreign zone is not links:no-broken: not yours to fix, but yours to know about.

Lighthouse

A separate Chrome via chrome-launcher (not Playwright — requires an installed Chrome). N runs per URL, per-metric aggregation: median (default) / p75 / best — a single run is ±10 points of noise. Metrics: performance, seo, accessibility, bestPractices, lcp, cls, tbt + the LCP element with a suggestion. Thresholds are route patterns and become regular error issues (→ exit 1). Results land in Report.lighthouse; diff compares by (url-pattern, metric) — a metric regression between branches is visible. formFactor: 'mobile' also sets the crawler viewport to 375×812 unless crawl.viewport is set.

Diff & CI

Issue key: checkId + url without origin + selector — stable across environments (UAT compares against prod). diffReports → newIssues / fixedIssues / pagesDelta.

Base report storages (ReportStorage): fs (a JSON directory, latest by mtime), GitLab artifacts (read-only, branch job artifact), GitHub artifacts (read-only: list by name, branch filter, zip download — a built-in minimal zip reader: EOCD → central directory → inflateRawSync; corrupt input → null, not an exception), S3 (for the monitor; the SDK is an optional peer, any S3-compatible endpoint).

Ready-made CI presets: presets/gitlab-ci/seo.yml (jobs .ranklint-audit, .ranklint-diff with an automatic MR comment, .ranklint-lighthouse, .ranklint-monitor with Playwright and report caches) and presets/github-actions/seo.yml. The github reporter emits PR annotations.

Production monitoring

ranklint audit --url https://prod --mode monitor — for cron/schedules. Exit is always 0: prod monitoring never fails the pipeline, it alerts instead. Flow: audit → enrichment → diff against the last stored report → save by timestamp → alerts only when newIssues > 0.

Watch mode

ranklint watch --url http://localhost:3000 — chokidar watches app/pages/** (and the rest of the app dir: editing a layout/composable re-checks recent routes). File → route → HttpFetcher hits the dev server → fast page checks without network rules → ESLint-style output (url + selector + suggestion).

Dynamic routes: [id][^/]+, [...slug].+, [[x]] — optional segments; sample URLs come from the dev server's /sitemap.xml (understands a 1-level sitemap index, up to 3 URLs per pattern, cache with refetch). No match → watch:no-sample-url; a failing check → watch:error, never a false "clean". No module integration by design — the CLI stays independent.

Custom rules & presets

import { defineCheck } from '@ranklint/checks'

customChecks: [
  defineCheck({
    id: 'myteam:no-lorem',          // clashing with a built-in id is an error with a hint
    category: 'meta',
    severity: 'warn',
    scope: 'page',                  // 'page' | 'site'
    docs: 'https://wiki.myteam.dev/seo/no-lorem',
    optionsSchema: z.object({...}), // optional — options validation
    async run(ctx) {
      // ctx: page, pages, document (linkedom DOM), config, site, fetcher
      return [{ checkId: 'myteam:no-lorem', severity: 'warn', message: '...', url: ctx.page!.url }]
    },
  }),
]

Custom rules are first-class: configured via rules, disabled with 'off' and useRanklintIgnore. The CheckContext contract is stable within a major version. Presets — via extends (an npm package or a local file); @ranklint/preset-default pins the built-in rules at their default severities — a "live" snapshot of the installed checks version.

Env variables

VariablePurpose
NUXT_RANKLINT_ENVenvironment override for the robots policy (prod / staging / dev)
NUXT_RANKLINT_SITE_URLper-environment override of the module's site.url
RANKLINT_CRUX_API_KEYCrUX API (field CWV in the monitor)
RANKLINT_GSC_TOKEN / RANKLINT_GSC_KEY_FILE / RANKLINT_GSC_PROPERTYSearch Console: token / service-account JSON / property
RANKLINT_SLACK_WEBHOOKmonitor alerts to Slack
RANKLINT_TELEGRAM_BOT_TOKEN + RANKLINT_TELEGRAM_CHAT_IDalerts to Telegram
CI_API_V4_URL, CI_PROJECT_ID, CI_JOB_TOKEN / RANKLINT_GITLAB_TOKEN, RANKLINT_AUDIT_JOBGitLab artifacts for diff (set by GitLab automatically, except the token and job name)
GITHUB_REPOSITORY, GITHUB_TOKEN / RANKLINT_GITHUB_TOKEN, RANKLINT_ARTIFACT_NAME, GITHUB_API_URLGitHub artifacts for diff (artifact name default: ranklint-report)

Nuances & limitations


Repository: github.com/aakazancev/ranklint · the rules reference is generated from the registry (docs/rules.md) · config JSON schema: schemas/ranklint-config.schema.json

ranklint — полный справочник

SEO-toolkit для Nuxt 4: генерация sitemap/robots/JSON-LD из коробки, живой SEO-линтер в DevTools и регрессионный контроль в CI. Всё, что умеет, все настройки, все нюансы.

v0.1 · 7 пакетов · 42 правила · Nuxt ^4.0.0 · Node ≥ 20 · MIT

Пакеты и архитектура

Зависимости строго вниз: core не знает ни про Nuxt, ни про Playwright — загрузку страниц описывает интерфейс PageFetcher (реализации: HttpFetcher — голый fetch, PlaywrightFetcher — реальный Chrome). Всё крутится вокруг четырёх типов: PageSnapshot (html после гидрации + ssrHtml до JS, статус, заголовки, ttfb, ссылки, картинки во вьюпорте) → CheckIssue (checkId, severity, message, url, selector, suggestion, docs) → Report (formatVersion: 1).

Каждый блок функциональности отключается — выключенный код вообще не регистрируется. Клиентский вклад модуля в бандл — 485 байт gzip (лимит 5 KB охраняется тестом в CI).

Nuxt-модуль

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@ranklint/nuxt'],
  ranklint: {
    site: { url: 'https://example.com', name: 'Example' },
    sitemap: {
      enabled: true,              // false или sitemap: false — роут не регистрируется
      path: '/sitemap.xml',
      sources: [ /* три вида источников, см. ниже */ ],
      cacheTtl: 3600,             // кеш ответа в памяти, секунд
    },
    robots: { mode: 'owner' },    // false — не трогать robots.txt вообще
    jsonLd: true,                 // false — useJsonLd не регистрируется
    devtools: true,               // false — таб SEO не регистрируется
  },
})

Работает без конфигурации: /sitemap.xml и /robots.txt доступны сразу после npx nuxi module add @ranklint/nuxt. Требует Nuxt 4 — при compatibilityVersion < 4 бросает понятную ошибку.

Sitemap: четыре источника записей

Нюансы: падение любого источника → console.warn + отдаём что собрали (неполный sitemap лучше 500). loc абсолютизируется против site.url, записи дедупятся, XML экранируется. Путь добавляется в nitro prerender — при nuxt generate источники опрашиваются на билде.

Robots: политика по окружению

Роут регистрируется только при mode: 'owner'. Окружение: NUXT_RANKLINT_ENV → иначе автодетект (dev, известные CI-переменные). Не-prod → Disallow: / (UAT не уедет в индекс), prod → Allow: / + директива Sitemap:. Явный override всегда побеждает автодетект. Команде, не владеющей корневым robots.txt, — ranklint generate robots-fragment.

runtimeConfig

Опции лежат в runtimeConfig.ranklint — на каждом окружении переопределяются env-переменными без пересборки: NUXT_RANKLINT_SITE_URL, и т.д.

Композаблы

useJsonLd(type, data)

useJsonLd('Product', {
  name: 'Widget',
  offers: { price: 9.99, priceCurrency: 'USD' },
})

Рендерит <script type="application/ld+json"> через useHead с @context/@type. В dev данные валидируются Zod-схемами (лениво импортируются — в prod-бандл валидатор не попадает) с warn'ами в консоль. Схемы: Product, Article, BreadcrumbList, Organization, WebSite, FAQPage, Event, LocalBusiness, JobPosting; поддерживаются @graph-графы. Тот же набор схем использует CLI-правило jsonld:valid-schema — никакого дублирования.

useRanklintIgnore([...ruleIds])

Кладёт <meta name="ranklint:ignore" content="...">; аудит вычитает эти правила для конкретной страницы. Работает и для кастомных правил.

DevTools-таб «SEO»

iframe-Vue-приложение, самодостаточный бандл ~166 KB (Vue-runtime, level1-чеки и zod заинлайнены). Работает live: пересчёт при навигации и мутациях DOM (MutationObserver, debounce 400мс / max-wait 2с). DOM панели самого DevTools вырезается перед анализом.

Чего в панели нет (нужен CLI): site-scope правила (дубли, orphans, sitemap, robots), indexability:ssr-content, redirect-цепочки, TTFB, Lighthouse. Таб работает только с собранным @ranklint/devtools — иначе модуль предупредит и не зарегистрирует его.

ranklint.config — все поля

Файл ranklint.config.{ts,js,mjs,json,jsonc}, грузится c12. Для TS — автодополнение через defineRanklintConfig, для JSON — JSON-схема. Весь конфиг валидируется Zod на старте: неизвестное правило — ошибка с did-you-mean (Левенштейн ≤ 3).

import { defineRanklintConfig } from '@ranklint/core'

export default defineRanklintConfig({
  extends: ['./agency-preset.ts', '@ranklint/preset-default'],

  site: { url: 'https://example.com', name: 'Example' },

  apps: {                                  // зоны multi-app домена
    self: { paths: ['/en/market/**'] },    // зона с именем self — краулится
    main: { paths: ['/**'], owner: 'external', checks: ['...'] },
  },

  rules: {
    'meta:title-length': ['error', { min: 30, max: 60 }],
    'meta:description-length': 'off',      // 'error' | 'warn' | 'info' | 'off' | [severity, options]
  },

  crawl: {
    entry: ['/en/market'],                 // сид-пути краула (по умолчанию — сам проверяемый URL)
    concurrency: 5,                        // параллельных вкладок
    delay: 0,                              // мс между страницами
    maxPages: 1000,                        // стоп + truncated: true в отчёте
    ignore: ['/admin/**'],                 // глобы — пропуск
    strategy: 'full',                      // 'full' | 'sitemap+sample'
    userAgent: 'custom UA',
    insecureTls: true,                     // self-signed сертификаты dev/staging
    viewport: { width: 375, height: 812 },
    auth: {                                // staging за заглушкой
      headers: { 'X-Auth': '...' },
      basic: { username: '...', password: '...' },
      cookies: [{ name: '...', value: '...' }],
    },
  },

  robots: {                                // ожидания для ВНЕШНЕГО robots.txt
    mode: 'owner',                         // 'owner' | 'external'
    expect: {
      allow: ['/'], disallow: ['/admin'],
      sitemaps: ['https://example.com/sitemap.xml'],
      indexable: true,
    },
  },

  lighthouse: {
    enabled: true,
    runs: 5,                               // прогонов на URL
    aggregation: 'median',                 // 'median' | 'p75' | 'best'
    formFactor: 'mobile',                  // 'mobile' | 'desktop'
    maxUrls: 5,
    thresholds: { '/listing/**': { performance: 90, lcp: 2500 } },
  },

  customChecks: [ /* defineCheck({...}) */ ],

  monitor: {
    storage: 'fs',                         // 'fs' | 's3'
    dir: '.ranklint/reports',              // для fs
    keep: 60,                              // ротация
    bucket: '...', prefix: '...', endpoint: '...', region: '...',  // для s3
  },

  profiles: {                              // накладываются defu поверх базы: --profile uat
    uat: { site: { url: 'https://uat.example.com' } },
  },
})

Приоритеты слоёв: сам конфиг > ранние элементы extends > поздние. Профиль накладывается патчем поверх базы. Локальные пути в extends требуют расширение файла (./preset.ts).

insecureTls держите только в dev/local-профиле — в прод-аудит он попасть не должен.

42 правила

Каждое: 'error' | 'warn' | 'off' или [severity, options]; подавление на странице — useRanklintIgnore(). Scope page — по каждой странице, site — один раз по всем.

meta
ПравилоScopeDefaultЧто проверяет / опции
meta:title-requiredpageerrorесть <title>
meta:title-lengthpagewarnдлина; { min, max }
meta:description-requiredpageerrorесть meta description
meta:description-lengthpagewarnдлина; { min, max }
meta:og-requiredpagewarnog:title, og:description, og:image
meta:twitter-cardpagewarntwitter:card корректен
meta:no-duplicate-titlesiteerrorодинаковые title на разных страницах
meta:no-duplicate-descriptionsiteerrorодинаковые description
canonical:requiredpageerrorесть rel=canonical
canonical:validpageerrorcanonical отвечает 200 (сетевой)
canonical:no-chainsitewarncanonical не ведёт на страницу с другим canonical
headings
ПравилоScopeDefaultЧто проверяет / опции
headings:single-h1pageerrorровно один h1
headings:no-emptypagewarnнет пустых заголовков
headings:hierarchypagewarnбез перескоков уровней (h2 → h4)
headings:h1-lengthpagewarnдлина h1; { min, max }
headings:unique-h1sitewarnh1 уникальны между страницами
links
ПравилоScopeDefaultЧто проверяет / опции
links:no-brokenpageerrorвнутренние ссылки не 4xx/5xx (HEAD с кешем на прогон)
links:no-redirect-chainpagewarnцепочки редиректов; { maxHops }
links:permanent-redirectspagewarn302 там, где должен быть 301
links:trailing-slash-consistentsitewarnединообразие завершающего слеша
links:no-orphanssitewarnстраницы sitemap, на которые никто не ссылается
i18n
ПравилоScopeDefaultЧто проверяет
hreflang:valid-targetspageerrorhreflang-цели отвечают 200
hreflang:symmetricsiteerrorвзаимность hreflang-ссылок
i18n:no-locale-leakpageerrorязык контента соответствует локали URL и html lang
structured-data · images
ПравилоScopeDefaultЧто проверяет / опции
jsonld:parseablepageerrorJSON-LD парсится
jsonld:valid-schemapageerrorвалидность по Schema.org-схемам; { schemas } — свои схемы
images:alt-requiredpagewarnу контентных img есть alt
images:dimensions-requiredpagewarnwidth/height против CLS
images:no-lazy-above-foldpagewarnнет loading=lazy во вьюпорте; { firstImages }
robots
ПравилоScopeDefaultЧто проверяет
robots:reachablesiteerrorrobots.txt отвечает 200
robots:zone-not-blockedsiteerrorсвоя зона не закрыта Disallow
robots:sitemap-declaredsitewarnSitemap-директива объявлена
robots:env-policysiteerrorprod открыт / не-prod закрыт
robots:expected-disallowsitewarnожидания из robots.expect выполняются
indexability · http
ПравилоScopeDefaultЧто проверяет / опции
indexability:ssr-contentpageerrorконтент есть в SSR, а не только после гидрации; { minRatio }
sitemap:reachablesiteerrorsitemap.xml отвечает 200 и парсится
sitemap:no-noindexsiteerrorв sitemap нет noindex-страниц
http:no-mixed-contentpageerrorнет http-ресурсов на https-странице
http:no-soft-404siteerror404 не маскируются под 200
http:x-robots-consistentpageerrorX-Robots-Tag не противоречит meta robots
http:ttfb-budgetsitewarnp75 TTFB по группам; { p75, budgets }
mobile:viewportpageerrorесть viewport meta

Спец-правила вне реестра

Умные эвристики

CLI — все команды

ranklint audit

ФлагОписание
--urlадрес живого сайта
--startпуть к .output/server/index.mjs — сам поднимет Nitro и погасит после; origin сервера подменяет site.url — зоны матчатся на localhost
--profileпрофиль из ranklint.config
--reportermarkdown (default) | json | junit | gitlab | html | github
--outputотчёт в файл вместо stdout
--json-outputдополнительно сырой json (вход для diff)
--cwdгде искать ranklint.config
--mode monitorрежим мониторинга (см. ниже)

Exit code 1 при error-issues. Без ranklint.config работает с минимальным конфигом из origin URL. Флоу: конфиг → кастомные чеки → seeds (пути из crawl.entry или сам проверяемый URL; при strategy: 'sitemap+sample' — выборка из sitemap: группировка по route-pattern, до 5 случайных URL из группы) → BFS-краул → чеки → crawl-budget анализ (группы параметрических URL: сколько, у скольких canonical/noindex — видно, куда утекает краул-бюджет) → Lighthouse → отчёт.

ranklint diff

ФлагОписание
--baseпуть к report.json или git-ref — тогда отчёт достаётся из артефактов CI
--currentтекущий report.json
--reportermarkdown | json
--outputв файл

CI-адаптер по окружению: GITHUB_ACTIONS=true → GitHub (артефакт по имени, zip распаковывается встроенным ридером), иначе GitLab (job-артефакт ветки). Политика ошибок: нет CI-контекста → warn + полный отчёт «first run» (base не найден — не ошибка); ошибка загрузки (401/403/сеть) → падение, чтобы протухший токен не маскировался вечным first run. Exit 1 только при новых error-issues.

Остальные

КомандаФлагиЧто делает
lighthouse--url (обяз.), --runs, --cwd, --outputотдельный прогон LH с агрегацией и порогами из конфига; exit 1 при нарушении
outline--url (обяз.), --output, --cwdmarkdown-дерево h1–h6 всех страниц краула — ревью структуры
watch--url (default localhost:3000), --pages, --applive-чеки при правке файлов (см. Watch)
history--dir, --limit (20), --csv, --cwdтренд накопленных monitor-отчётов таблицей или CSV
generate robots-fragment--output, --cwdфрагмент корневого robots.txt из robots.expect для команды-владельца

Зоны (multi-app домены)

Когда на одном домене живут несколько приложений разных команд:

apps: {
  self: { paths: ['/en/market/**', '/ar/market/**'] },  // зона с именем self краулится
  main: { paths: ['/**'], owner: 'external' },          // остальное — чужое
}

Каждый URL классифицируется по самому специфичному паттерну (глобы */**): своя зона → полный краул; чужая зона того же домена → только HEAD в reachability-очередь → правило links:reachable («наша ссылка в чужой раздел жива?»); crawl.ignore → пропуск; внешний домен → счётчик, не ходим. Тот же route-pattern-матчер используют lighthouse-пороги, ttfb-бюджеты и crawl-budget группировка. DevTools-панель тоже понимает зоны (через dev-эндпоинт). Битая ссылка в чужую зону — это не links:no-broken: чинить не вам, но знать нужно.

Lighthouse

Отдельный Chrome через chrome-launcher (не Playwright — нужен установленный Chrome). N прогонов на URL, агрегация per-metric: median (default) / p75 / best — одиночный прогон шумит на ±10 баллов. Метрики: performance, seo, accessibility, bestPractices, lcp, cls, tbt + LCP-элемент с подсказкой. Пороги задаются route-паттернами и превращаются в обычные error-issues (→ exit 1). Результат попадает в Report.lighthouse; diff сравнивает по паре (url-pattern, metric) — регрессия метрики между ветками видна. formFactor: 'mobile' заодно ставит краулеру вьюпорт 375×812, если crawl.viewport не задан.

Diff и CI

Ключ проблемы: checkId + url без origin + selector — стабилен между окружениями (UAT сравнивается с prod). diffReports → newIssues / fixedIssues / pagesDelta.

Хранилища базовых отчётов (ReportStorage): fs (директория JSON, latest по mtime), GitLab artifacts (read-only, по job-артефакту ветки), GitHub artifacts (read-only: список по имени, фильтр по ветке, скачивание zip — свой минимальный zip-ридер: EOCD → central directory → inflateRawSync; битый вход → null, не исключение), S3 (для монитора; SDK — опциональный peer, любой S3-совместимый endpoint).

Готовые CI-пресеты: presets/gitlab-ci/seo.yml (джобы .ranklint-audit, .ranklint-diff с автокомментом в MR, .ranklint-lighthouse, .ranklint-monitor с кешем Playwright и отчётов) и presets/github-actions/seo.yml. Репортер github — аннотации в PR.

Мониторинг прода

ranklint audit --url https://prod --mode monitor — для крона/schedule. Exit всегда 0: прод-мониторинг не роняет пайплайн, вместо этого алертит. Флоу: аудит → обогащение → diff против последнего сохранённого отчёта → сохранение по timestamp → алерты только при newIssues > 0.

Watch mode

ranklint watch --url http://localhost:3000 — chokidar следит за app/pages/** (и остальным app-каталогом: правка layout/композабла перепроверяет недавние роуты). Файл → роут → HttpFetcher дергает дев-сервер → быстрые page-чеки без сетевых → вывод в стиле ESLint (url + selector + suggestion).

Динамические роуты: [id][^/]+, [...slug].+, [[x]] — опциональные сегменты; сэмплы URL берутся из /sitemap.xml дев-сервера (понимает sitemap-index на 1 уровень, до 3 URL на паттерн, кеш с рефетчем). Нет совпадений → watch:no-sample-url; упал чек → watch:error, а не ложный «clean». Интеграции с модулем нет намеренно — CLI независим.

Кастомные правила и пресеты

import { defineCheck } from '@ranklint/checks'

customChecks: [
  defineCheck({
    id: 'myteam:no-lorem',          // коллизия со встроенным id — ошибка с подсказкой
    category: 'meta',
    severity: 'warn',
    scope: 'page',                  // 'page' | 'site'
    docs: 'https://wiki.myteam.dev/seo/no-lorem',
    optionsSchema: z.object({...}), // опционально — валидация опций
    async run(ctx) {
      // ctx: page, pages, document (linkedom DOM), config, site, fetcher
      return [{ checkId: 'myteam:no-lorem', severity: 'warn', message: '...', url: ctx.page!.url }]
    },
  }),
]

Кастомные правила равноправны: настраиваются через rules, отключаются 'off' и useRanklintIgnore. Контракт CheckContext стабилен в рамках мажорной версии. Пресеты — через extends (npm-пакет или локальный файл); @ranklint/preset-default фиксирует встроенные правила на дефолтных severity — «живой» снапшот от установленной версии checks.

Env-переменные

ПеременнаяНазначение
NUXT_RANKLINT_ENVoverride окружения для robots-политики (prod / staging / dev)
NUXT_RANKLINT_SITE_URLoverride site.url модуля per-окружение
RANKLINT_CRUX_API_KEYCrUX API (полевые CWV в мониторе)
RANKLINT_GSC_TOKEN / RANKLINT_GSC_KEY_FILE / RANKLINT_GSC_PROPERTYSearch Console: токен / JSON сервис-аккаунта / property
RANKLINT_SLACK_WEBHOOKалерты монитора в Slack
RANKLINT_TELEGRAM_BOT_TOKEN + RANKLINT_TELEGRAM_CHAT_IDалерты в Telegram
CI_API_V4_URL, CI_PROJECT_ID, CI_JOB_TOKEN / RANKLINT_GITLAB_TOKEN, RANKLINT_AUDIT_JOBGitLab-артефакты для diff (подставляются GitLab'ом автоматически, кроме токена и имени джобы)
GITHUB_REPOSITORY, GITHUB_TOKEN / RANKLINT_GITHUB_TOKEN, RANKLINT_ARTIFACT_NAME, GITHUB_API_URLGitHub-артефакты для diff (имя артефакта default: ranklint-report)

Нюансы и ограничения


Репозиторий: github.com/aakazancev/ranklint · справочник правил генерируется из реестра (docs/rules.md) · JSON-схема конфига: schemas/ranklint-config.schema.json