Part 3 — Automated CI/CD Pipelines, Starter Dry-Run Mode & Deploying to Any Static Host
How The Navigator's dual GitLab CI and GitHub Actions pipelines run in 5-second Starter Template Dry-Run Mode out of the box, how to deploy ./dist/ to Vercel, Netlify, or Cloudflare Pages, and how scripts/s3-deploy.sh pairs ETag delta sync with 4-tier Edge/Browser Cache-Control headers.
This is Part 3 of a three-part setup and deployment guide for The Navigator (GitHub · GitLab · Live Demo):
- Part 1 — Customizing This Astro Theme: Local setup, identity & theme configuration, and content authoring.
- Part 2 (Optional) — Provisioning the Multi-Region AWS Stack with Terraform: Multi-region S3S3Simple Storage ServiceCloudAmazon Web Services scalable object storage offering 99.999999999% (11 9s) of data durability with fine-grained IAM policies and versioning. buckets, MRAPMRAPMulti-Region Access PointCloudAn Amazon S3 global endpoint that dynamically routes client requests across multiple AWS regions using AWS Global Accelerator for lowest latency., CloudFrontCloudFrontAmazon CloudFrontCloudAWS global content delivery network (CDN) that securely delivers data, static assets, and APIs at low latency with edge caching and Origin Access Control. SigV4a OACOACOrigin Access ControlSecurityAWS CloudFront security mechanism that signs viewer requests with SigV4 to restrict Amazon S3 bucket access strictly to authorized CloudFront distributions., ACMACMAWS Certificate ManagerSecurityAWS service that provisions, manages, and automatically renews public and private SSL/TLS certificates for CloudFront, ALBs, and API Gateways. SSLSSLSecure Sockets LayerSecurityThe predecessor cryptographic protocol to TLS for encrypting network communications; commonly used as the umbrella term for SSL/TLS certificates., and Route 53 DNSDNSDomain Name SystemNetworkingThe distributed hierarchical naming system that translates human-readable hostnames into numerical IP addresses and directs mail routing via MX records. via GitLab CI, GitHub Actions, or Local CLICLICommand-Line InterfaceDevOpsA text-based user interface mechanism that processes terminal commands to interact directly with programs, operating systems, and cloud APIs..
- Part 3 (You are here): Deploying to zero-config static hosts (Vercel, Netlify, Cloudflare Pages), dual GitHub Actions & GitLab CI Starter Dry-Run pipelines, and 4-tier S3/CloudFront delta sync.
Because this theme compiles 100% of its pages, responsive WebP/AVIF photography variants, inline SVGSVGScalable Vector GraphicsArchitectureAn XML-based two-dimensional vector graphics format that renders crisp graphics at any display resolution and can be styled dynamically with CSS. Mermaid diagrams, Satori Open Graph social cards, and machine-readable /llms.txt indexes at build time into ./dist/ (output: 'static'), you are never locked into a single cloud provider.
In this final part of the setup series, we cover:
- How to deploy
./dist/to zero-config static hosts like Vercel, Netlify, or Cloudflare Pages in under a minute (just like the Live Interactive Demo). - How the repository’s dual GitLab CI (
.gitlab-ci.yml) and GitHub Actions (.github/workflows/deploy.yml) pipelines use Starter Template Dry-Run Mode so fresh forks get green✅status badges in ~5–10 seconds without needing AWSAWSAmazon Web ServicesCloudAmazon global cloud computing platform offering on-demand compute, object storage, content delivery, and managed infrastructure services across dozens of geographic regions. credentials upfront. - How
scripts/s3-deploy.shcombines MD5-vs-ETag delta sync with 4-tier split Edge/BrowserCache-Controlheaders so live pages update in seconds while CloudFront edge cache hit ratios stay near 100%.
Option 1: Zero-Config Static Hosting (Vercel, Netlify, Cloudflare Pages)
If you don’t need multi-region AWS S3 replication (Part 2), you can host the compiled ./dist/ folder on any static CDNCDNContent Delivery NetworkNetworkingA geographically distributed network of edge proxy servers that caches and serves static web assets close to end-users to reduce latency and origin load. with zero infrastructure setup:
A. Prebuilt Static Upload (Fastest — Zero Cloud Build Minutes)
Because npm run build generates a self-contained ./dist/ directory locally (including pre-rendered inline SVG Mermaid diagrams and cached 1200×630 Open Graph cards), you can upload ./dist/ directly to Vercel, Netlify Drop, or Cloudflare Pages:
# 1. Build the static site locally into ./dist/
npm run build
# 2. Deploy ./dist/ directly (e.g. Vercel CLI, Netlify CLI, or Cloudflare Wrangler)
npx vercel deploy ./dist --prod
# or: npx netlify-cli deploy --dir=./dist --prod
# or: npx wrangler pages deploy ./dist
B. Automated Git Integration on Vercel / Netlify / Cloudflare Pages
If you connect your GitHub or GitLab repository directly to Vercel, Netlify, or Cloudflare Pages, configure the build settings to install Playwright’s headless Chromium binary before running npm run build (required by rehype-mermaid to compile Markdown mermaid fences into inline SVGs):
| Setting | Value |
|---|---|
| Framework Preset | Astro |
| Build Command | npx playwright install chromium && npm run build |
| Output Directory | dist |
| Node.js Version | 22.x (or >= 20) |
Option 2: Dual CI/CD Pipelines with Starter Template Dry-Run Mode
A common frustration with open-source starter templates that include production CI/CDCI/CDContinuous Integration & Continuous DeliveryDevOpsDevOps automation pipeline practice that automatically compiles, tests, and deploys software changes directly to staging or production environments. workflows is the “Red Pipeline on First Fork” problem: the moment you fork or push the starter repository to GitHub or GitLab, the CI pipeline tries to authenticate with AWS or run aws s3 sync, fails because you haven’t configured cloud secrets yet, and plasters red ❌ failure badges across your repository.
To solve this cleanly, both .gitlab-ci.yml (GitLab CI/CD) and .github/workflows/deploy.yml (GitHub Actions) in The Navigator implement automatic Starter Template Dry-Run Mode:
How Dry-Run Mode Works Under the Hood
When S3_BUCKET is empty (the default on a fresh clone or fork):
- Build & Deploy Jobs (
build,deploytos3): Instead of spinning up a 2 GB headless browser container or failing on missing AWS credentials, the runner verifies thatpackage.json,astro.config.mjs,src/site.config.ts,src/theme.config.ts, andscripts/s3-deploy.share intact, prints a concise onboarding guide in the job log, and exits0in ~5–10 seconds. - Infrastructure Check (
terraform:dry-run): WhenTF_VAR_environment_domainis unset, a lightweight dry-run job verifies that all TerraformTerraformHashiCorp TerraformDevOpsAn open-source Infrastructure as Code (IaC) tool that provisions, manages, and versions multi-cloud resources declaratively via HashiCorp Configuration Language. modules (terraform/versions.tf,terraform/backend.tf,terraform/cloudfront.tf,terraform/s3-mrap.tf,terraform/acm-route53.tf, andterraform/bootstrap/main.tf) are present and passes with a green✅badge. - Zero-Edit Transition to Production: The moment you add
S3_BUCKET(and your AWS credentials) in GitLab CI/CD Variables or GitHub Actions Secrets & Variables, the exact same workflow automatically transitions to full production builds and S3 delta deployments—without editing a single line of.gitlab-ci.ymlor.github/workflows/deploy.yml.
Strict Environment Isolation: Neither scripts/s3-deploy.sh, .gitlab-ci.yml, .github/workflows/deploy.yml, nor terraform/ in The Navigator contain any hardcoded bucket names, distribution IDs, or personal domains. If scripts/s3-deploy.sh is invoked locally without S3_BUCKET set in your environment, it immediately aborts with an explicit error message (Error: S3_BUCKET environment variable is required).
Inside scripts/s3-deploy.sh: 4-Tier Edge/Browser Caching & ETag Delta Sync
When deploying to AWS S3 + CloudFront, scripts/s3-deploy.sh solves two classic static-hosting bottlenecks:
- Unnecessary Re-Uploads: Standard
aws s3 synccompares file modification timestamps (mtime). Because a static site generator recreates./dist/on every build,aws s3 syncre-uploads every HTML file, sitemap, and RSS feed even when 99% of your pages haven’t changed. - Stale Browser HTML vs. Low Edge Cache Hit Rate: If you set a single
Cache-Control: max-age=600header on HTML files, CloudFront has to re-fetch from S3 every 10 minutes—and browsers can still show stale HTML for up to 10 minutes after a deployment.
1. Separating Browser TTL (max-age) from CloudFront Edge TTL (s-maxage)
scripts/s3-deploy.sh inspects every file path in ./dist/ and assigns one of four split Cache-Control headers:
| Tier | File Patterns | Cache-Control Header | Browser Behavior (max-age) | CloudFront Edge Behavior (s-maxage) |
|---|---|---|---|---|
| Tier 1: Fingerprinted Assets | /_astro/* (including /_astro/styles.<hash>.css), /yt-thumbs/*, /pagefind/* | public, max-age=31536000, immutable | Cached 1 year without revalidation (filenames contain content hashes) | Cached 1 year at edge |
| Tier 2: Media & Fonts | /images/*, /fonts/*, .webp, .avif, .png, .jpg, .woff2 | public, max-age=2592000, s-maxage=31536000 | Cached 30 days in browser | Cached 1 year at edge |
| Tier 3: HTML & OG Cards | *.html, /og/*.png | public, max-age=0, s-maxage=31536000, must-revalidate | Always revalidates (304 Not Modified in milliseconds; instant updates after deploy) | Cached 1 year at edge until explicitly invalidated by s3-deploy.sh |
| Tier 4: Feeds & Indexes | *.xml, *.txt, *.json, *.xsl, *.webmanifest | public, max-age=3600, s-maxage=86400 | Cached 1 hour in browser/readers | Cached 24 hours at edge |
Why is Tier 3 (public, max-age=0, s-maxage=31536000, must-revalidate) so effective for HTML pages?
- At the CloudFront Edge:
s-maxage=31536000tells CloudFront edge POPs to keep HTML pages cached in memory for a full year—eliminating origin fetches to S3. - In the Visitor’s Browser:
max-age=0, must-revalidatetells the browser to check with the nearest CloudFront edge POP on navigation. When nothing has changed, CloudFront returns304 Not Modifiedat the edge in a few milliseconds. When you deploy an update andscripts/s3-deploy.shinvalidates that path on CloudFront, every visitor receives the fresh HTML immediately on their very next page load!
2. MD5-vs-ETag Delta Sync & Smart Invalidation Collapsing
As detailed in our earlier GitLab CI S3 deployment optimization write-up, on a typical content update:
scripts/s3-deploy.shfetches all existing object keys and ETags from your primary S3 bucket in a singleaws s3api list-objects-v2call.- It computes local MD5 checksums across
./dist/and skips every file whose MD5 matches its remote S3 ETag. - Only the handful of modified files are uploaded in parallel (
MAX_PARALLEL=16) with explicitContent-Typeand tieredCache-Controlmetadata. - Finally, it normalizes the changed paths (turning
/blog/my-post/index.htmlinto/blog/my-post/*) and submits a single CloudFront invalidation request—or automatically collapses the invalidation to/*if more thanMAX_INVALIDATION_PATHS(15) paths changed, staying well within AWS’s free invalidation tier.
Complete Setup Series Recap
You now have the full picture of The Navigator:
- Part 1 — Customizing This Astro Theme: Configure your identity in
src/site.config.ts, UI mechanics insrc/theme.config.ts, page copy insrc/content/pages/*.md, typed datasets insrc/data/*.ts, and Markdown articles/logs/photos insrc/content/. - Part 2 (Optional) — Provisioning the Multi-Region AWS Stack with Terraform: Provision multi-region S3 buckets, an S3 Multi-Region Access Point, native CloudFront SigV4a Origin Access Control, ACM TLSTLSTransport Layer SecuritySecurityCryptographic protocol operating above transport protocols to provide mutual authentication, confidentiality, and data integrity over networks. certificates, and Route 53 DNS via GitLab CI/CD, GitHub Actions, or Local CLI.
- Part 3 — Automated CI/CD Pipelines & Static Hosting: Deploy
./dist/to Vercel/Netlify/Cloudflare Pages in seconds, or let the built-in GitLab CI and GitHub Actions workflows transition seamlessly from 5-second Starter Template Dry-Run checks to production ETag delta deployments.

About the author
Ed Stuurman
Infrastructure and reliability engineer based in Japan. Passionate about resilient systems, bare-metal hardware, cloud automation with Terraform, and all things homelab.
Contact
Thoughts or questions on this article?
Found a bug, tested a similar setup, or want to discuss the details? Drop a message below.