GitHub Pages Custom Domain Setup
Configuring a custom domain for GitHub Pages requires coordinating DNS, repo settings, build tool config, and deployment mode — each can silently break the others.
Tags
GitHub Pages Custom Domain Setup
The Lesson
Custom domain setup for GitHub Pages is a four-layer coordination problem: DNS records, repository Pages settings, build tool configuration, and deployment mode. A mistake at any layer produces a 404 with no obvious error message. Understanding which layer controls what prevents the silent failures that make this setup frustrating.
Context
Lessons Hub is a static site built with Astro and deployed to GitHub Pages via GitHub Actions (actions/deploy-pages@v4). The site originally lived at bonjohen.github.io/lessons/ (subpath deployment). Moving it to lessons.johnboen.com (custom subdomain at root) required changes at every layer of the stack.
What Happened
- DNS configured correctly. Added a CNAME record:
lessons.johnboen.com→bonjohen.github.io. GitHub's DNS check passed immediately. - Custom domain set in GitHub Settings. Entered
lessons.johnboen.comin Settings → Pages → Custom domain. GitHub auto-created aCNAMEfile at the repository root containinglessons.johnboen.com. - First breakage: all navigation links 404'd. The Astro config still had
base: '/lessons', which prefixed every internal link with/lessons/. On the custom domain (serving from/), those paths didn't exist. Fixed by changingbaseto'/'andsiteto'https://lessons.johnboen.com'. - Second breakage: CNAME file conflict. Astro copies
public/intodist/at build time, so the correct location for the CNAME file ispublic/CNAME. But GitHub had auto-created a competingCNAMEat the repo root. During a merge, the root CNAME was deleted — which also cleared the custom domain setting in GitHub Pages, resettingcnametonull. - Third breakage: deployment mode mismatch. After the CNAME was cleared, GitHub Pages reverted to
build_type: "legacy"(deploy from branch). The repo usesactions/deploy-pages@v4(Actions-based deployment), which requiresbuild_type: "workflow". The site built and deployed successfully, but GitHub wasn't serving the artifact. - Resolution via API. Fixed both issues with a single API call:
gh api -X PUT repos/bonjohen/lessons/pages \ -f build_type=workflow \ -f cname=lessons.johnboen.com
Key Insights
GitHub auto-creates a root CNAME file when you set a custom domain in the UI. If your build tool manages its own CNAME file (Astro uses
public/CNAME), the auto-created file conflicts. Deleting the auto-created file can clear the custom domain setting entirely.build_typemust match your deployment method. Repos deploying viaactions/deploy-pagesneedbuild_type: "workflow". Repos deploying from a branch needbuild_type: "legacy". A mismatch produces a successful build but a 404 on the site, with no error in the Actions logs.basepath must change when the serving root changes. Moving fromusername.github.io/repo/tocustom.domain.commeansbasegoes from'/repo'to'/'. Every internal link, asset path, and navigation URL is affected. This is a build-time setting — you must rebuild and redeploy after changing it.The Pages API is the fastest diagnostic tool.
gh api repos/{owner}/{repo}/pagesshowscname,build_type,source, andstatusin one call. When the site 404s, check this before anything else — the answer is usually visible in the API response.public/CNAMEis the durable solution for Astro. Astro copiespublic/contents intodist/verbatim. Placing the CNAME file there ensures it survives every build and deploy, unlike a root CNAME file that gets overwritten by the build output.
Examples
Diagnosing a 404
# Check Pages config — look at cname, build_type, and status
gh api repos/bonjohen/lessons/pages
# Response showing the problem:
# { "cname": null, "build_type": "legacy", "status": "built" }
# ↑ domain cleared ↑ wrong for Actions deploys
Fixing via API
# Re-set custom domain and correct build type in one call
gh api -X PUT repos/bonjohen/lessons/pages \
-f build_type=workflow \
-f cname=lessons.johnboen.com
Astro config for custom domain
// Before (subpath deployment)
export default defineConfig({
output: 'static',
site: 'https://bonjohen.github.io',
base: '/lessons',
});
// After (custom domain at root)
export default defineConfig({
output: 'static',
site: 'https://lessons.johnboen.com',
base: '/',
});
Applicability
This applies to any static site generator (Astro, Next.js, Hugo, Jekyll) deployed to GitHub Pages with a custom domain. The specific file location for CNAME varies by tool (public/CNAME for Astro/Next.js, root CNAME for Jekyll since Jekyll doesn't overwrite it), but the coordination problem is the same. The build_type issue is specific to repos using GitHub Actions deployment rather than branch-based deployment.
Related Lessons
- GitHub Pages Build Pipeline — the deployment workflow this lesson's custom domain sits on top of
- Phased Multi-Cloud Infrastructure — the broader multi-deployment context where custom domains become one of several serving endpoints