Config & Integrations
The config file
Section titled “The config file”Every Astro project is configured from astro.config.mjs through a single defineConfig call. A handful of keys carry most of the weight:
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 */ },});siteis your deployed URL. It’s required for anything that emits absolute URLs (sitemap, canonical tags, RSS).basesets a path prefix when the site lives under a subpath (like this course under/astro).outputpicks the default rendering mode;adaptersupplies the runtime when you use on-demand rendering (from the rendering lesson).integrationsis the list of plugins that extend Astro.
The integrations system
Section titled “The integrations system”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:
# astro add writes the import + integrations entry for you, and installs depsnpx astro add reactnpx astro add mdx sitemapCommon 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 sitemapFor more integrations, check out: https://astro.build/integrationsAnything 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:
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}`); }, }, };}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.
Astro is built on Vite
Section titled “Astro is built on Vite”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
What changed in the config for Astro 7
Section titled “What changed in the config for Astro 7”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 7experimental: { cache: { ... } } cache: { ... }experimental: { routeRules: { ... } } routeRules: { ... }experimental: { logger: { ... } } logger: { ... }experimental: { rustCompiler: true } // removed — the Rust compiler is the only compilerexperimental: { queuedRendering: true } // removed — now the defaultexperimental: { advancedRouting: true } // removed — src/fetch.ts is always onNew 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
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.
Adding Tailwind (the current way)
Section titled “Adding Tailwind (the current way)”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:
npx astro add tailwindThat 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.
Content-Security-Policy (security.csp)
Section titled “Content-Security-Policy (security.csp)”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:
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-policycontent-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.
Reading AI-generated code
Section titled “Reading AI-generated code”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
experimentalthat 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, orloggerunderexperimental? Those are top-level keys as of Astro 7. - Does it add
@astrojs/tailwindtointegrations? That’s the pre-Tailwind-v4 integration; current Astro uses the@tailwindcss/viteVite plugin instead, installed viaastro add tailwind. - Does it recommend
@astrojs/dbfor a database layer? It’s deprecated on npm — verified directly:npm view @astrojs/db deprecatedreturns “This package is deprecated. Use a database client (Drizzle, Kysely, etc.) directly instead.” - Does an integration’s hook read
dir(the output directory) insideastro:config:setupinstead ofastro:build:done? Proven above:diris only meaningful once the build has actually produced output —astro:config:setupruns before anything builds.
Mental Model
Section titled “Mental Model”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.
Trade-offs
Section titled “Trade-offs”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 |
Common Misconceptions
Section titled “Common Misconceptions”- “All
experimentalflags from Astro 4/5 tutorials still apply, just maybe with a warning.” Proven above: an unrecognizedexperimentalkey is a hard config error on 7.3.5 (z.strictObject), not a warning — the build refuses to start. - “
astro addcan install any npm package into the config.” Proven above with the real--helpoutput: it only knows a fixed recipe list; anything else is a manualnpm installplus a manual config edit. - “An integration hook can read the finished
dist/output as soon as the build starts.” Proven above:astro:build:done— notastro:config:setup— is the one that fires after output exists, with a realdirpointing at it. - “
@astrojs/tailwindis still the right way to add Tailwind.” It’s the pre-v4 integration; current Astro’sastro add tailwindinstalls the@tailwindcss/viteVite plugin instead.