My site was on Astro 4.4.13. Astro 7.1.5 just dropped. That’s three major versions worth of changes — the Rust compiler, Sätteri markdown, Vite 8 with Rolldown, queued rendering, advanced routing, route caching, and AI dev server enhancements.
I bit the bullet and upgraded. Here’s what actually happened — including the stuff the migration guide doesn’t mention.
Before: What I Was Running
{
"astro": "^4.4.13",
"@astrojs/mdx": "^2.3.1",
"@astrojs/sitemap": "3.1.6",
"@astrojs/solid-js": "^4.0.1",
"@astrojs/tailwind": "^5.1.0",
"@astrojs/prism": "^2.2.6",
"remark-math": "^6.0.0",
"rehype-katex": "^7.0.1"
}
A typical Astro 4 static blog: MDX, SolidJS for interactive bits, Tailwind, sitemap, math rendering via KaTeX, Prism for syntax highlighting. Nothing exotic.
My config:
import { defineConfig } from "astro/config"
import mdx from "@astrojs/mdx"
import sitemap from "@astrojs/sitemap"
import tailwind from "@astrojs/tailwind"
import solidJs from "@astrojs/solid-js"
import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
export default defineConfig({
site: "https://rioges.xyz",
integrations: [mdx(), sitemap(), solidJs(), tailwind({ applyBaseStyles: false })],
markdown: {
shikiConfig: { theme: 'dracula' },
syntaxHighlight: "prism",
remarkPlugins: [remarkMath],
rehypePlugins: [rehypeKatex],
},
})
And content collections in src/content/config.ts using type: "content":
const blog = defineCollection({
type: "content",
schema: ({ image }) => z.object({
title: z.string(),
summary: z.string(),
date: z.coerce.date(),
tags: z.array(z.string()),
draft: z.boolean().optional(),
image: image().optional(),
}),
})
The Upgrade
Run the official upgrade tool:
npx @astrojs/upgrade
It bumps Astro and all official integrations in one shot. Then check what broke. Spoiler: quite a bit.
What Actually Broke
1. Markdown Pipeline: remark-math + rehype-katex → Sätteri
This was the biggest change — and the one that took the most debugging. Astro 7 replaces the entire unified (remark/rehype) pipeline with Sätteri, a Rust-based processor. Math rendering is now built-in.
Before (Astro 4):
markdown: {
remarkPlugins: [remarkMath],
rehypePlugins: [rehypeKatex],
}
After (Astro 7):
import { satteri } from '@astrojs/markdown-satteri'
markdown: {
processor: satteri({
features: {
math: true,
},
}),
}
Two npm packages (remark-math and rehype-katex) replaced by one config flag. Sätteri handles GFM tables, footnotes, smart punctuation, heading IDs, and math natively — no separate plugins needed.
But here’s the catch the migration guide doesn’t mention: Sätteri’s math mode uses \(...\) and \[...\] delimiters, not $...$ and $$...$$. My existing posts used $\sqrt{n}$ for inline math. Sätteri wrapped those in <code class="language-math"> blocks instead of rendering them as KaTeX.
The fix was straightforward — change the delimiters:
<!-- Before -->
Use $\sqrt{n}$ as your chunk size.
<!-- After -->
Use \(\sqrt{n}\) as your chunk size.
Only three inline expressions in my entire blog needed changing. Not a big deal, but something to watch for if you use dollar-sign math delimiters.
If you depend on remark/rehype plugins that Sätteri doesn’t cover, you can still use the unified pipeline:
import { unified } from '@astrojs/markdown-remark'
markdown: {
processor: unified({
remarkPlugins: [remarkToc],
}),
}
But I tried the unified pipeline with remark-math + rehype-katex as a fallback, and it didn’t render math correctly either — expressions still came out as code blocks. Sätteri’s native math with \(...\) delimiters was the only path that worked.
The build speed difference is real. On my site with 15+ posts, the markdown phase went from feeling like a coffee break to basically instant. The Astro team benchmarks show 15–61% overall build improvements, and Markdown-heavy sites see the biggest gains.
2. Content Collections: type: "content" → loader
Astro 5 introduced content layer APIs, and Astro 7 expects the new format. The type: "content" syntax still works but triggers deprecation warnings. The new way uses loader.
Old (src/content/config.ts):
const blog = defineCollection({
type: "content",
schema: ({ image }) => z.object({ /* ... */ }),
})
New (src/content.config.ts — note the file moved):
import { glob } from 'astro/loaders'
const blog = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
schema: ({ image }) => z.object({ /* ... */ }),
})
I went ahead and migrated all collections to the loader: glob() format. It’s the future, and astro check was noisy about the deprecation warnings.
Two more breaking changes from this migration:
-
.slug→.id: Content entries no longer have.slug. Use.idinstead. I had to update 7 files — blog posts, project pages, legal pages, RSS, and the ArrowCard component. -
.render()→render(): Theentry.render()method is now a standalone import:
// Before
const { Content } = await entry.render()
// After
import { render } from 'astro:content'
const { Content } = await render(entry)
3. SolidJS Integration
The @astrojs/solid-js integration works the same, but you need the version that’s compatible with Astro 7. The upgrade tool handled this automatically. No code changes needed.
4. The Rust Compiler Catches What Go Missed
Astro 7’s Rust compiler is stricter about markup. It caught a mismatched </div> in my blog index that the old Go compiler silently ignored. The template rendered fine before, but it was technically invalid HTML.
This is a good thing — but it means your first build after upgrading might fail on errors that were previously silent.
5. Node 24 ESM Strict Mode Bugs
This one took debugging. Astro 7.1.5 has two dependencies (cookie and neotraverse) that fail under Node 24’s stricter ESM resolution. The symptoms are cryptic build errors about missing exports.
The fix is adding them as direct dependencies so pnpm hoists them properly:
pnpm add cookie@1.1.1 neotraverse@1.0.1
And adding Vite config to handle them:
// astro.config.mjs
export default defineConfig({
vite: {
ssr: { noExternal: ['cookie'] },
optimizeDeps: { include: ['cookie'] },
},
})
These are known upstream issues that should be fixed in future Astro releases.
6. JSX Whitespace Handling
The new Rust compiler treats whitespace like JSX — newlines between inline elements no longer produce visible spaces. If you had:
<span>Hello</span>
<span>World</span>
This used to render as “Hello World” (with a space). Now it renders as “HelloWorld”. Add {' '} between them if you want the space:
<span>Hello</span>{' '}<span>World</span>
I didn’t hit this in my blog posts (markdown handles its own whitespace), but it’s worth checking if you have inline elements in .astro components.
7. The astro.config.mjs Cleanup
After all the changes, my final config:
import { defineConfig } from "astro/config"
import mdx from "@astrojs/mdx"
import sitemap from "@astrojs/sitemap"
import tailwind from "@astrojs/tailwind"
import solidJs from "@astrojs/solid-js"
import { satteri } from '@astrojs/markdown-satteri'
export default defineConfig({
site: "https://rioges.xyz",
integrations: [mdx(), sitemap(), solidJs(), tailwind({ applyBaseStyles: false })],
markdown: {
processor: satteri({
features: {
math: true,
},
}),
},
vite: {
ssr: { noExternal: ['cookie'] },
optimizeDeps: { include: ['cookie'] },
},
})
Cleaner than before. No more remarkPlugins or rehypePlugins arrays. No more syntaxHighlight: "prism" or shikiConfig (Sätteri handles it). The only addition is the Vite config for the Node 24 ESM workaround.
Also removed: @astrojs/prism (Sätteri handles syntax highlighting), remark-math, and rehype-katex (Sätteri handles math).
8. Tailwind v3 Still Works
Astro 7 ships with Vite 8, which uses Rolldown. If you’re on Tailwind v3 (I am), it still works. The @astrojs/tailwind integration is in maintenance mode — Tailwind v4 has its own Vite plugin — but it works fine.
For now, I’m staying on Tailwind v3. When I’m ready for v4:
pnpm add tailwindcss @tailwindcss/vite
Then swap the Astro integration for Tailwind’s own Vite plugin.
What Didn’t Break
Things I expected to break but didn’t:
- SolidJS islands — zero changes
- Sitemap integration — just a version bump
- MDX posts — all rendered correctly with Sätteri
- YouTube embeds (
@astro-community/astro-embed-youtube) — works fine - Docker deployment — no changes needed
- Nginx config — still serving the built static files the same way
Well, mostly. I did find a try_files bug in my nginx config after deploying, but it was pre-existing — try_files $uri $uri/ =404 doesn’t resolve /blog/ to /blog/index.html. Fixed it to try_files $uri $uri/ $uri/index.html =404.
Build Performance
On my 15-post blog, the numbers aren’t dramatic (it’s small). But the Markdown processing feels instant now, and the Rust compiler catches template errors that the old Go compiler silently ignored. That alone is worth the upgrade — no more mystery markup reordering.
The New Stuff Worth Noting
Even though I didn’t need them for this upgrade, these Astro 7 features are worth knowing about:
Advanced Routing (src/fetch.ts) — You can now add a fetch handler that runs before Astro’s router. Think Cloudflare Workers-style request pipeline. Great for auth, API proxying, and logging without middleware hacks.
Route Caching — Stabilized in v7. Set maxAge and swr per route, and Astro handles the rest. CDN providers for Netlify, Vercel, and Cloudflare are experimental. For a static blog this doesn’t matter much, but for on-demand rendered pages it’s a game-changer.
AI Enhancements — The dev server can now detect coding agents and output structured JSON logs. If you’re using Cursor, Copilot, or (in my case) Pi, the dev server plays nicer with agent-driven workflows.
Background Dev Server — astro dev can run in the background, which pairs well with the AI enhancements above.
The Full Migration Checklist
If you’re on Astro 4 and want to upgrade to 7:
- Run
npx @astrojs/upgrade— bumps all official packages - Move remark/rehype plugins to Sätteri — check if Sätteri’s built-in features cover your needs first
- Change math delimiters —
$...$→\(...\)and$$...$$→\[...\]for Sätteri’s math mode - Migrate content collections —
type: "content"→loader: glob(), move config file tosrc/content.config.ts - Replace
.slugwith.id— across all templates and components - Replace
.render()withrender()— import fromastro:content - Remove
syntaxHighlight: "prism"andshikiConfig— Sätteri handles this - Remove
@astrojs/prism— no longer needed - Add
cookie@1.1.1andneotraverse@1.0.1— Node 24 ESM workaround (temporary) - Add Vite SSR/optimizeDeps config for
cookie— see example above - Check JSX whitespace — look for inline elements in
.astrofiles - Run
astro check— catches type errors from schema changes - Build and visually check — the Rust compiler is stricter about markup
Would I Do It Again?
Yes. The build is faster (31 pages in ~2s), the markdown pipeline is simpler, and the stricter compiler caught markup issues I didn’t know I had. The migration from Astro 4 → 7 was mostly about the markdown pipeline change and content collection updates. Everything else was either automatic or a non-breaking improvement.
If you’re on Astro 5 or 6, the jump is even easier — you’re already past the content layer migration. If you’re on Astro 4 like I was, budget a couple hours for the Sätteri migration, content collection updates, and the math delimiter change, and you’re good.
Lighthouse scores after upgrade: Performance 83, Accessibility 100, Best Practices 100, SEO 100. No regression from the Astro 4 baseline.