One-off tip

Support checkout

  1. 1 Choose amount
  2. 2 Payment
  3. 3 Thank you

Choose amount

Pick the level of support that feels right. You can keep it simple or enter a custom amount, then continue to secure payment.

Choose a one-off amount
Deploying an Astro blog to Netlify
Cover image: Netlify, a cloud computing platform that provides hosting and serverless backend services for web applications.
astro netlify engineering

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
SeriesPart of How this blog was built — documenting every decision that shaped this site.

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:

netlify.toml
[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 get max-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 with max-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/subscribe

Functions 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:

netlify/edge-functions/preview-auth.ts
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:

VariableWhat it is
SHOW_DRAFTSSet 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):

VariableUsed byWhat it is
PREVIEW_PASSCODEpreview-auth.tsPasscode 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:

Terminal window
openssl rand -hex 32

Deploy 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:

src/utils/drafts.ts
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:

  1. Go to Formsblog-commentsForm notifications
  2. Add notification → Outgoing webhook
  3. 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
Get guidance

If this has been useful, you can back the writing with a one-off tip through a secure Stripe checkout.

Comments

Loading comments…

Leave a comment

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.