Two read-only checks, with no AI. Run larecipe:validate after every docs change to check that the docs are correct, and larecipe:doctor after a deploy to check what agents get from your app.
php artisan larecipe:validate
php artisan larecipe:doctorlarecipe:validate reads every page through the real renderer and reports broken links, schema errors, unknown components, missing snippets, live references that stopped resolving and more. It exits with 1 when it finds an error, so CI can stop on it. Warnings never fail it. Your coding agent runs it before every push.
Each issue names its level, rule, page and line, with a fix when LaRecipe knows one:
ERROR links.broken 3.x/agents/mcp line 220
Link to ../sources/docs-migrations.md: there is no page at "sources/docs-migrations".
Fix: Fix the link, or write the page it points to.That one is from this site, while these docs were being written: the page it links to was next in line.
larecipe:validate
Without options it checks every version and language of every doc set. Narrow it down, or ask for the report agents read:
php artisan larecipe:validate --set=docs --docs-version=3.x
php artisan larecipe:validate --jsonThe version option is --docs-version because Symfony reserves --version for the application. A set or version that does not exist exits with 2.
Every issue has a stable rule id, such as links.broken or components.unknown. Validation and rendering never disagree: the component, snippet, reference and placeholder rules come straight from the renderer, which reports every problem it meets while rendering the page.
Every rule
Page rules run on each page, set rules on the doc set as a whole.
| Rule | Level | What it finds |
|---|---|---|
page.empty |
error | A page with no content. |
frontmatter.schema |
error, warning | Frontmatter that is not valid YAML or has values of the wrong shape (error), or an unknown key that looks like a typo of a known one, such as descripton (warning). |
frontmatter.description.missing |
warning | A page without a description. It weakens llms.txt, search snippets and list_pages. |
components.unknown |
error | A component tag nothing defines. It renders as escaped text. |
components.props |
error | A missing or invalid prop, or a page that fails to render. |
references.missing |
error | A live reference (<Route>, <Artisan>, <Model>, <Event>) whose target no longer exists. |
snippets.missing |
error | A snippet include whose file or region is gone. |
snippets.denied |
error | An include outside the snippet root, or of a file that may hold secrets. |
snippets.disabled |
error | An include on a set with no snippet root (database sets, unless you configure one). |
media.placeholder |
warning | A video placeholder: a Video component with a placeholder prop and no src yet. |
links.broken |
error | A link to a page or an anchor that does not exist: relative .md links, absolute links into any version or language of the set, and #anchors on the same page. A link to a page not translated yet is fine: readers get the original, so the validator falls back to the default language the same way. |
links.private-from-public |
warning | A public page linking to a private one, where every guest gets a 404. |
links.unsafe |
error | A link in navigation.json, the navbar, the footer or the repository URL that is not http, https, mailto or a path on the site. Pages leave it out. |
markdown.size |
warning | A page whose agent Markdown is over agents.markdown_budget (50,000 characters by default), where agents start cutting. |
manifest.schema |
error, warning | larecipe.json that is not valid JSON or breaks the schema (error); a missing larecipe.json, or a key that looks like a typo (warning). |
manifest.integrations |
warning | Analytics integrations in a database set's larecipe.json, which it ignores. Move them to sets.{key}.integrations in config/larecipe.php. |
assets.missing |
error | A stylesheet listed under assets.css that is not next to larecipe.json. |
navigation.schema |
error | navigation.json that is not valid JSON or breaks the schema. |
navigation.missing-page |
error | A navigation entry with no page behind it. A language's own navigation.json may list pages not translated yet: they fall back to the original. |
pages.orphan |
warning | A page navigation.json leaves out and does not hide, so readers reach it only by URL. |
pages.duplicate |
error | Two files that become the same URL, such as guides.md and guides/index.md. |
redirects.loop |
error | Redirects (in larecipe.json or redirect_from) that lead back to where they started. |
redirects.shadowed |
warning | A redirect from a URL a page still lives at. The page wins, so the redirect never runs. |
search.budget |
warning | A version whose client search index would be over search.budget_kb. Only on the client search driver. |
skills.spec |
error, warning | A hand-written skills/{name}/SKILL.md that breaks the Agent Skills spec (error), or one over 500 lines (warning). |
cache.store |
error | A database set on the array or null cache store, outside tests. It reads the set's own source.cache first. |
access.gate-syntax |
error | A <Can> or <Cannot> tag the parser could not read: inside a sentence, a table or a heading, split over lines, or in lowercase. The block around it is hidden from every reader until it is fixed. |
access.unknown-ability |
warning | A gate block that asks about an ability your app never defined, so it denies every reader. The fix names the closest defined ability. |
access.redundant-gate |
warning | A gate block on a page that is itself private, whose readers already passed viewLarecipe. |
migrations.conflict |
warning | A docs migration that met a change made in production and waits as a conflict at the pass. The fix gives its address in the admin. |
The JSON report
--json prints the report agents read. Its shape is published as a JSON Schema, in the package at resources/schemas/validate.schema.json and on every set at /schemas/validate.json (under the set's prefix).
A link with a typo in its anchor, added to this page for a moment, came back like this:
{
"ok": false,
"summary": { "pages": 41, "errors": 48, "warnings": 3 },
"issues": [
{
"level": "error",
"rule": "links.broken",
"set": "docs",
"page": "3.x/operate/validation",
"file": "3.x/operate/validation.md",
"line": 8,
"message": "Link to caching.md#the-cache-stor: anchor #the-cache-stor not found.",
"fix": "Use #the-cache-store."
}
]
}The other 47 errors were links and navigation entries for pages not written yet.
okisfalsewhen there is at least one error.pageis{locale}/{version}/{path}, leaving out the default language, and the version on a set without versions.fileis relative to the set's root (docs/here), andlinepoints into that file, frontmatter included.fixis there when LaRecipe can suggest one: the closest anchor, the closest page, the frontmatter key you probably meant. Treat it as a suggestion. For a link to a page that does not exist yet, the closest page is usually the wrong answer.
How agents use the report
The report is built for the agent that wrote the page a moment ago. Stable rule ids tell it what kind of problem it has, file and line tell it where, and fix often tells it what to change. The larecipe-write-docs and larecipe-update-docs skills tell agents to run the validator and fix every error before they stop.
Agents connected to the MCP server can call the validate_docs tool instead of the command. It returns the same report as --json, for one set and optionally one version. It is always on the local server (php artisan larecipe:mcp). On a set's web endpoint it is offered only to callers the manageLarecipe gate allows, because it reads every page, private ones included.
When an agent writes a page through the MCP author tools, the same page rules run on that page first, and a page with errors is not written. Link problems are only warnings at that point, because an agent writing several pages links to the next one before it exists. The full larecipe:validate run catches whatever is still broken.
Every option
larecipe:validateCheck the docs: links, navigation, schemas, components, snippets, budgets and skills
php artisan larecipe:validate [--set [SET]] [--docs-version [DOCS-VERSION]] [--json]| Option | Description | Default |
|---|---|---|
--set=SET | Only this doc set | |
--docs-version=DOCS-VERSION | Only this version of the set (Symfony reserves --version) | |
--json | Print the report as JSON |
In CI
One step is enough. The exit code fails the job on errors:
- run: php artisan larecipe:validateFor GitHub Actions, the installer can write a ready-made workflow that also lists the pages a pull request left stale: see the CI check.
larecipe:doctor
The doctor grades each doc set the way the AFDocs checks grade a docs site for agents, from inside your app: Markdown twins, llms.txt and its links, 404s, discovery headers, redirects, skill.md, the MCP endpoint and the cache store. It sends real requests through the app's HTTP kernel, with the user agent LaRecipe-Doctor/1.0, so it needs no running server and sees your middleware, headers and exception handler exactly as an agent would. URLs are built from APP_URL.
Each check passes, warns, fails or is skipped, with a fix for every finding, and the set gets a grade from A to D. The command exits with 1 only when a check fails. --set limits it to one set, and --json prints the report.
The checks
| Check | Passes when |
|---|---|
llms-txt |
llms.txt (and the default version's, on a versioned set) is served as text, stays under 50,000 characters, and has the llms.txt shape: a title, a summary and sections. |
llms-txt-links |
Up to 10 page links from llms.txt answer with Markdown. |
markdown |
The first five pages in reading order answer with Markdown at their .md URL and to Accept: text/markdown, and at least 95% of each article's words appear in its Markdown. Between 80% and 95% is a warning: usually a custom component without a Markdown form. |
not-found |
A missing page is a real 404 in HTML and in Markdown, with a Markdown body. |
discovery |
The HTML sends Link headers to the Markdown twin and llms.txt, sends Vary: Accept, and links to llms.txt in the page body. |
redirects |
Every redirect in larecipe.json, and the unversioned URL of a versioned set, redirects to a page that answers 200. |
skill-md |
skill.md is served as Markdown with a valid name and description. |
mcp |
The set's MCP endpoint answers tools/list with search_docs and read_page. Skipped when it asks for authentication. |
cache-store |
A database set uses a persistent cache store. Skipped for repository sets. |
How the grade is scored
A pass counts fully, a warning half and a failure nothing; skipped checks do not count. The score becomes a grade: A from 90, B from 80, C from 70, D from 60.
This site's doctor run
From the local copy:
docs http://127.0.0.1:8780/
PASS llms.txt: llms.txt is served and every index is under 50,000 characters.
PASS llms.txt links: 10 sampled llms.txt links resolve to Markdown.
WARN Markdown twins: Only 93% of the words on http://127.0.0.1:8780/3.x are in its Markdown.
Fix: Give custom components a Markdown form (LaRecipe::component(...)->markdown(...)).
PASS Missing pages: A missing page is a real 404, with a Markdown body for agents.
PASS Discovery hints: Pages send Link headers to their Markdown twin and llms.txt, and link to llms.txt in the page.
PASS Redirects: 1 redirects land on a page.
PASS skill.md: skill.md is served as the "larecipe" skill.
PASS MCP endpoint: http://127.0.0.1:8780/mcp serves search_docs, read_page, list_pages, submit_feedback.
SKIP Cache store: Repository sets read from files; no cache store is needed.
Agent readiness: A (94 / 100)Every option
larecipe:doctorGrade how ready your docs are for agents, with fixes
php artisan larecipe:doctor [--set [SET]] [--json]| Option | Description | Default |
|---|---|---|
--set=SET | Only this doc set | |
--json | Print the report as JSON |
Which one when
larecipe:validate |
larecipe:doctor |
|
|---|---|---|
| Reads | The docs, through the renderer | The app's HTTP responses |
| Catches | Mistakes in the content | Problems in what is served: headers, routes, 404s, MCP |
| Fails on | Any error | A failed check |
| Run it | After every docs change, before every push, in CI | After changing routes, middleware, caching or the agent settings, and after a deploy |
A page can pass validation and still fail the doctor: a proxy that strips Link headers, or an exception handler that turns a 404 into a soft error page, never shows up in the content.
Next
- Deployment: where both commands fit in a deploy.
- The CI check: go deeper with the ready-made GitHub Actions workflow.
- Agent surfaces: the files the doctor checks.