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.

Sequence of build errors during the Astro 4 to Astro 5 upgrade Five errors in order. First, the output hybrid configuration failed validation. Second, content collections ran in legacy mode and the getEntryBySlug function was missing. Third, entry.slug was undefined in pages. Fourth, the render method on entries was not a function. Fifth, a remark plugin failed because a frontmatter field was now validated by the schema. After the fifth fix the build passed. Errors in the order they appeared output hybrid getEntryBySlug entry.slug undefined entry.render missing Schema rejects field 1 config validation 2 removed API 3 use entry.id 4 use render(entry) 5 stricter data
Each error stopped the build, so they surfaced one at a time; the whole sequence took about three hours.

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.

How a content collection definition changed between Astro 4 and Astro 5 Two panels. In Astro 4 the config lived in src/content/config.ts, collections declared type content, entries had a slug, and pages called entry.render. In Astro 5 the config lives in src/content.config.ts, collections declare a glob loader with a base directory, entries have an id, and pages call render of entry imported from astro content. The same collection in Astro 4 and Astro 5 Astro 4 src/content/config.ts type: 'content' entry.slug entry.render() legacy mode in Astro 5 Astro 5 src/content.config.ts loader: glob({ base }) entry.id render(entry) content layer
The URL-facing values stay identical: a file's id in Astro 5 equals its slug in Astro 4.

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

MeasureAstro 4.16Astro 5.2Tool
HTML files1,6121,612find
URL changes—0diff of URL lists
Cold build2 min 48 s2 min 21 sGitHub Actions
Warm build (cache restored)2 min 40 s1 min 12 sGitHub Actions
Time spent on the upgrade—~3 hoursCommit 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.

Cold and warm build times on Astro 4 and Astro 5 Paired bars. Cold build went from 168 seconds on Astro 4 to 141 seconds on Astro 5. Warm build went from 160 seconds to 72 seconds, because the content layer skips unchanged entries when its cache is restored. Build time before and after the upgrade Astro 4.16 Astro 5.2 Cold build 168 s 141 s Warm build 160 s 72 s
Median of five CI runs on a 4-core GitHub Actions runner, same commit apart from the upgrade.

Pitfalls & Rollback

  • Leaving collections in legacy mode. It builds, but loses the content layer's caching and leaves slug code that breaks later.
  • Search and replace of slug without 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.

Filed underFixesAstro