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'); +});