Host
Deployment
Two hosts, two opposite DNS postures. Getting them backwards causes HTTPS failures that look like certificate problems.
The host is chosen in INTAKE, not at deploy time. A public repository with no need for previews goes to GitHub Pages. A private repository with a public site, or an owner who wants preview deployments, goes to Cloudflare Pages. The choice is recorded in the spec and the state file.
| Condition | Host |
|---|---|
| Public repository, no previews needed | GitHub Pages from the default branch, output docs/ |
| Private repository, public site | Cloudflare Pages from the site branch, output docs |
| Public repository, preview deployments wanted | Cloudflare Pages |
| Public repository, owner prefers Cloudflare | Cloudflare Pages |
The two DNS postures
The custom-domain records are opposites. For GitHub Pages the record is a CNAME to the owner's GitHub Pages hostname, set to DNS-only, the grey cloud. For Cloudflare Pages the record is created by Cloudflare and stays proxied, the orange cloud.
GitHub Pages
The site is created from a branch and the /docs path. The custom domain is read from docs/CNAME, which holds the bare hostname: no protocol, no trailing slash, one line.
The build is polled until it reports built, and then the served commit is compared with the branch head. They must match. A site that answers is not proof that it serves the commit you think it does.
The certificate has four states: none, authorization pending, authorized and approved. HTTPS is enforced only after the state is approved. Enforcing earlier fails with a certificate error, and the fix is to wait, not to retry harder.
If the certificate stays at none, it is usually because the site was created before DNS existed. The playbook's nudge clears the custom domain, waits, and sets it again, then polls until approved.
Cloudflare Pages
Cloudflare Pages needs the owner to authorise the Cloudflare GitHub app for the repository. That is an owner-only step, and an agent must not attempt to bypass it. The project settings are: no framework preset, an empty build command because the output is committed, docs as the output directory, and the site branch as the production branch.
After the owner connects it, the coordinator confirms the project read-only before attaching the custom domain: the project exists, the GitHub source is attached, the output directory is docs, and the production branch is the one in the spec. Every call is recorded with its method, path and status.
The *.pages.dev address is checked as well. If it serves the same content, that is a second proof that the deployment is healthy. If it does not resolve, that is recorded as a cosmetic finding and the project URL is checked in the dashboard; it is not treated as a deployment failure.
Preview deployments are enabled for pull request branches. They are useful for reviewing a site change before it reaches production, and they cost nothing to leave on.
The launch-branch posture
When the owner wants presentation isolation, the site launches from the site branch and nothing merges to the default branch. The default branch keeps the product exactly as it was, the site branch is the presentation artifact, and it is retained indefinitely. Any future site change goes on the same branch and redeploys.
This posture is recorded in the spec and the tracker. It is never tidied up later by merging without a new decision.
Traps that bit
| Symptom | Cause | Fix |
|---|---|---|
| The social image returns 404 on the live site | A root ignore rule excluded it and it was never committed | Add an exception, commit, redeploy, and verify with git check-ignore |
| The certificate stays at none | The site was created before DNS existed | Clear the custom domain, wait, set it again, poll until approved |
| HTTPS enforcement fails with a certificate error | It was enforced before the certificate was approved | Wait for approved, then enforce |
| Pages serves an old commit | No rebuild after switching the source | Trigger a build and poll the commit |
| The site works but the sitemap 404s | The sitemap was generated but not committed | Regenerate, commit the output, push |
| A page is live but stale | The builder output was not regenerated | Run the builder, commit docs/, push |
Verification
The deploy is verified at the served commit, not just at the URL. The root answers over HTTPS, HTTP redirects to it, every sitemap URL returns 200, the social image is 1200 by 630, and the served commit equals the expected head. The deployment id and commit are recorded in the report.
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" http://<host>/
curl -s -o /dev/null -w "%{http_code}\n" https://<host>/og.png
curl -s https://<host>/sitemap.xml | grep -o "<loc>" | wc -lThe reusable script is scripts/verify_site.py. It checks the redirect, the root, the social image, robots and every sitemap URL, and it can run against a local docs/ directory without any network calls.