DEV Community

Roger Rajaratnam
Roger Rajaratnam

Posted on • Originally published at sourcier.uk

Deploying an Astro blog to Netlify

Original post: Deploying an Astro blog to Netlify

Series: Part 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.

Mermaid diagram

Diagram fallback for Dev.to. View the canonical article for the full version: https://sourcier.uk/blog/deploying-astro-netlify

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

export const config: Config = { path: "/*" };
Enter fullscreen mode Exit fullscreen mode

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 32
Enter fullscreen mode Exit fullscreen mode

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:

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)