Astro 6 dropped a few weeks ago and I finally got around to upgrading this blog. The short version: it went smoother than I expected, but there were a few spots where I had to stop and think.
This is a real-world migration report from an AstroPaper v5.5.1 blog running on Bun, deployed to Cloudflare Pages. Not a theoretical overview of the changelog. If you’re running a similar setup, this should save you some time.
Table of contents
Open Table of contents
What Astro 6 actually ships
The headline features:
- Vite 7 replaces Vite 6. Faster builds, better module resolution, and it fixes a real compatibility issue I’d been working around (more on that below).
- Zod 4 replaces Zod 3. Smaller bundles, faster type checking. The Zod team rewrote the internals from scratch.
- Shiki 4 for syntax highlighting. If you’re using astro-expressive-code like I am, you probably won’t notice the difference. It just works.
- Stable fonts API. This was
experimental.fontsin Astro 5. Now it lives at the top level of your config. - Stable
preserveScriptOrder. Same deal. Was experimental, now it’s the default behavior. You don’t need to opt in anymore.
None of these are flashy “look at this new component” features. They’re infrastructure improvements. Faster builds, smaller output, fewer workarounds. The kind of stuff that makes your day slightly less annoying.
The migration, step by step
Bumping dependencies
First, the obvious part. I updated astro from 5.18.1 to 6.1.9 and @astrojs/preact from 4.1.3 to 5.1.2, plus about eleven other minor and patch updates.
If you’re using Bun, bun update handles most of it. Check your package.json afterward to make sure nothing weird happened with version ranges.
Removing the experimental block
In Astro 5, my config had an experimental section with two entries:
export default defineConfig({ // ...other config experimental: { preserveScriptOrder: true, fonts: [ { name: "Atkinson Hyperlegible", cssVariable: "--font-atkinson", provider: fontProviders.fontsource(), fallbacks: ["sans-serif"], weights: [400, 700], styles: ["normal", "italic"], }, // ... ], },});Astro 6 will yell at you if you leave this in:
[config] Astro found issue(s) with your configuration:! Invalid or outdated experimental feature.The fix is to move fonts to the top level and delete preserveScriptOrder entirely (it’s now the default):
export default defineConfig({ // ...other config fonts: [ { name: "Atkinson Hyperlegible", cssVariable: "--font-atkinson", provider: fontProviders.fontsource(), fallbacks: ["sans-serif"], weights: [400, 700], styles: ["normal", "italic"], }, { name: "JetBrains Mono", cssVariable: "--font-display", provider: fontProviders.fontsource(), fallbacks: ["monospace"], weights: [400, 700], styles: ["normal"], }, ],});Removing the Vite 7 workaround
This one felt good. In Astro 5, the @tailwindcss/vite plugin had a type mismatch with Astro’s bundled Vite version. I had a block that looked like this:
vite: { // eslint-disable-next-line // @ts-ignore // This will be fixed in Astro 6 with Vite 7 support // See: https://github.com/withastro/astro/issues/14030 plugins: [tailwindcss()],},Four lines of workaround for one plugin call. Astro 6 ships Vite 7, which resolves the type incompatibility (#14030). Now it’s just:
vite: { plugins: [tailwindcss()],},Migrating to Zod 4
This is where I spent the most time debugging. Astro 6 bundles Zod 4, and the import path changes.
Before, I was importing z from astro:content:
import { defineCollection, z } from "astro:content";
const baseSchema = ({ image }: { image: () => z.ZodType }) => z.object({ // ...fields ogImage: image().or(z.string()).optional(), });In Astro 6, z moves to astro/zod:
import { defineCollection, type SchemaContext } from "astro:content";import { z } from "astro/zod";
const baseSchema = ({ image }: SchemaContext) => z.object({ // ...fields ogImage: image().or(z.string()).optional(), });Two changes here. First, z comes from a different package. If you forget, you’ll get:
src/content.config.ts:42:48 - error ts(2503): Cannot find namespace 'z'.Second, and this one tripped me up: the type annotation for the schema function parameter. I was using { image: () => z.ZodType } as a manual type, which worked fine in Zod 3. In Zod 4, that generic type loses the property definitions on the image helper. Concretely, I started getting errors like:
src/pages/links/[...slug]/index.astro:54:25error ts(2339): Property 'src' does not exist on type '{}'.The ogImage field was being typed as an empty object instead of an actual image with src, width, height, etc. The fix was to use Astro’s built-in SchemaContext type, which provides the correct ImageFunction typing:
const baseSchema = ({ image }: SchemaContext) =>Same change needed for any other schema functions that use the image helper. In my case, linksSchema had the same pattern.
Zod 4 deprecation warnings
After fixing the import and types, the build passed with zero errors. But there are deprecation warnings:
warning ts(6385): 'z.string().datetime()' is deprecated.Same for .url(). Both still work in Zod 4 but they’ll eventually go away. I left them as-is for now since they’re functional and the replacement patterns aren’t fully settled yet. Something to revisit later.
Breaking changes worth knowing about
Even if you don’t hit all of these, read through the list so you’re not surprised later.
Legacy content collections are gone
If you’re still using the old src/content/ directory with config.ts (the pre-Content Layer API setup), Astro 6 removes support for it completely. You need the glob/file loader pattern in src/content.config.ts. My blog was already using the new pattern, so this was a non-issue.
ViewTransitions component removed
The <ViewTransitions /> component from astro:transitions is gone. Use <ClientRouter /> instead. AstroPaper doesn’t use view transitions, so I didn’t need to change anything here.
Image cropping and upscaling defaults changed
Astro 6 changes the default behavior for image processing. If you were relying on specific cropping or upscaling behavior without explicit options, check your images after upgrading. I didn’t notice any visual differences on my blog, but YMMV depending on how you use <Image /> and getImage().
Script and style ordering
preserveScriptOrder is now the default. If you were relying on the old (nondeterministic) ordering for some reason, you’ll need to adjust. For most people, this is an improvement.
import.meta.env always inlined
Environment variables accessed via import.meta.env are now always inlined at build time. This was already the common case, but if you had code that expected runtime resolution of import.meta.env values, it won’t work anymore.
getStaticPaths params must be strings
If you’re passing numbers or other types as route params from getStaticPaths, they need to be strings now. This was already a best practice but is now enforced.
Was it worth it?
Yes. The build got faster (Vite 7 is doing real work there), I deleted four lines of workaround code that I never liked, and the fonts config is no longer behind an experimental flag. Nothing broke in production.
The Zod 4 migration was the only part that required actual debugging. If you’re using SchemaContext already, you’ll skip that entirely. If you’re manually typing your schema functions like I was, budget an extra ten minutes.
The official migration guide covers everything I mentioned here and more. Read it before you start. Don’t just bump the version and hope for the best.