From a4e57b1dcc34cd8dd64ebee1894b393d81e44ba1 Mon Sep 17 00:00:00 2001 From: hukla <129692708+huklaa@users.noreply.github.com> Date: Wed, 12 Aug 2026 07:11:43 +0300 Subject: [PATCH 01/10] tooling: add redirect destination auditor --- scripts/audit-redirects.js | 142 +++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 scripts/audit-redirects.js diff --git a/scripts/audit-redirects.js b/scripts/audit-redirects.js new file mode 100644 index 000000000..e5b267741 --- /dev/null +++ b/scripts/audit-redirects.js @@ -0,0 +1,142 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +function normalizeInternalPath(value) { + if (typeof value !== 'string' || !value.startsWith('/')) return null; + + const clean = value.split(/[?#]/, 1)[0].replace(/\/+$/, ''); + return clean || '/'; +} + +function collectRoutes(docsDir) { + const routes = new Set(['/']); + + function walk(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + walk(fullPath); + continue; + } + + if (!entry.isFile() || !entry.name.endsWith('.mdx')) continue; + + let route = path + .relative(docsDir, fullPath) + .split(path.sep) + .join('/') + .replace(/\.mdx$/, ''); + + if (route === 'index') route = ''; + if (route.endsWith('/index')) route = route.slice(0, -'/index'.length); + + routes.add(`/${route}`.replace(/\/+$/, '') || '/'); + } + } + + walk(docsDir); + return routes; +} + +function auditRedirects(config, routes) { + const redirects = Array.isArray(config.redirects) ? config.redirects : []; + const redirectMap = new Map(); + + for (const redirect of redirects) { + const source = normalizeInternalPath(redirect.source); + if (source && typeof redirect.destination === 'string') { + redirectMap.set(source, redirect.destination); + } + } + + function resolve(destination) { + let current = destination; + const visited = new Set(); + + while (true) { + const internal = normalizeInternalPath(current); + + // External destinations are valid terminal targets. + if (!internal) return { ok: true, terminal: current, reason: 'external' }; + if (routes.has(internal)) return { ok: true, terminal: internal, reason: 'page' }; + if (visited.has(internal)) return { ok: false, terminal: internal, reason: 'cycle' }; + + visited.add(internal); + const next = redirectMap.get(internal); + if (!next) return { ok: false, terminal: internal, reason: 'missing' }; + current = next; + } + } + + const brokenByDestination = new Map(); + + for (const redirect of redirects) { + if (typeof redirect.destination !== 'string') continue; + + const destination = normalizeInternalPath(redirect.destination); + if (!destination) continue; + + const result = resolve(redirect.destination); + if (result.ok) continue; + + const existing = brokenByDestination.get(destination) || { + destination, + terminal: result.terminal, + reason: result.reason, + count: 0, + sources: [], + }; + + existing.count += 1; + if (typeof redirect.source === 'string') existing.sources.push(redirect.source); + brokenByDestination.set(destination, existing); + } + + return [...brokenByDestination.values()].sort( + (a, b) => b.count - a.count || a.destination.localeCompare(b.destination), + ); +} + +function printReport(broken) { + if (broken.length === 0) { + console.log('All internal redirect destinations resolve to an existing docs page.'); + return; + } + + const totalEntries = broken.reduce((sum, item) => sum + item.count, 0); + console.log( + `Found ${broken.length} broken internal redirect destinations across ${totalEntries} redirect entries.`, + ); + console.log(''); + console.log('Count\tDestination\tTerminal\tReason'); + + for (const item of broken) { + console.log(`${item.count}\t${item.destination}\t${item.terminal}\t${item.reason}`); + } +} + +function main() { + const repoRoot = path.resolve(__dirname, '..'); + const docsDir = path.join(repoRoot, 'docs'); + const configPath = path.join(docsDir, 'docs.json'); + const strict = process.argv.includes('--strict'); + + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + const routes = collectRoutes(docsDir); + const broken = auditRedirects(config, routes); + + printReport(broken); + + if (strict && broken.length > 0) process.exitCode = 1; +} + +if (require.main === module) main(); + +module.exports = { + auditRedirects, + collectRoutes, + normalizeInternalPath, +}; From a6ef380a76a019a18d6ee0b37231ea0ac35e03ac Mon Sep 17 00:00:00 2001 From: hukla <129692708+huklaa@users.noreply.github.com> Date: Wed, 12 Aug 2026 07:11:55 +0300 Subject: [PATCH 02/10] test: cover redirect audit edge cases --- scripts/audit-redirects.test.js | 79 +++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 scripts/audit-redirects.test.js diff --git a/scripts/audit-redirects.test.js b/scripts/audit-redirects.test.js new file mode 100644 index 000000000..5d65bb997 --- /dev/null +++ b/scripts/audit-redirects.test.js @@ -0,0 +1,79 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { auditRedirects, normalizeInternalPath } = require('./audit-redirects'); + +test('normalizes internal paths without query, hash, or trailing slash', () => { + assert.equal(normalizeInternalPath('/apps/quickstart/?foo=1#bar'), '/apps/quickstart'); + assert.equal(normalizeInternalPath('/'), '/'); + assert.equal(normalizeInternalPath('https://example.com/docs'), null); +}); + +test('accepts destinations that resolve directly to a docs page', () => { + const config = { + redirects: [{ source: '/old', destination: '/new' }], + }; + + assert.deepEqual(auditRedirects(config, new Set(['/new'])), []); +}); + +test('follows redirect chains that terminate at a docs page', () => { + const config = { + redirects: [ + { source: '/old', destination: '/middle' }, + { source: '/middle', destination: '/new' }, + ], + }; + + assert.deepEqual(auditRedirects(config, new Set(['/new'])), []); +}); + +test('reports a missing terminal destination with usage count', () => { + const config = { + redirects: [ + { source: '/a', destination: '/legacy' }, + { source: '/b', destination: '/legacy' }, + { source: '/legacy', destination: '/missing' }, + ], + }; + + assert.deepEqual(auditRedirects(config, new Set(['/existing'])), [ + { + destination: '/legacy', + terminal: '/missing', + reason: 'missing', + count: 2, + sources: ['/a', '/b'], + }, + { + destination: '/missing', + terminal: '/missing', + reason: 'missing', + count: 1, + sources: ['/legacy'], + }, + ]); +}); + +test('reports redirect cycles instead of looping forever', () => { + const config = { + redirects: [ + { source: '/a', destination: '/b' }, + { source: '/b', destination: '/a' }, + ], + }; + + const broken = auditRedirects(config, new Set(['/real-page'])); + + assert.equal(broken.length, 2); + assert.equal(broken[0].reason, 'cycle'); + assert.equal(broken[1].reason, 'cycle'); +}); + +test('ignores redirects whose destination is external', () => { + const config = { + redirects: [{ source: '/old', destination: 'https://example.com/new' }], + }; + + assert.deepEqual(auditRedirects(config, new Set(['/existing'])), []); +}); From eb85a4dbe1e1a44e02e84081b549aa25340fb04a Mon Sep 17 00:00:00 2001 From: hukla <129692708+huklaa@users.noreply.github.com> Date: Wed, 12 Aug 2026 07:12:14 +0300 Subject: [PATCH 03/10] docs: document redirect audit workflow --- scripts/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scripts/README.md b/scripts/README.md index a7a9b9a01..04a1889f8 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -47,6 +47,23 @@ node scripts/lint-mdx.js all node scripts/lint-mdx.js all || exit 1 ``` +## Redirect destination auditor + +`audit-redirects.js` checks every internal destination in `docs/docs.json` against the current MDX route tree. It follows redirect chains, detects cycles, and groups repeated broken destinations so large redirect migrations can be audited without guessing from individual entries. + +```bash +# Report broken internal redirect destinations without failing +node scripts/audit-redirects.js + +# Exit with code 1 when broken destinations are found +node scripts/audit-redirects.js --strict + +# Run the focused unit tests +node --test scripts/audit-redirects.test.js +``` + +External redirect destinations are treated as valid terminal targets. The default report-only mode is useful while known redirect debt is being repaired; `--strict` can be used once the tree is clean or in targeted validation workflows. + ## Docs index generators Two generators emit AI-facing site indexes from the `docs/` tree. Both share helpers in `lib/docs-utils.js` (frontmatter parser, `.mintignore` loader, file walker, section discovery). From fea39b5fbf02d23da2042907ae3fe73630de35c0 Mon Sep 17 00:00:00 2001 From: hukla <129692708+huklaa@users.noreply.github.com> Date: Wed, 12 Aug 2026 09:17:02 +0300 Subject: [PATCH 04/10] test: cover docs route collection --- scripts/audit-redirects.test.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/scripts/audit-redirects.test.js b/scripts/audit-redirects.test.js index 5d65bb997..e9c0c11ef 100644 --- a/scripts/audit-redirects.test.js +++ b/scripts/audit-redirects.test.js @@ -1,7 +1,10 @@ const test = require('node:test'); const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); -const { auditRedirects, normalizeInternalPath } = require('./audit-redirects'); +const { auditRedirects, collectRoutes, normalizeInternalPath } = require('./audit-redirects'); test('normalizes internal paths without query, hash, or trailing slash', () => { assert.equal(normalizeInternalPath('/apps/quickstart/?foo=1#bar'), '/apps/quickstart'); @@ -9,6 +12,23 @@ test('normalizes internal paths without query, hash, or trailing slash', () => { assert.equal(normalizeInternalPath('https://example.com/docs'), null); }); +test('collects page routes and collapses index files to directory routes', (t) => { + const docsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'audit-redirects-')); + t.after(() => fs.rmSync(docsDir, { recursive: true, force: true })); + + fs.writeFileSync(path.join(docsDir, 'index.mdx'), '# Home\n'); + fs.writeFileSync(path.join(docsDir, 'guide.mdx'), '# Guide\n'); + fs.mkdirSync(path.join(docsDir, 'apps'), { recursive: true }); + fs.writeFileSync(path.join(docsDir, 'apps', 'index.mdx'), '# Apps\n'); + fs.writeFileSync(path.join(docsDir, 'apps', 'quickstart.mdx'), '# Quickstart\n'); + fs.writeFileSync(path.join(docsDir, 'ignored.md'), '# Ignored\n'); + + assert.deepEqual( + [...collectRoutes(docsDir)].sort(), + ['/', '/apps', '/apps/quickstart', '/guide'], + ); +}); + test('accepts destinations that resolve directly to a docs page', () => { const config = { redirects: [{ source: '/old', destination: '/new' }], From 2398697dd2b0556b54c3f39fc47b23305aadf008 Mon Sep 17 00:00:00 2001 From: hukla <129692708+huklaa@users.noreply.github.com> Date: Wed, 12 Aug 2026 12:15:16 +0300 Subject: [PATCH 05/10] fix: treat protocol-relative redirects as external --- scripts/audit-redirects.js | 2 +- scripts/audit-redirects.test.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/audit-redirects.js b/scripts/audit-redirects.js index e5b267741..c8e2f5585 100644 --- a/scripts/audit-redirects.js +++ b/scripts/audit-redirects.js @@ -4,7 +4,7 @@ const fs = require('fs'); const path = require('path'); function normalizeInternalPath(value) { - if (typeof value !== 'string' || !value.startsWith('/')) return null; + if (typeof value !== 'string' || !value.startsWith('/') || value.startsWith('//')) return null; const clean = value.split(/[?#]/, 1)[0].replace(/\/+$/, ''); return clean || '/'; diff --git a/scripts/audit-redirects.test.js b/scripts/audit-redirects.test.js index e9c0c11ef..bd21257b0 100644 --- a/scripts/audit-redirects.test.js +++ b/scripts/audit-redirects.test.js @@ -10,6 +10,7 @@ test('normalizes internal paths without query, hash, or trailing slash', () => { assert.equal(normalizeInternalPath('/apps/quickstart/?foo=1#bar'), '/apps/quickstart'); assert.equal(normalizeInternalPath('/'), '/'); assert.equal(normalizeInternalPath('https://example.com/docs'), null); + assert.equal(normalizeInternalPath('//example.com/docs'), null); }); test('collects page routes and collapses index files to directory routes', (t) => { @@ -92,7 +93,10 @@ test('reports redirect cycles instead of looping forever', () => { test('ignores redirects whose destination is external', () => { const config = { - redirects: [{ source: '/old', destination: 'https://example.com/new' }], + redirects: [ + { source: '/https-external', destination: 'https://example.com/new' }, + { source: '/protocol-relative', destination: '//example.com/new' }, + ], }; assert.deepEqual(auditRedirects(config, new Set(['/existing'])), []); From 442899ca79aa707e874dc876d22f6d671d30d947 Mon Sep 17 00:00:00 2001 From: hukla <129692708+huklaa@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:11:55 +0300 Subject: [PATCH 06/10] fix: report malformed relative redirect destinations --- scripts/audit-redirects.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/scripts/audit-redirects.js b/scripts/audit-redirects.js index c8e2f5585..bf603d95f 100644 --- a/scripts/audit-redirects.js +++ b/scripts/audit-redirects.js @@ -3,6 +3,13 @@ const fs = require('fs'); const path = require('path'); +function isExternalDestination(value) { + return ( + typeof value === 'string' && + (value.startsWith('//') || /^[A-Za-z][A-Za-z\d+.-]*:/.test(value)) + ); +} + function normalizeInternalPath(value) { if (typeof value !== 'string' || !value.startsWith('/') || value.startsWith('//')) return null; @@ -57,10 +64,12 @@ function auditRedirects(config, routes) { const visited = new Set(); while (true) { - const internal = normalizeInternalPath(current); + if (isExternalDestination(current)) { + return { ok: true, terminal: current, reason: 'external' }; + } - // External destinations are valid terminal targets. - if (!internal) return { ok: true, terminal: current, reason: 'external' }; + const internal = normalizeInternalPath(current); + if (!internal) return { ok: false, terminal: current, reason: 'invalid' }; if (routes.has(internal)) return { ok: true, terminal: internal, reason: 'page' }; if (visited.has(internal)) return { ok: false, terminal: internal, reason: 'cycle' }; @@ -75,10 +84,9 @@ function auditRedirects(config, routes) { for (const redirect of redirects) { if (typeof redirect.destination !== 'string') continue; + if (isExternalDestination(redirect.destination)) continue; - const destination = normalizeInternalPath(redirect.destination); - if (!destination) continue; - + const destination = normalizeInternalPath(redirect.destination) || redirect.destination; const result = resolve(redirect.destination); if (result.ok) continue; @@ -138,5 +146,6 @@ if (require.main === module) main(); module.exports = { auditRedirects, collectRoutes, + isExternalDestination, normalizeInternalPath, }; From a13ac270199b7137aa80d48313971cf14da53f2b Mon Sep 17 00:00:00 2001 From: hukla <129692708+huklaa@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:12:14 +0300 Subject: [PATCH 07/10] test: cover malformed relative redirect targets --- scripts/audit-redirects.test.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/scripts/audit-redirects.test.js b/scripts/audit-redirects.test.js index bd21257b0..74f2c8697 100644 --- a/scripts/audit-redirects.test.js +++ b/scripts/audit-redirects.test.js @@ -4,7 +4,12 @@ const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); -const { auditRedirects, collectRoutes, normalizeInternalPath } = require('./audit-redirects'); +const { + auditRedirects, + collectRoutes, + isExternalDestination, + normalizeInternalPath, +} = require('./audit-redirects'); test('normalizes internal paths without query, hash, or trailing slash', () => { assert.equal(normalizeInternalPath('/apps/quickstart/?foo=1#bar'), '/apps/quickstart'); @@ -13,6 +18,13 @@ test('normalizes internal paths without query, hash, or trailing slash', () => { assert.equal(normalizeInternalPath('//example.com/docs'), null); }); +test('recognizes only explicit external destination forms', () => { + assert.equal(isExternalDestination('https://example.com/docs'), true); + assert.equal(isExternalDestination('mailto:docs@example.com'), true); + assert.equal(isExternalDestination('//example.com/docs'), true); + assert.equal(isExternalDestination('apps/quickstart'), false); +}); + test('collects page routes and collapses index files to directory routes', (t) => { const docsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'audit-redirects-')); t.after(() => fs.rmSync(docsDir, { recursive: true, force: true })); @@ -76,6 +88,22 @@ test('reports a missing terminal destination with usage count', () => { ]); }); +test('reports malformed relative destinations instead of treating them as external', () => { + const config = { + redirects: [{ source: '/old', destination: 'apps/quickstart' }], + }; + + assert.deepEqual(auditRedirects(config, new Set(['/apps/quickstart'])), [ + { + destination: 'apps/quickstart', + terminal: 'apps/quickstart', + reason: 'invalid', + count: 1, + sources: ['/old'], + }, + ]); +}); + test('reports redirect cycles instead of looping forever', () => { const config = { redirects: [ From 7271b7b1d898e45cfa38a334e308975cf0e53b0e Mon Sep 17 00:00:00 2001 From: hukla <129692708+huklaa@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:10:07 +0300 Subject: [PATCH 08/10] fix: include markdown pages in redirect route collection --- scripts/audit-redirects.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/audit-redirects.js b/scripts/audit-redirects.js index bf603d95f..e5ad652b5 100644 --- a/scripts/audit-redirects.js +++ b/scripts/audit-redirects.js @@ -29,13 +29,14 @@ function collectRoutes(docsDir) { continue; } - if (!entry.isFile() || !entry.name.endsWith('.mdx')) continue; + const extension = path.extname(entry.name).toLowerCase(); + if (!entry.isFile() || !['.md', '.mdx'].includes(extension)) continue; let route = path .relative(docsDir, fullPath) .split(path.sep) .join('/') - .replace(/\.mdx$/, ''); + .replace(/\.mdx?$/, ''); if (route === 'index') route = ''; if (route.endsWith('/index')) route = route.slice(0, -'/index'.length); From 2f98f933135ea4caf99cd55ec06e86774e23378e Mon Sep 17 00:00:00 2001 From: hukla <129692708+huklaa@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:10:21 +0300 Subject: [PATCH 09/10] test: cover markdown redirect route targets --- scripts/audit-redirects.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/audit-redirects.test.js b/scripts/audit-redirects.test.js index 74f2c8697..78f920863 100644 --- a/scripts/audit-redirects.test.js +++ b/scripts/audit-redirects.test.js @@ -25,16 +25,16 @@ test('recognizes only explicit external destination forms', () => { assert.equal(isExternalDestination('apps/quickstart'), false); }); -test('collects page routes and collapses index files to directory routes', (t) => { +test('collects md and mdx page routes and collapses index files to directory routes', (t) => { const docsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'audit-redirects-')); t.after(() => fs.rmSync(docsDir, { recursive: true, force: true })); fs.writeFileSync(path.join(docsDir, 'index.mdx'), '# Home\n'); - fs.writeFileSync(path.join(docsDir, 'guide.mdx'), '# Guide\n'); + fs.writeFileSync(path.join(docsDir, 'guide.md'), '# Guide\n'); fs.mkdirSync(path.join(docsDir, 'apps'), { recursive: true }); - fs.writeFileSync(path.join(docsDir, 'apps', 'index.mdx'), '# Apps\n'); + fs.writeFileSync(path.join(docsDir, 'apps', 'index.md'), '# Apps\n'); fs.writeFileSync(path.join(docsDir, 'apps', 'quickstart.mdx'), '# Quickstart\n'); - fs.writeFileSync(path.join(docsDir, 'ignored.md'), '# Ignored\n'); + fs.writeFileSync(path.join(docsDir, 'ignored.txt'), 'Ignored\n'); assert.deepEqual( [...collectRoutes(docsDir)].sort(), From 24c8572c8729990a5de44a8b93800e57a10faece Mon Sep 17 00:00:00 2001 From: hukla <129692708+huklaa@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:14:29 +0300 Subject: [PATCH 10/10] fix: exclude hidden docs pages from redirect routes --- scripts/audit-redirects.js | 14 ++++++++++++++ scripts/audit-redirects.test.js | 10 +++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/scripts/audit-redirects.js b/scripts/audit-redirects.js index e5ad652b5..81e401f5c 100644 --- a/scripts/audit-redirects.js +++ b/scripts/audit-redirects.js @@ -17,6 +17,16 @@ function normalizeInternalPath(value) { return clean || '/'; } +function hasHiddenFrontmatter(content) { + if (typeof content !== 'string' || !content.startsWith('---')) return false; + + const closing = content.indexOf('\n---', 3); + if (closing === -1) return false; + + const frontmatter = content.slice(3, closing); + return /^\s*hidden\s*:\s*true\s*$/im.test(frontmatter); +} + function collectRoutes(docsDir) { const routes = new Set(['/']); @@ -32,6 +42,9 @@ function collectRoutes(docsDir) { const extension = path.extname(entry.name).toLowerCase(); if (!entry.isFile() || !['.md', '.mdx'].includes(extension)) continue; + const content = fs.readFileSync(fullPath, 'utf8'); + if (hasHiddenFrontmatter(content)) continue; + let route = path .relative(docsDir, fullPath) .split(path.sep) @@ -147,6 +160,7 @@ if (require.main === module) main(); module.exports = { auditRedirects, collectRoutes, + hasHiddenFrontmatter, isExternalDestination, normalizeInternalPath, }; diff --git a/scripts/audit-redirects.test.js b/scripts/audit-redirects.test.js index 78f920863..8ca6096ec 100644 --- a/scripts/audit-redirects.test.js +++ b/scripts/audit-redirects.test.js @@ -7,6 +7,7 @@ const path = require('node:path'); const { auditRedirects, collectRoutes, + hasHiddenFrontmatter, isExternalDestination, normalizeInternalPath, } = require('./audit-redirects'); @@ -25,7 +26,13 @@ test('recognizes only explicit external destination forms', () => { assert.equal(isExternalDestination('apps/quickstart'), false); }); -test('collects md and mdx page routes and collapses index files to directory routes', (t) => { +test('detects hidden true only inside frontmatter', () => { + assert.equal(hasHiddenFrontmatter('---\nhidden: true\ntitle: Hidden\n---\nBody\n'), true); + assert.equal(hasHiddenFrontmatter('---\nhidden: false\n---\nBody\n'), false); + assert.equal(hasHiddenFrontmatter('# Body\n\nhidden: true\n'), false); +}); + +test('collects visible md and mdx page routes and collapses index files to directory routes', (t) => { const docsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'audit-redirects-')); t.after(() => fs.rmSync(docsDir, { recursive: true, force: true })); @@ -34,6 +41,7 @@ test('collects md and mdx page routes and collapses index files to directory rou fs.mkdirSync(path.join(docsDir, 'apps'), { recursive: true }); fs.writeFileSync(path.join(docsDir, 'apps', 'index.md'), '# Apps\n'); fs.writeFileSync(path.join(docsDir, 'apps', 'quickstart.mdx'), '# Quickstart\n'); + fs.writeFileSync(path.join(docsDir, 'apps', 'hidden.mdx'), '---\nhidden: true\n---\n# Hidden\n'); fs.writeFileSync(path.join(docsDir, 'ignored.txt'), 'Ignored\n'); assert.deepEqual(