{"record":{"id":"ddc8cb87a2390266","repo":"withastro/astro","slug":"duplicatecontententryslugerror-ddc8cb","errorCode":"DuplicateContentEntrySlugError","errorMessage":"**${collection}** contains multiple entries with the same slug: `${id}`. Slugs must be unique.\n\nEntries: \n- ${existingEntry.filePath}\n- ${relativePath}","messagePattern":"\\*\\*(.+?)\\*\\* contains multiple entries with the same slug: `(.+?)`\\. Slugs must be unique\\.\n\nEntries: \n- (.+?)\n- (.+?)","errorType":"error_code","errorClass":"AstroError","httpStatus":null,"severity":"error","filePath":"packages/astro/src/content/loaders/glob.ts","lineNumber":198,"sourceCode":"\t\t\t\tconst parsedData = await parseData({\n\t\t\t\t\tid,\n\t\t\t\t\tdata,\n\t\t\t\t\tfilePath,\n\t\t\t\t});\n\n\t\t\t\tif (existingEntry && existingEntry.filePath && existingEntry.filePath !== relativePath) {\n\t\t\t\t\t// Check the old file still exists - if not, this is likely a rename and\n\t\t\t\t\t// the unlink event just hasn't been processed yet\n\t\t\t\t\tconst oldFilePath = new URL(existingEntry.filePath, config.root);\n\t\t\t\t\tif (existsSync(oldFilePath)) {\n\t\t\t\t\t\tconst message = AstroErrorData.DuplicateContentEntrySlugError.message(\n\t\t\t\t\t\t\tcollection,\n\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\texistingEntry.filePath,\n\t\t\t\t\t\t\trelativePath,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (config.prerenderConflictBehavior === 'error') {\n\t\t\t\t\t\t\tthrow new AstroError({\n\t\t\t\t\t\t\t\t...AstroErrorData.DuplicateContentEntrySlugError,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else if (config.prerenderConflictBehavior !== 'ignore') {\n\t\t\t\t\t\t\tlogger.warn(message);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (entryType.getRenderFunction && !globOptions.deferRender) {\n\t\t\t\t\tlet render = renderFunctionByContentType.get(entryType);\n\n\t\t\t\t\tif (!render) {\n\t\t\t\t\t\trender = await entryType.getRenderFunction(config);\n\t\t\t\t\t\t// Cache the render function for this content type, so it can re-use parsers and other expensive setup\n\t\t\t\t\t\trenderFunctionByContentType.set(entryType, render);\n\t\t\t\t\t}\n\t\t\t\t\tlet rendered: RenderedContent | undefined = undefined;","sourceCodeStart":180,"sourceCodeEnd":216,"githubUrl":"https://github.com/withastro/astro/blob/e294953aa8aadd98d5be92e60a03037b05dbdfd4/packages/astro/src/content/loaders/glob.ts#L180-L216","documentation":"The glob() loader generates an id per file (by default the slugified path). If an already-stored entry with the same id points at a different file and that older file still exists on disk, two live files claim one id: Astro reports DuplicateContentEntrySlugError — warn by default, throw when `prerenderConflictBehavior` is 'error'. If the old file no longer exists, it is treated as a rename in progress and no error is raised.","triggerScenarios":"`blog/post.md` and `blog/post.mdx` in the same collection (both slugify to 'post'); a custom `generateId` returning the same id for different files; same-stem files differentiated only by extension or casing on case-sensitive filesystems.","commonSituations":"Mixing markdown flavors in one collection; migrating .md to .mdx and leaving both copies; custom generateId that uses frontmatter (e.g. a shared `title`) instead of the filename; dev-server HMR where the rename detection has not caught up yet.","solutions":["Delete or rename the duplicate file so each id maps to exactly one file (e.g. remove post.md after creating post.mdx).","If both must exist, supply a custom `generateId` that includes the extension or another distinguishing part: `generateId: ({ entry, data }) => slugify(entry).replace(/\\.md$/, '') + (entry.endsWith('.mdx') ? '-mdx' : '')`.","Check whether the 'old' file really still exists — if it is a rename your watcher has not processed, saving again or restarting the dev server clears it.","Set `prerenderConflictBehavior` to 'ignore' only as a last resort; the store keeps one arbitrary winner."],"exampleFix":"// before\nsrc/content/blog/\n  post.md    // slug: post\n  post.mdx   // slug: post  -> duplicate id\n\n// after\nsrc/content/blog/\n  post.mdx   // slug: post  (old post.md deleted)\n\n// or keep both with distinct ids\nconst Blog = defineCollection({\n  loader: glob({\n    pattern: '**/*.{md,mdx}',\n    generateId: ({ entry, data }) => data.slug ?? slugify(entry),\n  }),\n});","handlingStrategy":"validation","validationCode":"import { readdirSync } from 'node:fs';\nimport { extname, join, relative } from 'node:path';\n\nfunction findSlugCollisions(dir: string, root: string): Map<string, string[]> {\n  const bySlug = new Map<string, string[]>();\n  const walk = (d: string) => {\n    for (const f of readdirSync(d, { withFileTypes: true })) {\n      const full = join(d, f.name);\n      if (f.isDirectory()) walk(full);\n      else {\n        const rel = relative(root, full).replace(/\\\\/g, '/');\n        const slug = rel.slice(0, rel.length - extname(rel).length);\n        bySlug.set(slug, [...(bySlug.get(slug) ?? []), rel]);\n      }\n    }\n  };\n  walk(dir);\n  return new Map([...bySlug].filter(([, files]) => files.length > 1));\n}","typeGuard":null,"tryCatchPattern":"try {\n  await getCollection('blog');\n} catch (err) {\n  if ((err as any)?.code === 'DuplicateContentEntrySlugError') {\n    // message lists both file paths; rename/delete one or adjust generateId\n  } else throw err;\n}","preventionTips":["Avoid same-stem files with different extensions (.md + .mdx) in one collection.","If using a custom generateId based on frontmatter, enforce its uniqueness in a lint step.","During .md-to-.mdx migrations, delete the old file in the same commit."],"tags":["content-collections","glob-loader","duplicate-slug","file-naming"],"backgroundTag":"duplicate-entry-id","analyzedSha":"e294953aa8aadd98d5be92e60a03037b05dbdfd4","analyzedAt":"2026-08-18T18:48:03.901Z","contentChangedAt":"2026-08-18T18:48:03.901Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}