Codifying scotthavird.com in Terraform For years this site lived in two places at once: the Docusaurus repo and a scattering of Cloudflare dashboard clicks. Pages project settings, custom…
Codifying scotthavird.com in Terraform For years this site lived in two places at once: the Docusaurus repo and a scattering of Cloudflare dashboard clicks. Pages project settings, custom domains, zone security toggles, and an email signup API that only existed in AWS — none of it was reviewable in a PR. That drift is fine until it isn't. I hit the www redirect trap on my own domain. Preview builds failed because Node 18 was still configured in the dashboard while Docusaurus 3.9 requires Node 20.
Each fix was another manual change with no audit trail. So I codified the whole stack in iac/ and retired the GitHub Actions deploy workflow in favor of native Cloudflare Pages builds.
What Moved Into Terraform The iac/ directory now owns four concerns that used to be split across dashboards: Concern Resource Why codify it Site hosting cloudflare_pages_project Build command, Node version, preview branch rules Zone security null_resource + Cloudflare API SSL strict, HSTS preload, TLS 1.3 Email signup API Gateway + Lambda + DynamoDB + SES Homepage newsletter form DNS for email cloudflare_record (SES verification, DKIM) Deliverability without manual TXT records The Docusaurus site itself stays in the same repo.
Cloudflare Pages builds main on every push — no separate deploy workflow. Cloudflare Pages: The Important Details The Pages project resource looks straightforward, but a few fields matter in practice: build_config { build_command = "yarn build" destination_dir = "build" } deployment_configs { production { environment_variables = { NODE_VERSION = "20" SUBSCRIBE_API_URL = aws_apigatewayv2_api.main.api_endpoint Node 20 — Docusaurus 3.9.2 requires it. The dashboard was still on 18, which meant every PR preview failed with yarn: command not found or engine warnings.
Setting it in Terraform means preview and production stay aligned. SUBSCRIBE_API_URL — Docusaurus reads this at build time via process.env.SUBSCRIBE_API_URL and exposes it as siteConfig.customFields.apiUrl. The homepage EmailSignup component POSTs to ${apiUrl}/subscribe. Wiring the API Gateway endpoint into the Pages env var means the frontend always targets the right backend without hardcoding URLs. Preview branch excludes — gh-pages is excluded from preview builds.
This is a source-linked briefing. Read the original report from Dev Community .