skip to content
Gift Egwuenu
Search articles
Back to articles

Migrating My Blog to Astro 6

· 6 min read

Astro 6 just dropped and I couldn’t resist upgrading my blog right away. I’ve been running on Astro 5.16 for a while and the release notes had a few features that caught my eye immediately. Let me walk you through the migration, what stood out, and the features I’m most excited about.

TL;DR

  • Migrated from Astro 5.16 to Astro 6 in about an hour.
  • The biggest wins for me: first-class Cloudflare support (migrated from Pages to Workers and consolidated a standalone likes API into the main codebase) and the built-in Fonts API (goodbye render-blocking Google Fonts imports).
  • Live Content Collections are now stable, bringing request-time content fetching to Astro’s content layer.
  • Zod now imports from astro/zod instead of astro:content.
  • Enabled experimental queued rendering for faster builds.
  • Zero errors on the first build. Smoothest major version upgrade I’ve done with Astro.

The Features I’m Loving

First-Class Cloudflare Support

This is the big one for me. I was previously deploying my blog to Cloudflare Pages with the old @astrojs/cloudflare adapter. It worked, but the dev experience had rough edges: the adapter didn’t run workerd locally, so I was always testing against Node.js and hoping nothing broke in production. I also had a per-post like counter running as a separate Worker with its own wrangler config and deployment pipeline. It was a small API, but maintaining it as a standalone project just to increment a number felt like overkill.

With Astro 6, the rebuilt adapter now runs workerd at every stage: dev, prerender, and production. That gave me the confidence to migrate the blog from Pages to Workers. I consolidated that standalone likes API directly into the Astro project. One codebase, one deployment, full access to bindings during development. It’s the setup I always wanted.

Built-in Fonts API

This is the other feature I was really excited about. Before Astro 6, I was loading Inter and JetBrains Mono via Google Fonts @import in my CSS. This is render-blocking, sends user data to Google, and honestly just feels wrong for a static site that cares about performance.

Now I just configure fonts directly in astro.config.ts:

import { defineConfig, fontProviders } from "astro/config";

export default defineConfig({
	fonts: [
		{
			provider: fontProviders.google(),
			name: "Inter",
			cssVariable: "--font-inter",
			weights: [400, 500, 600, 700],
			styles: ["normal"],
			fallbacks: ["sans-serif"],
		},
		{
			provider: fontProviders.google(),
			name: "JetBrains Mono",
			cssVariable: "--font-jetbrains-mono",
			weights: [400, 500],
			styles: ["normal"],
			fallbacks: ["monospace"],
		},
	],
});

Then add a <Font /> component in my layout’s <head>:

---
import { Font } from "astro:assets";
---

<head>
	<Font cssVariable="--font-inter" preload />
	<Font cssVariable="--font-jetbrains-mono" />
</head>

Astro downloads the fonts at build time, generates optimized fallbacks, and serves them from my domain. No more third-party requests. No more render-blocking imports. The fonts are just there, and they’re fast.

Experimental Queued Rendering

My blog has 25+ posts with OG image generation for each one, so build performance matters. The new queued rendering replaces Astro’s recursive rendering with a two-pass approach: first traverse the component tree, then render it in order. Early benchmarks show up to 2x faster rendering.

Enabling it is just a config flag:

export default defineConfig({
	experimental: {
		queuedRendering: {
			enabled: true,
		},
	},
});

I’ve enabled this and I’m curious to see how it performs as I add more content.

Live Content Collections

Live Content Collections are now stable in Astro 6. Content collections have always required a rebuild when content changed. Live collections flip that: they fetch content at request time using defineLiveCollection() and getLiveEntry(), with no rebuild needed. Your content updates the moment it’s published.

You define a live collection in src/live.config.ts:

import { defineLiveCollection } from "astro:content";
import { z } from "astro/zod";
import { cmsLoader } from "./loaders/my-cms";

const updates = defineLiveCollection({
	loader: cmsLoader({ apiKey: process.env.MY_API_KEY }),
	schema: z.object({
		slug: z.string(),
		title: z.string(),
		excerpt: z.string(),
		publishedAt: z.coerce.date(),
	}),
});

export const collections = { updates };

Then query it in your page with built-in error handling:

---
import { getLiveEntry } from "astro:content";

const { entry: update, error } = await getLiveEntry("updates", Astro.params.slug);

if (error || !update) {
	return Astro.redirect("/404");
}
---

<h1>{update.data.title}</h1>
<p>{update.data.excerpt}</p>
<time>{update.data.publishedAt.toDateString()}</time>

I’m not using this yet since my blog posts are all Markdown files in the repo, but now that I’m running on Workers with full binding access, I can see pairing this with a CMS or D1-backed content source down the line. The fact that live and build-time collections use the same APIs (getCollection(), getEntry(), schemas, loaders) makes it easy to adopt incrementally.

Zod 4

The Zod upgrade is mostly invisible if your schemas are straightforward. The main change is where you import it from. Instead of importing z from astro:content, you now import it from astro/zod:

import { defineCollection } from "astro:content";
import { z } from "astro/zod";

If you’re using .default() with .transform(), check the Zod 4 changelog because the behavior around default values changed.

How I Migrated

The actual migration took about an hour. Here’s the rough process:

  1. Created a branch. Always migrate on a separate branch.
  2. Ran pnpm dlx @astrojs/upgrade. This handles the package version bumps automatically.
  3. Updated Zod imports. z from astro/zod instead of astro:content.
  4. Migrated fonts. Removed Google Fonts @import, configured the new Fonts API, added <Font /> to my layout.
  5. Migrated from Pages to Workers. Switched to the rebuilt @astrojs/cloudflare adapter and consolidated my standalone likes API Worker into the Astro project.
  6. Enabled experimental features. Queued rendering for faster builds.
  7. Built and tested. pnpm build + pnpm check, zero errors on the first try.

What I’m Planning Next

Now that I’m on Astro 6, there are a few things I want to explore:

  • Live Content Collections with a CMS. Now that I’m on Workers with full binding access, I want to pair live collections with a headless CMS so content updates go live without a rebuild.
  • Responsive images. Astro’s image handling keeps getting better and I’m not using srcset/sizes anywhere yet.
  • View Transitions. I’ve been putting this off, but Astro’s <ClientRouter /> has matured a lot since it was introduced.
  • Tailwind v4. The @astrojs/tailwind integration works fine for now, but Tailwind v4 with its native Vite plugin is the future.

If you’re still on Astro 5, I’d recommend giving the upgrade a try. The migration path is smooth and the new features are worth it. Have you upgraded yet? I’d love to hear how it went.

Resources