The architecture behind nicklosier.com — the domain, the hosting, the build, and how the source stays private while the site stays public. Written to be reusable by anyone who owns a domain and wants to put a real site on it.
GoDaddy domain registration + DNS
| A records -> GitHub Pages edge
v
GitHub Pages static hosting + automatic TLS
^
| deploys the build artifact
GitHub Actions runs on every push to main
^
| builds
Private repo Vite + React + Tailwind source
Four moving parts, none of them a server. Running cost is the domain renewal. Nothing here needs patching, and the site will still be up and unchanged in three years.
The domain is registered at GoDaddy and its DNS stays there. GitHub only needs to be pointed at, not delegated to.
For an apex domain — example.com, no www — add four A records, all
with host @:
185.199.108.153
185.199.109.153
185.199.110.153
185.199.111.153
Then one CNAME record, host www, pointing at
username.github.io.
Two GoDaddy-specific notes. Do not use GoDaddy's Domain Forwarding to reach the site; it
breaks TLS and the address bar, and Pages will not issue a certificate through it. And GoDaddy ships a
parked-page A record on @ by default — delete it, or it competes with the four above.
Once DNS resolves, tick Enforce HTTPS in the repo's Pages settings. GitHub issues the certificate automatically, which takes somewhere between a few minutes and an hour. If the checkbox is greyed out, DNS has not propagated yet. Wait rather than changing things.
Pages serves static files from a repository, free, with TLS included.
Deploy with GitHub Actions, not the legacy branch mode. The old approach publishes a
gh-pages branch exactly as committed, which means committing build output. The current approach
runs a workflow: check out, npm ci, npm run build, upload the built folder as an
artifact, deploy it. Build output never enters the repo.
Turning Pages on for a repo that never had it needs an owner credential — the Actions token can configure a site but cannot create one:
gh api -X POST repos/OWNER/REPO/pages -f build_type=workflow
After that the workflow's configure-pages step is a harmless no-op.
Put a CNAME file in the build's static folder. For Vite that means
public/CNAME, containing one line with the domain. This is the tip worth the whole page. GitHub
stores the custom domain in a CNAME file at the root of the published output. If that
file exists only because the domain was typed into the Pages settings UI, the next deploy overwrites the
output, silently drops the domain, and the site falls back to username.github.io with a
certificate warning. Keeping CNAME in public/ means every build recreates it.
Vite + React + Tailwind, built as a multi-page app rather than a single-page one. Each page gets its own entry
in vite.config.js, its own .html, and a small mount shim, so the pages are genuinely
separate documents rather than client-side routes. That keeps them independently shareable and independently
indexable.
Set the Vite base to / when serving from the apex of your own domain. If it ever
moves back to a username.github.io/repo project path, base has to become
/repo/ or every asset 404s. That is the most common way a working Pages site breaks on a move.
Not every page needs the framework. This one is plain HTML with inline styles, dropped into
public/, which Vite copies through untouched. A page with no interactive behaviour does not need
to be a build entry.
The résumé is a static file — public/resume.pdf, served at /resume.pdf. It is
generated from LaTeX, so the PDF and the site never disagree about dates.
These are two different questions, and conflating them is where people get caught out.
The repository is private. The published output is public. Pages serves whatever the build produces to anyone, with no authentication. Publishing from a private repo requires a paid GitHub plan (Pro or above); from a public repo it is free.
A committed file is exposed whether or not the site links to it. If the repo is public,
moving an image out of public/ is not enough — it has to leave the tracked tree. Deleting it in a
later commit is not enough either, since the history still has it.
Anything sensitive lives in a gitignored directory. This project keeps a
.private/ folder at the repo root: inside the working tree for convenience, gitignored so it
never enters git history. Being ignored is the only protection, so nothing in it is force-added or copied into
a tracked path.
Scan before publishing, over the build and the tracked source. Scanning only the built output misses everything that is committed but not bundled — notes, scripts, docs. Both need checking. If the material originated somewhere with confidentiality obligations, keep the list of strings you are scanning for outside the repo as well; a checked-in list of things to redact is itself a leak of those things.
Some pages are written for one reader and should not turn up in a search for your name. Unlisted is not the same as secret — anyone with the link can read it, which is fine for a résumé.
Two mechanisms, and one trap:
<meta name="robots" content="noindex, nofollow"> in the <head>.
This is the one that actually keeps it out of the index.
sitemap.xml. The sitemap here lists the hub and this page only.Disallow line in robots.txt. That file is public and
routinely read by strangers, so a Disallow publishes the exact path it was meant to conceal. It
is the most common self-inflicted leak in this category.