From 7915f472bdc07a193ab1af683967d91d137113b7 Mon Sep 17 00:00:00 2001 From: Abdelrahman Essawy Date: Tue, 21 Jul 2026 23:31:27 +0300 Subject: [PATCH] fix(docs): repair the /mcp redirect and unpublish the internal analytics note Three things, all found by auditing docs.json against the file tree. /mcp/:slug* redirected to /mcp, but the page is mcp-server. Every /docs/mcp/* URL 308'd into a 405, including the two /docs/mcp/overview links in the public llms.txt. Repointed both the splat and a new bare /mcp entry at /mcp-server. /job-types/caption-burn fell through the splat to /jobs/caption-burn and then hopped again to /jobs/captions/burn. Added a direct rule. ANALYTICS.md sat in the publish root, so Mintlify served it at /docs/ANALYTICS and listed it in the sitemap despite it being an internal PostHog note. CLAUDE.md and README.md were already removed for the same reason. Moved to .github/. Added validate-nav-and-redirects.mjs to make all three classes fail CI: pages on disk but absent from the nav, redirects pointing at pages that do not exist, and nav entries with no file. The existing frontmatter validator only walks .mdx, and the workflow only triggered on .mdx, which is why a stray .md and a docs.json edit were never checked. --- ANALYTICS.md => .github/ANALYTICS.md | 0 .github/workflows/validate-frontmatter.yml | 11 ++ docs.json | 10 +- package.json | 1 + scripts/validate-nav-and-redirects.mjs | 128 +++++++++++++++++++++ 5 files changed, 149 insertions(+), 1 deletion(-) rename ANALYTICS.md => .github/ANALYTICS.md (100%) create mode 100644 scripts/validate-nav-and-redirects.mjs diff --git a/ANALYTICS.md b/.github/ANALYTICS.md similarity index 100% rename from ANALYTICS.md rename to .github/ANALYTICS.md diff --git a/.github/workflows/validate-frontmatter.yml b/.github/workflows/validate-frontmatter.yml index 5183138..9b21941 100644 --- a/.github/workflows/validate-frontmatter.yml +++ b/.github/workflows/validate-frontmatter.yml @@ -4,14 +4,23 @@ on: push: branches: [main] paths: + # `.md` and docs.json are listed alongside `.mdx` because the two bugs + # validate-nav-and-redirects.mjs exists to catch (an orphan ANALYTICS.md, + # a redirect into a dead page) both live in files the old filter ignored. - "**/*.mdx" + - "**/*.md" + - "docs.json" - "scripts/validate-frontmatter.mjs" + - "scripts/validate-nav-and-redirects.mjs" - "scripts/check-plan-limits.mjs" - "package.json" pull_request: paths: - "**/*.mdx" + - "**/*.md" + - "docs.json" - "scripts/validate-frontmatter.mjs" + - "scripts/validate-nav-and-redirects.mjs" - "scripts/check-plan-limits.mjs" - "package.json" @@ -26,5 +35,7 @@ jobs: node-version: "20" - name: Run frontmatter validator run: node scripts/validate-frontmatter.mjs + - name: Check nav, orphan pages, and redirect destinations + run: node scripts/validate-nav-and-redirects.mjs - name: Check plan-limit tables match the source of truth run: node scripts/check-plan-limits.mjs diff --git a/docs.json b/docs.json index c4e5c2f..e3240ad 100644 --- a/docs.json +++ b/docs.json @@ -143,13 +143,21 @@ "source": "/job-types/compose", "destination": "/jobs/compose" }, + { + "source": "/job-types/caption-burn", + "destination": "/jobs/captions/burn" + }, { "source": "/cli/:slug*", "destination": "/cli" }, + { + "source": "/mcp", + "destination": "/mcp-server" + }, { "source": "/mcp/:slug*", - "destination": "/mcp" + "destination": "/mcp-server" }, { "source": "/sdk/:slug*", diff --git a/package.json b/package.json index 185915d..8d8af0e 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "type": "module", "scripts": { "validate:frontmatter": "node scripts/validate-frontmatter.mjs", + "validate:nav": "node scripts/validate-nav-and-redirects.mjs", "validate:plan-limits": "node scripts/check-plan-limits.mjs" } } diff --git a/scripts/validate-nav-and-redirects.mjs b/scripts/validate-nav-and-redirects.mjs new file mode 100644 index 0000000..4b5ad59 --- /dev/null +++ b/scripts/validate-nav-and-redirects.mjs @@ -0,0 +1,128 @@ +#!/usr/bin/env node +/** + * Structural validator for docs.json against the file tree. + * + * Two classes of bug this catches, both of which shipped to production before + * this check existed: + * + * 1. Orphan pages. Mintlify serves EVERY .md/.mdx in the repo at its path, + * whether or not it appears in `navigation`. An internal engineering note + * (ANALYTICS.md) was therefore live at /docs/ANALYTICS and listed in the + * public sitemap. CLAUDE.md and README.md had already been deleted from this + * repo for the same reason. Note the sibling frontmatter validator only walks + * `.mdx`, so a stray `.md` passes every other check. + * + * 2. Redirects pointing at pages that do not exist. `/mcp/:slug*` pointed at + * `/mcp`, but the page is `mcp-server`. Every /docs/mcp/* URL therefore + * 308'd into a dead end, including two links in the public llms.txt. + * + * Both are static checks against docs.json plus the file tree. No network. + * + * Exit 0 = all pass. Exit 1 = at least one violation. + */ + +import { readFileSync, readdirSync, statSync } from 'fs'; +import { join, relative, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(__dirname, '..'); + +// Directories that never produce a published page. +// `snippets` holds Mintlify partials; the rest are tooling or assets. +const SKIP_DIRS = new Set(['snippets', 'node_modules', 'scripts', 'images', 'logo']); + +/** Every .md/.mdx in the repo that Mintlify would serve as a page. */ +function walkPages(dir, files = []) { + for (const entry of readdirSync(dir)) { + // ponytail: dot-dirs (.git, .github, .claude) are not part of the content + // tree. This is also the escape hatch for internal notes: put them there. + if (entry.startsWith('.')) continue; + const full = join(dir, entry); + if (statSync(full).isDirectory()) { + if (SKIP_DIRS.has(entry)) continue; + walkPages(full, files); + } else if (entry.endsWith('.mdx') || entry.endsWith('.md')) { + files.push(full); + } + } + return files; +} + +/** Slug as Mintlify routes it: repo-relative path, POSIX separators, no extension. */ +function toSlug(filePath) { + return relative(ROOT, filePath).replace(/\\/g, '/').replace(/\.mdx?$/, ''); +} + +/** Recursively collect every page string under any `pages` array in the nav. */ +function collectNavSlugs(node, out = new Set()) { + if (Array.isArray(node)) { + for (const item of node) collectNavSlugs(item, out); + return out; + } + if (node && typeof node === 'object') { + for (const [key, value] of Object.entries(node)) { + if (key === 'pages' && Array.isArray(value)) { + for (const page of value) { + if (typeof page === 'string') out.add(page); + else collectNavSlugs(page, out); + } + } else { + collectNavSlugs(value, out); + } + } + } + return out; +} + +function main() { + const config = JSON.parse(readFileSync(join(ROOT, 'docs.json'), 'utf8')); + const navSlugs = collectNavSlugs(config.navigation); + const pageSlugs = new Set(walkPages(ROOT).map(toSlug)); + const violations = []; + + // --- 1. Orphan pages: on disk (therefore live) but absent from navigation --- + for (const slug of [...pageSlugs].sort()) { + if (!navSlugs.has(slug)) { + violations.push( + `Orphan page "${slug}" is not in docs.json navigation, but Mintlify still ` + + `serves it at https://rendobar.com/docs/${slug} and lists it in the sitemap. ` + + `Add it to the nav, or move it into a dot-directory (e.g. .github/) if it is internal.` + ); + } + } + + // --- 2. Redirect destinations that resolve to nothing --- + for (const { source, destination } of config.redirects ?? []) { + if (/^https?:\/\//.test(destination)) continue; // off-site, not ours to verify + if (destination.includes(':')) continue; // dynamic (:slug*), not statically resolvable + const slug = destination.replace(/^\//, '').replace(/#.*$/, ''); + if (!pageSlugs.has(slug)) { + violations.push( + `Redirect "${source}" points at "${destination}", which is not a page in this repo. ` + + `Every URL matching that source is a dead end.` + ); + } + } + + // --- 3. Nav entries with no file behind them --- + for (const slug of [...navSlugs].sort()) { + if (!pageSlugs.has(slug)) { + violations.push(`Navigation lists "${slug}", but no matching .md/.mdx file exists.`); + } + } + + if (violations.length === 0) { + console.log( + `Checked ${pageSlugs.size} pages, ${navSlugs.size} nav entries, ` + + `${(config.redirects ?? []).length} redirects. All consistent.` + ); + process.exit(0); + } + + console.error(`\ndocs.json structural violations (${violations.length}):\n`); + for (const v of violations) console.error(` ${v}\n`); + process.exit(1); +} + +main();