From 42df98b061b6e285393381a863cf6e215f90c389 Mon Sep 17 00:00:00 2001 From: Marek Honzal Date: Mon, 3 Aug 2026 16:24:41 +0200 Subject: [PATCH] feat: add llms.txt discovery blockquote to generated .md pages --- scripts/addNavHeaders.mjs | 16 ++++++++++++++-- scripts/checkNavHeaders.mjs | 20 ++++++++++++++++---- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/scripts/addNavHeaders.mjs b/scripts/addNavHeaders.mjs index 32a55afccd..0a28bb2a40 100644 --- a/scripts/addNavHeaders.mjs +++ b/scripts/addNavHeaders.mjs @@ -3,11 +3,14 @@ import path from 'node:path'; // Prepend a Vercel-style nav header (frontmatter) to every per-page `.md` file // generated by `@signalwire/docusaurus-plugin-llms-txt`. See issue #2557. +// Also prepends a Claude-style "documentation index" discovery blockquote to the +// page body, pointing agents at `llms.txt`. See issue #2822. // // Why post-build (not a remark/rehype plugin): the llms-txt plugin writes // `llms-full.txt` from the SAME per-page content during its own `postBuild`. // Editing the per-page `.md` files AFTER that — in npm `postbuild` — keeps the -// header out of `llms-full.txt` for free, as required by the issue. +// header AND the blockquote out of `llms-full.txt` for free, as required by +// both issues. // // Data sources (both generated by `docusaurus build`): // - `.docusaurus/docusaurus-plugin-content-docs//p/*.json` @@ -57,6 +60,15 @@ function mdLink(label, route) { // `url` rather than a route. See issue #2557. const ROOT_PARENT = { label: 'Apify documentation', url: `${SITE_URL}/llms.txt` }; +// Agent-discovery blockquote, mirrored from code.claude.com/docs/*.md (issue +// #2822). Prepended to the body of every served per-page `.md`, below the +// nav-header frontmatter. Added here (after llms-full.txt is finalized) so it +// never leaks into llms-full.txt — same timing guarantee as the frontmatter. +const DOC_INDEX_BLOCKQUOTE = + `> ## Documentation index\n` + + `> Fetch the complete documentation index at: ${SITE_URL}/llms.txt\n` + + `> Use this file to discover all available pages before exploring further.\n`; + // A nav item is either {label, route} (a real per-page `.md`) or {label, url} // (a literal link, e.g. the llms.txt root). Render whichever it carries. function navLink(item) { @@ -311,7 +323,7 @@ async function main() { next: nav.next, }); - await fs.writeFile(filePath, header + content, 'utf8'); + await fs.writeFile(filePath, `${header}${DOC_INDEX_BLOCKQUOTE}\n${content}`, 'utf8'); processed++; } diff --git a/scripts/checkNavHeaders.mjs b/scripts/checkNavHeaders.mjs index 7078206e14..edd8c02ba9 100644 --- a/scripts/checkNavHeaders.mjs +++ b/scripts/checkNavHeaders.mjs @@ -34,6 +34,12 @@ const SITE_URL = 'https://docs.apify.com'; // llms-full.txt leak check below. const ROOT_PARENT = `[Apify documentation](${SITE_URL}/llms.txt)`; +// The Claude-style discovery blockquote addNavHeaders.mjs prepends to every +// page body (issue #2822). URL-independent, so it holds regardless of SITE_URL, +// and — like the root breadcrumb — appears nowhere else, so it doubles as the +// llms-full.txt leak marker. +const DOC_INDEX_MARKER = '> ## Documentation index'; + // A nav value is always a markdown link to a docs URL, e.g. [Label](https://...). // The label part allows escaped sequences (`\\`, `\[`, `\]`) because the producer's // escapeLinkLabel can emit them, so a literal `]` in a label won't end the match early. @@ -141,6 +147,9 @@ function checkPage(path, body, expectedKeys) { return; } + // The discovery blockquote must be present in the page body (issue #2822). + if (!body.includes(DOC_INDEX_MARKER)) fail(path, 'missing documentation-index blockquote'); + // Required on every page: title, url (exactly the page's own URL), parents. const title = getScalar(front, 'title'); if (!title) fail(path, 'missing or empty `title`'); @@ -197,11 +206,14 @@ for (const { path, keys } of PAGES) { // means a leak. try { const { source, text } = await readLlmsFull(); - if (text.includes(ROOT_PARENT)) { - fail('llms-full.txt', `nav header leaked into ${source} (found the root breadcrumb)`); - console.log('❌ llms-full.txt (nav header leaked in)'); + const leakedHeader = text.includes(ROOT_PARENT); + const leakedBlockquote = text.includes(DOC_INDEX_MARKER); + if (leakedHeader) fail('llms-full.txt', `nav header leaked into ${source} (found the root breadcrumb)`); + if (leakedBlockquote) fail('llms-full.txt', `discovery blockquote leaked into ${source}`); + if (leakedHeader || leakedBlockquote) { + console.log('❌ llms-full.txt (per-page content leaked in)'); } else { - console.log(`✅ llms-full.txt (no nav header leaked in; read ${source})`); + console.log(`✅ llms-full.txt (no nav header or blockquote leaked in; read ${source})`); } } catch (err) { fail('llms-full.txt', err.message);