From 28f2eabef789da3a2be380acf2cc3d6ffe2cc44f Mon Sep 17 00:00:00 2001 From: rossrdme <168011594+rossrdme@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:19:28 -0500 Subject: [PATCH] fix(lint): treat category folders (folder/index.md) as slug-bearing in duplicate detection The duplicates validator skipped index.md/index.mdx entirely, so a category page (a folder containing index.md, whose slug is the folder name) was invisible to duplicate detection. A `.md` page and a `/index.md` category in the same section share a slug but were never flagged. Count a folder's index.md as the folder-name slug (skipping only a section-root index like reference/index.md). Docs and reference remain separate namespaces. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/validators/duplicates.js | 16 ++++++++++++++-- test/duplicates.test.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/validators/duplicates.js b/src/validators/duplicates.js index 926d5da..33ee80d 100644 --- a/src/validators/duplicates.js +++ b/src/validators/duplicates.js @@ -15,9 +15,21 @@ export function validateAll(files) { if (!CHECKED_DIRS.includes(topDir)) continue; const filename = path.basename(relPath); - if (filename === 'index.md' || filename === 'index.mdx') continue; + const isIndex = filename === 'index.md' || filename === 'index.mdx'; + + // A folder's index.md is a category page whose slug is the folder name, so + // it competes for a slug just like a `.md` file does. Any other file's + // slug is its own filename. Skip a section-root index (docs/index.md, + // reference/index.md) — it has no competing leaf slug. + let slug; + if (isIndex) { + const parent = path.basename(path.dirname(relPath)); + if (parent === topDir) continue; + slug = parent; + } else { + slug = filename.replace(/\.(md|mdx)$/, ''); + } - const slug = filename.replace(/\.(md|mdx)$/, ''); const key = `${topDir}:${slug}`; if (!slugMap.has(key)) slugMap.set(key, []); slugMap.get(key).push(relPath); diff --git a/test/duplicates.test.js b/test/duplicates.test.js index 436ae17..10eaf8a 100644 --- a/test/duplicates.test.js +++ b/test/duplicates.test.js @@ -13,3 +13,36 @@ test('same slug within docs (different subdirs) is flagged', () => { assert.match(res[0].message, /Duplicate slug: "intro"/); assert.equal(res[0].severity, 'error'); }); + +test('a page slug colliding with a category folder (folder/index.md) is flagged', () => { + const res = validateAll([ + 'reference/API/Other/guides.md', + 'reference/API/Other/guides/index.md', + ]); + assert.ok(Array.isArray(res) && res.length >= 1, 'expected a duplicate-slug error'); + assert.match(res[0].message, /Duplicate slug: "guides"/); +}); + +test('category-folder slug collisions are section-wide, not per-folder', () => { + // A page in one subdir and a category folder in another, same section. + const res = validateAll([ + 'reference/A/guides.md', + 'reference/B/guides/index.md', + ]); + assert.ok(Array.isArray(res) && res.length >= 1); + assert.match(res[0].message, /Duplicate slug: "guides"/); +}); + +test('a section-root index page is not treated as a competing slug', () => { + // reference/index.md is the section landing; it must not collide with anything. + const res = validateAll(['reference/index.md', 'reference/foo.md']); + assert.equal(res, null); +}); + +test('a category folder slug in reference does not collide with the same slug in docs', () => { + const res = validateAll([ + 'docs/guides.md', + 'reference/API/Other/guides/index.md', + ]); + assert.equal(res, null, 'docs and reference are separate namespaces'); +});