Production-Ready Deployment & CI/CD Workflows
Deploying a static site reliably comes down to two disciplines: builds that produce the same artifact every time, and deploys that flip atomically so a half-finished release never reaches users. Get those right and "deploy" stops being an event you brace for and becomes something you do many times a day without thinking about it. This guide is for engineers and documentation teams who already ship a static site and want a pipeline that is reproducible, reviewable, and instantly reversible.
The patterns here apply across Astro, Eleventy, Hugo, Jekyll, and a Next.js static export — only the build output directory and a few host flags differ. We move through the full release lifecycle in the order it actually runs: commit, build, preview deploy, automated checks, promotion to production, and rollback when something slips through. Every stage ties back to a concrete host behavior and a command you can run to verify it.
What You Will Learn
This guide is organized around the deploy lifecycle, and each stage has its own deep-dive section you can follow when you implement it:
- Edge delivery and caching — controlling Cloudflare's global edge with a
_headersfile so repeat visits are nearly free, in Cloudflare Pages Edge Caching Setup. - Reproducible builds in CI — deterministic installs, artifacts, and matrix testing, in GitHub Actions for Automated SSG Builds.
- Host selection and routing — how the major managed hosts differ on builds, functions, and previews, in Netlify vs Vercel Deployment Strategies.
- Preview-per-PR — giving every pull request a real, isolated deploy URL before merge, in Preview Environments for Pull Requests.
- Faster builds through caching — incremental builds and shared CI cache so build time does not grow with the site, in Incremental Builds and Build Caching for SSGs.
This work sits alongside the other two halves of running a static site in production: Choosing the Right Static Site Generator for Production covers the framework decision that shapes your build, and Performance Optimization & Core Web Vitals for SSGs covers the runtime metrics your deploy pipeline is ultimately protecting.
Choosing a Host for Your SSG
The framework you picked dictates the build; the host dictates routing, edge-compute limits, preview ergonomics, and cost. Every major host can serve pre-rendered HTML quickly, so the decision is about everything around the artifact.
Cloudflare Pages leans on the largest edge network and an unmetered bandwidth model, which makes it the cheapest choice for high-traffic content sites; its _headers and _redirects files keep cache and routing in version control. Netlify pioneered the deploy-preview-per-PR workflow and has the most polished build plugins and forms/redirects ergonomics. Vercel is the natural home for a Next.js static export and offers incremental static regeneration when you have a handful of pages that genuinely need to revalidate. The detailed trade-off lives in Netlify vs Vercel Deployment Strategies.
A useful rule: pick the host whose native build covers 90% of your needs, then reach for GitHub Actions for Automated SSG Builds only for the steps the host cannot do — custom asset processing, matrix testing across Node versions, or a shared cache spanning multiple jobs.
Reproducible Build Pipelines
A build is reproducible when the same commit produces a byte-identical artifact on any runner. That starts with installing from the lockfile, never the loose package.json range, and pinning the runtime so a host's default Node version cannot silently change your output.
name: Deploy SSG
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: ./dist
npm ci installs exactly what the lockfile specifies for a deterministic dependency tree, and the uploaded artifact is handed to a separate deploy job so build and deploy are decoupled. Set path to your generator's output directory: Astro emits dist, Hugo emits public, and Eleventy and Jekyll both default to _site. The end-to-end Cloudflare example is in Automating Eleventy Deployments with Cloudflare Pages.
Faster Builds: Incremental Compilation & Caching
The single biggest threat to a pleasant pipeline is build time growing with the site. A 200-page site that builds in 20 seconds becomes a 2,000-page site that builds in four minutes, and suddenly every preview deploy is a coffee break. Two techniques keep it flat: incremental builds that only re-render changed pages, and a CI cache that survives between runs so dependencies and processed assets are not recomputed.
# Restore the generator's build cache between runs
- uses: actions/cache@v4
with:
path: |
node_modules/.cache
.eleventy-cache
key: ${{ runner.os }}-ssg-${{ hashFiles('package-lock.json') }}
restore-keys: ${{ runner.os }}-ssg-
A warm cache routinely turns a 90-second cold build into a 15-second incremental one. The full set of techniques — per-generator incremental flags, cache-key hygiene, and sharing a cache across runners — is in Incremental Builds and Build Caching for SSGs.
Preview Environments for Every Pull Request
The cheapest place to catch a broken link, a rendering error, or a Core Web Vitals regression is a preview URL, not production. Every managed host can build a pull request into an isolated, fully addressable deploy with its own subdomain, and you should require that preview to pass before merge.
A preview deploy is where your automated checks belong — link checking, accessibility audits, and a Lighthouse budget all run against the real deployed URL rather than a local guess:
lhci autorun \
--collect.url=https://deploy-preview-128--your-site.netlify.app/ \
--assert.preset=lighthouse:recommended
When the check fails, the pull request is blocked and nothing reaches production. The mechanics of wiring previews into branch protection, and tearing them down to control cost, are covered in Preview Environments for Pull Requests.
Edge Delivery, Caching & Routing
Serving pre-built HTML well is mostly about cache headers and keeping routing in version control. Distribute through a CDN's points of presence to cut Time to First Byte, and use the two-tier cache policy so a deploy does not stampede your origin: fingerprinted assets are immutable for a year, HTML is short-lived.
/assets/*
Cache-Control: public, max-age=31536000, immutable
/*.html
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400
s-maxage controls the shared edge cache while max-age=0 keeps the browser revalidating, and stale-while-revalidate lets the edge serve a slightly stale page while it refreshes in the background. This is safe only because SSGs emit content-hashed asset filenames, so caching the old URLs forever can never serve a wrong asset. The host-specific syntax and purge automation are in Cloudflare Pages Edge Caching Setup.
Keep routing and security headers next to the site, not in a dashboard, so they are reviewable. A Netlify example:
# netlify.toml
[[redirects]]
from = "/blog/*"
to = "/posts/:splat"
status = 301
force = true
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
Use explicit 301 redirects to preserve link equity through URL changes, and set security headers at the edge so every route gets them.
Security & Compliance Hardening
Static output has a small attack surface, but the build pipeline does not. Treat build-time secrets carefully: anything injected into client-visible files — for example an Astro variable with the PUBLIC_ prefix — ships to the browser. Keep tokens server-side, scope each one to the minimum the build needs, and never give a secret a client-exposed prefix.
At the edge, enforce a Content Security Policy alongside X-Frame-Options and X-Content-Type-Options. For third-party scripts you cannot drop, add Subresource Integrity hashes so a compromised CDN file fails closed instead of executing. The CI tokens that drive cache purges and deploys deserve the same discipline — scope a Cloudflare token to Zone → Cache Purge only, rather than handing it account-wide rights.
Promotion & Atomic Deploys
Atomic deploys are the foundation of fast, safe releases. Each build becomes an immutable, versioned artifact, and promotion swaps the live version in a single pointer flip — visitors mid-request either get the old version fully or the new version fully, never a mix. There is no window where half the assets are updated.
Promotion strategy is usually one of three: deploy straight to production on merge to main for fast-moving content sites, promote a previously built preview to production for teams that want a manual gate, or use a release branch that production tracks. Whichever you pick, the artifact that ran your checks on the preview URL should be the exact artifact you promote — rebuilding at promotion time reintroduces the non-determinism you worked to eliminate.
Rollback & Incident Response
Because each release is an immutable artifact, rolling back is just re-pointing at the previous build, which Cloudflare Pages, Netlify, and Vercel all do in seconds from the dashboard or a single CLI command. There is no partial state to clean up.
The cache policy is what makes instant rollback actually work. If HTML is cached long, users keep seeing the old release after you roll back, because their browser never revalidates:
/*.html
Cache-Control: public, max-age=0, must-revalidate
/assets/*
Cache-Control: public, max-age=31536000, immutable
Watch build logs, CDN error rates, and synthetic uptime checks so alerts fire before users feel an incident, and keep a short runbook for cache purges and DNS failover. When a Core Web Vital drops in field data, line it up against your deploy timeline — a sudden regression almost always maps to a specific release, which the Performance Optimization & Core Web Vitals for SSGs guide explains how to read.
Common Pitfalls
- Cache stampede on deploy: invalidating the entire CDN at once can hammer your origin. Use atomic deploys plus
stale-while-revalidateso the edge serves slightly-stale content while it revalidates in the background. - Environment variable leakage: secrets injected into client bundles are public. Keep them server-side and never give a secret a client-exposed prefix.
- Long TTLs on HTML: caching HTML aggressively serves stale content and silently breaks rollbacks. Keep HTML short-lived; reserve
immutablefor hashed assets. - Rebuilding at promotion time: building again to promote reintroduces non-determinism. Promote the exact artifact that passed your checks.
- Broken caches across runners: a stale or corrupted CI cache produces missing pages. Use explicit cache keys tied to the lockfile and validate the output with a link check before promoting.
Key Takeaways
- Reproducible builds start with
npm ciand a pinned runtime — the same commit must produce the same artifact anywhere. - Give every pull request a preview deploy and run your link, accessibility, and performance checks against it before merge.
- Promote the exact artifact that passed your checks; never rebuild at promotion time.
- Use the two-tier cache policy — immutable hashed assets, short-lived HTML — so rollback is instant and repeat visits are free.
- Keep routing, headers, and secrets in version control and scoped to the minimum, so deploys are reviewable and reversible.
FAQ
How do I handle dynamic content in a static CI/CD pipeline?
Decouple it from the build. Push genuinely dynamic data to edge or serverless functions, use incremental regeneration where your host supports it, or fetch from a cached API on the client. The static build stays deterministic while the dynamic parts live at the edge.
What is the optimal cache TTL for SSG deployments?
Use a two-tier policy. Cache fingerprinted assets for one year as immutable, and keep HTML short-lived with max-age=0 or a small s-maxage paired with stale-while-revalidate. This keeps rollbacks instant while making repeat visits nearly free.
How can I prevent broken builds from reaching production?
Gate merges on branch protection, a required preview deploy, automated link checking, and a performance budget. Run those checks on the pull request so a broken build fails before it ever promotes to the production branch.
What makes a deploy atomic and why does it matter?
An atomic deploy publishes an immutable versioned artifact and flips the live pointer in one step, so users never see a half-written release. It also makes rollback a one-step re-point at the previous version instead of a cleanup operation.
Should I build on my host or in GitHub Actions?
Build in GitHub Actions when you need custom steps, matrix testing, or shared caching across jobs, then deploy the artifact. Build on the host when you want the simplest possible Git-push workflow and the host's native build covers your needs.
How fast can I roll back a bad release?
On Cloudflare Pages, Netlify, and Vercel a rollback is selecting a previous deployment or running one CLI command, and it takes seconds because each deploy is an immutable artifact. The only thing that slows it down is HTML cached too aggressively.
Related
- Sibling guide: Choosing the Right Static Site Generator for Production — the framework decision that shapes your build.
- Sibling guide: Performance Optimization & Core Web Vitals for SSGs — the runtime metrics your pipeline protects.
- Cloudflare Pages Edge Caching Setup — control the global edge with a
_headersfile. - GitHub Actions for Automated SSG Builds — reproducible builds and artifacts.
- Netlify vs Vercel Deployment Strategies — pick the host that fits your stack.
- Preview Environments for Pull Requests — a real deploy URL for every PR.
- Incremental Builds and Build Caching for SSGs — keep build time flat as the site grows.