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.jsonsrc/content/config.tssrc/pages/datasets/[...slug].astrosrc/pages/news/[...slug].astrosrc/pages/news/index.astrosrc/pages/services/[...slug].astrosrc/components/NewsSection.astrosrc/components/DatasetSection.astrosrc/components/ServiceSection.astrosrc/components/Footer.astrosrc/components/SidePanel.astrosrc/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.