
How to deploy an Astro site to Netlify?
To deploy an Astro site to Netlify, push your project to a Git provider, choose Add new site > Import an existing project in the Netlify dashboard, and confirm the detected settings: build command astro build (or npm run build) and publish directory dist. A fully static site needs nothing else. If your project renders any page on demand, or you want Netlify Image CDN, run npx astro add netlify first to install the @astrojs/netlify adapter. After the first deploy, every push triggers a new build, and pull requests get deploy previews. You can also deploy from your terminal with the Netlify CLI using netlify deploy.
Netlify works well with Astro and the adapter does a lot of setup for you, including functions, image optimisation, session storage and redirects. In this article you'll learn when you need the adapter, how to deploy from the dashboard and from the CLI, what a netlify.toml should contain, how to manage environment variables and the Node.js version, how to run Astro middleware on Netlify Edge Functions, how to cache on-demand pages, and how to use Netlify Forms with an Astro site.
Do You Need the Netlify Adapter?
Astro sites are static by default, and Netlify can host the dist folder without any adapter. You need @astrojs/netlify when:
- Any route renders on demand: a page or endpoint with
export const prerender = false, oroutput: 'server'in your config. These routes run as Netlify Functions. - You use Astro Actions, server islands or sessions: they all need a server.
- You want Netlify Image CDN: the adapter makes Astro's
<Image />component use Netlify's image service, even on a static site. - You define
redirectsin your Astro config: the adapter writes them into a_redirectsfile that Netlify understands.
Netlify's own Astro guide recommends installing the adapter for most projects. If none of the points above apply to you, it's optional. The trade-offs between adapters are covered in Astro adapters and which one to choose.
Adding the Adapter
Run this in your project folder:
npx astro add netlify
It installs the package and updates your config:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify';
export default defineConfig({
adapter: netlify(),
});
What the Build Produces
Run npx astro build locally to see the output. In a test project with four prerendered pages and two on-demand routes, the adapter produced:
dist/index.html
dist/about/index.html
.netlify/v1/config.json
.netlify/v1/functions/ssr/ssr.mjs
Static pages and assets stay in dist, which is still your publish directory. All on-demand routes are bundled into a single function called ssr, configured with path: '/*' and preferStatic: true, so Netlify serves an existing file whenever there is one and only calls the function for the rest. The generated config.json adds a long-lived Cache-Control header for the hashed files in /_astro/.
The build log also tells you what the adapter set up:
[@astrojs/netlify] Enabling sessions with Netlify Blobs
[@astrojs/netlify] Emitted _redirects
[@astrojs/netlify] Bundling function ../../../build/entry.mjs
[@astrojs/netlify] Generated SSR Function
Add .netlify to .gitignore, because Netlify regenerates it on every build.
Method 1: Deploy From the Netlify Dashboard
This route works with GitHub, GitLab, Bitbucket and Azure DevOps.
- Push your code to your Git provider.
- In the Netlify dashboard, click Add new site and choose Import an existing project.
- Pick the repository. Netlify detects Astro and pre-fills the settings.
- Check the settings: build command
astro buildornpm run build, publish directorydist. - Click Deploy. The site gets a
netlify.appURL, which you can replace with a custom domain later.
From now on, every push to the production branch triggers a production deploy, and pull requests get deploy previews with their own URLs.
Configuring the Build With netlify.toml
Settings in the dashboard work, but keeping them in a netlify.toml file at the root of the repository makes them versioned and reviewable:
# netlify.toml
[build]
command = "npm run build"
publish = "dist"
The same file can hold redirects, headers and context-specific settings. If you use the Astro redirects config with the adapter, you don't need to repeat those redirects here; the adapter writes them into dist/_redirects. For a config containing redirects: { '/old': '/about' }, it generated:
/old/ /about 301
/old /about 301
Your own public/_redirects file still works too; redirects from the Astro config are appended after yours.
Method 2: Deploy With the Netlify CLI
Install the CLI, log in and connect the folder to a site:
npm install --global netlify-cli
netlify login
netlify init
netlify init detects the Astro build settings, offers to create a netlify.toml, and links the repository so future pushes deploy automatically.
To deploy straight from your machine instead, use netlify deploy. It builds the site first by default and creates a draft deploy with a unique URL:
netlify deploy
When you're happy with the draft, deploy to production:
netlify deploy --prod
Use --no-build if you've already built locally and only want to upload the result.
Environment Variables
Set variables in the Netlify UI under your site's environment variable settings, or with the CLI:
netlify env:set API_URL https://api.example.com
netlify env:set API_SECRET s3cr3t --context production --secret
--context limits a variable to production, deploy previews, branch deploys or local development, and --secret stops the value from being read back. netlify env:import .env copies variables from a local file.
In your Astro code, remember the difference between build time and runtime:
import.meta.env.MY_VARis replaced at build time. In Astro 6 and later it's always inlined and never converted to a number or boolean.process.env.MY_VARinside on-demand routes is read when the function runs.astro:envvalidates variables against a schema and reads secrets through the adapter at runtime. See type-safe environment variables with astro:env.
During astro dev, the adapter doesn't load your Netlify site's variables by default. Turn that on with the devFeatures option, after linking the folder with netlify link:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify';
export default defineConfig({
adapter: netlify({
devFeatures: {
environmentVariables: true,
},
}),
});
Setting the Node.js Version
Astro 7.4.2 requires Node.js 22.23.4 or later, according to the engines field in its package.json. Netlify's current build image picks a version for you, but pinning one avoids surprises. Add a .nvmrc file to the base directory:
24
Or set a NODE_VERSION environment variable in the site's settings. The Astro docs specifically warn that sites on Netlify's legacy Xenial build image must set the version explicitly.
Local Development With Netlify Features
When the adapter is installed, astro dev emulates several Netlify features without the Netlify CLI: a local Netlify Image CDN, a local Netlify Blobs server for sessions, redirects and headers from your Netlify config, the edge context on on-demand pages, and any edge functions in netlify/edge-functions. These work best after netlify link. You can switch them off individually with devFeatures, for example edgeFunctions: false if a package fails inside the edge function sandbox.
Running Middleware on Netlify Edge Functions
By default, Astro middleware runs at build time for prerendered pages and inside the ssr function for on-demand pages. That means it can't add headers to, or protect, a static page at request time. Set middlewareMode: 'edge' to run it as a Netlify Edge Function in front of everything:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify';
export default defineConfig({
adapter: netlify({
middlewareMode: 'edge',
}),
});
With a src/middleware.ts file in place, the build adds a line to the log and an edge function to .netlify/v1/edge-functions/middleware/:
[@astrojs/netlify] Generated Middleware Edge Function
The edge function then runs for every request, including static assets and prerendered pages. Keep it fast, because it's now on the path of every file you serve. In on-demand pages, the edge context is available through Astro.locals.netlify.context, which includes geolocation data:
---
// src/pages/hello.astro
export const prerender = false;
const { geo } = Astro.locals.netlify.context;
---
<h1>Hello, visitor from {geo.city}!</h1>
Writing the middleware itself is covered in What is Astro middleware.
Caching On-Demand Pages
On-demand pages run a function on every request unless you cache them. The adapter has a broad switch that caches all on-demand pages for up to a year:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify';
export default defineConfig({
adapter: netlify({
cacheOnDemandPages: true,
}),
});
That's only safe if none of those pages show personalised content. For finer control, set caching headers per page. Netlify supports standard headers such as CDN-Cache-Control and Vary:
---
// src/pages/news.astro
export const prerender = false;
Astro.response.headers.set('CDN-Cache-Control', 'public, max-age=45, must-revalidate');
---
<h1>Latest news</h1>
Other useful options include imageCDN: false to opt out of Netlify Image CDN, includeFiles and excludeFiles to control what's bundled into the function, and staticHeaders to send headers such as a Content Security Policy as real HTTP headers for prerendered pages. Netlify's skew protection is set up automatically for Astro's own internal requests, such as actions and server islands.
Using Netlify Forms With Astro
Netlify Forms can collect submissions without any server code, which suits static Astro sites well. Enable form detection in the Netlify UI under Forms, then add a form with a name and the data-netlify attribute to a prerendered page:
---
// src/pages/contact.astro
---
<form name="contact" method="POST" data-netlify="true">
<label>
Email
<input type="email" name="email" required />
</label>
<label>
Message
<textarea name="message" required></textarea>
</label>
<button>Send</button>
</form>
Netlify finds forms by parsing the HTML files produced by the build. A form inside an on-demand page or a client-rendered component isn't in those files, so Netlify's docs say you must also include a matching static HTML form with the same field names, and add a hidden form-name input to the dynamic form yourself. If you'd rather handle submissions in your own code, see handling form submissions in Astro.
FAQ: Deploying Astro to Netlify
Use astro build or npm run build as the build command and dist as the publish directory. Netlify usually detects both automatically, and you can store them in a netlify.toml file.
Not necessarily. A fully static site deploys without it. Add it if you want Netlify Image CDN, Astro config redirects written to _redirects, or any on-demand rendering, actions, server islands or sessions.
They run in a single Netlify Function called ssr that the adapter generates. Static files in dist are served first, and the function only handles requests that don't match a file.
Set middlewareMode: 'edge' in the adapter options. Your middleware is then deployed as a Netlify Edge Function and runs for all requests, including static assets and prerendered pages.
The adapter doesn't inject your Netlify site's variables during development by default. Link the folder with netlify link and set devFeatures.environmentVariables to true, or keep the values in a local .env file.
No. By default it builds the site and creates a draft deploy with its own URL. Add --prod to publish to your live site.
Conclusion
Netlify can host a static Astro site straight from Git with the build command astro build and the publish directory dist. As soon as you need on-demand pages, actions, sessions or image optimisation, add @astrojs/netlify, which generates a single SSR function, wires up Netlify Blobs and Image CDN, and turns your Astro redirects into a _redirects file.
From there, the important decisions are about runtime behaviour: where environment variables come from, which Node.js version builds the site, whether middleware should run at the edge, and which on-demand pages are safe to cache. Settle those in netlify.toml and your adapter config, test the result with a draft deploy, and production deploys become a matter of pushing to your main branch.


