Skip to content

Upgrade to Astro v6

This guide will help you migrate from Astro v5 to Astro v6.

Need to upgrade an older project to v5 first? See our older migration guide.

Need to see the v5 docs? Visit this older version of the docs site (unmaintained v5.xx snapshot).

Update your project’s version of Astro to the latest version using your package manager:

Terminal window
# Upgrade Astro and official integrations together
npx @astrojs/upgrade

You can also upgrade your Astro integrations manually if needed, and you may also need to upgrade other dependencies in your project.

Astro v6.0 includes potentially breaking changes, as well as the removal and deprecation of some features.

If your project doesn’t work as expected after upgrading to v6.0, check this guide for an overview of all breaking changes and instructions on how to update your codebase.

See the Astro changelog for full release notes.

Any major upgrades to Astro’s dependencies may cause breaking changes in your project.

Node 18 reached its End of Life in March 2025 and Node 20 is scheduled to reach its End of Life in April 2026.

Astro v6.0 drops Node 18 and Node 20 support entirely so that all Astro users can take advantage of Node’s more modern features.

Check that both your development environment and your deployment environment are using Node 22.12.0 or higher.

  1. Check your local version of Node using:

    Terminal window
    node -v
  2. Check your deployment environment’s own documentation to verify that they support Node 22.

    You can specify Node 22.12.0 for your Astro project either in a dashboard configuration setting or a .nvmrc file.

    .nvmrc
    22.12.0

Astro v6.0 upgrades to Vite v7.0 as the development server and production bundler.

If you are using Vite-specific plugins, configuration, or APIs, check the Vite migration guide for their breaking changes and upgrade your project as needed.

Astro v6.0 introduces significant changes to how Astro manages different runtime environments (client, server, and prerender) after an internal refactor to use Vite’s new Environments API.

Integration and adapter maintainers should pay special attention to changes affecting these parts of the Integration API and Adapter API (full details included below with other breaking changes to these APIs):

Astro v6.0 upgrades to Zod 4, a major dependency update that may require changes to custom Zod schemas in your project.

If you have custom Zod schemas in your content.config.ts or other configuration files, you’ll need to update them for Zod 4. Refer to the Zod migration guide for detailed changes in the Zod API.

Notably, many string() formats have been deprecated (e.g. z.string().email(), z.string.url()), and their APIs have been moved to the top-level z namespace. You may need to update how you validate form input for your Astro Actions:

src/actions/index.ts
email: z.string().email(),
email: z.email(),

Additionally, Zod has made some changes to handling error messages and has dropped support for a custom errorsMap which was useful to redefine or translate your error messages. You may need to update any custom error messages:

src/actions/index.ts
z.string().min(5, { message: "Too short." });
z.string().min(5, { error: "Too short." });

Also, if you use .default() with transforms, you may need to update your schemas. In Zod 4, default values must match the output type (after transforms), not the input type. The default value short-circuits parsing when the input is undefined:

src/content.config.ts
import { z } from 'astro/zod';
const blog = defineCollection({
schema: z.object({
// Zod 3: default matched input type (string)
views: z.string().transform(Number).default("0"),
// Zod 4: default must match output type (number)
views: z.string().transform(Number).default(0),
})
});

For the old behavior where defaults are parsed, use the new .prefault() method.

These are only some of the many changes upgrading from Zod 3 to Zod 4. If you encounter any issues with your Zod schemas after upgrading to Astro 6, please consult the Zod 4 changelog for complete upgrade guidance.

You can ensure you’re the same version of Zod that Astro uses internally by importing Zod from astro/zod.

import { z } from 'astro/zod';
See more about the astro/zod module.

The following features are now considered legacy features. They should function normally but are no longer recommended and are in maintenance mode. They will see no future improvements and documentation will not be updated. These features will eventually be deprecated, and then removed entirely.

The following deprecated features are no longer supported and are no longer documented. Please update your project accordingly.

Some deprecated features may temporarily continue to function until they are completely removed. Others may silently have no effect, or throw an error prompting you to update your code.

In Astro 5.x, it was possible to access an Astro object inside getStaticPaths(). However, despite being typed the same as the Astro object accessible in the frontmatter, this object only had site and generator properties. This could lead to confusion about which Astro object properties were available inside getStaticPaths().

Astro 6.0 deprecates this object for getStaticPaths() to avoid confusion and improves error handling when attempting to access Astro values that are unavailable. Using Astro.site or Astro.generator within getStaticPaths() will now log a deprecation warning, and accessing any other property will throw a specific error with a helpful message. In a future major version, this object will be removed entirely, and accessing Astro.site or Astro.generator will also throw an error.

Update your getStaticPaths() function if you were attempting to access any Astro properties inside its scope. Remove Astro.generator entirely, and replace all occurrences of Astro.site() with import.meta.env.SITE:

src/pages/blog/[slug].astro
---
import { getPages } from "../../../utils/data";
export async function getStaticPaths() {
console.log(Astro.generator);
return getPages(Astro.site);
return getPages(import.meta.env.SITE);
}
---

In Astro 5.x, it was possible to access build.assetsPrefix in your Astro config via the built-in environment variable import.meta.env.ASSETS_PREFIX. However, Astro v5.7.0 introduced the astro:config virtual model to expose a non-exhaustive, serializable, type-safe version of the Astro configuration which included access to build.assetsPrefix directly. This became the preferred way to access the prefix for Astro-generated asset links when set, although the environment variable still existed.

Astro 6.0 deprecates this variable in favor of build.assetsPrefix from the astro:config/server module.

Replace any occurances of import.meta.env.ASSETS_PREFIX with the build.assetsPrefix import from astro:config/server. This is a drop-in replacement to provide the existing value, and no other changes to your code should be necessary:

import { someLogic } from "./utils"
import { build } from "astro:config/server"
someLogic(import.meta.env.ASSETS_PREFIX)
someLogic(build.assetsPrefix)
Read more about the astro:config virtual modules.

Deprecated: astro:schema and z from astro:content

Section titled “Deprecated: astro:schema and z from astro:content”

In Astro 5.x, astro:schema was introduced as an alias of astro/zod. z was also exported from astro:content for convenience. However this occasionally created confusion for users who were unsure about where they should be importing from.

Astro 6.0 deprecates astro:schema and z from astro:content in favor of astro/zod.

Replace any occurrences of astro:schema with astro/zod:

import { z } from "astro:schema"
import { z } from "astro/zod"

Remove z from your astro:content imports and import z separately from astro/zod instead:

src/content.config.ts
import { defineCollection, z } from "astro:content"
import { defineCollection } from "astro:content"
import { z } from "astro/zod"

Deprecated: exposed astro:transitions internals

Section titled “Deprecated: exposed astro:transitions internals”

In Astro 5.x, some internals were exported from astro:transitions and astro:transitions/client that were not meant to be exposed for public use.

Astro 6.0 removes the following functions and types as exports from the astro:transitions and astro:transitions/client virtual modules. These can no longer be imported in your project files:

  • createAnimationScope()
  • isTransitionBeforePreparationEvent()
  • isTransitionBeforeSwapEvent()
  • TRANSITION_BEFORE_PREPARATION
  • TRANSITION_AFTER_PREPARATION
  • TRANSITION_BEFORE_SWAP
  • TRANSITION_AFTER_SWAP
  • TRANSITION_PAGE_LOAD

Remove any occurrences of createAnimationScope():

import { createAnimationScope } from 'astro:transitions';

Update any occurrences of the other deprecated exports:

import {
isTransitionBeforePreparationEvent,
TRANSITION_AFTER_SWAP,
} from 'astro:transitions/client';
console.log(isTransitionBeforePreparationEvent(event));
console.log(event.type === 'astro:before-preparation');
console.log(TRANSITION_AFTER_SWAP);
console.log('astro:after-swap');
Learn more about all utilities available in the View Transitions Router API Reference.

The following features have now been entirely removed from the code base and can no longer be used. Some of these features may have continued to work in your project even after deprecation. Others may have silently had no effect.

Projects now containing these removed features will be unable to build, and there will no longer be any supporting documentation prompting you to remove these features.

In Astro 5.x, it was still possible to use the original Content Collections API first introduced in Astro v2.0, either through a legacy configuration flag or via built-in backwards compatibility. These methods allowed you to upgrade to Astro v5 even if you were not yet ready or able to update your existing content collections to those powered by the new Content Layer API.

**Astro v6.0 removes this previously deprecated Content Collections API support entirely, including the legacy.collections flag and some existing backwards compatibility that was not previously behind a flag. All content collections must now use the Content Layer API introduced in Astro v5.0 that powers all content collections. No backwards compatibility support is available.

If you had previously enabled the legacy flag, you must remove it.

astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
legacy: {
collections: true,
}
})

Additionally, if you did not upgrade your collections for Astro v5.0, ensure that your content collections are fully updated for the new API.

Astro v5.x included some automatic backwards compatibility to allow content collections to continue to work even if they had not been updated to use the new API. Therefore, your v5 collections may contain one or more legacy features that need updating to the newer API for v6, even if your project was previously error-free.

If you have content collections errors or warnings after upgrading to v6, use the following list to help you identify and upgrade any legacy features that may exist in your code.

no content collections configuration file Create src/content.config.ts and define your collections in it.

a configuration file located at src/content/config.ts / (LegacyContentConfigError) Rename and move this file to src/content.config.ts

a collection that does not define a loader/ (ContentCollectionMissingALoaderError)

Import Astro’s built-in glob() loader and define the pattern and base for your collection entries:

src/content.config.ts
import { defineCollection } from 'astro:content';
import { z } from 'astro/zod';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: "./src/data/blog" }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
}),
});

a collection that defines a collection type (type: 'content' or type: 'data') / (ContentCollectionInvalidTypeError) There are no longer different types of collections. This must be deleted from your collection definition.

src/content.config.ts
import { defineCollection } from 'astro:content';
import { z } from 'astro/zod';
import { glob } from 'astro/loaders';
const blog = defineCollection({
// For content layer you no longer define a `type`
type: 'content',
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: "./src/data/blog" }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
}),
});

legacy collection querying methods getDataEntryById() and getEntryBySlug() / (GetEntryDeprecationError) Replace both methods with getEntry().

legacy collection querying and rendering methods that depend on a slug property / (ContentSchemaContainsSlugError) Previously, the id was based on the filename, and there was a slug property that could be used in a URL. Now the CollectionEntry id is a slug. If you need access to the filename (previously available as the id), use the filePath property. Replace instances of slug with id:

src/pages/[slug].astro
---
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.slug },
params: { slug: post.id },
props: post,
}));
}
---

content rendered using entry.render() Collection entries no longer have a render() method. Instead, import the render() function from astro:content and use render(entry):

src/pages/index.astro
---
import { getEntry, render } from 'astro:content';
const post = await getEntry('pages', 'homepage');
const { Content, headings } = await post.render();
const { Content, headings } = await render(post);
---
<Content />
See the Astro v5 upgrade guide for previous guidance about backwards compatibility of legacy collections in Astro v5 and full step-by-step instructions for upgrading legacy collections to the new Content Layer API.

In Astro 5.0, the <ViewTransitions /> component was renamed to <ClientRouter /> to clarify the role of the component. The new name makes it more clear that the features you get from Astro’s <ClientRouter /> routing component are slightly different from the native CSS-based MPA router. However, a deprecated version of the <ViewTransitions /> component still existed and may have functioned in Astro 5.x.

Astro 6.0 removes the <ViewTransitions /> component entirely and it can no longer be used in your project. Update to the <ClientRouter /> component to continue to use these features.

Replace all occurrences of the ViewTransitions import and component with ClientRouter:

src/layouts/MyLayout.astro
import { ViewTransitions } from 'astro:transitions';
import { ClientRouter } from 'astro:transitions';
<html>
<head>
...
<ViewTransitions />
<ClientRouter />
</head>
</html>

In Astro 5.6.2, the emitESMImage() function was deprecated in favor of emitImageMetadata(), which removes two deprecated arguments that were not meant to be exposed for public use: _watchMode and experimentalSvgEnabled.

Astro 6.0 removes emitESMImage() entirely. Update to emitImageMetadata() to keep your current behavior.

Replace all occurrences of the emitESMImage() with emitImageMetadata() and remove unused arguments:

import { emitESMImage } from 'astro/assets/utils';
import { emitImageMetadata } from 'astro/assets/utils';
const imageId = '/images/photo.jpg';
const result = await emitESMImage(imageId, false, false);
const result = await emitImageMetadata(imageId);
Read more about emitImageMetadata().

In Astro 5.0, Astro.glob() was deprecated in favor of using getCollection() to query your collections, and import.meta.glob() to query other source files in your project.

Astro 6.0 removes Astro.glob() entirely. Update to import.meta.glob() to keep your current behavior.

Replace all use of Astro.glob() with import.meta.glob(). Note that import.meta.glob() no longer returns a Promise, so you may have to update your code accordingly. You should not require any updates to your glob patterns.

src/pages/blog.astro
---
const posts = await Astro.glob('./posts/*.md');
const posts = Object.values(import.meta.glob('./posts/*.md', { eager: true }));
---
{posts.map((post) => <li><a href={post.url}>{post.frontmatter.title}</a></li>)}

Where appropriate, consider using content collections to organize your content, which has its own newer, more performant querying functions.

You may also wish to consider using glob packages from NPM, such as fast-glob.

In Astro 5.x, some internals were exported from astro:actions that were not meant to be exposed for public use.

Astro 6.0 removes the following functions, classes and types as exports from the astro:actions virtual module. These can no longer be imported in your project files:

  • ACTION_ERROR_CODES
  • ActionInputError
  • appendForwardSlash
  • astroCalledServerError
  • callSafely
  • deserializeActionResult
  • formDataToObject
  • getActionQueryString
  • serializeActionResult
  • type Actions
  • type ActionAccept
  • type AstroActionContext
  • type SerializedActionResult

Replace all imports of serializeActionResult() and deserializeActionResult() with getActionContext(). These two methods are now available through getActionContext():

src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import { serializeActionResult, deserializeActionResult } from 'astro:actions';
import { getActionContext } from 'astro:actions';
export const onRequest = defineMiddleware(async (context, next) => {
const { serializeActionResult, deserializeActionResult } = getActionContext(context);
// ...
});

Remove any occurrences of the other removed exports:

import {
ACTION_ERROR_CODES,
ActionInputError,
appendForwardSlash,
astroCalledServerError,
callSafely,
formDataToObject,
getActionQueryString,
type Actions,
type ActionAccept,
type AstroActionContext,
type SerializedActionResult,
} from 'astro:actions';
Learn more about all utilities available in the Actions API Reference.

In Astro 5.x, it was possible to include a percent-encoded percent sign (%25) in filenames.

Astro 6.0 removes support for the characters %25 in filenames for security reasons. This restriction prevents encoding-based security bypasses where %25 decodes to %, potentially leading to ambiguous or invalid encoding sequences.

If you have route files with %25 in the filename, rename them to use a different character:

Terminal window
src/pages/test%25file.astro
src/pages/test-file.astro

Removed: astro:ssr-manifest virtual module (Integration API)

Section titled “Removed: astro:ssr-manifest virtual module (Integration API)”

In Astro 5.x, the deprecated astro:ssr-manifest virtual module could still be used to access configuration values.

Astro 6.0 removes the astro:ssr-manifest virtual module entirely. It is no longer used by integrations or internally by Astro. The manifest is now passed directly through integration hooks and adapter APIs rather than through a virtual module. For build-specific manifest data, use the astro:build:ssr integration hook, which receives the manifest as a parameter.

If your integration or code imports from astro:ssr-manifest, use astro:config/server instead to access configuration values:

import { manifest } from 'astro:ssr-manifest';
import { srcDir, outDir, root } from 'astro:config/server';
// Use srcDir, outDir, root, etc. for configuration values

Removed: RouteData.generate() (Adapter API)

Section titled “Removed: RouteData.generate() (Adapter API)”

In Astro 5.x, routes could be generated using the generate() method on RouteData.

Astro 6.0 removes RouteData.generate() because route generation is now handled internally by Astro.

Remove any calls to route.generate() in your code. This method is no longer needed:

const generated = route.generate(params);
Learn more about the Adapter API.

Removed: routes on astro:build:done hook (Integration API)

Section titled “Removed: routes on astro:build:done hook (Integration API)”

In Astro 5.0, accessing routes on the astro:build:done hook was deprecated.

Astro 6.0 removes the routes array passed to this hook entirely. Instead, the astro:routes:resolved hook should be used.

Remove any instance of routes passed to astro:build:done and replace it with the new astro:routes:resolved hook. Access distURL on the newly exposed assets map:

my-integration.mjs
const integration = () => {
let routes
return {
name: 'my-integration',
hooks: {
'astro:routes:resolved': (params) => {
routes = params.routes
},
'astro:build:done': ({
routes
assets
}) => {
for (const route of routes) {
const distURL = assets.get(route.pattern)
if (distURL) {
Object.assign(route, { distURL })
}
}
console.log(routes)
}
}
}
}
Learn more about the Integration API astro:routes:resolved hook for building integrations.

Removed: entryPoints on astro:build:ssr hook (Integration API)

Section titled “Removed: entryPoints on astro:build:ssr hook (Integration API)”

In Astro 5.0, functionPerRoute was deprecated. That meant that entryPoints on the astro:build:ssr hook was always empty.

Astro 6.0 removes the entryPoints map passed to this hook entirely.

Remove any instance of entryPoints passed to astro:build:ssr:

my-integration.mjs
const integration = () => {
return {
name: 'my-integration',
hooks: {
'astro:build:ssr': (params) => {
someLogic(params.entryPoints)
},
}
}
}

Removed: old app.render() signature (Adapter API)

Section titled “Removed: old app.render() signature (Adapter API)”

In Astro 4.0, the app.render() signature that allowed passing routeData and locals as optional arguments was deprecated in favor of a single optional renderOptions argument.

Astro 6.0 removes this signature entirely. Attempting to pass these separate arguments will now cause an error in your project.

Review your app.render calls and pass routeData and locals as properties of an object instead of as multiple independent arguments:

my-adapter/entrypoint.ts
app.render(request, routeData, locals)
app.render(request, { routeData, locals })
Learn more about the Adapter API.

Removed: app.setManifestData() (Adapter API)

Section titled “Removed: app.setManifestData() (Adapter API)”

In Astro 5.0, the app.setManifestData() method was available on App and NodeApp, but is no longer used nor needed.

Astro 6.0 removes this method entirely.

Remove any call to app.setManifestData(). If you need to update the manifest, create a new App instance.

Learn more about the Adapter API.

Removed: handleForms prop for the <ClientRouter /> component

Section titled “Removed: handleForms prop for the <ClientRouter /> component”

In Astro 4.0, the handleForms prop of the <ClientRouter /> component was deprecated, as it was no longer necessary to opt in to handling submit events for form elements. This functionality has been built in by default and the property, if still included in your project, silently had no impact on form submission.

Astro 6.0 removes this prop entirely and it now must be removed to avoid errors in your project.

Remove the handleForms property from your <ClientRouter /> component if it exists. It has provided no additional functionality, and so removing it should not change any behavior in your project:

src/pages/index.astro
---
import { ClientRouter } from "astro:transitions";
---
<html>
<head>
<ClientRouter handleForms />
</head>
<body>
<!-- stuff here -->
</body>
</html>
Learn more about transitions with forms.

In Astro 4.8.4, the with option of the programmatic prefetch() function was deprecated in favor of a more sensible default behavior that no longer required specifying the priority of prefetching for each page.

Astro 6.0 removes this option entirely and it is no longer possible to configure the priority of prefetching by passing the with option. Attempting to do so will now cause errors.

By default, Astro’s prefetching now uses an automatic approach that will always try to use <link rel="prefetch> if supported, or will fall back to fetch().

Review your prefetch() calls and remove the with option if it still exists:

prefetch('/about', { with: 'fetch' });
prefetch('/about');
Learn more about prefetching.

In Astro 5.5.6, the ActionAPIContext.rewrite method was deprecated because custom endpoints should be used instead of rewrites.

Astro 6.0 removes the rewrite() method from ActionAPIContext entirely and it may no longer be used.

Review your Actions handlers and remove any call to rewrite():

src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';
export const server = {
getGreeting: defineAction({
input: z.object({
// ...
}),
handler: async (input, context) => {
context.rewrite('/')
// ...
}
})
}
Learn more about rewrites.

Removed: schema function signature (Content Loader API)

Section titled “Removed: schema function signature (Content Loader API)”

In Astro 5.x, a content loader could choose to define a schema as a function instead of defining a Zod schema object for validation. This is useful to dynamically generate the schema based on the configuration options or by introspecting an API.

Astro 6.0 removes this signature and introduces a new createSchema() property as a replacement for those who still want to dynamically define a schema in their content loader.

Providing a schema function in the old way will log a warning message that the loader’s schema is being ignored, but otherwise the loader will continue to work as if no schema had been provided. In a future major version, loaders that provide a schema function will throw an error and cannot be used.

If you are building a content loader and using a function to dynamically return a collection schema property, you must remove your existing function and use the new createSchema() property to define your schema instead.

For example, you can reproduce Astro’s previous behavior by using zod-to-ts directly with createSchema() and any previous function logic:

import type { Loader } from 'astro/loaders'
import { createTypeAlias, zodToTs } from 'zod-to-ts'
import { getSchemaFromApi } from './utils'
function myLoader() {
return {
name: 'my-loader',
load: async (context) => {
// ...
},
schema: async () => await getSchemaFromApi(),
createSchema: async () => {
const schema = await getSchemaFromApi()
const identifier = 'Entry'
const { node } = zodToTs(schema, identifier)
const typeAlias = createTypeAlias(node, identifier)
return {
schema,
types: `export ${typeAlias}`
}
}
} satisfies Loader
}
Learn more about createSchema() in the Content Loader API reference.

Experimental flags allow you to opt in to features while they are in early development. Astro may also use experimental flags to test breaking changes to default behavior. The following experimental flags have been removed in Astro 6.0 and are now stable, or the new default behavior.

Remove these experimental flags from your Astro config if you were previously using them:

astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
experimental: {
liveContentCollections: true,
preserveScriptOrder: true,
staticImportMetaEnv: true,
headingIdCompat: true,
failOnPrerenderConflict: true
},
})
Read about exciting new features and more in the v6.0 Blog post.

Some default behavior has changed in Astro v5.0 and your project code may need updating to account for these changes.

In most cases, the only action needed is to review your existing project’s deployment and ensure that it continues to function as you expect, making updates to your code as necessary. In some cases, there may be a configuration setting to allow you to continue to use the previous default behavior.

Changed: i18n.routing.redirectToDefaultLocale default value

Section titled “Changed: i18n.routing.redirectToDefaultLocale default value”

In Astro v5.0, the i18n.routing.redirectToDefaultLocale default value was true. When combined with the i18n.routing.prefixDefaultLocale default value of false, the resulting redirects could cause infinite loops.

In Astro v6.0, i18n.routing.redirectToDefaultLocale now defaults to false. Additionally, it can now only be used if i18n.routing.prefixDefaultLocale is set to true.

Review your Astro i18n config as you may now need to explicitly set values for redirectToDefaultLocale and prefixDefaultLocale to recreate your project’s previous behavior.

astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
i18n: {
routing: {
prefixDefaultLocale: true,
redirectToDefaultLocale: true
}
}
})

If you are using manual routing, you may also need to update your middleware configuration:

src/middleware.js
import { middleware } from "astro:i18n"; // Astro's own i18n routing config
export const onRequest = middleware({
prefixDefaultLocale: false,
prefixDefaultLocale: true,
redirectToDefaultLocale: true,
})
Learn more about Internationalization routing.

Changed: <script> and <style> tags are rendered in the order they are defined

Section titled “Changed: <script> and <style> tags are rendered in the order they are defined”

In Astro v5.5, the experimental.preserveScriptOrder flag was introduced to render multiple <style> and <script> tags in the same order as they were declared in the source code. Astro 5.x reversed their order in your generated HTML output. This could give unexpected results, for example, CSS styles being overridden by earlier defined style tags when your site was built.

Astro 6.0 removes this experimental flag and makes this the new default behavior in Astro: scripts and styles are now rendered in the order defined in your code.

If you were previously using this experimental feature, you must remove this experimental flag from your configuration as it no longer exists.

Review your <script> and <style> tags to make sure they behave as desired. You may need to reverse their order:

src/components/MyComponent.astro
<p>I am a component</p>
<style>
body {
background: red;
background: yellow;
}
</style>
<style>
body {
background: yellow;
background: red;
}
</style>
<script>
console.log("hello")
console.log("world")
</script>
<script>
console.log("world!")
console.log("hello!")
</script>
Read more about using script and style tags.

The following changes are considered breaking changes in Astro v5.0. Breaking changes may or may not provide temporary backwards compatibility. If you were using these features, you may have to update your code as recommended in each entry.

Changed: endpoints with a file extension cannot be accessed with a trailing slash

Section titled “Changed: endpoints with a file extension cannot be accessed with a trailing slash”

In Astro v5.0, custom endpoints whose URL ended in a file extension (e.g. /src/pages/sitemap.xml.ts ) could be accessed with a trailing slash (/sitemap.xml/) or without (/sitemap.xml), regardless of the value configured for build.trailingSlash.

In Astro v6.0, these endpoints can only be accessed without a trailing slash. This is true regardless of your build.trailingSlash configuration.

Review your links to your custom endpoints that include a file extension in the URL and remove any trailing slashes:

src/pages/index.astro
<a href="/sitemap.xml/">Sitemap</a>
<a href="/sitemap.xml">Sitemap</a>
Learn more about custom endpoints.

Changed: import.meta.env values are always inlined

Section titled “Changed: import.meta.env values are always inlined”

In Astro 5.13, the experimental.staticImportMetaEnv flag was introduced to update the behavior when accessing import.meta.env directly to align with Vite’s handling of environment variables and ensures that import.meta.env values are always inlined.

In Astro 5.x, non-public environment variables were replaced by a reference to process.env. Additionally, Astro could also convert the value type of your environment variables used through import.meta.env, which could prevent access to some values such as the strings "true" (which was converted to a boolean value), and "1" (which was converted to a number).

Astro 6 removes this experimental flag and makes this the new default behavior in Astro: import.meta.env values are always inlined and never coerced.

If you were previously using this experimental feature, you must remove this experimental flag from your configuration as it no longer exists.

If you were relying on coercion, you may need to update your project code to apply it manually:

src/components/MyComponent.astro
const enabled: boolean = import.meta.env.ENABLED;
const enabled: boolean = import.meta.env.ENABLED === "true";

If you were relying on the transformation into process.env, you may need to update your project code to apply it manually:

src/components/MyComponent.astro
const enabled: boolean = import.meta.env.DB_PASSWORD;
const enabled: boolean = process.env.DB_PASSWORD;

You may also need to update types:

src/env.d.ts
interface ImportMetaEnv {
readonly PUBLIC_POKEAPI: string;
readonly DB_PASSWORD: string;
readonly ENABLED: boolean;
readonly ENABLED: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
namespace NodeJS {
interface ProcessEnv {
DB_PASSWORD: string;
}
}

If you need more control over environment variables in Astro, we recommend you use astro:env.

Learn more about environment variables in Astro, including astro:env.

Changed: Cropping by default in default image service

Section titled “Changed: Cropping by default in default image service”

In Astro 5.0, the default image service would only apply cropping when the fit option was provided.

Astro 6.0 applies cropping by default without requiring setting the fit option.

No changes are needed to your existing cropped images as the fit property is still valid. However, if you were previously setting fit to contain (its default value) in order to crop your images, you may now remove this option and still achieve the same cropping behavior by specifying width and height alone:

src/components/MyImage.astro
---
import { Image } from 'astro:assets';
import myImage from '../assets/photo.jpg';
---
<Image src={myImage} width={400} height={300} fit="contain" />
<Image src={myImage} width={400} height={300} />

Changed: Never upscale images in default image service

Section titled “Changed: Never upscale images in default image service”

In Astro 5.0, the default image service would upscale images when the requested dimensions were larger than the source image.

Astro 6.0 removes this behavior: the default image service never upscales images.

Review your images and update dimensions as needed. If you do need to upscale images, you may consider upscaling the images manually or using a custom image service that supports upscaling.

In Astro 5.x, an additional default processing step to Markdown stripped trailing hyphens from the end of IDs for section headings ending in special characters. This provided a cleaner id value, but could lead to incompatibilities rendering your Markdown across platforms.

In Astro 5.5, the experimental.headingIdCompat flag was introduced to allow you to make the IDs generated by Astro for Markdown headings compatible with common platforms like GitHub and npm, using the popular github-slugger package.

Astro 6.0 removes this experimental flag and makes this the new default behavior in Astro: trailing hyphens from the end of IDs for headings ending in special characters are no longer removed.

If you have manual links to headings, you may need to update the anchor link value with a new trailing hyphen. For example, the following Markdown heading:

## `<Picture />`

will now generate the following HTML with a trailing hyphen in the heading id:

<h2 id="picture-"><code>&lt;Picture /&gt;</h2>

and must now be linked to as:

See [the Picture component](/en/guides/images/#picture-) for more details.

If you were previously using the experimental feature to enforce trailing hyphens, you must remove this experimental flag from your configuration as it no longer exists.

If you were previously using the rehypeHeadingIds plugin directly to enforce compatibility, remove the headingIdCompat option as it no longer exists:

astro.config.mjs
import { defineConfig } from 'astro/config';
import { rehypeHeadingIds } from '@astrojs/markdown-remark';
import { otherPluginThatReliesOnHeadingIDs } from 'some/plugin/source';
export default defineConfig({
markdown: {
rehypePlugins: [
[rehypeHeadingIds, { headingIdCompat: true }],
[rehypeHeadingIds],
otherPluginThatReliesOnHeadingIDs,
],
},
});

If you want to keep the old ID generation for backward compatibility reasons, you can create a custom rehype plugin that will generate headings IDs like Astro 5.x. This will allow you to continue to use your existing anchor links without adding trailing hyphens.

Create a custom rehype plugin to strip trailing hyphens
  1. Install required dependencies:

    Terminal window
    npm i github-slugger hast-util-heading-rank unist-util-visit hast-util-to-string
  2. Create a custom rehype plugin that will generate headings IDs like Astro v5:

    plugins/rehype-slug.mjs
    import GithubSlugger from 'github-slugger';
    import { headingRank } from 'hast-util-heading-rank';
    import { visit } from 'unist-util-visit';
    import { toString } from 'hast-util-to-string';
    const slugs = new GithubSlugger();
    export function rehypeSlug() {
    /**
    * @param {import('hast').Root} tree
    */
    return (tree) => {
    slugs.reset();
    visit(tree, 'element', (node) => {
    if (headingRank(node) && !node.properties.id) {
    let slug = slugs.slug(toString(node));
    // Strip trailing hyphens like in Astro v5 and below:
    if (slug.endsWith('-')) slug = slug.slice(0, -1);
    node.properties.id = slug;
    }
    });
    };
    }
  3. Add the custom plugin to your Markdown configuration in astro.config.mjs:

    astro.config.mjs
    import { defineConfig } from 'astro/config';
    import { rehypeSlug } from './plugins/rehype-slug';
    export default defineConfig({
    markdown: {
    rehypePlugins: [rehypeSlug],
    },
    });
Learn more about Heading IDs.

Changed: getStaticPaths() cannot return params of type number

Section titled “Changed: getStaticPaths() cannot return params of type number”

In Astro 5.x, getStaticPaths() could return params of type number, which would always be stringified by Astro. However, that could be confusing because it conflicted with Astro.params types.

Astro 6.0 removes this behavior: getStaticPaths() must now return string or undefined params values.

Review your dynamic routes using getStaticPaths() and convert any number params to strings:

src/pages/post/[id]/[label].astro
---
export function getStaticPaths() {
return [
{
params: {
id: 1,
id: "1",
label: "foo",
}
},
{
params: {
id: 2,
id: "2",
label: "bar",
}
},
]
}
---

Changed: Integration hooks and HMR access patterns (Integration API)

Section titled “Changed: Integration hooks and HMR access patterns (Integration API)”

In Astro 5.x, Astro relied on certain patterns for integration hooks and HMR access that were incompatible with or could be improved by integrating Vite’s Environment API.

Astro 6.0 uses Vite’s new Environment API for build configuration and dev server interactions. This primarily enables dev mode in runtimes like workerd, but means that some integration hooks and HMR access patterns have changed.

For integrations using astro:build:setup:

The hook is now called once with all environments configured (ssr, client, prerender), instead of being called separately for each build target. Remove the target parameter and use vite.environments to configure specific environments:

my-integration.mjs
{
hooks: {
'astro:build:setup': ({ target, vite }) => {
if (target === 'client') {
vite.build.minify = false;
}
}
'astro:build:setup': ({ vite }) => {
vite.environments.client.build.minify = false;
}
}
}

For dev toolbar and integration code accessing HMR:

Replace server.hot.send() with server.environments.client.hot.send():

server.hot.send(event)
server.environments.client.hot.send(event)
Learn more about the Vite Environment API and Astro integration hooks.

Changed: SSRManifest interface structure (Adapter API)

Section titled “Changed: SSRManifest interface structure (Adapter API)”

In Astro 5.x, path properties of the SSRManifest interface like srcDir, outDir, cacheDir, publicDir, buildClientDir, and buildServerDir were URL strings.

Astro 6.0 changes the form of these path properties to URL objects instead of URL strings. With this change, several new properties are now available on the manifest, and others have been updated or removed.

If you were treating these path properties as strings, you will now need to handle the URL object. For example, you will now need to access the href property of the URL object:

// To retrieve the same format (e.g., "file:///path/to/src"), make the following change:
const srcPath = manifest.srcDir;
const srcPath = manifest.srcDir.href;

If you were accessing the hrefRoot property, you will need to remove it, as it is no longer available on the manifest.

Update any use of serverIslandMappings and sessionDriver. These are now async methods:

const mappings = manifest.serverIslandMappings;
const driver = manifest.sessionDriver;
const mappings = await manifest.serverIslandMappings?.();
const driver = await manifest.sessionDriver?.();
Learn more about the Adapter API.

Changed: schema types are inferred instead of generated (Content Loader API)

Section titled “Changed: schema types are inferred instead of generated (Content Loader API)”

In Astro 5.x, the types for content collections were generated using zod-to-ts when provided by a content loader and not defined by a user-provided schema.

Astro 6.0 removes this behavior: types are no longer generated using zod-to-ts. Instead, types are inferred.

If you are providing a schema in a content loader, you must use the TypeScript’ satisfies operator:

import type { Loader } from 'astro/loaders'
function myLoader(): Loader {
function myLoader() {
return {
name: 'my-loader',
load: async (context) => {
// ...
},
schema: z.object({/* ... */})
}
} satisfies Loader
}
Learn more about defining loader schema types.

Know a good resource for Astro v5.0? Edit this page and add a link below!

Please check Astro’s issues on GitHub for any reported issues, or to file an issue yourself.

Contribute Community Sponsor