CLAUDE.md@packages/docs · diff
git:20260926.05f4d43 to git:20260926.32c11b5
6 added, 0 removed. Audit A to A.
# 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`).
+ `/benchmark` and `/comparison` hold no hand-typed number: their tables come from
+ `bench/results/latest.json` (`scripts/bench-blocks.ts`), which `bench/score.mjs` writes and
+ `bench.yml` checks weekly against a fresh run. `tests/bench-blocks.test.ts` fails when a page holds
+ tables older than the file. To change a number, rerun `node score.mjs` in `bench/`, then `generate`;
+ the prose around the tables is hand-written, so keep it free of figures.
+
## 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.