Mastering Maintenance Mode in Static Sites
Build a robust maintenance mode for Astro static sites using middleware and build-time configuration, without external proxies or complex infrastructure.
Static sites (SSG) have one major flaw: you can’t turn them off.
Once your HTML is deployed to a CDN, it’s immutable. To put up a “Under Construction” sign, you typically need to intervene at the infrastructure level—updating Nginx rules, deploying a Cloudflare Worker, or fiddling with Vercel dashboard toggles.
I hate dashboard toggles. They are hidden state that lives outside your repository.
If my site is down for maintenance, I want that state tracked in Git. I want my team to see it in the commit history. I want it to be reproducible.
Here is how I implemented a “Build-Time Maintenance Mode” for this Astro blog using nothing but a Node script and middleware.
The Strategy: Build-Time Interception
Since we don’t have a running server to check a database flag on every request, we have to bake the decision into the build artifact itself.
The flow is simple:
- The Trigger: A script flips a boolean in a config file.
- The Commit: We push this change.
- The Build: Astro’s middleware sees the flag and generates redirects for every single route.
- The Result: The deployed site redirects all traffic to
/maintenance.
It’s not instant—it requires a deployment cycle (usually 1-2 minutes). But for planned maintenance, it’s perfect.
Step 1: The State
First, we need a source of truth. I keep this in src/config/site.ts.
// src/config/site.ts
export const siteConfig = {
// Site maintenance mode
maintenanceMode: false, // Set to true to enable maintenance mode
// ... other config
title: "Mangesh Bide",
};
This boolean controls everything. If it’s true, the site is effectively closed.
Step 2: The Switch
I could edit that file manually, but I’m lazy and manual edits are prone to typos. I wrote a small script to toggle this safely.
It lives in scripts/toggle-maintenance.js and uses simple regex replacement. It’s crude, but it works.
// scripts/toggle-maintenance.js
const fs = require("fs");
const path = require("path");
const configPath = path.join(__dirname, "../src/config/site.ts");
try {
let configContent = fs.readFileSync(configPath, "utf8");
const isCurrentlyEnabled = configContent.includes("maintenanceMode: true");
if (isCurrentlyEnabled) {
configContent = configContent.replace(
"maintenanceMode: true",
"maintenanceMode: false"
);
console.log("✅ Maintenance mode disabled - Site is now ONLINE");
} else {
configContent = configContent.replace(
"maintenanceMode: false",
"maintenanceMode: true"
);
console.log("🚧 Maintenance mode enabled - Site is now OFFLINE");
}
fs.writeFileSync(configPath, configContent);
} catch (error) {
console.error("❌ Error toggling maintenance mode:", error.message);
process.exit(1);
}
I wired this up in package.json so I can just run npm run maintenance:toggle.
Step 3: The Enforcer (Astro Middleware)
This is where the magic happens. Astro’s middleware runs during the build process for SSG sites. We can intercept every page generation and force a redirect.
In src/middleware.ts:
// src/middleware.ts
import { defineMiddleware } from "astro:middleware";
import { siteConfig } from "./config/site";
export const onRequest = defineMiddleware(async (context, next) => {
// CRITICAL: Allow access to the maintenance page itself
// If you forget this, you'll create an infinite redirect loop.
if (context.url.pathname === "/maintenance") {
return next();
}
// Check if maintenance mode is enabled
if (siteConfig.maintenanceMode) {
// In SSG, this generates a redirect (meta refresh or provider specific)
return context.redirect("/maintenance");
}
return next();
});
When maintenanceMode is true, Astro sees a redirect return for /, /about, /blog, etc. It generates the appropriate redirect instruction (HTML meta refresh or _redirects file depending on your adapter/config).
The Trade-offs
This approach isn’t for everyone.
Pros:
- Infrastructure Agnostic: Works on Vercel, Netlify, GitHub Pages, or an S3 bucket.
- Version Controlled: You know exactly when maintenance started and ended via Git history.
- Zero Cost: No edge functions or paid features required.
Cons:
- Latency: It takes a full build/deploy cycle to turn on or off. Do not use this for emergency “stop the bleeding” scenarios.
- Git Noise: Your commit history will have “Toggle maintenance” commits.
For a personal blog or a documentation site, the pros heavily outweigh the cons. It keeps the complexity where it belongs: in the code, not in the cloud provider’s dashboard.
Related Posts
Cloudflare Bought My Framework, So I Audited My Whole Stack
Cloudflare acquired Astro in January 2026. My site is Astro on GitHub Pages, behind Cloudflare DNS, with a Cloudflare Worker running its AI chat. An audit of what the acquisition and Astro 6 actually change for a real site, and why I'm not migrating yet.
This Post Published Itself: Scheduled Posts on a Static Site with One Predicate and a Cron
How to schedule blog posts on a fully static Astro site with no CMS and no server: a pure date predicate, a publish filter on every route, and a weekly GitHub Actions cron. Full code included.
Portfolio Chatbot v2: Streaming, Memory, and Two Bugs the Tutorials Skip
I rebuilt my portfolio chatbot to stream replies token-by-token, remember the conversation, and render real Markdown links. Here's the Cloudflare Worker and Astro code, plus the two bugs I only caught by testing the deployed thing.