If you are on Astro 6, the upgrade to Astro 7 is one npx @astrojs/upgrade and a build. If you are still on Astro 5, go through 6 first: 6 removed legacy content collections, Astro.glob() and the old Cloudflare runtime API, and 7 then swaps the compiler and the Markdown pipeline underneath you. Two hops, each with its own set of things to fix.
Astro 6.0 shipped on 10 March 2026 and 7.0 on 22 June 2026, so the two majors landed fourteen weeks apart. As of September 2026 the latest release is 7.3.1 and the @astrojs/upgrade tool only knows dist-tags (latest, beta), not major numbers. Run it from a 5.x project and it takes you straight to 7, which is the wrong move if any of the 6.0 removals apply to you.
Which version am I actually upgrading from?
Check node -v before anything else. Astro 6’s upgrade guide asks for Node 22.12.0 or newer, and the astro@7 package enforces it in engines (>=22.12.0). Node 20 went end-of-life in April 2026, so most hosts have moved on, but a stale .nvmrc or a pinned CI image will fail the install before you see a single Astro error.
Then look at three things in your project.
Your content collections. If src/content/config.ts still exists (rather than src/content.config.ts), if any collection is declared with type: 'content' or type: 'data' and no loader, or if templates call entry.slug or entry.render(), you are on the pre-5 collections API. Astro 5 kept these working silently. Astro 6 does not.
Your adapter. @astrojs/cloudflare had a rewrite at v13 (the Astro 6 pairing) that dropped Cloudflare Pages and removed Astro.locals.runtime. Node, Netlify and Vercel adapters had major bumps too, but nothing on that scale.
Your Markdown config. Every remarkPlugins or rehypePlugins entry needs a decision in the 7 hop.
What breaks going from Astro 5 to 6?
The 6.0 guide lists a long tail of removals; these are the ones that stop builds on real sites.
Legacy content collections are gone. Every collection needs a loader (glob() or file() from astro/loaders, or a custom one), entry.slug becomes entry.id, and entry.render() becomes render(entry) imported from astro:content. getEntryBySlug() and getDataEntryById() are replaced by getEntry(). There is a legacy.collectionsBackwardsCompat flag documented as a temporary helper, and it is fine as a way to get the site building while you migrate, but it is legacy-tier: no improvements, eventual removal. Our earlier post on writing a custom Content Layer loader covers the loader shape if your content comes from an API rather than files.
Astro.glob() is removed. The replacement is Vite’s own glob:
// Before (Astro 5)
const posts = await Astro.glob('./posts/*.md');
// After (Astro 6+)
const posts = Object.values(import.meta.glob('./posts/*.md', { eager: true }));
<ViewTransitions /> is removed in favour of <ClientRouter /> from the same astro:transitions module, and the handleForms prop on it went too.
import.meta.env is now always inlined and never type-coerced. In 5.x a variable set to true in .env could arrive as a boolean; in 6 it is the string "true", and non-public variables are no longer swapped for a process.env reference. Read secrets from process.env (or astro:env/server) and compare strings explicitly.
Zod moves to v4, imported from astro/zod. The z export from astro:content and the astro:schema module are gone. Zod 4 changes error customisation from { message } to { error } and deprecates z.string().email() in favour of z.email(), so schema files may need edits beyond the import line.
Smaller ones that still bite: i18n.routing.redirectToDefaultLocale now defaults to false; heading IDs follow github-slugger behaviour, so anchors on headings that end in punctuation change; script and style tags render in source order instead of reversed; .cjs and .cts config files are not supported. Shiki goes to v4.
If you enabled experimental.csp, experimental.fonts, experimental.liveContentCollections, experimental.staticImportMetaEnv or experimental.headingIdCompat in 5.x, delete the flags. CSP moves to security.csp; the others are stable or default.
What changed in the Cloudflare adapter?
@astrojs/cloudflare v13 (peer astro@^6) and v14 (peer astro@^7.2) run your app in workerd for astro dev, prerendering and production. The adapter now targets Workers only; the adapter docs say plainly that Pages is no longer supported for on-demand rendering. If you are still on Pages, our Cloudflare Pages vs Workers comparison covers what the move involves.
The Astro.locals.runtime object is gone. Bindings come from the platform module:
// src/pages/api/visits.ts
import type { APIRoute } from 'astro';
import { env } from 'cloudflare:workers';
export const GET: APIRoute = async ({ request, locals }) => {
const country = request.cf?.country ?? 'unknown';
const count = Number((await env.VISITS.get(country)) ?? 0) + 1;
// was Astro.locals.runtime.ctx.waitUntil(...)
locals.cfContext.waitUntil(env.VISITS.put(country, String(count)));
return Response.json({ country, count });
};
runtime.env becomes env from cloudflare:workers, runtime.cf becomes request.cf, runtime.ctx becomes locals.cfContext, and runtime.caches is just the global caches. Wrangler config becomes optional for simple sites; when you do need one, main points at @astrojs/cloudflare/entrypoints/server and the old workerEntryPoint adapter option is removed. The default image service switched from compile to cloudflare-binding, and session drivers are configured as objects via sessionDrivers rather than strings.
What breaks going from Astro 6 to 7?
The 7.0 guide is shorter, but two of its changes touch every page.
The Rust compiler is now the only compiler, and it is strict. The Go compiler silently closed unclosed tags and reordered invalid nesting (a <div> inside a <p>, say) to match how browsers parse. The Rust one errors on unclosed non-void elements and passes invalid nesting through untouched, leaving the browser to break your layout. Run the build; the errors name the file. Then diff rendered HTML on a few templates to catch nesting the old compiler used to repair for you.
compressHTML defaults to 'jsx'. Whitespace between elements is collapsed the way React does it, so a <span> and an <em> separated by a line break in the template now render with no space between the words. Either add {" "} where the space matters or put compressHTML: true in the config to get the 6.x behaviour back. On a content-heavy site the second option is the safer first step; you can tighten later.
Markdown moves to Sätteri, Astro’s native pipeline, and @astrojs/markdown-remark is no longer installed by default. If you have no remark or rehype plugins, nothing changes: GFM and SmartyPants still apply. If you do, either port them to Sätteri MDAST/HAST plugins or keep unified:
npm install @astrojs/markdown-remark
// astro.config.mjs
import { defineConfig } from 'astro/config';
import { unified } from '@astrojs/markdown-remark';
import remarkToc from 'remark-toc';
export default defineConfig({
markdown: {
processor: unified({ remarkPlugins: [remarkToc] }),
},
});
The top-level markdown.remarkPlugins, rehypePlugins, gfm and smartypants keys still work in 7 but are deprecated, and they now require @astrojs/markdown-remark to be present.
Vite goes to 8, which means Rolldown as the bundler. Most projects will not notice; integrations that reach into Vite internals will. What actually breaks in the Vite 8 move is its own post.
src/fetch.ts is now reserved for advanced routing. Rename an existing file of that name, or set fetchFile: './src/router.ts' (or null to switch the feature off). @astrojs/db is removed outright; the guide points at node:sqlite or Drizzle. And if you were on the 6.x experimental flags for logger, queuedRendering, rustCompiler, advancedRouting, cache or routeRules, move them out of experimental: cache, routeRules and logger become top-level keys, the rest are simply default.
Should I go straight to 7 or stop at 6?
Stop at 6 if any of the 5-to-6 removals apply. Because @astrojs/upgrade resolves against a dist-tag rather than a major, the 6 hop is a manual install:
npm install astro@6 @astrojs/cloudflare@13 @astrojs/mdx@5
npm run build
Pin each official integration to the major that pairs with Astro 6 (npm view @astrojs/mdx@5 peerDependencies.astro tells you in one line), fix the collections, adapter and env changes, ship it, and only then run npx @astrojs/upgrade for 7. Keeping the compiler swap and the whitespace change in a separate commit from the collections rewrite means that when a page looks wrong, you know which change to blame.
Go straight to 7 when you are on 6 already, or when a 5.x site is small, uses Content Layer collections with loaders, has no Cloudflare adapter and no Markdown plugins. That combination hits none of the hard removals, and one upgrade with compressHTML: true set is usually a clean build.
Do not upgrade at all yet if the site depends on @astrojs/db. That is a rewrite of the data layer, and it deserves its own plan before the framework version moves.