Fixing Astro Build Errors After Upgrading to Astro 5
Astro 5 replaced the content collections API with the content layer, merged hybrid output into static, and tightened a few defaults that Astro 4 sites leaned on. Most sites upgrade with npx @astrojs/upgrade and a handful of edits. A larger site with years of accumulated patterns usually hits several errors in a row, each of which stops the build before the next one appears, which makes the upgrade feel longer than it is.
This guide works through the errors in the order they appeared on a 1,600-page documentation and blog site built with Astro 4.16, with three content collections, a custom remark pipeline, and a few server-rendered routes using output: 'hybrid'. Each section starts from the error text, gives the cause, and shows the fix. It belongs to Astro vs Eleventy for Documentation Sites, where Astro's upgrade cadence is part of the long-term cost.
Before You Start
Build the site on Astro 4 and keep the output so you can prove the upgrade changes nothing a reader can see:
npm run build && cp -r dist /tmp/dist-astro4
find /tmp/dist-astro4 -name '*.html' | sed 's#/tmp/dist-astro4##' | sort > /tmp/urls-astro4.txt
Then run the upgrade tool, which updates Astro and its official integrations together:
npx @astrojs/upgrade
On this site it moved astro from 4.16.18 to 5.2, and @astrojs/mdx, @astrojs/sitemap and @astrojs/starlight to their matching majors.
Error 1: output: "hybrid" Is No Longer Valid
[config] Astro found issue(s) with your configuration:
! output: Invalid enum value. Expected 'static' | 'server', received 'hybrid'
Cause. Astro 5 merged hybrid into static. A static site can still render individual routes on demand.
Fix. Change output: 'hybrid' to output: 'static' (or remove it, since static is the default). The three routes that were server-rendered already exported export const prerender = false, which in Astro 5 opts them into on-demand rendering as long as an adapter is configured. The site kept its @astrojs/cloudflare adapter, upgraded to its Astro 5 version.
Error 2: getEntryBySlug Is Not Exported
SyntaxError: The requested module 'astro:content' does not provide an export named 'getEntryBySlug'
Cause. getEntryBySlug and getDataEntryById were deprecated in Astro 4 and removed in 5.
Fix. Replace both with getEntry(collection, id). Eleven files used them; a search and replace handled all of them, with slug arguments becoming IDs (see the next error for why the values stay the same).
Error 3: entry.slug Is Undefined
The build passed this point but generated pages at /docs/undefined/. The sitemap made it obvious: 1,200 URLs ending in /undefined/.
Cause. Collections were still defined the Astro 4 way (type: 'content', no loader), which Astro 5 runs in legacy mode where slug exists. Moving them to the content layer, which is what the rest of the upgrade needed, removes slug in favour of id.
Fix. Move src/content/config.ts to src/content.config.ts and give each collection a glob loader:
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const docs = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/docs' }),
schema: z.object({ title: z.string(), description: z.string(), sidebar: z.object({ order: z.number() }).optional() }),
});
Then replace entry.slug with entry.id everywhere (27 occurrences). The glob loader derives IDs from file paths the same way Astro 4 derived slugs — lowercased, without the extension, with index collapsed — and respects a slug field in frontmatter, which 40 pages used for custom URLs. Starlight's own docs collection uses docsLoader(), which handles this for you.
Error 4: entry.render is not a function
TypeError: entry.render is not a function
Cause. Content layer entries are plain data objects; rendering moved to a function.
Fix. Import render from astro:content and call it with the entry:
---
import { getEntry, render } from 'astro:content';
const entry = await getEntry('docs', Astro.params.slug);
const { Content, headings } = await render(entry);
---
<Content />
Fourteen layouts and pages changed. headings and remarkPluginFrontmatter are returned the same way as before.
Error 5: A Schema Rejects Existing Frontmatter
[InvalidContentEntryDataError] blog → 2023/launch-week data does not match collection schema.
pubDate: Expected date, received string
Cause. Astro 4's legacy collections had coerced some values more loosely. Under the content layer the schema is applied strictly, and nine blog posts had dates written as "March 3, 2023".
Fix. Use z.coerce.date() for date fields, which parses those strings, and fix the two posts whose dates were genuinely ambiguous. A remark plugin that read file.data.astro.frontmatter.draft also needed updating, because draft filtering moved into a getCollection filter rather than a plugin.
Verify the Output
With the build passing, compare URLs and content against the Astro 4 output:
find dist -name '*.html' | sed 's#^dist##' | sort > /tmp/urls-astro5.txt
diff /tmp/urls-astro4.txt /tmp/urls-astro5.txt
The diff was empty: 1,612 HTML files in both builds. An HTML diff after replacing hashed asset filenames with a placeholder showed differences in 41 files, all in the order of attributes on <img> elements produced by the updated image service — rendering-equivalent, confirmed by visual comparison of five of them.
Third-Party Integrations and Custom Code
The five errors above came from Astro itself. Two more problems came from code around it. A community sitemap filter integration, last released in 2023, hooked into astro:build:done and read routes from a shape that changed in Astro 5; it built without error but produced a sitemap missing every page in the blog collection. The official @astrojs/sitemap integration with a filter option replaced it in ten lines. Checking the sitemap's URL count against the HTML file count is now part of the verification step, because a missing page in a sitemap does not fail any build.
The other problem was a set of helper functions in src/lib/content.ts that wrapped collection queries and returned { slug, data, body } objects to components. The upgrade fixed the obvious call sites, but these helpers kept returning slug: entry.slug, which was now undefined, and three components used it to build links. TypeScript would have caught it if the helpers had been typed against CollectionEntry<'docs'>; they had been typed with a hand-written interface instead. After the upgrade they were retyped with Astro's generated types, so the next breaking change in entry shape will fail type checking (astro check) instead of producing broken links.
The lesson for large sites is to run astro check in CI before the upgrade, so that the type errors the upgrade introduces stand out against a clean baseline. On this site astro check reported 31 errors after the upgrade, and every one of them pointed at code that would have produced a wrong URL or an empty page.
Measured Impact
| Measure | Astro 4.16 | Astro 5.2 | Tool |
|---|---|---|---|
| HTML files | 1,612 | 1,612 | find |
| URL changes | — | 0 | diff of URL lists |
| Cold build | 2 min 48 s | 2 min 21 s | GitHub Actions |
| Warm build (cache restored) | 2 min 40 s | 1 min 12 s | GitHub Actions |
| Time spent on the upgrade | — | ~3 hours | Commit timestamps |
The warm build improvement comes from the content layer's data store, which skips unchanged entries when node_modules/.astro is cached between runs.
Pitfalls & Rollback
- Leaving collections in legacy mode. It builds, but loses the content layer's caching and leaves
slugcode that breaks later. - Search and replace of
slugwithout checking frontmatter slugs. Pages with custom slugs keep them only if the loader sees the field. - Skipping the URL diff. A wrong ID scheme silently changes URLs; the sitemap and the diff catch it.
- Upgrading integrations separately. Use the upgrade tool so majors stay matched.
- Rollback: the upgrade is one branch; reverting it and restoring the lockfile returns to Astro 4 exactly.
Conclusion
The Astro 5 upgrade errors arrive one at a time, but they come from four changes: hybrid output merged into static, removed entry helpers, slug replaced by id under the content layer, and rendering moved to a function. On a 1,600-page site, fixing them took about three hours, changed no URLs, and halved warm build time.
FAQ
Why does Astro 5 say my content config is missing or invalid?
Astro 5 expects the content configuration at src/content.config.ts, and collections defined the Astro 4 way, with a type of content or data and no loader, run through a legacy compatibility mode. If the file is still at src/content/config.ts it is found, but mixing the old location with new loader features or with the legacy flag disabled produces errors. Moving the file and adding a glob loader to each collection resolves it.
What replaced entry.slug in Astro 5?
Entries loaded by the glob loader have an id, which is derived from the file path by default in the same way slug was. Code that used entry.slug should use entry.id. If the old slugs came from a slug field in frontmatter, the glob loader respects that field when generating the id, so URLs stay the same.
What happened to output hybrid in Astro 5?
It was merged into static. A static site can now opt individual routes into on-demand rendering by exporting prerender false, as long as an adapter is installed. Configs that set output to hybrid fail validation and should be changed to static, or removed since static is the default.
How do I know the upgrade did not change my URLs?
Build with Astro 4 and Astro 5 into separate directories and compare the list of generated files and the sitemap. Any added or missing path is a URL change. Comparing page HTML after normalising hashed asset names catches rendering differences as well.
Related
- Parent: Astro vs Eleventy for Documentation Sites — upgrade cost as a selection factor.
- Loading Remote Content With Astro Content Layer Loaders — what loaders can do beyond glob.
- Incremental Builds in Astro with the Content Layer — the cache behind the warm build.
- Upgrading Eleventy 2 to 3 With ESM Configuration — the equivalent upgrade on Eleventy.
- Server Islands in Astro — on-demand rendering inside a static site.