withastro/astro · error · AstroError
DuplicateContentEntrySlugError
DuplicateContentEntrySlugError
Error message
**${collection}** contains multiple entries with the same slug: `${id}`. Slugs must be unique.
Entries:
- ${existingEntry.filePath}
- ${relativePath} What it means
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.
Source
Thrown at packages/astro/src/content/loaders/glob.ts:198
const parsedData = await parseData({
id,
data,
filePath,
});
if (existingEntry && existingEntry.filePath && existingEntry.filePath !== relativePath) {
// Check the old file still exists - if not, this is likely a rename and
// the unlink event just hasn't been processed yet
const oldFilePath = new URL(existingEntry.filePath, config.root);
if (existsSync(oldFilePath)) {
const message = AstroErrorData.DuplicateContentEntrySlugError.message(
collection,
id,
existingEntry.filePath,
relativePath,
);
if (config.prerenderConflictBehavior === 'error') {
throw new AstroError({
...AstroErrorData.DuplicateContentEntrySlugError,
message,
});
} else if (config.prerenderConflictBehavior !== 'ignore') {
logger.warn(message);
}
}
}
if (entryType.getRenderFunction && !globOptions.deferRender) {
let render = renderFunctionByContentType.get(entryType);
if (!render) {
render = await entryType.getRenderFunction(config);
// Cache the render function for this content type, so it can re-use parsers and other expensive setup
renderFunctionByContentType.set(entryType, render);
}
let rendered: RenderedContent | undefined = undefined;View on GitHub (pinned to e294953aa8)
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.
Example fix
// before
src/content/blog/
post.md // slug: post
post.mdx // slug: post -> duplicate id
// after
src/content/blog/
post.mdx // slug: post (old post.md deleted)
// or keep both with distinct ids
const Blog = defineCollection({
loader: glob({
pattern: '**/*.{md,mdx}',
generateId: ({ entry, data }) => data.slug ?? slugify(entry),
}),
}); Defensive patterns
Strategy: validation
Validate before calling
import { readdirSync } from 'node:fs';
import { extname, join, relative } from 'node:path';
function findSlugCollisions(dir: string, root: string): Map<string, string[]> {
const bySlug = new Map<string, string[]>();
const walk = (d: string) => {
for (const f of readdirSync(d, { withFileTypes: true })) {
const full = join(d, f.name);
if (f.isDirectory()) walk(full);
else {
const rel = relative(root, full).replace(/\\/g, '/');
const slug = rel.slice(0, rel.length - extname(rel).length);
bySlug.set(slug, [...(bySlug.get(slug) ?? []), rel]);
}
}
};
walk(dir);
return new Map([...bySlug].filter(([, files]) => files.length > 1));
} Try / catch
try {
await getCollection('blog');
} catch (err) {
if ((err as any)?.code === 'DuplicateContentEntrySlugError') {
// message lists both file paths; rename/delete one or adjust generateId
} else throw err;
} Prevention
- 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.
When it happens
Trigger: `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.
Common situations: 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.
Related errors
- **${collection}** contains multiple entries with the same sl
- Glob patterns cannot start with `../`. Set the `base` option
- Glob patterns cannot start with `/`. Set the `base` option t
- No contents found for ${entry}
- The base directory "${fileURLToPath(baseDir)}" does not exis
AI-assisted analysis of withastro/astro@e294953aa8 (2026-08-18).
Data as JSON: /api/errors/ddc8cb87a2390266.
Report an issue: GitHub.