Moving my portfolio from GitHub Pages to Cloudflare Pages
My portfolio started life as a GitHub Pages project site —
stayz3ro.github.io/portfolio/ — with the real domain, chrisalorenzo.com,
sitting at Porkbun doing nothing but showing a parking page and a URL
forward. That was fine for a placeholder. It stopped being fine once the
site was something I actually wanted people to land on.
This is the migration to Cloudflare Pages: hosting, DNS, TLS, and email alignment, plus the parts that bit me.
Why move at all
GitHub Pages is a perfectly good static host. The reasons to leave were specific:
- Apex domain. I wanted the site at
chrisalorenzo.com, not a subdirectory of agithub.iosubdomain. Pages can do apex domains, but the DNS side (fourArecords to GitHub’s IPs, plus a separateCNAMEforwww) is clunky and GitHub’s IPs have changed before. - One control plane. With Cloudflare I get DNS, CDN, and the TLS certificate in the same dashboard instead of split between a registrar and GitHub.
- It sets up everything after it. The blog you’re reading now, and the VPS services later, all sit behind Cloudflare. Doing the portfolio first meant doing the move once and reusing the runbook.
Cloudflare Pages itself is close enough to GitHub Pages that the hosting swap was the easy part — connect the repo, set a build command, done. DNS was the work.
Step 1 — DNS authority to Cloudflare
The registration stays at Porkbun. Only the nameservers move.
- Add the site in Cloudflare (Websites → Add a site), free plan. Cloudflare scans the existing zone and imports what it finds.
- Cloudflare assigns two nameservers — mine were
felipe.ns.cloudflare.comandmelissa.ns.cloudflare.com. - In Porkbun: Details → Authoritative Nameservers → Edit, replace Porkbun’s with the two Cloudflare ones.
- Wait for Cloudflare to confirm the zone is active. Mine took under an hour; it can take longer.


Before flipping, I went through the imported records and:
- Deleted the parking
Arecords and the URL-forward record — dead weight from the placeholder era. - Kept the
MXrecords and theSPFTXTrecord. Porkbun still does my email forwarding for the domain, and losing those would silently break inbound mail.
Check what your registrar was doing for you before you take DNS away from it. Email forwarding and parking redirects live in DNS, and they stop the moment the nameservers change if you didn’t carry the records over.
DNSSEC was already off, so there was nothing to coordinate there. If yours is on, disable it at the registrar first, wait for it to clear, then move — otherwise resolvers reject the zone during the handover.
Step 2 — the Cloudflare Pages project
-
Workers & Pages → Create → Pages → Connect to Git, authorise the repo.
-
Build settings:
Setting Value Framework preset Vite (Astro for this blog) Build command npm run buildBuild output directory distProduction branch main -
First deploy runs on save. After that, every push to
maintriggers a build — same model as the GitHub Actions workflow I deleted, minus the workflow file.

The GitHub Pages deploy workflow (.github/workflows/deploy-pages.yml)
came out in the same commit. GitHub is source-only now.
The gotcha: base path
The Vite config still had base: '/portfolio/' from the GitHub Pages
project-site days. On a real domain the site serves from the root, so every
asset URL was off by a directory and the first deploy rendered as
unstyled HTML.
export default defineConfig({
- base: '/portfolio/',
+ base: '/',
plugins: [react()],
});
Astro has the same setting (base in astro.config.mjs) and the same
trap. If you scaffolded for a project site, this is the line to fix.
Step 3 — custom domains
In the Pages project, Custom domains → Set up a domain, added both
chrisalorenzo.com and www.chrisalorenzo.com.
Because DNS is now on Cloudflare, this is one click each — Cloudflare creates the records itself:
chrisalorenzo.com→CNAMEto thepages.devtarget, with CNAME flattening at the apex (a plain DNS provider can’t put a CNAME on the apex; Cloudflare fakes it correctly).www→CNAMEto the same target.
Both went to Active with certificates issued in a few minutes. No manual certificate step — Cloudflare provisions and renews it.

Step 4 — TLS and email posture
Two settings I changed while I was in there:
- SSL/TLS mode → Full (strict). Pages serves a valid cert on its origin, so there’s no reason to run in Flexible (which leaves the Cloudflare-to-origin hop unencrypted) or plain Full (which doesn’t validate it).
- Managed DMARC. Cloudflare’s Email → DMARC Management adds the
_dmarcTXTrecord for you. I started atp=noneto collect reports without affecting delivery; the plan is to tighten top=rejectafter a couple of weeks of clean aggregate reports.
What actually broke
Nothing user-facing, but for the record:
- The base path (above) — caught on the first preview deploy, not in production.
- A few minutes of overlap where
stayz3ro.github.io/portfolio/andchrisalorenzo.comboth served the site. GitHub Pages keeps serving until you unpublish it in repo settings; do that once the custom domain is live so there’s one canonical URL. og:urland the “live site” links in the README still pointed at thegithub.iopath. Easy to forget because the site looks fine — search and link previews don’t.
The lesson
The hosting migration was 20 minutes. DNS was the rest, and the risk lived entirely in the records I wasn’t thinking about — MX, SPF, the forward — because those fail silently and days later. When you move nameservers, inventory the whole zone first and treat every record you don’t recognise as load-bearing until you’ve proven otherwise.
Same move is queued for stayz3ro.dev next, which is where this blog will
live. That one has a stale A record pointing at an address that hasn’t
answered in months — so the pre-migration cleanup matters there too.