Skip to content

Config & Integrations

Every Astro project is configured from astro.config.mjs through a single defineConfig call. A handful of keys carry most of the weight:

astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import react from '@astrojs/react';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
site: 'https://example.com', // absolute site URL (used for sitemap, canonical, RSS)
base: '/docs', // path prefix if not served at the domain root
output: 'static', // 'static' (default) or 'server' for on-demand
adapter: cloudflare(), // runtime for on-demand rendering
integrations: [mdx(), sitemap(), react()],
vite: { /* escape hatch to raw Vite config */ },
});
  • site is your deployed URL. It’s required for anything that emits absolute URLs (sitemap, canonical tags, RSS). base sets a path prefix when the site lives under a subpath (like this course under /astro).
  • output picks the default rendering mode; adapter supplies the runtime when you use on-demand rendering (from the rendering lesson).
  • integrations is the list of plugins that extend Astro.

An integration is an official or community plugin that hooks into Astro’s build to add a capability — rendering a UI framework, generating a sitemap, adding MDX support, wiring an adapter. You install it and add it to the integrations array. Many have a one-command installer that edits the config for you:

Terminal window
# astro add writes the import + integrations entry for you, and installs deps
npx astro add react
npx astro add mdx sitemap

Common official integrations (@astrojs/*): react / vue / svelte / solid / preact (UI framework islands), mdx (components in Markdown), sitemap, and the platform adapters (cloudflare, node, vercel, netlify). Note that @astrojs/rss is not an integration — it’s a helper package you import inside an endpoint to generate a feed, so there’s no astro add rss and it never goes in the integrations array.

astro add isn’t a generic “add any npm package” command — it only knows the integrations it ships a recipe for, listed by the real CLI help on the installed 7.3.5:

$ npx astro add --help
astro add [...integrations] [...adapters]
UI Frameworks
react astro add react
preact astro add preact
vue astro add vue
svelte astro add svelte
solid-js astro add solid-js
lit astro add lit
alpinejs astro add alpinejs
Documentation Frameworks
starlight astro add starlight
SSR Adapters
netlify astro add netlify
vercel astro add vercel
deno astro add deno
cloudflare astro add cloudflare
node astro add node
Others
tailwind astro add tailwind
mdx astro add mdx
markdoc astro add markdoc
partytown astro add partytown
sitemap astro add sitemap
For more integrations, check out: https://astro.build/integrations

Anything not on that list (a community integration, or a raw Vite plugin) is added by hand: npm install it, then push it onto integrations (or vite.plugins) yourself.

Integration hooks: what actually runs, and when

Section titled “Integration hooks: what actually runs, and when”

An integration is a plain object with a name and a hooks map — each hook fires at a specific point in Astro’s own lifecycle. The two you’ll reach for most: astro:config:setup (as Astro is resolving config, before anything builds) and astro:build:done (after the whole build finishes, with the output directory). Proven with a real, minimal integration and a real build:

my-log-integration.mjs
export default function myLogIntegration() {
return {
name: 'my-log-integration',
hooks: {
'astro:config:setup': ({ logger }) => {
logger.info('config:setup ran');
},
'astro:build:done': ({ logger, dir }) => {
logger.info(`build:done, output at ${dir}`);
},
},
};
}
astro.config.mjs
import { defineConfig } from 'astro/config';
import myLogIntegration from './my-log-integration.mjs';
export default defineConfig({
integrations: [myLogIntegration()],
});
$ npx astro build --outDir dist
[my-log-integration] config:setup ran
[my-log-integration] build:done, output at file:///.../dist/client/

Both lines are real logger output from the real build — astro:config:setup fires once, early, while Astro is still assembling the config (it’s where most integrations register Vite plugins or content collections); astro:build:done fires once, at the very end, and hands you dir — the real output location — which is exactly what an integration that needs to post-process build output (write a manifest, upload assets) waits for.

Under the hood, Astro’s dev server and build are powered by Vite — Vite 8 since Astro 7, whose default bundler is Rolldown, a Rust rewrite that is 10–30× faster than Rollup while keeping the same plugin API. That’s why the dev server starts instantly, why HMR is fast, and why you can use most of the Vite/Rollup plugin ecosystem (check a plugin’s Vite 8 support when you upgrade). When you need something Astro doesn’t expose directly, drop into the vite key and configure Vite itself:

export default defineConfig({
vite: {
plugins: [/* any Vite plugin */],
resolve: { alias: { '@': '/src' } },
},
});
flowchart TB
  cfg["astro.config.mjs (defineConfig)"] --> integ["integrations: UI frameworks, mdx, sitemap, adapter"]
  cfg --> vite["vite: raw Vite 8 (Rolldown) plugins and config"]
  integ --> build["Astro build"]
  vite --> build
Astro composes integrations on top of Vite

Astro 7 stabilised several features, which means the corresponding experimental flags are gone — leaving them in causes a config error. Move them to the top level:

// Astro 6 // Astro 7
experimental: { cache: { ... } } cache: { ... }
experimental: { routeRules: { ... } } routeRules: { ... }
experimental: { logger: { ... } } logger: { ... }
experimental: { rustCompiler: true } // removed — the Rust compiler is the only compiler
experimental: { queuedRendering: true } // removed — now the default
experimental: { advancedRouting: true } // removed — src/fetch.ts is always on

New top-level keys worth knowing: compressHTML now defaults to 'jsx' (JSX whitespace rules); fetchFile relocates or disables the reserved src/fetch.ts entrypoint; markdown.processor lets you swap Sätteri for unified() from @astrojs/markdown-remark. And one package is gone: @astrojs/db is no longer maintained — use node:sqlite, Drizzle, or a hosted database (Turso, Neon, PlanetScale) instead.

What’s still under experimental in 7.3.5

Section titled “What’s still under experimental in 7.3.5”

Not everything moved to the top level — some flags are genuinely still experimental. This is the complete, real list, read straight from the installed package’s own config schema (node_modules/astro/dist/core/config/schemas/base.js), not a blog post that may be stale by the time you read it:

experimental: {
clientPrerender, // pre-render <a> targets on hover/viewport-entry via speculation rules
contentIntellisense, // editor JSON schemas for content collection frontmatter
chromeDevtoolsWorkspace,// wires the dev server into Chrome DevTools' Workspaces feature
incrementalBuild, // only rebuild what changed, for faster repeated builds
svgOptimizer, // hook to run your own optimizer over inlined SVGs
collectionStorage, // 'single-file' | 'chunked' | { type: 'chunked', chunkSize } — content store layout
}

If a config snippet (yours or AI-generated) sets experimental.rustCompiler, experimental.queuedRendering, or experimental.advancedRouting, it’s stale — those three were removed, not renamed, because their behavior is simply always-on now (proven above). Setting an unknown key under experimental is a real schema validation error, since the schema above is a z.strictObject.

🐛 Spot the bug

astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
experimental: {
rustCompiler: true,
contentIntellisense: true,
},
});
Show the answer

contentIntellisense is real and still experimental in 7.3.5 — that part is fine. rustCompiler isn’t: the Rust compiler stopped being optional back when Astro 7 made it the only compiler, so the flag was deleted along with the old JS-based one it used to toggle away from. Because experimental is validated as a z.strictObject (verified in the installed schema), an unrecognized key like this one fails config validation for real — proven with this exact config against a real build:

[config] Astro found issue(s) with your configuration:
! Invalid or outdated experimental feature.
Check for incorrect spelling or outdated Astro version.
See https://docs.astro.build/en/reference/experimental-flags/ for a list of all current experiments.

Not a silent no-op — the build refuses to start at all. The fix is simply deleting the rustCompiler line.

Tailwind in current Astro is installed as a Vite plugin (@tailwindcss/vite, Tailwind v4) — not the old @astrojs/tailwind integration. The one-command installer wires it up:

Terminal window
npx astro add tailwind

That installs the plugin, adds it to your Vite config, and creates the CSS entry. You then import your CSS once (e.g. in a layout) and use Tailwind utility classes in your markup. If you ever see guides referencing the @astrojs/tailwind integration, that’s the older approach; the current path is the Vite plugin via astro add tailwind.

Stable since Astro 6.0, security.csp generates a real Content-Security-Policy for every page — hashing Astro’s own inline scripts/styles automatically, so you don’t hand-maintain a hash list every time the build output changes:

astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
security: {
csp: true, // or an object: { algorithm: 'SHA-512', directives: [...], scriptDirective: {...}, styleDirective: {...} }
},
});

Astro delivers the same policy through two different mechanisms depending on how the page renders — proven against a real build with security: { csp: true }. An on-demand page (prerender = false, or the whole project under output: 'server') gets it as a real HTTP response header, since there’s a live response to attach it to:

$ curl -sD - -o /dev/null http://127.0.0.1:4351/ | grep -i content-security-policy
content-security-policy: script-src 'self' 'sha256-mzyhaPlEXQku46rgza6MekztMi+pKj5BkQ4z75dLyDw=' 'sha256-BF0290pkb3jxQsE7z00xR8Imp8X34FLC88L0lkMnrGw=' ...; style-src 'self' ;

A prerendered page (prerender = true, or the 'static' default) has no live response to attach a header to — the exact same directives are written into the built HTML instead, as a <meta http-equiv> tag:

$ grep -o '<meta[^>]*content-security-policy[^>]*>' dist/client/static-about/index.html
<meta http-equiv="content-security-policy" content="script-src 'self' 'sha256-mzyhaPlEXQku46rgza6MekztMi+pKj5BkQ4z75dLyDw=' ...; style-src 'self' ;">

Both come from the exact same security.csp config and carry the exact same generated script/style hashes — only the delivery mechanism (header vs. <meta>) changes, chosen automatically per route based on prerender, the same setting covered in the SSR & adapters lesson.

A generated astro.config.mjs — or a whole “here’s how to configure Astro” answer — is worth checking against what 7.3.5 actually validates:

  • Does it nest anything under experimental that isn’t in the six real flags proven above (clientPrerender, contentIntellisense, chromeDevtoolsWorkspace, incrementalBuild, svgOptimizer, collectionStorage)? A stale flag from an Astro 4/5 blog post is a real startup error, proven above, not a warning.
  • Does it still nest cache, routeRules, or logger under experimental? Those are top-level keys as of Astro 7.
  • Does it add @astrojs/tailwind to integrations? That’s the pre-Tailwind-v4 integration; current Astro uses the @tailwindcss/vite Vite plugin instead, installed via astro add tailwind.
  • Does it recommend @astrojs/db for a database layer? It’s deprecated on npm — verified directly: npm view @astrojs/db deprecated returns “This package is deprecated. Use a database client (Drizzle, Kysely, etc.) directly instead.”
  • Does an integration’s hook read dir (the output directory) inside astro:config:setup instead of astro:build:done? Proven above: dir is only meaningful once the build has actually produced output — astro:config:setup runs before anything builds.

astro.config.mjs isn’t a flat bag of settings — it’s closer to a pipeline description: integrations (and adapter) are pre-packaged behavior that hooks into specific build lifecycle moments (astro:config:setup, astro:build:done, …, proven above with a real logging integration), while vite is the raw escape hatch underneath all of it. experimental is explicitly a temporary, strictObject-validated holding area — proven above, an unrecognized key there fails loudly rather than being ignored, precisely so a stale flag from an old tutorial doesn’t silently do nothing.

Official integration (astro add x) Raw Vite plugin (vite.plugins)
Setup One command, edits config for you Manual npm install + wire into vite.plugins
Astro-aware Yes — can hook astro:config:setup, register content collections, etc. No — plain Vite plugin API only
Coverage UI frameworks, MDX, sitemap, adapters, Tailwind Anything in the Vite/Rollup-compatible ecosystem
Version drift risk Low — maintained against Astro’s own release cadence Depends on the plugin author tracking Vite 8/Rolldown
  • “All experimental flags from Astro 4/5 tutorials still apply, just maybe with a warning.” Proven above: an unrecognized experimental key is a hard config error on 7.3.5 (z.strictObject), not a warning — the build refuses to start.
  • “astro add can install any npm package into the config.” Proven above with the real --help output: it only knows a fixed recipe list; anything else is a manual npm install plus a manual config edit.
  • “An integration hook can read the finished dist/ output as soon as the build starts.” Proven above: astro:build:done — not astro:config:setup — is the one that fires after output exists, with a real dir pointing at it.
  • “@astrojs/tailwind is still the right way to add Tailwind.” It’s the pre-v4 integration; current Astro’s astro add tailwind installs the @tailwindcss/vite Vite plugin instead.
What does the site config option do?
What is the easiest way to add an official integration like React?
What build tool underpins Astro, and how do you access it directly?
What is the current recommended way to add Tailwind to Astro?
After upgrading to Astro 7 your config still has experimental: { cache: ... }. What happens?
On the installed [email protected], what does a real build do with this config?
// astro.config.mjs
export default defineConfig({
  experimental: {
    rustCompiler: true,
    contentIntellisense: true,
  },
});
When does astro:build:done fire, and what does dir point to?
// my-log-integration.mjs
export default function myLogIntegration() {
  return {
    name: 'my-log-integration',
    hooks: {
      'astro:build:done': ({ logger, dir }) => {
        logger.info(`build:done, output at ${dir}`);
      },
    },
  };
}