Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions scripts/addNavHeaders.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>/p/*.json`
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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++;
}

Expand Down
20 changes: 16 additions & 4 deletions scripts/checkNavHeaders.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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`');
Expand Down Expand Up @@ -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);
Expand Down
Loading