Deploying an Astro blog to Netlify
Build config, functions, cache headers, environment variables, and deploy previews
- Published
- 14 May 2026
- Read time
- 6 min read
Was this useful?
This blog runs entirely on Netlify’s free tier. Static HTML goes out over the CDN, serverless functions handle comments and the mailing list, and an edge function gates deploy previews — all without any infrastructure to manage.
This post covers the configuration details: what goes in netlify.toml, how
functions are set up, which environment variables are required, and how the deploy
preview workflow integrates with the draft post system.
netlify.toml
Everything Netlify needs to know about building and running the site is in
netlify.toml at the project root:
[dev] framework = "astro" command = "astro dev" targetPort = 4321 autoLaunch = false
[build] command = "astro build" publish = "dist"
[functions] directory = "netlify/functions" node_bundler = "esbuild"
[[headers]] for = "/*" [headers.values] Cache-Control = "public, max-age=0, must-revalidate"
[[headers]] for = "/_astro/*" [headers.values] Cache-Control = "public, max-age=31536000, immutable"The [dev] section configures Netlify Dev — netlify dev in the terminal starts
both the Astro dev server and the function runtime together, so you can test
serverless functions against the local site. autoLaunch = false prevents Netlify
Dev from opening a browser tab automatically.
[build] points to the Astro build command and the output directory. Astro outputs
to dist/ by default.
[functions] tells Netlify where to find the serverless functions and which
bundler to use. esbuild is significantly faster than webpack for bundling Node.js
functions and handles ES module imports correctly.
Cache headers
The two [[headers]] blocks implement a split caching strategy:
/*— HTML pages getmax-age=0, must-revalidate. The browser caches the response but revalidates on every request. When a new deploy lands, Netlify invalidates the CDN edge cache, so clients pick up the new version immediately./_astro/*— Astro outputs hashed filenames for all JS and CSS bundles (e.g._astro/index.B1fJkLmN.js). Because the hash changes whenever the content changes, these assets can be cached indefinitely withmax-age=31536000, immutable.
Without the second rule, browsers would re-fetch unchanged bundles on every page load. Without the first, stale HTML pages could reference bundle URLs that no longer exist.
The functions directory
Netlify Functions are TypeScript files in netlify/functions/. Each file is a
separate function, accessible at /.netlify/functions/{filename}:
netlify/ functions/ approve-comment.ts → /.netlify/functions/approve-comment comment-handler.ts → /.netlify/functions/comment-handler get-comments.ts → /.netlify/functions/get-comments subscribe.ts → /.netlify/functions/subscribeFunctions are not bundled with the site — Netlify deploys them separately. The
node_bundler = "esbuild" setting handles tree-shaking and resolves import
statements so each function file can use npm packages.
Edge functions
Edge functions run at Netlify’s CDN edge — before the response is served — rather
than as on-demand Lambda invocations. They live in netlify/edge-functions/ and
are configured through the exported config object in each file:
export const config: Config = { path: "/*" };preview-auth.ts runs on every request. It reads the PREVIEW_PASSCODE
environment variable. When no passcode is configured (production), the function
calls context.next() immediately and is a transparent pass-through. When a
passcode is set (preview deploys), it gates the entire site behind a passcode form
and sets an HttpOnly; Secure; SameSite=Strict session cookie on success.
This is how draft posts are safely visible on deploy previews without being
publicly accessible. The SHOW_DRAFTS=true build variable makes the Astro build
include draft posts; the edge function ensures only someone with the passcode can
reach them.
Environment variables
None of the secrets are stored in netlify.toml. Environment variables split into
two groups depending on when they are consumed.
Build-time variables
These are read by astro build and baked into the generated HTML. Any variable
referenced via import.meta.env falls into this category and must be present when
the build runs:
| Variable | What it is |
|---|---|
SHOW_DRAFTS | Set to "true" on preview branch deploys to include draft and scheduled posts |
Runtime variables
These are read by serverless and edge functions at request time and are never embedded in the built HTML. Keep them in the Netlify dashboard only (Site configuration → Environment variables):
| Variable | Used by | What it is |
|---|---|---|
PREVIEW_PASSCODE | preview-auth.ts | Passcode protecting deploy previews — leave unset in production |
For local development, copy these into a .env file in the project root. The
functions read them via process.env. Never commit .env — add it to .gitignore.
PREVIEW_PASSCODE note: Leave this unset in the production site context. When
unset, preview-auth.ts is a transparent pass-through and adds no overhead.
Generate a strong value with:
openssl rand -hex 32Deploy previews and draft posts
Netlify automatically generates a deploy preview URL for every pull request and
branch push. The URL takes the form https://deploy-preview-{n}--{site-name}.netlify.app.
Draft posts are hidden by default. The isPublished() helper in
src/utils/drafts.ts reads the SHOW_DRAFTS build-time variable:
export const showDrafts: boolean = import.meta.env.SHOW_DRAFTS === "true";
export function isPublished(post: { data: { draft: boolean; pubDate: Date };}): boolean { if (showDrafts) return true; return isPubliclyPublished(post);}Setting SHOW_DRAFTS=true on the preview branch context in the Netlify dashboard
makes the build include draft and scheduled posts. The preview-auth edge function
then gates that deploy behind a passcode, so the preview URL is not publicly accessible.
This is more reliable than temporarily setting draft: false in frontmatter and
remembering to reset it before merging. There is no risk of accidentally publishing
a post that was only meant to be previewed.
One-time Netlify dashboard setup for comments
The comments webhook isn’t in netlify.toml — it’s a one-time setup in the
Netlify dashboard:
- Go to Forms →
blog-comments→ Form notifications - Add notification → Outgoing webhook
- URL:
https://your-site.netlify.app/.netlify/functions/comment-handler
This wires up the webhook that triggers the moderation email whenever a new comment arrives. It only needs to be configured once per site, which is why it’s not in the TOML file.
Wrapping up
With netlify.toml in place, the deployment configuration is declarative and version-controlled alongside the site code. The split caching strategy — aggressive immutable caching for hashed assets, revalidate-always for HTML — keeps the site fast without ever serving stale pages after a deploy.
The functions and edge-functions directories draw a clear line between work that happens at request time on the server and at the CDN edge. preview-auth in particular is what makes safe draft previewing possible — SHOW_DRAFTS controls what gets built, and the passcode gate controls who can see it.
All the secrets stay in the Netlify dashboard, nothing sensitive is in the repository, and a fresh deploy of the whole setup is reproducible from the TOML file and the environment variable list above.
Working on something similar?
Need help raising the bar?
I help teams improve engineering practice through hands-on delivery, pragmatic reviews, and mentoring. If you want a second pair of eyes or practical support, let's talk.
- Engineering practice review
- Hands-on delivery
- Team mentoring
If this has been useful, you can back the writing with a one-off tip through a secure Stripe checkout.
Free · Practical · One email per post
Get practical engineering notes
One short email when a new article goes live. Useful if you are breaking into tech, growing as an engineer, or improving engineering practice on your team.
Comments
Loading comments…
Leave a comment