Fix: Content Schema Should Not Contain Slug in Astro

Error message:
Content Schema should not contain `slug`.
Content Collections 2025-01-25

What Causes This Error?

This error occurs when you define a slug field in your content collection schema. The slug field is reserved by Astro and handled automatically - it should not be included in your schema.

The Problem

// src/content/config.ts
const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    // ❌ slug is reserved and should not be in schema
    slug: z.string(),
  }),
});

The Fix

Remove Slug from Schema

// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    date: z.date(),
    // ✅ No slug field - Astro handles it automatically
  }),
});

export const collections = { blog };

Common Scenarios

Slug is Auto-Generated

# File: src/content/blog/my-first-post.md
# Slug is automatically "my-first-post" from filename

---
title: "My First Post"
---

Content here...
---
import { getCollection } from 'astro:content';

const posts = await getCollection('blog');

posts.forEach(post => {
  console.log(post.slug);  // ✅ "my-first-post" - available automatically
  console.log(post.data.title);  // "My First Post"
});
---

Custom Slug in Frontmatter

---
# You can still override slug in frontmatter
# Just don't define it in the schema
title: "My Post"
slug: "custom-url-slug"
---

The slug in frontmatter overrides the filename-based slug.

Accessing Slug

---
import { getCollection, getEntry } from 'astro:content';

// Get all entries - slug is on the entry object
const posts = await getCollection('blog');
posts.forEach(post => {
  console.log(post.slug);  // Entry's slug
  console.log(post.data);  // Schema-defined data
});

// Get single entry
const post = await getEntry('blog', 'my-post');
console.log(post.slug);  // "my-post"
---

Using Slug in Routes

---
// src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => ({
    params: { slug: post.slug },  // ✅ Use entry.slug
    props: { post },
  }));
}

const { post } = Astro.props;
---

<h1>{post.data.title}</h1>

If You Need Slug Validation

// Instead of validating slug in schema,
// validate in getStaticPaths or when accessing

import { getCollection } from 'astro:content';

const posts = await getCollection('blog', (entry) => {
  // Filter or validate entries
  return entry.slug.startsWith('2024/');
});

Reserved Fields

// These fields are reserved and should NOT be in schema:
// - slug
// - id
// - collection
// - body (for content collections)
// - data (contains your schema fields)

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    // ❌ Don't include these:
    // slug: z.string(),
    // id: z.string(),
    // collection: z.string(),
    // body: z.string(),

    // ✅ Only include your custom fields:
    title: z.string(),
    description: z.string(),
    author: z.string(),
  }),
});

Migration from Old Schema

// Before (wrong)
const blog = defineCollection({
  schema: z.object({
    title: z.string(),
    slug: z.string(),  // Remove this
    permalink: z.string(),  // Use slug instead
  }),
});

// After (correct)
const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    // slug removed - use entry.slug instead
    // If you need permalink, compute it from slug
  }),
});

Quick Checklist

  • Remove slug from schema definition
  • Access slug via entry.slug not entry.data.slug
  • Custom slugs go in frontmatter, not schema
  • Other reserved fields: id, collection, body
  • Filename becomes default slug automatically