Audit rules

On this page

kilim check reads the last build (.kilim/manifest.json and the files in dist/) and runs the rules below over every page. Errors make it exit with 1; warnings only do with --max-warnings <n>. Run kilim build first; kilim deploy runs both for you.

The meta rules and links/orphan skip noindex pages, such as the 404 page and drafts shown in kilim dev.

Built-in rules

RuleSeverityOptionsChecks that
meta/title-missingerrorevery page has a non-empty <title>.
meta/title-lengthwarnmax (60)titles stay within max characters.
meta/title-duplicatewarnno two pages share a title; translations may.
meta/description-missingwarnevery page has a meta description.
meta/description-lengthwarnmax (160)descriptions stay within max characters.
meta/description-duplicatewarnno two pages share their own description.
meta/canonical-missingwarnevery page has a canonical URL, which needs config url.
meta/multiple-h1warna page has at most one <h1>.
links/broken-internalerrorlinks to the site lead to a page or file, and #anchors to an id.
links/broken-resourceerrorimages, stylesheets, scripts, media and CSS url()s exist.
links/duplicate-iderrorraw HTML in Markdown reuses no heading id.
links/orphanwarnevery page but the home page has a link from another page.
links/redirect-chainwarnredirects lead straight to a page, and links skip redirects.
images/alt-missingwarnevery <img> has alt, and every Markdown image alt text.
images/size-missingwarnevery <img> has width and height.
budget/page-weightwarnmax in KB (500)a page's HTML, CSS, images and fonts stay within max.
budget/image-weightwarnmax in KB (200)each image stays within max.

Plugins add rules named after them. The search() plugin adds search/index-size (warn, max in KB, 500 by default) for its index files, and the llmsTxt() plugin adds llms-txt/full-size (warn, max in KB, 500 by default) for each llms-full.txt.

Notes on the rules

  • Lengths count characters as readers see them, so é and an emoji are one each.
  • Duplicates compare titles and descriptions without case. The site-wide description from the config does not count as a page's own, and the language versions of one page may share a title.
  • Anchors: #top is always fine, as browsers scroll to the top for it.
  • Duplicate ids: a ## Install heading gets the id install, so a <div id="install"> in the same Markdown file makes #install ambiguous. The error names the content file and the line of the raw HTML; rename one of them, or give the heading its own id with {#id}.
  • Images: alt="" marks an image as decoration and passes. Markdown has no way to say that, so ![](photo.jpg) is reported on the line of the content file. Kilim adds width and height to local images; images/size-missing finds remote ones written without them.
  • Budgets: 1 KB is 1000 bytes. An image counts at its largest srcset candidate, and fonts and images that the page's CSS loads count too.

Changing severities and options

Set a rule to "off", "warn" or "error", or give a severity with options:

import { defineConfig } from "@kilim-dev/core";

export default defineConfig({
  audit: {
    rules: {
      "meta/title-length": ["warn", { max: 70 }],
      "meta/canonical-missing": "off",
      "links/orphan": "error",
      "budget/page-weight": ["warn", { max: 800 }],
    },
  },
});

defineConfig completes the rule names and their options, those of plugins included. An unknown rule name or option stops kilim check with the config file and the valid names.

Rules of your own

A rule is a check over the whole build. Write one with defineRule and add it to audit.custom:

import { defineConfig, defineRule, textContent } from "@kilim-dev/core";

const noLoremIpsum = defineRule({
  name: "site/no-lorem-ipsum",
  description: "Pages do not contain placeholder text.",
  severity: "error",
  check({ pages, report }) {
    for (const page of pages) {
      if (textContent(page.tree).includes("Lorem ipsum")) {
        report({ page, message: "page still contains placeholder text." });
      }
    }
  },
});

export default defineConfig({
  audit: { custom: [noLoremIpsum] },
});

Each page has its url, content file, meta (title, description, canonical URL) and the built HTML as a tree. Rules that take options declare an options schema; see Writing plugins for an example.

Output formats

--format picks how problems are printed:

FormatFor
textPeople, the default. One line per problem and a summary.
jsonOther tools. --json is a shorthand.
githubGitHub Actions: problems become annotations on the content files of a pull request.

text names the content file, or the built page and line with the content file it came from:

warning: content/about.md: page has no description; set `description` in its frontmatter or in kilim.config.ts. [meta/description-missing]
error: dist/blog/index.html:14 (from content/blog/index.md): link "/blgo/" leads to no page or file of the build. [links/broken-internal]
Checked 12 pages: 1 error, 1 warning.

json prints { pages, errors, warnings, issues }, where each issue has rule, severity ("warn" or "error"), message, file, output, url and line.

github writes workflow commands that GitHub shows as annotations:

::error file=content/blog/index.md,title=links/broken-internal::link "/blgo/" leads to no page or file of the build. (dist/blog/index.html:14)

The built HTML is not in the repository, so a problem on a page points at its content file and names the built line in the message. When the site is in a subfolder of the repository, the paths start with it.