How I built this blog · Part 2
How I built this blog, Part 2: putting it on the internet
The 30-second version
Cloudflare stores the finished pages and serves them from near every visitor. Publishing in Sanity pings Cloudflare, which rebuilds the site in about a minute. I bought the domain through Cloudflare, pointed it at the site, and HTTPS just worked.
On this page
We’re live!
What's hosting, for a static site?
The bakery analogy from Part 1: the cakes are already baked; hosting is the shop window. No oven (server code) needed, so it's cheap/free and fast.
Why Cloudflare
It hass a free tier for static sites, a global CDN, and builds from GitHub. Domains are sold at cost, and it’s got DNS + SSL all in one place.
Part A: From GitHub to a live URL (done)
Step 1:Push the repo to GitHub
Cloudflare builds the site straight from GitHub, so the code needs to live there first.
Step 2:Import the repository in Cloudflare
Workers & Pages → Create → Import a repository, then pick the repo.
Step 3:Fill in the build settings
- Root directory:
web - Worker name: matches
nameinwrangler.jsonc - Build command:
npx astro build - Deploy command:
npx wrangler deploy - Build variables:
NODE_VERSION=24,SITE_URL
Forgot the root directory?
The build fails with Cannot resolve entry module astro/entrypoints/prerender. My first build failed as I had the root set to / and not /web. As soon as I edited that, builds were fixed.
Name mismatch
If the Worker name doesn’t match name in wrangler.jsonc, the deploy fails.
Step 4:Run the first build
The first build gives you a *.workers.dev URL, and the site is on the internet. We’re not just localhost anymore! Yay!
Two small files decide how Cloudflare serves the site:
// Cloudflare Workers config: static assets only, no Worker code runs.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "gilltrish-dev",
"compatibility_date": "2026-10-27",
// Every deploy applies this file, so dashboard-only domain settings get reset. Keep them here.
"routes": [{"pattern": "gilltrish.dev", "custom_domain": true}],
// The *.workers.dev address would be a second copy of the site; only the real domain serves it.
"workers_dev": false,
// Builds of other branches still get their own preview URL.
"preview_urls": true,
"assets": {
"directory": "./dist",
// /blog/my-post serves dist/blog/my-post.html; /blog/my-post/ redirects to it.
"html_handling": "auto-trailing-slash",
// Unknown URLs get dist/404.html with a real 404 status.
"not_found_handling": "404-page",
},
}Explain it like I’m five
Most of this file is Cloudflare housekeeping, but two settings change what visitors see:
- Clean URLs (
html_handling). Astro bakes each post into a file like my-post.html, but nobody wants .html in their address bar. This setting serves that file at /blog/my-post. If someone types it with a slash on the end, they get sent to the version without one, so every page has exactly one address. - Real 404s (
not_found_handling). If someone asks for a page that doesn't exist, they get my own "page not found" page, and Cloudflare also tells the browser it's a 404. That second part matters: without it, a missing page can look like a real one to Google.
This is the finished version of the file. The routes, workers_dev and preview_urls lines came later, when I connected my domain. I'll explain them in that step.
# Cloudflare reads this file at deploy time; it isn't served as a page.
# Astro fingerprints build assets (CSS, JS, fonts): a changed file gets a new name,
# so browsers can keep these forever without ever seeing a stale copy.
/_astro/*
Cache-Control: public, max-age=31536000, immutable
# Baseline security headers for every response.
/*
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
X-Frame-Options: DENY
Permissions-Policy: camera=(), microphone=(), geolocation=(), browsing-topics=()Caching and safety headers
Headers are little notes the server sends along with every file, telling the browser how to handle it.
Caching: When Astro builds the site, it gives the CSS and JavaScript files names with a random-looking code in them, like index.a1b2c3.css. If I change the file, the code changes, so it gets a brand-new name. That means the browser can safely keep its copy for a whole year (max-age=31536000 is a year in seconds) without asking "is this still fresh?" (immutable). Pages load faster on a second visit, and nobody ever sees an old stylesheet. The pages themselves don't get this rule, so new posts show up right away.
Safety: The other four lines are a basic set of precautions for every page:
- X-Content-Type-Options: nosniff: "Believe the label." The browser treats each file as the type it says it is, and doesn't guess.
- Referrer-Policy: When you click a link from my site to another one, that site only learns you came from gilltrish.dev, not which page you were on.
- X-Frame-Options: DENY: Other sites can't show my pages inside a frame on theirs, which is a trick used to get people to click things they can't see.
- Permissions-Policy: My pages can't ask for your camera, microphone or location, and they opt out of the browser's ad-interest tracking (browsing-topics).
None of these are needed for a simple blog to work. They're cheap, sensible defaults.
Test the real setup locally
npm run preview:cloudflare builds the site and serves it the way Cloudflare will.
Part B: Rebuild when I hit Publish
Webhook
A doorbell. Sanity rings it when something’s published; Cloudflare answers by rebuilding the site.
Publish to live in about a minute
- Publish in Studio
- Sanity webhookRings the doorbell
- Cloudflare Deploy HookAnswers the door
- BuildAstro bakes fresh pages
- Live~1 minute later
Step 5:Create a Cloudflare Deploy Hook
Here’s a great walkthrough via Cloudflare Docs, screenshots and steps included!
Step 6:Add a Sanity webhook
- Dataset:
production - Trigger on: create, update, delete
- Filter by document type
_type in ["post", "series", "tag", "author", "project"] - Trigger on drafts: off
Leave drafts off
With “Trigger on drafts” on, every save while typing kicks off a rebuild.
What I’d tell past me
Several publishes in a row get collapsed into one build.
Part C: My own domain + SSL
Domain, registrar, DNS
- Domain: the shop’s street address.
- Registrar: the city office you rent it from (yearly).
- DNS: the map that turns the address into “the shop is here”.
SSL / HTTPS
The padlock: a sealed envelope between visitor and site. Cloudflare issues the certificate automatically.
Step 7:Buy the domain through Cloudflare Registrar
There are two routes (Cloudflare docs, checked 2026-10-30): the Worker’s Domains tab (new May 2026) can buy a domain and attach it in one flow, or the classic Domain Registration → Register domains page. I used the Worker route:
Workers & Pages → `gilltrish-dev` Worker → Domains → Add Domain → Purchase Domain → Domain Look up
- The lookup shows the name across many endings (.com, .net, .org…), not just the one you typed.
- You need a verified account email, and the domain goes to whichever account you’re logged into.
- Price for
gilltrish.dev: $8.20 the first year, renews at $12.20/year (2026-10-30). Budget for the renewal price.
Checkout
- Registration period: defaults to 1 year. Kept it.
- Automatic renewal: on by default (“Auto-renews at $12.20 per year”). Kept it on.
- Domains automatically use Cloudflare nameservers: Cloudflare is the map-maker for this address, so there’s no DNS setup at another company.
Check the spelling
“Domain registrations are non-refundable… Confirm spelling before purchase.”
Why .dev needs HTTPS
.dev requires HTTPS (it’s on browsers’ HSTS preload list). Without SSL the site won’t load at all. Cloudflare provides the certificate free and automatically.
Registrant information
Easy to miss: it shows “No registrant contact” with a small + Add button, while the main button says “Continue to payment”. Add the contact first (name, address, email).
WHOIS
A public phone book of who owns each domain. Cloudflare hides your personal details in its WHOIS (check it out here), but “the registry may still display this information in its own WHOIS service, outside Cloudflare’s control.”
Included at no extra cost:
- WHOIS Privacy: hides your personal information.
- DNSSEC: DNS security extensions.
- Email Forwarding: email aliases, e.g.
hello@gilltrish.dev→ your Gmail. I may look at adding this later, but haven’t just yet.
Entered billing info → Register domain. On success it sent me straight back to the Worker’s Domains tab (docs say activation takes up to 30 seconds).
Registrations show under Domain management, not Active Subscriptions.
What I’d tell past me
Cloudflare sells domains at cost. The catch: DNS must stay on Cloudflare, which for me, was just convenient.
Step 8:Connect it to the Worker
Buying didn’t connect it
Buying from the Worker’s Domains tab did not connect the domain. Back on the tab, nothing was listed. Fix: Add domain again; the new domain now appears in the list, so select it.
- Subdomain (optional): “Leave empty for root domain”. Left blank.
- Enable for: Production: “Routes production traffic to this domain”. Kept Production.
- Clicked Add domain.
Root vs subdomain
The root (or apex) is gilltrish.dev itself; a subdomain is something.gilltrish.dev. www is technically a subdomain too.
The Domains tab now shows the Worker URL (*.workers.dev, plus a Preview one) and Custom Domains and Routes with gilltrish.dev. No status column, no mention of SSL.
Time until live: almost instant. https://gilltrish.dev loaded with the padlock right away. Whoohoo!
Live, but still using the old name
The canonical link, og:url, og:image and the sitemap line in robots.txt all still said https://gilltrish-dev.gilltrish-developer.workers.dev, because SITE_URL was still set to that. That’s what step C3 fixes.
Also noticed: http://gilltrish.dev answered 200 instead of redirecting to https. Browsers never go there for .dev (HSTS preload), but turn on Always Use HTTPS anyway (next step). www.gilltrish.dev didn’t answer at all yet, as expected.
Two different doors
Custom domains match exactly, so gilltrish.dev and www.gilltrish.dev are two different doors.
Pick one address
Pick one canonical version (I chose no www) and redirect the other, or you get duplicate pages.
Step 9:Turn on Always Use HTTPS and redirect www
These are domain settings, not Worker settings: account home → Domains → gilltrish.dev.
- SSL/TLS → Overview: encryption mode was already Full (if it’s Off, the next toggle is hidden). [verify: one-line eli5 of Full vs Flexible vs Strict, or skip: there’s no origin server]
- SSL/TLS → Edge Certificates → Always Use HTTPS: on.
- DNS → Records → Add: type A, name
www, address192.0.2.0, Proxied.
A pretend address
192.0.2.0 comes from a range reserved for examples. Because the record is proxied, Cloudflare answers before anything tries to go there.
The redirect rule
Rules → Redirect Rules → Add rule → Create new Single Redirect. There are three templates; I chose Redirect from WWW to root → Create from template.
What I’d tell past me
The “HTTP to HTTPS” template does the same job as the Always Use HTTPS toggle. Use one, not both.
- Rule name: dropped “[Template]” (it’s just a label in the rules list).
- If incoming requests match: Wildcard pattern, Request URL
https://www.* - Then: Target URL
https://${1}, status 301 – Permanent Redirect - Preserve query string: ticked it.
Preserve query string is off by default
Without it, www.gilltrish.dev/blog?utm_source=linkedin would lose the ?utm… part.
The pattern
https://www.* catches anything starting with www; ${1} is whatever the * matched, pasted after https://. Deep links keep their path, and it works without typing the domain. 301 means “moved permanently” and tells search engines to move everything to the main address.
The scary deploy warning
Clicking Deploy warned: “This rule may not apply to your traffic. Your DNS configuration may not be proxying traffic for www…”. The proxied www record already existed, so I chose Ignore and deploy rule anyway. This is why the DNS record comes first; if you skip it, the other option creates it for you.
http://www… works too: Always Use HTTPS upgrades it first, then this rule drops the www (two quick hops).
Step 10:Tell the site its new address
- Worker → Settings → Build variables: set
SITE_URLtohttps://gilltrish.dev. (Removing it also works, since the code default is the same, but setting it is explicit and visible in the dashboard.) - Worker → Deployments → ⋯ on the latest build → Retry build. Saving a variable doesn’t rebuild by itself. About a minute.
A 522 on www
Right after deploying, https://www… returned 522: Cloudflare tried the placeholder address and timed out because the rule wasn’t live yet. A 522 on www means DNS is fine but the redirect rule isn’t live.
Final checks (2026-10-30). curl -I shows redirects without the browser cache getting in the way:
curl -I "https://www.gilltrish.dev/blog/test?utm_source=x"
301 → https://gilltrish.dev/blog/test?utm_source=x (path + query kept)
curl -IL http://www.gilltrish.dev/about
301 → https://www.gilltrish.dev/about → 301 → https://gilltrish.dev/about → 200
curl -I http://gilltrish.dev
301 → https://gilltrish.dev/
View source on a post: canonical, og:url and og:image are all https://gilltrish.dev/.... Check /sitemap.xml, /robots.txt and /rss.xml.
Step 11:Check everything
https://gilltrish.devloads with the padlock;http://redirects to https;wwwredirects to the root.- Sanity Studio is unaffected: I published a small change and it showed up on
gilltrish.devabout a minute later, so the webhook still works. - Share preview check with LinkedIn Post Inspector.
Retiring the workers.dev address
I turned off the Production toggle under the Worker’s Domains tab → Worker URL, and left Preview on. The canonical already pointed to the real domain, so this is tidying, not required.
The dashboard toggle doesn’t stick
With the toggle off, the old address still loaded (CF-Cache-Status: HIT). Bigger catch: every deploy reapplies wrangler.jsonc, and with no routes in it Wrangler treats workers_dev as on, so the next build would quietly switch the address back on. The fix is to declare it in the file (the routes, workers_dev and preview_urls lines in the wrangler.jsonc above).
Config file vs dashboard
The config file is the boss; the dashboard is a sticky note the next deploy throws away.
After pushing that change (~1 minute to build): gilltrish.dev 200, workers.dev 404, www 301 with path + query kept.
Google Search Console
- Add property → Domain (not URL prefix):
gilltrish.dev. - Google detected Cloudflare and offered Verify with Cloudflare → Authorize → back in Search Console. No copy-pasting DNS records.
- “Ownership verified”, method: Domain name provider.
Domain vs URL prefix
A Domain property covers www/non-www and http/https in one; URL prefix is one exact address.
Don’t delete the TXT record
“To stay verified, don’t remove the DNS record.” The google-site-verification=… TXT record now lives in Cloudflare DNS; leave it alone.
Submit the sitemap: Indexing → Sitemaps → Add a new sitemap.
The sitemap box is empty
On a Domain property the box isn’t pre-filled with your address. Type the full URL: https://gilltrish.dev/sitemap.xml.
Result: Success, 19 discovered pages, matching the live sitemap exactly.
What I’d tell past me
The Performance and Pages reports stay empty (“Processing data”) for a few days on a new property. That’s normal.
I haven’t used URL inspection → Request indexing yet, on purpose; waiting until there are a few more posts.
Surprises along the way
What didn’t match my expectations
- Buying from the Worker’s Domains tab didn’t connect the domain.
- No status or SSL step shown at all; it was just live.
- The site still called itself the old address until
SITE_URLchanged and it rebuilt. - The redirect template leaves Preserve query string off.
- A deploy warning about the www DNS record even though it existed.
- Testing www before the rule was deployed gave a 522.
- Search Console’s sitemap box isn’t pre-filled on a Domain property.
- The dashboard workers.dev toggle loses to
wrangler.jsoncon the next deploy.
Common ones I didn’t hit: DNS taking minutes, a certificate stuck on “pending”, the browser caching an old error page (try a private window).
Wrap-up
Whew! What a whirlwind on a Friday night. While this may seem like a lot of steps, it is pretty straight forward and didn’t take long (under an hour).
What’s next? I want to try something I’ve never done before, so come along with me while I build interactive demos via islands!