Configuration

Reference

Every field of ranklint.config with layer precedence rules.

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',                // also selects the robots.txt group
    respectRobots: true,                   // skip links disallowed by <origin>/robots.txt (entry is always crawled)
    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,                            // cap; without urls — the first 200 pages of the crawl
    runTimeout: 180000,                    // ms per run; a hung run is skipped, Chrome is killed
    urls: ['/', '/pricing'],               // exact list of paths/URLs, measured even if not crawled
    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.