Background

A website built with Astro 4 + MDX + Tailwind CSS had an issue. Due to pnpm overrides, astro 5.x was being force-installed, causing a compatibility issue with @astrojs/mdx@2.x (designed for Astro 4):

Package subpath './jsx/renderer.js' is not defined by "exports" in astro/package.json

Rather than rolling back to Astro 4, I decided to upgrade to Astro 5.

Changes

1. Package Updates

// Before
"@astrojs/mdx": "^2.0.0",
"astro": "^4.0.3"

// After
"@astrojs/mdx": "^4.0.0",
"astro": "^5.0.0"

@astrojs/tailwind and tailwindcss remained unchanged.

2. Remove type from Content Collections

In Astro 5, the type: "content" option in defineCollection is no longer needed.

// Before
const news = defineCollection({
  type: "content",
  schema,
});

// After
const news = defineCollection({
  schema,
});

3. entry.slug to entry.id

Astro 5 removed the slug property from Content Collections entries, replacing it with id.

However, id includes the file extension (.md, .mdx), so you need to strip it when using it as a URL slug.

// Before
params: { slug: entry.slug }
link={import.meta.env.BASE_URL + "datasets/" + dataset.slug}

// After
params: { slug: entry.id.replace(/\.\w+$/, '') }
link={import.meta.env.BASE_URL + "datasets/" + dataset.id.replace(/\.\w+$/, '')}

The same applies to filtering and sorting against constant lists:

// Before
.filter((service) => datasetList.includes(service.slug))

// After
.filter((service) => datasetList.includes(service.id.replace(/\.\w+$/, '')))

4. entry.render() to render(entry)

In Astro 5, render is now imported from astro:content as a standalone function.

// Before
import { getCollection } from "astro:content";
const { Content } = await entry.render();

// After
import { getCollection, render } from "astro:content";
const { Content } = await render(entry);

5. entry.body Removed

Direct access to entry.body (raw markdown text) is no longer available in Astro 5. Use frontmatter fields like entry.data.description instead.

// Before
<p>{item.body}</p>

// After
<p>{item.data.description}</p>

Files Changed

  • package.json
  • src/content/config.ts
  • src/pages/datasets/[...slug].astro
  • src/pages/news/[...slug].astro
  • src/pages/news/index.astro
  • src/pages/services/[...slug].astro
  • src/components/NewsSection.astro
  • src/components/DatasetSection.astro
  • src/components/ServiceSection.astro
  • src/components/Footer.astro
  • src/components/SidePanel.astro
  • src/layouts/ListLayout.astro

Takeaway

The migration was mostly mechanical — replacing slug with id (plus extension stripping) and updating the render() call signature. For projects using Content Collections, the whole process can be done in well under a day.