Skip to content
gilltrish.dev

How I built this blog · Part 2

How I built this blog, Part 2: putting it on the internet

Beginner12 min read

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 name in wrangler.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:

web/wrangler.jsonc· JSON
// 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.

web/public/_headers· Text
# 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

  1. Publish in Studio
  2. Sanity webhookRings the doorbell
  3. Cloudflare Deploy HookAnswers the door
  4. BuildAstro bakes fresh pages
  5. 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, address 192.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_URL to https://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:

Checking the redirects

    
        
        
        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.dev loads with the padlock; http:// redirects to https; www redirects to the root.
  • Sanity Studio is unaffected: I published a small change and it showed up on gilltrish.dev about 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_URL changed 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.jsonc on 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!