Repository navigation
doc: make the docs friendlier to coding agents (agent docs 1-7) - #39470
Conversation
Agent-friendly docs change 1. list.skill.md appended the full content of every page below a section to the section's own Markdown page, so section pages such as sql/index.md reached several hundred KB. Each of those pages is already published as its own file. List the pages below a section instead, with their titles, links and descriptions. In the website's Markdown tree, pages over 100,000 characters go from 14 to 3 and pages over 50,000 from 40 to 11; sql/index.md goes from 750,064 to 26,135 bytes. The remaining large pages are large in their own content. Links in the list are absolute URLs to the child's Markdown on the website, and bundle-root paths in the bin/gen-claude-skill bundle, via a new markdown-link-url partial. markdown-url now also works from the Markdown pass, where RelPermalink already ends in index.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-friendly docs change 2. Source links are written relative to the docs root, such as /sql/create-cluster/. The HTML pass resolves them in _markup/render-link.html, but .RenderShortcodes leaves Markdown links as written, and the website serves the Markdown under /docs/markdown-docs/, where they do not resolve: 139 of 150 sampled targets returned 404. A new markdown-body partial rewrites each root-relative link in the website build: a link to a page becomes the absolute URL of the page's Markdown, keeping its #fragment, and any other link, such as an image, becomes an absolute URL under /docs. In a local build, all 5,927 rewritten links resolve to a built file, and no root-relative links remain. The bin/gen-claude-skill bundle is byte-identical, because its links name directories under the bundle root and stay as written. Extract the origin logic from markdown-url into a site-origin partial so both share it, and build the preview's Markdown with --environment preview so its links point at preview.materialize.com. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-friendly docs change 3. After the docs preview deploys, report: - ci/test/check-docs-markdown.py, a new check of the built Markdown: pages over the Agent-Friendly Documentation Spec's 50,000 and 100,000 character thresholds, links to Markdown pages that name no built file, and root-relative links, which do not resolve away from the site. - An afdocs (https://afdocs.dev) 0.22.2 scorecard of the preview, over every tenth page in llms.txt. afdocs cannot map llms.txt's markdown-docs links back to pages on its own. Neither fails the build. The preview status is posted first, so the checks do not delay the preview link. Against production, the afdocs command scores materialize.com/docs 74. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-friendly docs change 4. The only per-page pointer to a page's Markdown was the "View as Markdown" button, 82% of the way through the raw HTML, and no page pointed at llms.txt. afdocs found no llms.txt directive on any of 50 sampled pages. - HTML pages get a visually hidden pointer to llms.txt and the page's Markdown at the top of <body>, before the navigation. It is aria-hidden and its links take no focus, so it is invisible on screen and to assistive technology. - HTML pages get <link rel="alternate" type="text/markdown"> in <head>. - Markdown pages get a blockquote pointer to llms.txt after their title, as the Agent-Friendly Documentation Spec recommends. The skill bundle is unchanged, because it has no llms.txt. The 404 page and the self-managed-docs builds, which have no Markdown, get neither HTML pointer. The HTML links are base-relative, so the docs lint checks them against the built files; it passes locally. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-friendly docs change 5.
- Drop Hugo heading attributes, such as
"## Object Introspection (SHOW) { #show }", from the Markdown in both
builds. 193 headings carried one.
- include-example.skill.md printed "<no value>" before the code block
of an example with no description, 29 times across the Markdown.
Print the description only when it exists.
- Fix two page descriptions copied from other pages. Descriptions are
what an agent reads in llms.txt to choose a page:
- Reaction Time, Freshness, and Query Latency said "Learn about
indexes in Materialize."
- bytea type said "Expresses a Unicode string", the text type's
description.
Eight other descriptions are shared by two or three pages, such as the
Cloud and self-managed versions of a page. Those are left for the docs
owners.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-friendly docs change 6. Every <loc> in /docs/sitemap.xml was a bare path such as /docs/sql/create-source/, because Hugo's built-in sitemap template emits .Permalink, which has no host under --baseURL /docs. The sitemap protocol requires full URLs, and afdocs found no sitemap URLs under /docs to check llms.txt coverage against. Override the built-in template, copied from Hugo v0.152.2, with an absolute-permalink partial that takes the scheme and host from site-origin. A local build lists the same 565 URLs, now absolute. The sitemap's two future lastmod dates, on the docs home and the releases page, come from the scheduled v26.45 release notes and are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-friendly docs change 7. /docs/llms.txt listed every page and had grown to 85,604 characters, over the Agent-Friendly Documentation Spec's 50,000, and afdocs warned on llms-txt-size. The spec recommends nested llms.txt files for large sites. /docs/llms.txt now lists the 15 section indexes, at 1,624 characters, and each section's pages move to /docs/<section>/llms.txt, published from the same template with resources.FromString. The largest, for sql, is 31,071 characters. The same 488 page entries are listed as before. The preview's afdocs sample now reads the section indexes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MaterializeInc/materialize#39470 splits /docs/llms.txt into a root index of per-section llms.txt files, which list the pages. Tell the agent to follow a section index when llms.txt lists those instead of pages, so the skill works before and after that change deploys. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
QA LLM Review1. MEDIUM -- llms.txt pointer is glued onto the H1 or description line of 507 of 568 Markdown pages
The pointer is supposed to be a blockquote after the title, but on every single page it lands on the same line as whatever precedes it. On 86 pages that is the H1 itself, so the page title an agent reads becomes Details
Fix: start the pointer with a blank line, for example |
antiguru
left a comment
There was a problem hiding this comment.
Posted by Claude Code on behalf of @antiguru.
I built this branch and its merge-base with --baseURL /docs, both the HTML and the Markdown pass. Several claims hold: the sitemap has 565 of 565 absolute <loc> values, every one of the 569 rel="alternate" type="text/markdown" targets exists, the checker reports 0 broken and 0 relative links, and 16 llms.txt files are published. The Markdown build goes from 0.33 s to 0.47 s, so the regex rewrite costs nothing that matters. zizmor 1.30.1 --pedantic reports nothing on the new workflow.
Blocking
afdocs-report.py runs npx --yes afdocs@0.22.2 with secrets in its environment. The script pins afdocs, but npm resolves its transitive dependencies fresh on each run, without a lockfile. In afdocs_report.yml, SLACK_TOKEN is set on the same step, and the npx subprocess inherits it. In preview-docs.sh, npx runs in the job that holds $GITHUB_TOKEN for the status curl and the S3 credentials for hugo deploy, on every docs PR. A compromised dependency could read all of these. Split the workflow into a scoring step without secrets that writes JSON and a posting step with SLACK_TOKEN, and run npx with a scrubbed environment in both places (env -i PATH="$PATH" HOME="$HOME" npx ...).
Strong suggestions
- The fence tracking in
check-docs-markdown.pymiscounts.prose_linestoggles on any line whose stripped text starts with three backticks. Six generated pages have unbalanced fences, caused by existing generator defects: glued fences such as``````hcland``````nofmtinsql/create-table/kafka,sql/create-table/sql-serverandsql/functions/normalize, and fences joined to prose (connection: ```mzsql) iningest-data/postgres/amazon-aurora,developer-tools/mz-debugandtransform-data/dataflow-troubleshooting. After the first broken fence, the checker treats prose as code. With fence filtering off, 31 pages carry raw shortcodes, not 29. The two it misses areingest-data/postgres/amazon-aurora(raw{{< tabs >}}and{{% include-example %}}) andsql/create-table/index.md(```{{< include-md). The checker should report unbalanced or glued fences as a defect of their own, because they also break the Markdown for agents. - Nothing prevents a regression. The checks run only in the preview step, and their failures are swallowed.
lint-docs.shalready builds the Markdown tree, and htmltest does not read.mdfiles. Broken and relative links are now at 0, solint-docs.shcould fail on them and leave size and shortcodes as warnings. Without that gate, the drop from 9,651 relative links to 0 can come back unnoticed. - Split the PR. The seven template changes, a new checker, and a Slack-posting workflow that uses a secret belong to different reviewers. The nightly workflow and
afdocs-report.pywould fit in a separate PR, which is also where the secret handling above gets reviewed.
Nits
markdown-link-target.htmlstrips/docs/-prefixed data-file links throughprintf "%s/" $basePath, which works only when the base path is/docs. In the preview, where it is/materialize/$PR,/docs/sql/...becomespreview.../materialize/$PR/docs/sql/..., and the checker does not count it because the URL is outsidemarkdown-docs/. Stripping a literal/docs/would fix this.- The partials decide "website build or not" in three ways:
ne Path "/"inmarkdown-link-url, a non-empty trimmed path inmarkdown-body, and a substring match on"self-managed"inhas-markdownandindex.llmstxt.txt. One partial per decision would keep them from drifting apart. - Root-level pages go to
about/llms.txtthrough.key | default (urlize .title), but noaboutsection exists, and the builtdocs/about/contains onlyllms.txt. - In
post(), thedict[str, str]annotation forces"unfurl_links": "false"to be a string, while Slack documents a boolean. The afdocs check messages reach Slack mrkdwn without escaping&,<and>. - The link rewrite in
markdown-bodyand the heading-attribute strip also apply inside fenced code. I found no affected output, but nothing documents or enforces this. sample_urls: whenllms.txthas no links,all([])is true, and afdocs is called with--urls "".sitemap.xmlcopies the built-in template of Hugo 0.152.2. The header names the version, so check the copy on each Hugo bump.
Once the secret handling is fixed, the template changes (1 to 7) are a clear improvement and could land, preferably without the nightly workflow.
|
I'm in favor, although I'm OOO in a few hrs and won't be able to review in detail. As long as this doesn't affect SEO (i'll let @nisarhassan12 confirm) let's ship it! |
Agent-friendly docs change 2, continued. The first pass rewrote inline links that start with "/", and left three kinds of link unresolvable once the Markdown is read away from the site: - path-relative links such as ../create-role, 1,103 in the Markdown; - reference definitions such as "[`SELECT`]: /sql/select/"; - href and src attributes in the HTML some shortcodes emit, such as href="/docs/sql/drop-source/". With Markdown at the page URL simulated locally, afdocs failed markdown-link-portability on 13 to 15 of 50 pages for these. Move the URL mapping into a markdown-link-target partial and apply it to all three forms. A path-relative link resolves against the page's directory, as it does from the HTML page. A reference definition must start with "/" or "." and end its line or precede a title, so that prose such as "[Note]: Materialize ..." is not mistaken for one. In a local build, no relative links remain, and every rewritten target names a built file except one: an absolute link in the v0.27 release notes to docs/lts/release-notes/, which already returns 404. The skill bundle and the HTML are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-friendly docs change 3, continued. check-docs-markdown.py counted inline links that start with "/", so it reported no problem links while afdocs failed path-relative ones. Read links from inline links, reference definitions, and href and src attributes, and count root-relative and path-relative links separately. A relative link now makes the check report a problem, as a broken one does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-friendly docs change 7, continued. llms.txt left out the per-version release notes to keep the single index small, but the sitemap lists the 77 that still render, so afdocs' llms-txt-coverage found 488 of 565 sitemap pages, under its 95% pass line. With the index split by section, the notes cost 78 lines in releases/llms.txt, 7,386 characters in all. List them like any other page. llms.txt now covers 564 of the 565 sitemap pages, all but the docs home, and every listed page's Markdown exists in a local build. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-friendly docs change 6, continued. /docs/sitemap.xml was served as application/rss+xml, on the website and on PR previews alike. hugo deploy sets a file's Content-Type from the first Hugo media type with its suffix, and for ".xml" that is RSS. Add a deployment matcher that sets application/xml for sitemap.xml. The preview's deploy config keeps the matcher, so a docs preview shows the result. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-friendly docs change 4, fixed. single.skill.md writes a page's description just before its body without a newline, so on 507 of the 569 Markdown pages the llms.txt pointer joined the description's line, as in "...to Materialize> For the complete documentation index...", and was not a blockquote. Start the pointer with a blank line. In a local build it is now on its own line on 568 pages; the docs home, whose Markdown is index.skill.md, has none. The skill bundle is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-friendly docs change 3, continued. Shortcodes nested inside YAML
data, such as the include-example and tabs calls in a data file's
description, reach the Markdown as literal "{{< ... >}}" text, while
the HTML renders them. The skill shortcodes print a data field as
written, and the HTML ones pass it through markdownify, which renders
nested shortcodes. 29 Markdown pages carry such syntax, including the
MySQL and PostgreSQL ingestion guides and the CREATE TABLE pages.
Report those pages, outside fenced code blocks, so the count shows on
every docs preview until the gap is closed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent-friendly docs change 3, revised after review. The preview step ran `npx --yes afdocs@0.22.2` in the job that holds $GITHUB_TOKEN for the status update and the AWS credentials for `hugo deploy`, on every docs PR. npm resolves afdocs' dependencies fresh on each run, so a compromised dependency could read those credentials. Scrubbing the environment would not cover credentials an agent can reach without it, such as an instance profile. Drop the afdocs run. The step keeps check-docs-markdown.py, which uses the Python standard library. The nightly afdocs report on production, with afdocs pinned by a lockfile and run without secrets, is in #39484. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
63cb0a7 to
bab4efe
Compare
|
@maheshwarip in parallel to waiting for confirmation from @nisarhassan12 I will address the other review feedback, including moving the nightly check out into another PR. |
nisarhassan12
left a comment
There was a problem hiding this comment.
Approving. The sitemap fix is the big SEO win here: the live /docs/sitemap.xml has relative <loc>s and is served as application/rss+xml, so Google most likely ignores it today. In my local build of this branch, all 565 URLs are full addresses and none of them is noindex.
Two non-blocking notes for follow-ups:
1. Canonical and og:url are relative. In head.html, both print .Permalink, which with --baseURL /docs is a path like /docs/sql/create-source/. Google handles a relative canonical fine, and this predates the PR. og:url is supposed to be absolute, though, and some share scrapers mishandle a path. absolute-permalink.html from this PR would fix both with one line each.
2. Other search engines can index the Markdown copies. Live robots.txt has Disallow: /docs/markdown-docs/ for Googlebot only, and Allow: / for everyone else. That means Bing and other engines can index the .md files as duplicates of the HTML pages, and the new rel="alternate" link and the agent pointer make them easier to find.
- A CloudFront
X-Robots-Tag: noindexheader on/docs/markdown-docs/*would keep other engines from indexing them. It wouldn't change anything for Google while theDisallowis in place, because Googlebot never fetches the files. - While Googlebot is blocked, links to those URLs can still produce bare, URL-only Google results. The only way to prevent that is to remove the
Disallowand let thenoindexheader keep the files out of the index. /docs/llms.txtand/docs/<section>/llms.txtaren't covered by theDisallow, so Google can index them. Worth deciding whether we want that.
MaterializeInc/materialize#39470 splits /docs/llms.txt into a root index of per-section llms.txt files, which list the pages. Tell the agent to follow a section index when llms.txt lists those instead of pages, so the skill works before and after that change deploys. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MaterializeInc/materialize#39470 splits /docs/llms.txt into a root index of per-section llms.txt files, which list the pages. Tell the agent to follow a section index when llms.txt lists those instead of pages, so the skill works before and after that change deploys. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@bosconi how are we looking on this? Can we aim to merge it in this week? |
|
@pm yes, I'll get it merged and send out an update when I do. When I do, I'll also send an update on our Anthropic plugin directory submission (based on a branch that already has the changes). Currently on its 7th day waiting to be reviewed. Maybe we should call in a favor from Parker. |
QA LLM Review1. MEDIUM -- Preview Markdown now has 560 links with a doubled base path, and the new link gate reports 0
The new trim strips a literal DetailsThe bug is live on the deployed preview: The new gate cannot see this. Strip the base path as before, and also - {{- $path = strings.TrimPrefix "/docs/" $path | strings.TrimPrefix "/" | printf "/%s" -}}
+ {{- $path = strings.TrimPrefix (printf "%s/" $basePath) $path | strings.TrimPrefix "/docs/" | strings.TrimPrefix "/" | printf "/%s" -}}To catch this in the checker, flag links whose path repeats the base path itself, for example |
…w base path 793a9ea stripped only the literal /docs/ prefix from root-relative links. In a preview, shortcode links already carry the base path from relURL, so they came out as /materialize/<PR>/materialize/<PR>/... and 404ed: 560 links on 72 pages. Strip the base path and then /docs/, which restores the preview output of bab4efe exactly and leaves the production output byte-identical. The checker now flags a link that repeats the base path itself, not only a repeated docs/ segment. lint-docs.sh also builds the Markdown with a base path other than /docs and gates it, since under /docs the two strips agree and the regression is invisible. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Thanks, all. 793a9ea and 42d11e2 are pushed with the remaining link fixes. CI is green, so merging. Review items handled in follow-up PRs
None of these changes what this PR publishes: the relative canonical and
|
Linear: [EDU-101](https://linear.app/materializeinc/issue/EDU-101/make-the-materialize-docs-agent-friendly) ### Motivation [afdocs](https://afdocs.dev) checks a docs site against the [Agent-Friendly Documentation Spec](https://agentdocsspec.com/spec/web/). materialize.com/docs scores 74 today, and [#39470](#39470) makes the template changes that raise it. This workflow reports the score to #docs every night, so the docs team sees it move and sees regressions. It was part of #39470, and is split out here so that its secret handling gets its own review, as suggested there. ### Description The "Agent-friendly docs report" workflow runs every day at 06:00 UTC / 02:00 EDT. It scores materialize.com/docs with afdocs on every tenth page listed in `llms.txt`, and posts to #docs. - **Top-level message:** always one line, the same shape at any score, differing only in its emoji. For example: ":x: The docs score 74 / 100 for agent-friendliness on afdocs, over 49 pages: 9 of 28 checks fail or warn. Workflow run." At 100 the emoji is :white_check_mark:. - **Thread reply:** the checks that fail or warn, one bullet each, then the full report with every check and the category scores. A run at 100 gets the reply too, so every run has the same shape and the full report. - **Dry run:** a manual run is a dry run by default. It prints the message and the reply in the log of the "Score the docs" step, and skips the `post` job. Pull requests that change the workflow, `ci/test/afdocs-report.py` or `ci/test/afdocs/` always dry-run. afdocs and its npm dependencies never run where a secret is available: - **Pinned dependency tree.** `ci/test/afdocs/package.json` and `package-lock.json` pin afdocs 0.22.2 and its 64 dependencies by integrity hash. The workflow installs them with `npm ci --ignore-scripts`, so no install script runs and npm resolves nothing fresh. - **Scoring without secrets.** The `score` job holds no secrets, and checks out with `persist-credentials: false`. `afdocs-report.py score` runs afdocs with only `PATH` and `HOME` in its environment. It writes the afdocs JSON, the message and the thread reply, which the job uploads as an artifact. - **Posting on a fresh runner.** The `post` job downloads the artifact and posts it with `SLACK_TOKEN`. It runs only `afdocs-report.py post`, which uses the Python standard library, so no npm code runs on that runner. **Slack token:** the workflow uses the repo's existing `SLACK_TOKEN` secret, the one the `slack_notify_*` workflows use, so no new secret is needed if that bot is a member of #docs. The report goes in a thread reply rather than an attached file, because uploading a file needs the `files:write` scope, and `SLACK_TOKEN`'s scopes are unknown. `afdocs-report.py` reads `llms.txt` whether it lists the pages itself, as on production today, or links one `llms.txt` per section, as after #39470. It exits with an error when `llms.txt` lists no pages, rather than calling afdocs with no URLs. It escapes `&`, `<` and `>` in afdocs' messages for Slack, and sends `unfurl_links` as a boolean. ### Verification - **Dry run against production:** `afdocs-report.py score https://materialize.com/docs` scored 74 over 49 pages, with 9 of 28 checks failing or warning, and wrote the JSON, the message and the reply. - **Message at 100:** checked by substituting the score in the saved report; only the emoji and the counts change. - **Scrubbed environment:** with `SLACK_TOKEN`, `GITHUB_TOKEN` and `AWS_SECRET_ACCESS_KEY` set, a stand-in afdocs binary saw only `HOME`, `PATH` and the variables the shell sets itself. - **Posting:** `post` was run against a stubbed Slack API. It sent the message, then the reply with the message's `thread_ts`, with `unfurl_links` as `false`. - **Lints:** ruff 0.16.5, pyright 1.1.407 `--warnings`, zizmor `--persona=pedantic` with the repo's config, `check-copyright` and `check-whitespace` pass. - **This PR's own run:** the workflow runs on this PR as a dry run. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…9913) Follow-ups promised in review of #39470. Part of: [EDU-101](https://linear.app/materializeinc/issue/EDU-101) ### What changed - **Canonical and `og:url` are absolute.** `head.html` printed `.Permalink` for both, which under `--baseURL /docs` is a path such as `/docs/sql/create-source/`. `og:url` must be an absolute URL. Both now use the `absolute-permalink` partial that the sitemap already uses. - **Root-level pages are listed in the root `llms.txt`.** License and Support have no section, so they were published in `about/llms.txt`, a directory with nothing else in it. The root index now lists them under their sidebar header ("About"), and `about/llms.txt` is no longer generated. The root index's orienting note says that pages outside any section follow the section list. - **One partial per "which build is this" decision.** The templates make two different decisions, and checked them three ways: - Is this Markdown published on the website, so that links in it must be absolute URLs, or is it the `bin/gen-claude-skill` bundle, where root-relative links name bundle directories? `markdown-link-url` checked `ne Path "/"`, and `markdown-body` checked for a non-empty trimmed path. `markdown-links-absolute.html` now owns this. The two checks only disagreed for a host-only `--baseURL`, which no build uses. - Does the website publish a Markdown tree for this build at all? `self-managed-docs/*` branches build the HTML alone. `has-markdown` and `index.llmstxt.txt` each matched "self-managed" in the base path. `site-has-markdown.html` now owns this, and `has-markdown` calls it. - **A comment next to the Hugo pin** in `ci/builder/Dockerfile` says to compare `doc/user/layouts/sitemap.xml` with the built-in template on each Hugo bump. This changes the ci-builder image hash, so CI rebuilds the builder image for this PR. ### Verification Built with Hugo 0.152.2 before and after: the production HTML and Markdown passes (`--baseURL /docs`), the skill bundle (no `--baseURL`, as `bin/gen-claude-skill` runs it), the preview passes, the `lint-docs.sh` passes with a host in the base URL, and a self-managed HTML build. Each build was byte-identical across two runs of the same commit, so every difference below comes from this PR. - **Partials refactor alone** (first commit): all six builds byte-identical to the base branch. - **Whole PR:** - Every Markdown tree, production, preview, lint and skill bundle, is byte-identical. - In the production, preview and self-managed HTML, the only changed lines are `og:url` and `canonical`, on all 570 pages, and all 570 of each are now absolute. Production uses `https://materialize.com/docs/...`, self-managed `https://materialize.com/docs/self-managed/<version>/...`, preview `https://preview.materialize.com/materialize/<PR>/...`. The lint build's HTML is unchanged, because its base URL already has a host. - `llms.txt` changes as described above in the production, preview and lint builds. The self-managed build still publishes none. - `check-docs-markdown.py --fail-on-links` passes on the production Markdown, and `test_check_docs_markdown.py` passes. Not changed: the 542 alias pages, rendered by `layouts/alias.html`, keep a path-only canonical. In production, `website.sh` replaces each with an S3 redirect, so their HTML is never served. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Linear: EDU-101
Motivation
Coding agents read materialize.com/docs through the Markdown tree at
/docs/markdown-docs/and through/docs/llms.txt, and both have defects that cost agents context or send them to dead links. afdocs 0.22.2, the checker for the Agent-Friendly Documentation Spec, scores materialize.com/docs 74. Comparable docs sites score higher: RisingWave 96, Cloudflare 99.This PR makes the seven agent-friendly docs changes that can be built and checked locally. Each commit names the change it belongs to:
llms.txtand their Markdown<no value>, two wrong descriptionsapplication/xml/docs/llms.txtsplits into per-section indexes, which also list the per-version release notesThe nightly afdocs report to the #docs Slack channel is now its own PR, #39484, as suggested in review. The preview step no longer runs afdocs: it ran
npxin the job that holds$GITHUB_TOKENand the S3 credentials, and #39484 runs afdocs from a lockfile in a job without secrets.Three more changes are in this repo but need a design decision or the docs owners, and are not in this PR:
CREATE TABLEpages, carry literal{{< ... >}}text where the HTML shows content. The.skill.mdshortcodes print a data field as written, while the HTML ones render shortcodes nested in it.releases/,observability/appendix-metrics/andsql/system-catalog/mz_internal/.Three more need the web team or the CDN owners, and are not in this PR:
/docs/<path>/index.md, inci/deploy_website/website.sh, with its alias redirects, and link that form fromllms.txt. It can only be tested by deploying.<main>inbaseof.html, and render less of the navigation tree on each page. It needs visual and accessibility review.Accept: text/markdown. This will let agents such as Claude Code, Cursor and OpenCode get Markdown from any page URL.afdocsceiling is 97 without this :-) The distribution ismaterialize-comini2/website.py.Description
Each commit message explains its change. Decisions the diff does not show:
website.sh, with--baseURL /docs, and bybin/gen-claude-skill, with none. Change 1's index links and change 2's link rewrite produce absolute URLs in the website build. In the skill bundle they produce bundle-root paths, which is the form the bundle's existing links use. Change 4'sllms.txtpointer is website-only, because the bundle has nollms.txt. Themarkdown-urlpartial already keyed off the base URL in the same way.site-originis extracted frommarkdown-url, so changes 2, 4 and 6 share one origin.markdown-bodypost-processes.RenderShortcodesfor changes 2, 4 and 5, andmarkdown-link-targetmaps each link it finds for change 2.markdown-link-url,llms-txt-url,has-markdownandabsolute-permalinkeach do one job, described in their header comments.htmltestthen checks them against the built files, instead of treating them as external links to the lint build's host.mz-docsskill read/docs/llms.txtto find pages. It also follows per-section indexes, and was tested against this PR's preview, so the two can land in either order.lastmoddates come from the scheduled v26.45 release notes. One absolute link in the v0.27 release notes, todocs/lts/release-notes/, already returns 404.Verification
Built with Hugo 0.152.2, the version
ci/builderinstalls, and compared with a build oforigin/main.sql/index.md<loc>values/docs/llms.txtllms.txttext/markdown, and scored with--canonical-origin https://materialize.com/docson the same 49 pages, every tenth page inllms.txt.origin/mainbuild scores 74, the same as production.llms.txtlinks of the form/docs/<path>/index.md, it scores 97, and 98 when afdocs chooses its own 50 pages.markdown-link-portabilityandllms-txt-coveragepass there.markdown-url-support,content-negotiationandllms-txt-directive-mdfail until change 11 or 13 lands.llms-txt-coveragefails until change 11 switchesllms.txtto the page-URL form, because afdocs cannot mapmarkdown-docs/<path>/index.mdlinks back to pages.content-start-positionandpage-size-htmlneed change 12, andpage-size-markdownchange 9.hugoandhtmlteststeps ofci/test/lint-docs.shpass locally.check-copyright,check-whitespace, ruff 0.16.5 and pyright 1.1.407--warningspass on the changed files.lint-docs.shnow runs the Markdown checker in link-only failure mode. Four new fixture tests cover broken Markdown links, relative links, doubled/docs/prefixes, and warning-only size/shortcode findings. On local production and preview builds, the three link counts were 0; the existing 3 oversized pages and 29 pages with raw shortcodes remained warnings.🤖 Generated with Claude Code