# Alex Mercer - Full Site Documentation Archive
> Complete concatenated archive of example.com for LLM ingestion, agent analysis, and retrieval.
> Canonical URL: https://www.example.com
---
# About Alex Mercer
- Role: Principal Cloud & Platform Architect
- Location: San Francisco, CA
- URL: https://www.example.com/about/
## Summary
I’m a Principal Cloud & Platform Architect with over a decade of experience designing resilient distributed systems, multi-region edge delivery networks, and declarative infrastructure-as-code workflows.
My engineering philosophy centers on simplicity, observability, and removing moving parts at runtime. Whether architecting global storage replication with AWS S3 Multi-Region Access Points or building zero-SSR static web applications, I prefer systems whose failure modes are predictable and whose deployments are fully reproducible from version control.
Outside the terminal, I spend my time photographing minimalist architecture, coastal geometry, and alpine landscapes.
## Career History
### Principal Cloud & Platform Architect — Aether Cloud Systems
- Period: March 2024 – Present
- Location: San Francisco, CA
- Skills: AWS, Terraform, Infrastructure, CI/CD, Architecture
- Architected active-active multi-region AWS edge platforms utilizing S3 Multi-Region Access Points (MRAP), CloudFront SigV4a Origin Access Control, and Global Accelerator.
- Standardized organization-wide Terraform modules and GitLab CI/CD pipelines with native S3 state locking, cutting deployment lead times by 68%.
- Led reliability engineering reviews, SLO error-budget governance, and automated chaos testing across 14 global points of presence.
### Staff Site Reliability Engineer — Veridian Edge Networks
- Period: June 2020 – February 2024 (3 yrs 9 mos)
- Location: Seattle, WA
- Progression: Senior SRE → Staff Site Reliability Engineer
- Skills: AWS, Terraform, CI/CD, AstroJS, Tailwind CSS
- Designed high-throughput observability pipelines and zero-downtime release automation supporting 40,000+ requests per second at the edge.
- Engineered automated TLS/ACM certificate lifecycle tooling, DNSSEC validation, and strict IAM least-privilege boundaries across production AWS accounts.
- Authored internal developer documentation and static architecture portals built with Astro and Tailwind CSS.
### Senior Systems & Virtualization Engineer — Helios Datacenter Solutions
- Period: January 2017 – May 2020 (3 yrs 5 mos)
- Location: Austin, TX
- Skills: Infrastructure, Architecture
- Managed hybrid bare-metal Linux clusters, Proxmox VE hypervisors, and ZFS storage pools with PCIe HBA passthrough for low-latency workloads.
- Automated bare-metal provisioning, BGP Anycast routing policies, and hardware health telemetry across three regional facilities.
## Peer Recommendations
### Peer Recommendation (JL)
“Working alongside Alex over the past three years transformed how our platform group approaches multi-region resilience and infrastructure automation. Alex has a rare ability to take deeply complex distributed systems problems and distil them into clean, maintainable Terraform and CI/CD workflows.
Whenever a critical architectural decision or cross-region latency bottleneck surfaced, Alex combined rigorous first-principles diagnostics with pragmatic execution—delivering production-grade solutions without unnecessary complexity.
Beyond technical depth, Alex is a generous mentor who elevates the engineering bar for everyone on the team.”
### Peer Recommendation (SR)
“Alex is the kind of principal engineer every infrastructure organization hopes to find. From SigV4a multi-region storage routing to zero-SSR static edge delivery, every system Alex designs is fast, observable, and rock-solid.”
### Peer Recommendation (MK)
“Clear communicator, methodical debugger, and relentless automator. Alex turned our manual cloud provisioning into a reproducible, self-documenting GitLab CI/CD pipeline in a matter of weeks.”
### Peer Recommendation (DT)
“An exceptional systems architect who pairs deep Linux and networking fundamentals with modern cloud-native patterns.”
### Peer Recommendation (EC)
“Alex brings clarity, calm, and deep technical mastery to every architecture review and production incident.”
### Peer Recommendation (RN)
“Collaborating with Alex on our global edge migration was a highlight of my career. The documentation, diagrams, and Terraform modules Alex authored remain the gold standard across our engineering org.”
### Peer Recommendation (HP)
“Thoughtful, proactive, and detail-oriented—Alex consistently bridges the gap between low-level infrastructure reliability and polished developer experience.”
---
# Articles Archive
---
# Part 1 — Customizing This Astro Theme: Identity, Feature Toggles, and Content
- URL: https://www.example.com/blog/2026-11-03-part-1-customizing-this-astro-theme/
- Published Date: Nov 03, 2026
- Updated Date: Nov 06, 2026
- Tags: AstroJS, Architecture, Tailwind CSS, Design, Engineering
- Description: A practical guide to cloning, running, and customizing this modular Astro 7.4 + Tailwind CSS v4 site—toggling features, choosing your homepage, and adding content without editing a single .astro template.
> [!NOTE]
> This is **Part 1** of a three-part guide on setting up and deploying **The Navigator** ([**GitHub (`edstuurman/the-navigator`)**](https://github.com/edstuurman/the-navigator) · [**GitLab (`src9061948/the-navigator`)**](https://gitlab.com/src9061948/the-navigator) · [**Live Interactive Demo**](https://the-navigator-sage.vercel.app/)):
> - **Part 1 (You are here)**: Local setup, identity, feature toggles, configurable homepage, and content authoring.
> - [**Part 2 (Optional) — Provisioning the Multi-Region AWS Stack with Terraform**](/blog/2026-11-03-part-2-provisioning-aws-mrap-cloudfront-terraform-gitlab-ci/): Setting up AWS S3, CloudFront, SSL certificates, and Route 53 DNS via GitLab CI, GitHub Actions, or Local CLI.
> - [**Part 3 — Automated CI/CD Pipelines & Deploying to Any Static Host**](/blog/2026-11-03-part-3-ci-cd-pipelines-dry-run-mode-and-static-hosting/): Zero-config deployment to Vercel, Netlify, or Cloudflare Pages, plus GitHub Actions & GitLab CI pipelines.
**The Navigator** is built around a simple rule: **you can customize 100% of your identity, layout, bio, blog posts, career timeline, and photo gallery—and turn entire sections (`blog`, `photography`, `log`, `now`, `about`, `tags`, `contact`, `contactForm`, `rss`, `llms`) on or off or choose which page serves as your `/` homepage—without ever opening an `.astro` template file.**
Here is how the configuration, content folders, and data files fit together—and the step-by-step guide to making the site your own.
---
## How the Project Is Organized
Instead of mixing personal content with layout code, `src/` is split into three simple layers that build your static site:
```mermaid
flowchart TD
subgraph Configs["1. Configuration Files (src/*.config.ts)"]
SC["src/site.config.ts (Your Name, Domain, Bio, Avatar, Social Links)"]
TC["src/theme.config.ts (Feature Toggles, Default Homepage, Logo, Nav, Integrations)"]
end
subgraph Content["2. Markdown Pages & Posts (src/content/)"]
PG["src/content/pages/*.md (Page Titles & Intro Copy: home, about, now, contact...)"]
BL["src/content/blog/*.md (Blog Articles)"]
LG["src/content/log/*.md + CHANGELOG.md (Short Status Notes & Release History)"]
PH["src/content/photography/*.md (Photo Titles, Stories & Dates)"]
end
subgraph Data["3. Structured Data (src/data/)"]
EX["experiences.ts (Career Timeline)"]
RC["recommendations.ts (Peer Recommendations)"]
TT["techTerms.ts (Technical Term Tooltips)"]
end
subgraph Outputs["4. Static Site Output (dist/)"]
HTML["Static HTML Pages & Photo Lightbox"]
OG["1200x630 Social Preview Cards (/og/*.jpg)"]
FEEDS["Sitemap, RSS/Atom & AI Feeds (/llms.txt)"]
end
Configs --> Outputs
Content --> Outputs
Data --> Outputs
```
- **`src/*.config.ts`** controls **who owns the site** (`site.config.ts`) and **which features and integrations are enabled** (`theme.config.ts`).
- **`src/content/`** holds your **Markdown pages and posts** (`pages`, `blog`, `log`, `photography`), supporting diagrams, YouTube embeds, callout boxes, and automatic term tooltips.
- **`src/data/`** holds **structured lists** (`experiences.ts` for your career timeline, `recommendations.ts` for peer quotes, and `techTerms.ts` for hover definitions).
---
## Step 1: Local Prerequisites & Quickstart
Before cloning and running the theme locally, make sure you have:
- **Node.js `>= 20`** and `npm`
- **Playwright Chromium** *(used at build time to render Markdown Mermaid diagrams into crisp inline SVGs)*
- *(Optional)* **A [Formspree](https://formspree.io/) account (free)** — only needed if you want the embedded message form on `/contact/`, `/about/`, and blog posts. Set `features.contactForm: false` or leave the endpoint blank to use simple email and social links instead.
- *(Optional)* **An [Umami](https://umami.is/) account or self-hosted instance** — if you want privacy-friendly analytics (`integrations.umami` in `src/theme.config.ts`).
- *(Optional)* **A GitLab or GitHub account (free)** — for repository hosting and automated CI/CD builds.
Clone the starter repository from **GitHub** or **GitLab** and start the development server:
```bash
# 1. Clone from GitHub (or GitLab: https://gitlab.com/src9061948/the-navigator.git)
git clone https://github.com/edstuurman/the-navigator.git
cd the-navigator
npm ci
# 2. Install headless Chromium for build-time diagram rendering
npx playwright install chromium
# 3. Start the local dev server at http://localhost:4321
npm run dev
```
To run a full production build and preview the generated `./dist/` folder locally:
```bash
npm run build
npm run preview
```
> [!TIP]
> **Fast Warm Builds**: Optimized images and social preview cards are cached in `.cache/`. While the very first build takes ~45–60 seconds to process all sample photos and preview cards, subsequent builds finish in **~1.5 to 2 seconds**.
---
## Step 2: Configure Your Identity (`src/site.config.ts`)
Start by opening `src/site.config.ts`. This file holds your site URL, name, bio, portrait, and social links:
```ts
export const siteConfig: SiteIdentityConfig = {
site: {
url: 'https://www.example.com',
domain: 'example.com',
title: 'Alex Mercer',
description:
'Alex Mercer — Principal Cloud & Platform Architect. Distributed edge systems, multi-region AWS infrastructure, Terraform automation, and architectural photography.',
locale: 'en_US',
language: 'en-us',
timeZone: 'America/Los_Angeles',
defaultKeywords: [
'Alex Mercer',
'example.com',
'Platform Architect',
'Site Reliability Engineering',
'Terraform',
'AWS',
'CloudFront',
'Astro',
'Tailwind CSS',
],
},
author: {
name: 'Alex Mercer',
alternateName: 'Alexander Mercer',
avatar: 'author.jpg', // Small circular chip on blog & log cards (src/images/author.jpg)
aboutImage: 'author.jpg', // Large portrait on /about/ (can be a separate photo in src/images/)
tagline: 'Cloud & Platform Architecture',
jobTitle: 'Principal Cloud & Platform Architect',
bio: 'Principal Cloud & Platform Architect specializing in multi-region edge infrastructure, declarative Terraform automation, high-throughput distributed systems, and architectural photography.',
location: 'San Francisco, CA',
email: 'alex@example.com',
knowsAbout: [
'Cloud Architecture',
'AWS Multi-Region Infrastructure',
'Terraform',
'Site Reliability Engineering',
'Linux Systems',
'Edge Networking',
'Static Web Architecture',
],
},
socials: {
github: 'https://github.com',
gitlab: 'https://gitlab.com',
linkedin: 'https://www.linkedin.com',
instagram: 'https://www.instagram.com',
// Optional: twitter, bluesky, mastodon (omit or leave empty to hide)
},
};
```
Updating `src/site.config.ts` automatically updates your name, domain, portrait (`src/images/author.jpg`), social links, RSS/Atom feeds, sitemap, and search engine metadata across every page.
---
## Step 3: Toggle Features & Choose Your Homepage (`src/theme.config.ts`)
Next, open `src/theme.config.ts` to choose which sections of the theme you want to use, which page acts as your front page, and how the interface looks:
```ts
export const themeMechanics: ThemeMechanicsConfig = {
logo: {
text: 'ALEX MERCER',
ariaLabel: siteConfig.site.title,
letterSpacing: '0.22em',
fontSize: 14,
showCompass: true,
compassAngle: 42,
},
integrations: {
formspree: {
endpoint: 'https://formspree.io/f/your-form-id', // Optional: leave empty or set contactForm: false to hide
},
umami: {
enabled: false,
scriptUrl: 'https://cloud.umami.is/script.js',
websiteId: '',
},
},
navigation: {
items: [
{ label: 'Home', href: '/', enabled: true },
{ label: 'About', href: '/about/', enabled: true },
{ label: 'Now', href: '/now/', enabled: true },
{ label: 'Blog', href: '/blog/', enabled: true, activePrefixes: ['/tags'] },
{ label: 'Photography', href: '/photography/', enabled: true },
{ label: 'Log', href: '/log/', enabled: true },
{ label: 'Contact', href: '/contact/', enabled: true },
],
},
features: {
defaultHomePage: 'home', // 'home' | 'blog' | 'about' | 'now' | 'photography' | 'log' | 'tags' | 'contact'
home: true,
about: true,
now: true,
blog: true,
photography: true,
log: true,
tags: true,
contact: true,
contactForm: true,
rss: true,
llms: true,
},
pagination: {
olderBlogsCount: 20,
logCount: 20,
galleryCount: 50,
homepageCarouselCount: 7,
},
openGraph: {
brandLabel: 'ALEX MERCER',
locationPinLabel: siteConfig.author.location,
},
tooltips: {
enabled: true,
autoLink: 'first', // 'first' | 'all' | 'off'
},
footer: {
siteColumnTitle: 'Example.com',
showPoweredBy: true,
poweredByLinks: [
{ label: 'AWS S3', href: 'https://aws.amazon.com/s3/', icon: 's3' },
{ label: 'AWS MRAP', href: 'https://aws.amazon.com/s3/features/multi-region-access-points/', icon: 'mrap' },
{ label: 'AWS CloudFront', href: 'https://aws.amazon.com/cloudfront/', icon: 'cloudfront' },
{ label: 'Colophon', href: '/blog/2026-11-03-part-2-provisioning-aws-mrap-cloudfront-terraform-gitlab-ci/', icon: 'info', highlight: true },
],
showThemeAttribution: false, // Set to `true` to display the optional footer credit
},
};
```
### What You Can Toggle & Customize Here
1. **Turn Any Section On or Off (`features` & `navigation`)**:
- Don't need a photography gallery, a `/now/` page, or an activity log? Set `photography: false` (or `enabled: false` on its item in `navigation.items`).
- Turning a feature off removes it completely—skipping its pages at build time and automatically removing its navigation links, homepage carousel, `/log/` filter tab, `/tags/` section, sitemap entries, and RSS/AI feed items.
2. **Choose Your Front Page (`features.defaultHomePage`)**:
- By default, `/` renders the multi-stream `home` overview with carousels.
- Want your blog index or about page to be the front page of your site? Set `defaultHomePage: 'blog'` (or `'about'`, `'photography'`, `'log'`, `'now'`, `'tags'`, `'contact'`) and that section is rendered directly at `/`.
3. **Optional Contact Form & Feeds (`contactForm`, `rss`, `llms`)**:
- **Formspree (`contactForm` & `integrations.formspree.endpoint`)**: Completely optional. Paste a free Formspree URL to enable the message form on [`/contact/`](/contact/), [`/about/#contact`](/about/#contact), and at the bottom of blog posts—or set `contactForm: false` (or leave `endpoint` blank) to hide the form and display your email and social links instead.
- **RSS & AI Feeds (`rss`, `llms`)**: Set `rss: false` to disable `/rss.xml` and `/atom.xml`, or `llms: false` to disable `/llms.txt`, `/llms-full.txt`, and `/llms.json`.
- **Umami Analytics (`integrations.umami`)**: Optional privacy-friendly analytics (`enabled: true/false`).
4. **Header Logo, Social Preview Cards & Tooltips (`logo`, `openGraph`, `tooltips`, `footer`)**:
- **`logo`**: Change the SVG header text, font size, letter spacing, and compass icon.
- **`openGraph`**: Set the `brandLabel` and `locationPinLabel` shown on generated 1200×630 social preview cards.
- **`tooltips`**: Automatically turns the first mention of technical terms from `src/data/techTerms.ts` in each article into a hover/tap definition card (`autoLink: 'first' | 'all' | 'off'`).
- **`footer`**: Customize footer links, toggle the "Powered by" column (`showPoweredBy`), or toggle the theme credit (`showThemeAttribution`).
---
## Step 4: Edit Page Titles & Intro Copy (`src/content/pages/`)
Every top-level page (`/`, `/about/`, `/now/`, `/contact/`, `/blog/`, `/log/`, `/photography/`, and `/tags/`) reads its title, description, headings, and intro copy from Markdown files in **`src/content/pages/`**:
| File | Route | What You Customize Here |
| :--- | :--- | :--- |
| `src/content/pages/home.md` | [`/`](/) | Homepage title, hero intro card, optional `setupGuide` quickstart banner, and carousel section titles. |
| `src/content/pages/about.md` | [`/about/`](/about/) | Your biography paragraphs (in the Markdown body), spoken `languages` badges, and `faq` accordion items. |
| `src/content/pages/now.md` | [`/now/`](/now/) | Your `/now` status cards (Location, Current Focus, Projects, Reading). |
| `src/content/pages/contact.md` | [`/contact/`](/contact/) | Contact page intro text, timezone/response-time labels, and form heading. |
| `src/content/pages/blog.md` | [`/blog/`](/blog/) | Blog page title, description, and archive heading. |
| `src/content/pages/log.md` | [`/log/`](/log/) | Activity log page title, description, and heading. |
| `src/content/pages/photography.md` | [`/photography/`](/photography/) | Photo gallery page title and description. |
| `src/content/pages/tags.md` | [`/tags/`](/tags/) | Topic index page title, description, and heading. |
> [!TIP]
> **Hiding the Starter Quickstart Banner on `/` (`setupGuide.enabled: false`)**: In **The Navigator** starter template, `src/content/pages/home.md` includes `setupGuide.enabled: true` to show the 3-step onboarding banner inside the homepage hero card. Set `enabled: false` under `setupGuide:` in `src/content/pages/home.md` whenever you're ready to hide it.
---
## Step 5: Update Your Career Timeline & Dictionaries (`src/data/`)
Three files in `src/data/` power the career timeline, peer recommendations, and term tooltips:
1. **`src/data/experiences.ts`**: Powers the [career timeline](/about/#experience) on `/about/` and links each role's `tags` to matching topic pages under [`/tags/`](/tags/).
2. **`src/data/recommendations.ts`**: Powers the peer recommendations grid on [`/about/#recommendations`](/about/#recommendations).
3. **`src/data/techTerms.ts`**: Holds ~60 technical term definitions (`Cloud`, `Networking`, `Storage`, `Security`, `Hardware`, `Virtualization`, `Architecture`, `DevOps`, `AI`) that automatically display hover/tap tooltips in your articles.
---
## Step 6: Adding Blog Posts, Logs & Photos
Day-to-day publishing happens in Markdown:
### 1. Blog Articles (`src/content/blog/*.md`)
Create a `.md` file in `src/content/blog/`:
```markdown
---
title: "Designing Resilient Multi-Region Storage"
description: "Practical notes on replication topologies, RPO/RTO trade-offs, and automated failover testing."
pubDate: "Nov 03 2026"
tags: ["AWS", "Infrastructure", "Architecture"]
sectionNav: "h1 to h2"
---
```
- **`sectionNav: "h1 to h2"`**: Automatically builds the floating table-of-contents bar from your article's headings.
- **Built-in Markdown Extras**:
- **Callout Boxes**: Use `> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`, `> [!WARNING]`, or `> [!CAUTION]`.
- **Diagrams**: Standard ` ```mermaid ` code blocks render as inline SVGs with a click-to-zoom lightbox.
- **YouTube Videos**: Use a ` ```youtube ` code block (video URL on line 1, title on line 2) for a privacy-friendly click-to-play video card.
### 2. Activity Logs (`src/content/log/*.md` & `CHANGELOG.md`)
- **Short Status Notes**: Add Markdown files to `src/content/log/YYYY-MM-DD-slug.md`.
- **Release Notes**: Add release headings (`## [YYYY-MM-DD] - Title`) to root `CHANGELOG.md`. They automatically appear in the `/log/` timeline.
### 3. Photography (`src/images/photography/` & `src/content/photography/`)
- Drop `.jpg` photos into `src/images/photography/` (e.g., `1.jpg`) with a matching `src/content/photography/1.md` file containing `id`, `filename`, `title`, `pubDate`, `category`, `location`, `tags`, and optional story notes.
---
## Next Up: Choose Your Deployment Path
Once `npm run build` succeeds locally, your static site is ready in `./dist/`:
1. **Provision the Multi-Region AWS S3 + CloudFront Stack (Optional)**: Continue to [**Part 2 (Optional) — Provisioning the Multi-Region AWS Stack with Terraform**](/blog/2026-11-03-part-2-provisioning-aws-mrap-cloudfront-terraform-gitlab-ci/).
2. **Deploy to Any Static Host (Vercel, Netlify, Cloudflare Pages) or Explore CI/CD**: Jump to [**Part 3 — Automated CI/CD Pipelines & Deploying to Any Static Host**](/blog/2026-11-03-part-3-ci-cd-pipelines-dry-run-mode-and-static-hosting/).
---
# Part 2 (Optional) — Provisioning the Multi-Region AWS Stack with Terraform (GitLab CI, GitHub Actions & Local CLI)
- URL: https://www.example.com/blog/2026-11-03-part-2-provisioning-aws-mrap-cloudfront-terraform-gitlab-ci/
- Published Date: Nov 03, 2026
- Updated Date: Nov 05, 2026
- Tags: AWS, Terraform, CI/CD, Infrastructure, Architecture
- Description: How to provision globally replicated AWS S3 buckets, a Multi-Region Access Point (MRAP), native CloudFront SigV4a Origin Access Control (OAC), ACM SSL, and Route 53 DNS using Terraform 1.10+ via GitLab CI/CD, GitHub Actions, or Local CLI.
> [!NOTE]
> This is **Part 2 (Optional)** of a three-part setup and deployment guide for **The Navigator** ([**GitHub**](https://github.com/edstuurman/the-navigator) · [**GitLab**](https://gitlab.com/src9061948/the-navigator) · [**Live Demo**](https://the-navigator-sage.vercel.app/)):
> - [**Part 1 — Customizing This Astro Theme**](/blog/2026-11-03-part-1-customizing-this-astro-theme/): Local setup, identity & theme configuration, and content authoring.
> - **Part 2 (Optional — You are here)**: Provisioning the multi-region AWS S3 MRAP + CloudFront SigV4a stack with Terraform.
> - [**Part 3 — Automated CI/CD Pipelines, Starter Dry-Run Mode & Deploying to Any Static Host**](/blog/2026-11-03-part-3-ci-cd-pipelines-dry-run-mode-and-static-hosting/): Zero-config deployment to Vercel/Netlify/Cloudflare Pages, dual GitHub Actions & GitLab CI dry-run pipelines, and two-tier S3/CloudFront delta sync.
> [!IMPORTANT]
> **⏱️ Automated Provisioning — Expect ~20–30 Minutes for First-Time Greenfield Creation**:
> While the entire infrastructure stack is automated via Terraform, provisioning from zero for the first time typically takes **20 to 30 minutes** end-to-end inside AWS:
> - **ACM TLS Certificate DNS Validation (`us-east-1`)**: ~2–5 minutes
> - **S3 Multi-Region Access Point (`aws_s3control_multi_region_access_point`)**: **~10–20 minutes** (AWS provisions global anycast routing tables and cross-region control plane endpoints behind the scenes)
> - **CloudFront Global Edge Distribution + MRAP Policy Attachment**: ~5–10 minutes
>
> Grab a coffee during your initial `terraform apply`—subsequent runs and site deployments (`scripts/s3-deploy.sh`) complete in **~10–15 seconds**.
The `terraform/` directory provisions an optional, enterprise-grade multi-region AWS static hosting stack built around four core capabilities:
1. **Native CloudFront SigV4a Origin Access Control (OAC)**: CloudFront signs requests directly to the S3 Multi-Region Access Point (MRAP) origin using `signing_protocol = "sigv4a"`, requiring zero `Lambda@Edge` functions.
2. **CloudFront Function (`viewer-request`)**: Handles apex-to-`www` `301` redirects (`example.com` → `https://www.example.com`) and static directory index rewrites (`/about/` → `/about/index.html`) at the edge.
3. **Managed Security Response Headers (`Managed-SecurityHeadersPolicy`)**: Attaches AWS's managed security headers policy (`Strict-Transport-Security`, `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`, `Referrer-Policy`, and `X-XSS-Protection`) at the CloudFront edge.
4. **Terraform `>= 1.10` Native S3 State Locking & AWS Provider `~> 6.0`**: Remote state locking uses native S3 `.tflock` lockfiles (`use_lockfile = true` without DynamoDB), and multi-region S3 buckets use the AWS Provider 6.0 per-resource `region` attribute.
---
## Multi-Region Request & Replication Topology
When deployed, your static site is replicated across your chosen AWS regions and served globally from the nearest healthy S3 bucket behind CloudFront:
```mermaid
flowchart TD
Visitor["Visitor Browser"] --> R53["Route 53 Public Hosted Zone (A & AAAA Alias Records)"]
R53 --> CF["CloudFront Global Edge Network + ACM TLS Certificate (us-east-1)"]
subgraph Edge["CloudFront Edge Layer"]
CFF["CloudFront Function (viewer-request) 1. 301 Redirect apex → www 2. Rewrite /path/ → /path/index.html"]
OAC["Native Origin Access Control (OAC) signing_protocol = sigv4a"]
end
CF --> CFF --> OAC
OAC --> MRAP["S3 Multi-Region Access Point (MRAP) Global Anycast Endpoint"]
subgraph Storage["Multi-Region Private S3 Buckets (SSE-S3 + Versioning)"]
direction TB
B1["Primary Bucket (ap-northeast-1) is_main = true (CI/CD Upload Target)"]
B2["Replica Bucket #1 (eu-west-1)"]
B3["Replica Bucket #2 (us-east-1)"]
B1 -->|"Cross-Region Replication (CRR)"| B2
B1 -->|"Cross-Region Replication (CRR)"| B3
end
MRAP -->|"Lowest-Latency Geo-Routing & Failover"| Storage
```
---
## Choose Your Deployment Path
You can provision and manage this Terraform stack using whichever workflow fits your setup:
| Path | Best For | How It Runs |
| :--- | :--- | :--- |
| **[Option A — GitLab CI/CD](#step-2a-provision-via-gitlab-cicd-option-a)** | Teams & individuals hosting their repo on **GitLab** | Built-in `.gitlab-ci.yml` manual jobs (`terraform:bootstrap` → `terraform:plan` → `terraform:apply`) |
| **[Option B — GitHub Actions](#step-2b-provision-via-github-actions-option-b)** | Teams & individuals hosting their repo on **GitHub** | One-time `terraform/bootstrap` initialization + `.github/workflows/deploy.yml` automated builds & S3 deploys |
| **[Option C — Local CLI](#step-2c-provision-via-local-cli-option-c)** | Engineers who prefer running Terraform & deployments directly from their workstation | Local `terraform init && terraform apply` + `bash ./scripts/s3-deploy.sh ./dist` |
---
## Prerequisites
Before running `terraform:bootstrap` or `terraform:apply`, make sure you have the following in place:
1. **An Active AWS Account**:
- Standard AWS account with default regions enabled. *(Note: CloudFront OAC for S3 Multi-Region Access Points supports all 17 default-enabled AWS regions; opt-in regions are not supported by AWS for MRAP OAC.)*
2. **A Registered Domain Hosted on Amazon Route 53**:
- Your root domain (e.g. `example.com`) must already exist as a **Public Hosted Zone** in Route 53 (whether registered directly through Route 53 or an external registrar).
- **Verify Nameservers (`NS`) First**: If your domain was registered outside AWS, ensure its `NS` records at your registrar already point to the four Route 53 nameservers listed in your Hosted Zone. During `terraform apply`, `aws_acm_certificate_validation` waits for public DNS validation of the ACM TLS certificate (`example.com` and `*.example.com` in `us-east-1`); if your domain's nameservers don't delegate to Route 53 yet, certificate validation will wait until they do.
3. **AWS IAM Credentials (`AWS_ACCESS_KEY_ID` & `AWS_SECRET_ACCESS_KEY`)**:
- An IAM User or CI/CD role with permissions to provision and manage:
- **S3 & S3 Control**: Regional buckets, versioning, encryption, lifecycle rules, replication, and Multi-Region Access Points (`s3:*`, `s3control:*`)
- **CloudFront**: Distributions, SigV4a Origin Access Control (OAC), and CloudFront Functions (`cloudfront:*`)
- **ACM (`us-east-1`)**: Public SSL/TLS certificate creation and DNS validation (`acm:*`)
- **Route 53**: Hosted Zone lookup plus ACM validation `CNAME` and `A` (IPv4) / `AAAA` (IPv6) Alias records (`route53:*`)
- **IAM**: Creating and attaching the least-privilege S3 Cross-Region Replication role (`iam:CreateRole`, `iam:CreatePolicy`, `iam:AttachRolePolicy`, `iam:PassRole`)
4. **Your Site Domain Configured in `src/site.config.ts` ([Part 1](/blog/2026-11-03-part-1-customizing-this-astro-theme/))**:
- Ensure `site.url` and `site.domain` in `src/site.config.ts` match your Route 53 domain so build-time canonical links, sitemaps, feeds, and Open Graph cards match your live infrastructure.
---
## Step 1: Select Your Active Regions & Domain (`terraform/variables.tf`)
Open `terraform/variables.tf` and inspect `variable "buckets"`. By default, all **17 default-enabled AWS regions** supported by S3 MRAP + CloudFront OAC are pre-listed so you can simply comment out (`#`) the regions you don't need:
```hcl
variable "buckets" {
description = "List of regional S3 buckets behind the Multi-Region Access Point (MRAP)."
type = list(object({
region = string
is_main = bool
priority = optional(number)
}))
default = [
# --- Primary Region (Deploy Target) ---
{ region = "ap-northeast-1", is_main = true }, # Asia Pacific (Tokyo)
# --- Europe ---
{ region = "eu-north-1", is_main = false }, # Europe (Stockholm)
{ region = "eu-west-1", is_main = false }, # Europe (Ireland)
# --- Americas ---
{ region = "us-east-1", is_main = false }, # US East (N. Virginia)
{ region = "us-west-2", is_main = false }, # US West (Oregon)
]
}
```
> [!IMPORTANT]
> **Choose Your Regions Before First Apply**: In AWS, S3 Multi-Region Access Point bucket membership is immutable after creation. Select the regions you want in `terraform/variables.tf` before running your initial `terraform apply`. Exactly one bucket must have `is_main = true`—this is the primary bucket where your CI/CD pipeline uploads build artifacts, which S3 Cross-Region Replication then fans out to all replica regions.
### Setting `environment_domain` (Naked Root Domain)
Whether you pass `environment_domain` via `terraform/terraform.tfvars` (`environment_domain = "example.com"`) or CI/CD variables (`TF_VAR_environment_domain = example.com`), **always set it to your naked root domain without `https://` or `www.`** (for example, `example.com` or `yourdomain.com`).
From that single value, Terraform automatically derives:
- Your S3 state bucket name (`example-com-tfstate`)
- Your regional S3 bucket names (`example-com-ap-northeast-1`, `example-com-eu-west-1`, etc.)
- Your ACM wildcard TLS certificate (`example.com` and `*.example.com`)
- Your canonical `https://www.example.com` primary URL and `301` edge redirect from `https://example.com`
---
## Step 2A: Provision via GitLab CI/CD (Option A)
The repository root `.gitlab-ci.yml` runs on two independent tracks so normal content commits never block on manual infrastructure jobs:
```mermaid
flowchart LR
subgraph Track1["Track 1: Infrastructure (Manual / On terraform/* Changes)"]
TB["terraform:bootstrap (One-time S3 state bucket)"] --> TP["terraform:plan (Saves tfplan artifact)"] --> TA["terraform:apply (Provisions AWS stack)"]
end
subgraph Track2["Track 2: Site Build & Deploy (Every Push to main)"]
BLD["build (Astro build + warm .cache)"] --> DEP["deploytos3 (MD5/ETag delta sync + CF invalidation)"]
end
```
### 1. Add Initial Bootstrap & Provisioning Variables in GitLab
On a brand-new setup, your S3 buckets and CloudFront distribution do not exist yet—so you cannot set `S3_BUCKET` or `CLOUDFRONT_DISTRIBUTION_ID` upfront. Start by opening **Settings → CI/CD → Variables** in your GitLab project and adding only the credentials and domain needed to bootstrap remote state and run Terraform:
| Variable Key | Required? | Example / Default | Purpose |
| :--- | :--- | :--- | :--- |
| `AWS_ACCESS_KEY_ID` | **Required** | `AKIA...` | AWS IAM Access Key ID for Terraform & S3 deploys (Protected, Masked) |
| `AWS_SECRET_ACCESS_KEY` | **Required** | `wJalrXUtnFEMI/...` | AWS IAM Secret Access Key (Protected, Masked) |
| `TF_VAR_environment_domain` | **Required** | `example.com` | Naked root domain in Route 53 (no `https://` or `www.`); auto-derives `-tfstate` |
| `AWS_DEFAULT_REGION` | Optional | `ap-northeast-1` *(default)* | Default AWS CLI region and fallback region for the `-tfstate` bucket |
| `TF_VAR_environment_domain_slug` | Optional | `example-com` *(dots → hyphens)* | Custom prefix override for state bucket and main stack resource names |
| `TF_STATE_BUCKET` / `TF_STATE_REGION` | Optional | `-tfstate` / `AWS_DEFAULT_REGION` | Explicit overrides if you want the Terraform state bucket named or hosted separately |
### 2. Bootstrap Remote State (`terraform:bootstrap` — One-Time Only)
In GitLab **Build → Pipelines** on `main`, click the play button (**▶**) on **`terraform:bootstrap`**. This creates the dedicated, versioned, SSE-S3 encrypted, TLS-enforced `-tfstate` bucket (which also stores native Terraform `>= 1.10` `.tflock` lockfiles).
### 3. Plan & Apply the Multi-Region Stack (`terraform:plan` → `terraform:apply`)
1. Trigger **`terraform:plan`** (or push a change to `terraform/variables.tf`). Terraform initializes against your newly bootstrapped `-tfstate` bucket, validates the configuration across your selected regions in `var.buckets`, and saves the `tfplan` artifact.
2. Review the planned resources in the job log, then click play (**▶**) on **`terraform:apply`** to provision the regional S3 buckets, cross-region replication IAM & rules, S3 Multi-Region Access Point, `us-east-1` ACM certificate, CloudFront distribution, and Route 53 `A`/`AAAA` records *(allow ~20–30 minutes on the first greenfield run)*.
3. At the end of the **`terraform:apply`** job log, Terraform prints your newly created resource identifiers:
```text
Outputs:
active_bucket_regions = [ "ap-northeast-1", "eu-north-1", "eu-west-1", "us-east-1", "us-west-2" ]
cloudfront_distribution_id = "E1A2B3C4D5E6F7"
primary_upload_bucket = "example-com-ap-northeast-1"
primary_upload_region = "ap-northeast-1"
site_urls = [ "https://www.example.com", "https://example.com" ]
```
### 4. Add Deployment Target Variables & Run `deploytos3`
Return to **Settings → CI/CD → Variables** in GitLab and add the two post-provisioning variables from the `terraform:apply` output:
| Variable Key | Value Source (`terraform:apply` output) | Purpose |
| :--- | :--- | :--- |
| `S3_BUCKET` | `primary_upload_bucket` (e.g. `example-com-ap-northeast-1`) | Primary S3 bucket where `scripts/s3-deploy.sh` uploads `./dist` |
| `CLOUDFRONT_DISTRIBUTION_ID` | `cloudfront_distribution_id` (e.g. `E1A2B3C4D5E6F7`) | CloudFront distribution invalidated after S3 delta sync |
Click retry/play (**▶**) on **`deploytos3`** in your pipeline (or push a commit to `main`) to sync `./dist` to your primary S3 bucket and issue a targeted CloudFront cache invalidation.
---
## Step 2B: Provision via GitHub Actions (Option B)
If you host your repository on **GitHub**, the included `.github/workflows/deploy.yml` workflow handles automated Astro builds, S3 delta deployments, and Terraform validation.
### 1. Provision the Initial AWS Stack
Run the one-time Terraform bootstrap and initial apply from your terminal (or a GitHub Codespace) using your AWS credentials:
```bash
# 1. Create the dedicated S3 state bucket (one-time bootstrap)
cd terraform/bootstrap
terraform init
terraform apply -var="environment_domain=example.com"
# 2. Initialize and apply the main multi-region stack (~20-30 mins first run)
cd ..
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars to set: environment_domain = "example.com"
terraform init \
-backend-config="bucket=example-com-tfstate" \
-backend-config="region=ap-northeast-1"
terraform apply
```
### 2. Configure GitHub Actions Secrets & Variables
Open your GitHub repository **Settings → Secrets and variables → Actions** and add:
**Repository Secrets**:
- `AWS_ACCESS_KEY_ID`: Your AWS IAM Access Key ID
- `AWS_SECRET_ACCESS_KEY`: Your AWS IAM Secret Access Key
**Repository Variables**:
- `TF_VAR_environment_domain`: `example.com`
- `AWS_DEFAULT_REGION`: `ap-northeast-1` (or your primary upload bucket region)
- `S3_BUCKET`: Value of `terraform output -raw primary_upload_bucket` (e.g. `example-com-ap-northeast-1`)
- `CLOUDFRONT_DISTRIBUTION_ID`: Value of `terraform output -raw cloudfront_distribution_id` (e.g. `E1A2B3C4D5E6F7`)
Once `S3_BUCKET` is set in GitHub Actions Variables, `.github/workflows/deploy.yml` automatically switches out of **Starter Template Dry-Run Mode** and builds + deploys your site on every push to `main`!
---
## Step 2C: Provision via Local CLI (Option C)
If you prefer provisioning and deploying directly from your workstation terminal without CI/CD:
```bash
# 1. Create the dedicated S3 state bucket (one-time bootstrap)
cd terraform/bootstrap
terraform init
terraform apply -var="environment_domain=example.com"
# 2. Initialize and apply the main multi-region stack (~20-30 mins first run)
cd ..
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars to set: environment_domain = "example.com"
terraform init \
-backend-config="bucket=example-com-tfstate" \
-backend-config="region=ap-northeast-1"
terraform apply
```
Once `terraform apply` finishes, build and deploy `./dist` directly from your machine at any time using `scripts/s3-deploy.sh`:
```bash
npm run build
S3_BUCKET="$(terraform -chdir=terraform output -raw primary_upload_bucket)" \
CLOUDFRONT_DISTRIBUTION_ID="$(terraform -chdir=terraform output -raw cloudfront_distribution_id)" \
bash ./scripts/s3-deploy.sh ./dist
```
---
## Under the Hood: Key Terraform Resources
Key implementation details across `terraform/` and `scripts/s3-deploy.sh`:
### 1. Native SigV4a Origin Access Control & Managed Security Headers (`terraform/cloudfront.tf`)
`aws_cloudfront_origin_access_control` signs origin requests with `sigv4a` directly against the MRAP endpoint. Because an MRAP hostname ends in `.accesspoint.s3-global.amazonaws.com` rather than `.s3..amazonaws.com`, the distribution's `origin` block **must** include `custom_origin_config` alongside `origin_access_control_id` (omitting `custom_origin_config` causes Terraform to send `S3OriginConfig`, which CloudFront rejects with `The parameter Origin DomainName does not refer to a valid S3 bucket`):
```hcl
resource "aws_cloudfront_origin_access_control" "mrap_oac" {
name = "${local.domain_slug}-mrap-oac"
description = "Native SigV4a Origin Access Control for S3 Multi-Region Access Point"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4a"
}
# Inside aws_cloudfront_distribution.mrap_distribution:
origin {
domain_name = aws_s3control_multi_region_access_point.mrap.domain_name
origin_id = "mrap_origin_id"
origin_access_control_id = aws_cloudfront_origin_access_control.mrap_oac.id
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
```
In addition, `default_cache_behavior` attaches AWS's `Managed-SecurityHeadersPolicy` (`67f7725c-6f97-4210-82d7-5512b31e9d03`) so every response includes `Strict-Transport-Security`, `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`, and `Referrer-Policy` headers automatically.
### 2. Apex Redirect & Directory Index Rewriting (`terraform/cloudfront.tf`)
Because an S3 Multi-Region Access Point uses the S3 REST API endpoint rather than the legacy S3 Website endpoint, requests for `/about/` do not automatically resolve to `/about/index.html` on the S3 side. The `viewer-request` CloudFront Function handles both canonical host redirection and directory index resolution at the edge:
```javascript
function handler(event) {
var request = event.request;
var host = request.headers.host ? request.headers.host.value : '';
// 1. Canonical redirect: naked domain -> www
if (host === 'example.com') {
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: {
location: { value: 'https://www.example.com' + request.uri }
}
};
}
// 2. Static directory index rewrite
var uri = request.uri;
if (uri.endsWith('/')) {
request.uri += 'index.html';
} else if (!uri.includes('.')) {
request.uri += '/index.html';
}
return request;
}
```
---
## Next Up: Automated CI/CD Dry-Run Workflows, Static Hosts & Two-Tier Edge Caching
Now that you know how the optional multi-region AWS stack is provisioned, continue to [**Part 3 — Automated CI/CD Pipelines, Starter Dry-Run Mode & Deploying to Any Static Host**](/blog/2026-11-03-part-3-ci-cd-pipelines-dry-run-mode-and-static-hosting/) for a deep-dive into:
- Deploying `./dist/` to zero-config platforms (**Vercel, Netlify, Cloudflare Pages**)
- How the dual **GitLab CI (`.gitlab-ci.yml`)** and **GitHub Actions (`.github/workflows/deploy.yml`)** pipelines run in **Starter Template Dry-Run Mode** out of the box
- How `scripts/s3-deploy.sh` uses **4-tier split Edge/Browser `Cache-Control` headers** and **MD5-vs-ETag delta sync** to keep CloudFront cache hit ratios near 100% while updating live pages in seconds.
---
# Part 3 — Automated CI/CD Pipelines, Starter Dry-Run Mode & Deploying to Any Static Host
- URL: https://www.example.com/blog/2026-11-03-part-3-ci-cd-pipelines-dry-run-mode-and-static-hosting/
- Published Date: Nov 03, 2026
- Updated Date: Nov 06, 2026
- Tags: CI/CD, Architecture, AWS, CloudFront, AstroJS
- Description: 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.
> [!NOTE]
> This is **Part 3** of a three-part setup and deployment guide for **The Navigator** ([**GitHub**](https://github.com/edstuurman/the-navigator) · [**GitLab**](https://gitlab.com/src9061948/the-navigator) · [**Live Demo**](https://the-navigator-sage.vercel.app/)):
> - [**Part 1 — Customizing This Astro Theme**](/blog/2026-11-03-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**](/blog/2026-11-03-part-2-provisioning-aws-mrap-cloudfront-terraform-gitlab-ci/): Multi-region S3 buckets, MRAP, CloudFront SigV4a OAC, ACM SSL, and Route 53 DNS via GitLab CI, GitHub Actions, or Local CLI.
> - **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 SVG 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:
1. 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](https://the-navigator-sage.vercel.app/)).
2. 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 AWS credentials upfront.
3. How `scripts/s3-deploy.sh` combines **MD5-vs-ETag delta sync** with **4-tier split Edge/Browser `Cache-Control` headers** 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](/blog/2026-11-03-part-2-provisioning-aws-mrap-cloudfront-terraform-gitlab-ci/)), you can host the compiled `./dist/` folder on any static CDN 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:
```bash
# 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/CD 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**:
```mermaid
flowchart TD
Push["Push to main (GitLab CI or GitHub Actions)"] --> CheckVar{"Is S3_BUCKET configured?"}
CheckVar -->|"No (Default Starter Template)"| DryRun["🧪 Starter Template Dry-Run Mode (~5-10s) • Verifies package.json, astro.config.mjs, src/site.config.ts & src/theme.config.ts • Verifies terraform/*.tf modules • Skips Chromium download & AWS calls"]
DryRun --> GreenBadge["✅ Pipeline Passes (Green Badge) Prints step-by-step setup instructions in job log"]
CheckVar -->|"Yes (Production Site Configured)"| ProdBuild["🚀 Full Production Mode 1. Restores warm .cache/astro & .cache/og-images 2. Runs Astro 7.4 + Playwright Mermaid build 3. Runs scripts/s3-deploy.sh (ETag delta sync) 4. Issues targeted CloudFront invalidation"]
ProdBuild --> LiveSite["🌐 Live Multi-Region Site Updated"]
```
### How Dry-Run Mode Works Under the Hood
When `S3_BUCKET` is empty (the default on a fresh clone or fork):
1. **Build & Deploy Jobs (`build`, `deploytos3`)**: Instead of spinning up a 2 GB headless browser container or failing on missing AWS credentials, the runner verifies that `package.json`, `astro.config.mjs`, `src/site.config.ts`, `src/theme.config.ts`, and `scripts/s3-deploy.sh` are intact, prints a concise onboarding guide in the job log, and exits `0` in **~5–10 seconds**.
2. **Infrastructure Check (`terraform:dry-run`)**: When `TF_VAR_environment_domain` is unset, a lightweight dry-run job verifies that all Terraform modules (`terraform/versions.tf`, `terraform/backend.tf`, `terraform/cloudfront.tf`, `terraform/s3-mrap.tf`, `terraform/acm-route53.tf`, and `terraform/bootstrap/main.tf`) are present and passes with a green `✅` badge.
3. **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.yml` or `.github/workflows/deploy.yml`.
### Enabling Automated CI/CD on Your Fork
To prevent consuming shared runner minutes and avoid redundant builds on the public demo template, automated CI is intentionally disabled on the upstream `the-navigator` repository (`src9061948/the-navigator` / `edstuurman/the-navigator`). CI runs only on the production site (`stuurman`).
When you fork or clone the theme for your own site, here is the exact one-time interaction to enable automated builds:
#### A. For GitLab CI/CD (`.gitlab-ci.yml`)
- **Renamed Projects (Default)**: If you rename your repository to your own domain or project name (e.g. `my-awesome-site`), GitLab CI runs automatically out of the box.
- **Projects Retaining "navigator"**: If your fork retains `navigator` in its project name or path, `.gitlab-ci.yml` includes an upstream guard that skips CI. To enable it, either:
1. Add a CI/CD Variable in **Settings > CI/CD > Variables**: `ENABLE_CI = "true"`.
2. Or delete the first rule (`- if: '$ENABLE_CI != "true" && ($CI_PROJECT_NAME =~ /navigator/i ...)'`) under `workflow: rules:` in `.gitlab-ci.yml`.
#### B. For GitHub Actions (`.github/workflows/deploy.yml`)
- Automated `push` triggers are commented out by default so fresh forks don't trigger unwanted builds until you are ready.
- You can manually test and run the workflow at any time via the **Actions** tab in your GitHub repository (**Run workflow** button).
- To enable automatic builds on every `git push` to `main`, simply un-comment the `push:` block at lines 6–10 of `.github/workflows/deploy.yml`:
```yaml
on:
push:
branches:
- main
tags-ignore:
- '**'
workflow_dispatch:
```
> [!IMPORTANT]
> **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:
1. **Unnecessary Re-Uploads**: Standard `aws s3 sync` compares file modification timestamps (`mtime`). Because a static site generator recreates `./dist/` on every build, `aws s3 sync` re-uploads every HTML file, sitemap, and RSS feed even when 99% of your pages haven't changed.
2. **Stale Browser HTML vs. Low Edge Cache Hit Rate**: If you set a single `Cache-Control: max-age=600` header 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..css`), `yt-thumbs/*` | `public, max-age=31536000, immutable` | Cached **1 year** without revalidation (filenames contain content hashes) | Cached **1 year** at edge |
| **Tier 2: Static Media & OG Cards** | `og/*`, `*.png`, `*.svg`, `*.ico`, `*.webp`, `*.jpg`, `*.jpeg` | `public, max-age=86400, s-maxage=31536000, stale-while-revalidate=86400` | Cached **24 hours** in browser with 24h stale-while-revalidate | Cached **1 year** at edge until explicitly invalidated |
| **Tier 3: 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 |
| **Tier 4: HTML Documents** | `*.html` (`*`) | `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` |
Why is **Tier 4 (`public, max-age=0, s-maxage=31536000, must-revalidate`)** so effective for HTML pages?
- **At the CloudFront Edge**: `s-maxage=31536000` tells 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-revalidate` tells the browser to check with the nearest CloudFront edge POP on navigation. When nothing has changed, CloudFront returns `304 Not Modified` at the edge in a few milliseconds. When you deploy an update and `scripts/s3-deploy.sh` invalidates 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
```mermaid
flowchart LR
Dist["Local ./dist/ Files (Compute MD5 Hashes)"] --> Compare{"Compare Local MD5 vs Remote S3 ETag"}
S3List["aws s3api list-objects-v2 ( Single API Call )"] --> Compare
Compare -->|"Match (Unchanged)"| Skip["⏭️ Skip Upload (Zero S3 PUT cost)"]
Compare -->|"Diff or New File"| Upload["⬆️ Parallel Upload (16x) + Tiered Cache-Control Header"]
Compare -->|"Removed Locally"| Delete["🗑️ Delete Stale S3 Object"]
Upload --> Invalidate["⚡ Smart CloudFront Invalidation • /about/index.html → /about/* • Collapses >15 paths into /*"]
Delete --> Invalidate
```
As detailed in our earlier [GitLab CI S3 deployment optimization write-up](/blog/2026-11-01-optimizing-gitlab-ci-s3-deployments/), on a typical content update:
1. `scripts/s3-deploy.sh` fetches all existing object keys and ETags from your primary S3 bucket in a single `aws s3api list-objects-v2` call.
2. It computes local MD5 checksums across `./dist/` and skips every file whose MD5 matches its remote S3 ETag.
3. Only the handful of modified files are uploaded in parallel (`PARALLEL_JOBS=16`) with tiered `Cache-Control` headers and explicit `--content-type` metadata for `.xsl`, `.webmanifest`, and `.txt` files.
4. Finally, it normalizes the changed paths (turning `/blog/my-post/index.html` into `/blog/my-post/*`) and submits a single CloudFront invalidation request—or automatically collapses the invalidation to `/*` if more than `MAX_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**:
1. **[Part 1 — Customizing This Astro Theme](/blog/2026-11-03-part-1-customizing-this-astro-theme/)**: Configure your identity in `src/site.config.ts`, UI mechanics in `src/theme.config.ts`, page copy in `src/content/pages/*.md`, typed datasets in `src/data/*.ts`, and Markdown articles/logs/photos in `src/content/`.
2. **[Part 2 (Optional) — Provisioning the Multi-Region AWS Stack with Terraform](/blog/2026-11-03-part-2-provisioning-aws-mrap-cloudfront-terraform-gitlab-ci/)**: Provision multi-region S3 buckets, an S3 Multi-Region Access Point, native CloudFront SigV4a Origin Access Control, ACM TLS certificates, and Route 53 DNS via GitLab CI/CD, GitHub Actions, or Local CLI.
3. **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.
---
# Kitchen-Sink Markdown Showcase: Mermaid, Alerts, Tooltips & YouTube Facades
- URL: https://www.example.com/blog/2026-11-02-kitchen-sink-markdown-showcase/
- Published Date: Nov 02, 2026
- Tags: AstroJS, Tailwind CSS, Architecture, Infrastructure
- Description: A self-documenting tour of every custom Remark and Rehype build-time extension included in this Astro 7.4 starter theme: inline SVG Mermaid diagrams, GitHub-style callout alerts, zero-JS technical acronym tooltips, click-to-load YouTube facades, and syntax-highlighted code blocks.
This post serves as a live visual reference and kitchen-sink test suite for the custom Markdown pipeline configured in `astro.config.mjs`. Every component below compiles at build time with near-zero client-side JavaScript overhead.
---
## 1. Automatic Technical Acronym Tooltips
Any acronym defined in `src/data/techTerms.ts` (such as MRAP, OAC, SigV4A, BGP, NVMe, ZFS, or AST) is automatically upgraded at build time by `src/plugins/rehype-tech-tooltips.mjs` into an accessible, viewport-clamped glassmorphic popover card with domain category badges and keyboard `Escape` support.
By default (`tooltips.autoLink: 'first'` in `src/theme.config.ts`), only the first occurrence of each term per article receives a tooltip so paragraphs stay clean and readable.
---
## 2. Build-Time Inline SVG Mermaid Diagrams
Fenced ```mermaid code blocks are compiled at build time via `rehype-mermaid` and enriched with accessible `role="img"` titles by `src/plugins/rehype-mermaid-a11y.mjs`—shipping crisp inline `