CLAUDE.md@packages/docs · git:20260926.05f4d43 · 2026-09-26 · sha256 05e13f273a3b3749

CLAUDE.md@packages/docs git:20260926.05f4d43A

Immutable. This exact content never changes and is served at /api/v1/blob/05e13f273a3b3749.

# CLAUDE.md — docs site

## Rule pages are generated — edit the source, not the output

`rules/<rule>.md` and `es/rules/<rule>.md` are generated by `scripts/generate-rules.ts` and
overwritten on every build/generate. Editing them directly is silently lost. Change the body via the
`_extras` partial — `rules/_extras/<rule>.md` (EN), `es/rules/_extras/<rule>.md` (ES) — then run
`pnpm -C packages/docs generate`. Mirror EN edits into ES (neutral tuteo, never voseo).

## Every page has its own title and description

- A rule page's description lives in the frontmatter of its `_extras` file, per locale
  (`description: "…"`, JSON-quoted; inline code allowed). The generator (`scripts/rule-page.ts`,
  pure and tested) prints it as the page's first paragraph and, as plain text, as the `<meta>`
  description; it titles the page `<rule> — Tailwind CSS lint rule` (ES:
  `— regla de lint para Tailwind CSS`) and builds the "At a glance" table from `meta`. A missing or
  over-160-character description fails `generate`.
- Hand-written pages carry `title` and `description` in their own frontmatter. The Spanish ones are
  written in Spanish, not copied — `tests/frontmatter.test.ts` checks every page (both locales
  present, ≤ 160 characters, unique per locale, ES ≠ EN). Run the docs tests with
  `pnpm -C packages/docs test` (after `pnpm build`).

## `<head>`: `.vitepress/seo.ts`

Canonical, hreflang (en / es / x-default), `og:*`, the JSON-LD (`WebSite` on `/` only,
`SoftwareSourceCode` on the home page, `BreadcrumbList` on rule pages) and `noindex` on the 404 come
from `seo.ts`, pure and tested (`tests/seo.test.ts`). `SITE_URL` / `SITE_NAME` are the single
source, and the npm `homepage` must equal `SITE_URL`. The `WebSite` JSON-LD is what fixes Google
showing "Cloudflare" as our site name — keep it, `og:site_name` and the home title in agreement.
Images: `public/favicon.svg` and `scripts/assets/og.svg` are the sources (render commands in the
SVG's `<desc>`).

## Crawlers and LLMs

- `sitemap.xml` is VitePress's, with `x-default` added and the 404 left out (`sitemapItems` in
  `seo.ts`); its URLs are the pages' canonicals. `public/robots.txt` (Content-Signal + Sitemap) and
  `public/_headers` (immutable assets, `text/markdown` + `noindex` for the `.md` copies) are static
  and tested.
- `buildEnd` writes `llms.txt`, `llms-full.txt` and a markdown copy of every page next to its HTML
  (`/setup.md`, `/rules/index.md`) — `scripts/llms.ts`, pure and tested. Each page links its copy
  with `<link rel="alternate" type="text/markdown">`.

## The built site is checked; so is the live one

- `pnpm -C packages/docs build` ends with `scripts/check-dist.ts`, which fails the build (so PRs and
  the deploy) on: internal files published, a wrong `<html lang>`, a missing, overlong, duplicated
  or copied-from-English title/description, a wrong canonical, missing `og:*`, broken or one-sided
  hreflang, an unrendered custom container, a rule page without breadcrumbs, a page without its
  markdown copy, a home page without the `WebSite` site name, an indexable 404, a sitemap that
  doesn't match the canonicals, a robots.txt without the sitemap, or missing `llms*.txt`. One test
  per failure class in `tests/check-dist.test.ts` — extend both together.
- `scripts/check-live.ts [base-url]` checks what Cloudflare Pages serves (our robots.txt, the
  `_redirects`/`_headers` rules, the site name, crawler access). `release.yml` runs it after the
  deploy (`verify-live`, informational). `npx wrangler pages dev .vitepress/dist` emulates Pages
  locally to run it before a release.

## Rule-page examples are tests

Every code block under `### ✗` / `### ✓` in `rules/_extras/<rule>.md` **and** its `es/` twin runs
through the real oxlint (`packages/oxlint-tailwindcss/tests/docs/doc-examples.test.ts`, against
`tests/fixtures/docs-examples.css`: the default theme plus the tokens, typography plugin, custom
utility and shadcn-style variables the pages mention). Examples are split at blank lines; each ✗
example must be reported by the page's rule, each ✓ example must not be, and must hold a class
string the plugin reads (a ✓ example the rule never looks at would pass without meaning anything).
Inside a block:

- `// options: { … }` — the rule's options (JSON), for the whole block as its first line, else for
  its example. Don't write options as prose ("with `max: 5`") without this line.
- `// → <whole line>` after code — what `oxlint --fix --fix-suggestions` makes of it, byte for byte;
  `//   ` lines right after it continue a multi-line result.
- `//   ~~~~ ~~~~ → text` under a line, or `<div … />  →  text` at its end — the fixed code contains
  the text (surrounding quotes dropped).
- `// reports: text` — one of the rule's messages contains the text (messages are English in both
  locales).
- `<!-- doc-test: skip — why -->` right before a block that isn't a runnable example.

Snippets can stay snippets: comment lines are dropped, an example that starts with JSX is wrapped in
a fragment, and a JSX line after other code gets the `;` it needs. The root README's hero example is
pinned the same way by `tests/docs/readme-hero.test.ts`.

## Config snippets are loaded by the real tools

Every `json` / `jsonc` block in the docs, the READMEs, `CONTRIBUTING.md` and the skills is loaded by
oxlint or oxfmt (`packages/oxlint-tailwindcss/tests/docs/doc-configs.test.ts`), so an unknown rule,
a bad severity, an option outside a rule's schema, a `settings.tailwindcss` key or type that
`PluginSettings` doesn't have, or a `sortTailwindcss` key oxfmt doesn't know fails the build. A
block can hold several documents. Name a document's file in the comment right above it
(`// packages/ui/.oxlintrc.json`, `// .oxfmtrc.json`, `// .prettierrc`), or it's read as an
`.oxlintrc.json`; a bare `{ "tailwindcss/…": … }` or a `rules` / `settings` excerpt is completed
with the plugin, while a document with `$schema` or `extends` is shown whole and must stand on its
own. A comment starting with `v0` marks old syntax (the migration guide), which is not loaded, and
`.prettierrc` isn't either (Prettier isn't installed).

## Generated blocks outside the rule pages

The recommended configs (`setup.md`, `es/setup.md`, the package README) and the rule list by
category (root README) sit between `<!-- generated:… -->` markers and come from each rule's
`meta.docs` (`scripts/blocks.ts`). Edit the rule's `ruleDocs(...)`, not the block, then run
`generate`.

`/shadcn`'s table and combined config come from `data/shadcn-lint.json`: per concern, examples, the
rules of each plugin that report them, and who reports it under the combined config. Its
oxlint-tailwindcss column is checked by `tests/docs/shadcn-interop.test.ts` (every example, every
rule on); its @shadcn/lint column and the combined config by `bench/interop.mjs`, weekly in
`interop.yml`. When @shadcn/lint changes what it reports, update the data, not the page. The same
goes for `/migration/from-better-tailwindcss` and `data/better-tailwindcss.json`: every row's
example is reported by both of its rules (`bench/interop.mjs`), and the rules and settings it names
exist here (`tests/docs/better-tailwindcss-map.test.ts`).

## Markdown is formatted by oxfmt (`generate` formats its own output)

All `.md` is formatted by oxfmt (`proseWrap: always`, so prose is wrapped at the print width — don't
hand-wrap, the formatter owns it). Two consequences here:

- The `generate` script runs `oxfmt --write` over the generated pages as its last step, so generated
  and source markdown stay consistent and `format:check` passes. Always regenerate via
  `pnpm -C packages/docs generate`, not the raw script.
- Fenced code blocks are **not** reformatted (`embeddedLanguageFormatting: off`) — example snippets
  (e.g. JSON config) stay exactly as written, so keep them copy-paste correct by hand.

## What the site config enforces

- `_extras/`, `AGENTS.md` and `CLAUDE.md` are `srcExclude`d: they are inputs and notes, not pages. A
  new internal markdown file needs the same treatment, or it gets published.
- A dead internal link fails the build (`ignoreDeadLinks: false`), and so does a link to a heading
  that doesn't exist: VitePress doesn't check anchors, `check-dist` does.
- The sidebar and nav are the same in both locales, link only to pages that exist, and reach every
  hand-written page (`tests/sidebar-parity.test.ts`): a new page needs a sidebar entry in both.
- "Edit this page" on a generated rule page points at its `_extras` file (`editLinkFor`), and
  `public/_redirects` sends the `_extras` URLs that used to be published to the rule page.
  `packages/oxlint-tailwindcss/tests/docs/site-structure.test.ts` holds both.