From 804d2117106a666c62622a412652967c14e3341b Mon Sep 17 00:00:00 2001 From: Aniket Date: Mon, 17 Aug 2026 18:45:55 +0530 Subject: [PATCH] fix(docs): emit error on slug collision to prevent silent overwriting --- .../version-with-slug-conflicts/demo/demo.md | 4 +++ .../version-with-slug-conflicts/demo/index.md | 4 +++ .../site-broken-versions/versions.json | 3 +- .../versions/__tests__/loadVersion.test.ts | 15 +++++++++ .../src/versions/loadVersion.ts | 33 +++++++++++++++++++ 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/site-broken-versions/versioned_docs/version-with-slug-conflicts/demo/demo.md create mode 100644 packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/site-broken-versions/versioned_docs/version-with-slug-conflicts/demo/index.md diff --git a/packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/site-broken-versions/versioned_docs/version-with-slug-conflicts/demo/demo.md b/packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/site-broken-versions/versioned_docs/version-with-slug-conflicts/demo/demo.md new file mode 100644 index 000000000000..50ab5ccb4a7b --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/site-broken-versions/versioned_docs/version-with-slug-conflicts/demo/demo.md @@ -0,0 +1,4 @@ +--- +id: demo +--- +# Demo diff --git a/packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/site-broken-versions/versioned_docs/version-with-slug-conflicts/demo/index.md b/packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/site-broken-versions/versioned_docs/version-with-slug-conflicts/demo/index.md new file mode 100644 index 000000000000..74b06b612fb3 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/site-broken-versions/versioned_docs/version-with-slug-conflicts/demo/index.md @@ -0,0 +1,4 @@ +--- +id: index +--- +# Index diff --git a/packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/site-broken-versions/versions.json b/packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/site-broken-versions/versions.json index bd6349302e01..183f7ea1056a 100644 --- a/packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/site-broken-versions/versions.json +++ b/packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/site-broken-versions/versions.json @@ -1,3 +1,4 @@ [ - "with-id-conflicts" + "with-id-conflicts", + "with-slug-conflicts" ] diff --git a/packages/docusaurus-plugin-content-docs/src/versions/__tests__/loadVersion.test.ts b/packages/docusaurus-plugin-content-docs/src/versions/__tests__/loadVersion.test.ts index 0beb0c6b8f3e..3c34d77a2a5b 100644 --- a/packages/docusaurus-plugin-content-docs/src/versions/__tests__/loadVersion.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/versions/__tests__/loadVersion.test.ts @@ -118,5 +118,20 @@ describe('loadVersion', () => { ] `); }); + + it('rejects version with doc slug conflict', async () => { + await expect(() => loadTestVersion('with-slug-conflicts')).rejects + .toThrowErrorMatchingInlineSnapshot(` + [Error: The docs plugin found docs sharing the same slug: + + - /demo/ found in 2 docs: + - versioned_docs/version-with-slug-conflicts/demo/demo.md + - versioned_docs/version-with-slug-conflicts/demo/index.md + + Docs should have distinct slugs. + In case of conflict, you can use the \`slug\` front matter to assign an explicit distinct slug to each doc. + ] + `); + }); }); }); diff --git a/packages/docusaurus-plugin-content-docs/src/versions/loadVersion.ts b/packages/docusaurus-plugin-content-docs/src/versions/loadVersion.ts index c47dde2ef452..0aa77c96cf91 100644 --- a/packages/docusaurus-plugin-content-docs/src/versions/loadVersion.ts +++ b/packages/docusaurus-plugin-content-docs/src/versions/loadVersion.ts @@ -74,6 +74,38 @@ In case of conflict, you can rename the docs file, or use the ${logger.code( } } +function ensureNoDuplicateSlug(docs: DocMetadataBase[]): void { + const duplicatesBySlug = _.chain(docs) + .groupBy((d) => d.slug) + .pickBy((group) => group.length > 1) + .value(); + + const duplicateSlugEntries = Object.entries(duplicatesBySlug); + + if (duplicateSlugEntries.length) { + const slugMessages = duplicateSlugEntries + .map(([slug, duplicateDocs]) => { + return logger.interpolate`- url=${slug} found in number=${ + duplicateDocs.length + } docs: + - ${duplicateDocs + .map((d) => aliasedSitePathToRelativePath(d.source)) + .join('\n - ')}`; + }) + .join('\n\n'); + + const message = `The docs plugin found docs sharing the same slug: +\n${slugMessages}\n +Docs should have distinct slugs. +In case of conflict, you can use the ${logger.code( + 'slug', + )} front matter to assign an explicit distinct slug to each doc. + `; + + throw new Error(message); + } +} + async function loadVersionDocsBase({ tagsFile, context, @@ -106,6 +138,7 @@ async function loadVersionDocsBase({ } const docs = await Promise.all(docFiles.map(processVersionDoc)); ensureNoDuplicateDocId(docs); + ensureNoDuplicateSlug(docs); return docs; }