{"record":{"id":"58c9f00a933e328d","repo":"facebook/docusaurus","slug":"duplicate-permalinks-found-in-tags-file-duplica","errorCode":null,"errorMessage":"Duplicate permalinks found in tags file:\n${duplicateList}","messagePattern":"Duplicate permalinks found in tags file:\n(.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/docusaurus-utils-validation/src/tagsFile.ts","lineNumber":47,"sourceCode":"\nexport function ensureUniquePermalinks(tags: TagsFile): void {\n  const permalinks = new Set<string>();\n  const duplicates = new Set<string>();\n\n  for (const [, tag] of Object.entries(tags)) {\n    const {permalink} = tag;\n    if (permalinks.has(permalink)) {\n      duplicates.add(permalink);\n    } else {\n      permalinks.add(permalink);\n    }\n  }\n\n  if (duplicates.size > 0) {\n    const duplicateList = Array.from(duplicates)\n      .map((permalink) => `  - ${permalink}`)\n      .join('\\n');\n    throw new Error(\n      `Duplicate permalinks found in tags file:\\n${duplicateList}`,\n    );\n  }\n}\n\nexport function normalizeTagsFile(data: TagsFileInput): TagsFile {\n  return _.mapValues(data, (tag, key) => {\n    return {\n      label: tag?.label || _.capitalize(key),\n      description: tag?.description,\n      permalink: tag?.permalink || `/${_.kebabCase(key)}`,\n    };\n  });\n}\n\ntype GetTagsFileParams = {\n  tags: TagsPluginOptions['tags'];\n  contentPaths: ContentPaths;","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/facebook/docusaurus/blob/3f483e80e326cc646b54b83d564b3f0c4881b9a6/packages/docusaurus-utils-validation/src/tagsFile.ts#L29-L65","documentation":"Thrown by ensureUniquePermalinks() after a tags file (tags.yml) is parsed and normalized. The function iterates every tag entry, collects permalinks into a Set, and if any permalink value appears more than once it raises this error listing the offending permalinks. Permalinks become URLs for tag listing pages, so collisions would produce ambiguous routing and are rejected up front.","triggerScenarios":"Calling getTagsFile() (or ensureUniquePermalinks() directly) where two tag keys resolve to the same permalink. This happens both when two tags set an identical explicit permalink (e.g. two entries with permalink: /foo) and when defaults collide: normalizeTagsFile() derives a default permalink of `/${_.kebabCase(key)}`, so keys like 'foo bar' and 'foo-bar' both collapse to /foo-bar.","commonSituations":"Author edits tags.yml and copy-pastes a tag entry forgetting to change its permalink. Two semantically different tag labels whose names kebab-case to the same slug (e.g. 'C++' vs 'cpp', or 'node.js' vs 'nodejs' depending on kebab rules). Migrating from inline front matter tags to a centralized tags.yml and accidentally aliasing several tags to one permalink.","solutions":["Open the tags.yml reported in the build output and find the permalinks listed under 'Duplicate permalinks found in tags file:' — every listed permalink appears on 2+ entries.","For each duplicate, edit one of the colliding tag entries and assign a distinct explicit permalink (e.g. permalink: /foo-v2).","If the collision is from default-derived permalinks (no explicit permalink field), rename the YAML key itself so its kebab-case slug is unique, or add an explicit permalink to disambiguate.","Re-run the build; ensureUniquePermalinks is called on every tags file load so the error clears as soon as all permalinks are unique."],"exampleFix":"# before (tags.yml)\nfoo bar:\n  label: Foo Bar\nfoo-bar:\n  label: FooBar\n# both normalize to permalink /foo-bar => duplicate\n\n# after (tags.yml)\nfoo bar:\n  label: Foo Bar\n  permalink: /foo-bar-with-space\nfoo-bar:\n  label: FooBar","handlingStrategy":"validation","validationCode":"import _ from 'lodash';\n\nfunction findDuplicatePermalinks(tags: Record<string, { permalink?: string }>): string[] {\n  const seen = new Map<string, number>();\n  for (const [key, tag] of Object.entries(tags)) {\n    const permalink = tag.permalink ?? `/${_.kebabCase(key)}`;\n    seen.set(permalink, (seen.get(permalink) ?? 0) + 1);\n  }\n  return [...seen.entries()].filter(([, n]) => n > 1).map(([p]) => p);\n}\n\n// run before passing tags to getTagsFile\nconst dupes = findDuplicatePermalinks(myTags);\nif (dupes.length) throw new Error(`Fix duplicate permalinks: ${dupes.join(', ')}`);","typeGuard":"function hasUniquePermalinks(tags: Record<string, { permalink?: string }>): boolean {\n  const seen = new Set<string>();\n  for (const [key, tag] of Object.entries(tags)) {\n    const p = tag.permalink ?? `/${_.kebabCase(key)}`;\n    if (seen.has(p)) return false;\n    seen.add(p);\n  }\n  return true;\n}","tryCatchPattern":"try {\n  await getTagsFile({ tags, contentPaths });\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('Duplicate permalinks')) {\n    // surface the listed permalinks to the user for editing\n  }\n  throw err;\n}","preventionTips":["Always assign explicit permalink values in tags.yml instead of relying on kebab-case defaults for tag keys that could collide.","Add a pre-commit lint script that loads tags.yml and runs ensureUniquePermalinks so collisions never reach the build.","Treat permalink values as you would slugs: lowercase, hyphenated, and globally unique within the file."],"tags":["tags","validation","permalink","yaml","routing"],"backgroundTag":null,"analyzedSha":"3f483e80e326cc646b54b83d564b3f0c4881b9a6","analyzedAt":"2026-08-12T13:25:04.382Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}