There is one fact that organises everything else: no server ever executes my code to serve responses to client requests. GitHub Actions runs my build script once per trigger, in a throwaway container, to produce a pile of static files; GitHub Pages then serves that pile without touching my code again. After that, “serving” is just a CDN handing out pre-computed bytes.

The build and the distribution happen on completely different machines, owned by completely different fleets, at completely different times. This post traces the path of a single page - the one you’re reading - from a Markdown file in my private notes to bytes painted in your browser.


1. The build is a content heist, not a checkout

This site is generated by Quartz, a static-site generator written in TypeScript that turns Markdown into HTML. I run my own fork to carry the CI/CD pipeline described below. The stock setup keeps your content in a content/ folder inside the Quartz repo itself. Mine doesn’t. The deploy workflow does something more deliberate:

- name: Clone vault
  run: |
    git clone --depth 1 --no-checkout \
      https://x-access-token:${{ secrets.VAULT_TOKEN }}@github.com/suisuss/.obsidian.git /tmp/vault
    cd /tmp/vault
    git sparse-checkout init --cone
    git sparse-checkout set "Writing" "Knowledge Base" "Files" "About Me" "index.md"
    git checkout

The Quartz repo is the machinery; the writing lives in a different, private repo - my Obsidian vault. At build time the CI runner clones the vault with a scoped token, sparse-checks out only a few directories, and copies them into Quartz’s content/.

Then the part that matters:

if head -c 9 "$f" 2>/dev/null | grep -q "GITCRYPT"; then
  continue
fi

The vault is encrypted at rest with git-crypt, and the runner never unlocks it. So every private note still carries its \0GITCRYPT magic bytes, and the copy step skips anything bearing that header. Encryption isn’t only protecting the repo - it’s the publish filter. A note reaches this website if and only if it’s plaintext in the vault. The default is secrecy; publication is the deliberate act of not encrypting a file.

That’s the right way round, and there is a human gate: I commit and merge every file into the vault myself, so nothing reaches the build that I didn’t put there. But the gate guards authorship, not classification. The decision that actually controls exposure - encrypt this, leave that plaintext - is one I make by hand, once, with no second reviewer. A file I forget to encrypt, or one that doesn’t match a crypt pattern, publishes on the next build exactly as cleanly as something I meant to share. The encryption rules fail open, so the real risk isn’t an unreviewed file - it’s a misjudged one.

After the copy, it’s ordinary Quartz: install dependencies, parse and transform the Markdown (wikilinks, LaTeX, syntax highlighting), and render everything to static HTML, CSS, and JavaScript in a public/ directory. That directory is the entire product. Everything after this point is logistics.


2. The artifact is uploaded to a CDN, not deployed on a server I administer

The build ends by packaging public/ as an artifact and handing it to GitHub’s Pages infrastructure, which ingests it and makes it the new content for suisuss.github.io/quartz. No file ever lands on a server I administer. There is nothing to SSH into.

The trigger block hides a subtlety:

on:
  push: { branches: [v4] }
  workflow_dispatch:
  schedule:
    - cron: "0 6 * * *"   # Rebuild daily to pick up vault changes

The Quartz repo rarely changes, but the vault changes constantly - and a vault edit doesn’t fire the Quartz repo’s workflow, because GitHub gives you no cross-repo event for it. The daily 06:00 UTC cron exists to paper over exactly that gap: once a day the pipeline re-clones the vault and rebuilds, so my writing surfaces within a day even when I never touch the site’s own repo. It’s a polling workaround for a dependency the platform won’t notify you about.


3. You open the URL: a name becomes four IP addresses

Now the distribution half. You open https://suisuss.github.io/quartz/. Only the hostname, suisuss.github.io, is a DNS question - /quartz/ never touches the Domain Name System at all. The path is just bytes your browser sends later, inside the HTTP request itself, once it already has a connection to send them over.

Resolving that hostname means climbing a delegation hierarchy, one referral at a time - not one query, four. dig +trace suisuss.github.io shows every hop live (DNSSEC records trimmed for readability):

;; a root server: who's authoritative for .io?
io.  NS  a0.nic.io.
io.  NS  b0.nic.io.
io.  NS  c0.nic.io.
io.  NS  a2.nic.io.
;; from g.root-servers.net

;; a .io server: who's authoritative for github.io?
github.io.  NS  dns1.p05.nsone.net.
github.io.  NS  ns-692.awsdns-22.net.
;; from a0.nic.io

;; a github.io nameserver: what's the record?
suisuss.github.io.  3600  IN  A  185.199.108.153
suisuss.github.io.  3600  IN  A  185.199.109.153
suisuss.github.io.  3600  IN  A  185.199.110.153
suisuss.github.io.  3600  IN  A  185.199.111.153
;; from dns1.p05.nsone.net

Three referrals, three separate organisations - IANA’s root operators, the .io registry, then GitHub’s own nameservers - and the final answer is a plain A record. A custom domain pointed at Pages would need one more indirection, a CNAME aliasing it to username.github.io; a bare address like mine skips straight to the A record.

Those four addresses are the canonical GitHub Pages endpoints, and the property that matters is that they’re anycast. The same four IPs are announced from points of presence all over the planet. Your packets don’t travel to one datacentre - the internet’s routing fabric delivers them to the topologically nearest edge that advertises the route.

I can prove that from the response headers. My own request to this site was answered by a cache identifying itself as cache-mel11280-MEL - a Fastly node in Melbourne. A reader in Frankfurt hitting those identical four IPs would land on a German node instead. One address, many machines, geography resolved by routing rather than by a different number.


4. TCP, then TLS: building a trusted pipe

With an IP in hand, your browser opens a TCP connection to port 443 - the three-way handshake (SYN, SYN-ACK, ACK) that turns IP’s unreliable, possibly-out-of-order packets into an ordered, reliable byte stream. That is TCP/IP’s entire job: a dependable pipe built on an undependable network.

Immediately on top of it, TLS. Here’s what actually crosses the wire for this exact connection (curl -v, trimmed):

TLSv1.3 (OUT)  Client Hello
TLSv1.3 (IN)   Server Hello
TLSv1.3 (IN)   Encrypted Extensions
TLSv1.3 (IN)   Certificate
TLSv1.3 (IN)   CERT verify
TLSv1.3 (IN)   Finished
TLSv1.3 (OUT)  Finished

SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
ALPN: server accepted h2
Server certificate: subject CN=*.github.io, issuer Let's Encrypt

Seven messages and the pipe is authenticated and encrypted before a single byte of the page has moved. The certificate covers *.github.io, not suisuss.github.io specifically - one wildcard cert serves every Pages site. The ALPN line is where HTTP/2 actually gets chosen, as a field inside this same handshake rather than a separate negotiation afterward - which is why the eventual response status line simply reads HTTP/2 200, with nothing left to negotiate. The response also carries strict-transport-security: max-age=31556952, so after your first visit the browser refuses to even attempt unencrypted HTTP to this host for a year.

That HTTP/2 choice matters here because a page like this pulls many small files (fonts, the site’s script bundle, prefetched pages). HTTP/2 multiplexes them all over the single TLS connection, instead of paying for a fresh handshake per file the way older HTTP effectively forced.


5. The request, and what’s actually on the far end

Only now does an HTTP request travel down the pipe - and over HTTP/2 there’s no literal request-line text on it. A real capture against this exact URL shows framed pseudo-header fields instead:

:method: GET
:scheme: https
:authority: suisuss.github.io
:path: /quartz/
user-agent: curl/8.14.1
accept: */*

A browser sends the same fields, plus accept-encoding and any cached validators. That’s all HTTP is, framing aside - a method, a target, headers, an optional body, and a structured response. Everything below it exists to carry these few fields safely.

What answers is a layered system, and the response headers expose every layer:

  • via: 1.1 varnish and x-fastly-request-id - the first thing to touch the request is Fastly’s cache at the edge. Fastly sits in front of github.io and caches every successful response. For a static site this is almost the whole game: most reads never travel further than your nearest city.
  • x-cache: MISS - this particular object wasn’t in the edge cache yet (cold, or freshly purged after a deploy), so the edge had to fetch it from origin.
  • server: GitHub.com - the origin. This is GitHub’s own Pages serving tier, holding the public/ artifact the deploy job uploaded. It’s a fileserver fronted by a routing layer that maps the hostname to the right backend and streams the file. It runs no code of mine.

So the chain is: your browser → Fastly edge cache → GitHub Pages origin → the static file. On a cache hit it’s just the first two boxes. There is no database lookup for this content, no template rendered on demand, no application process. The expensive work happened once, in step 1, in a container that no longer exists.


6. Caching is the actual serving strategy

The freshness headers are where the design philosophy lives:

cache-control: max-age=600
etag: "6a12b809-92df"
last-modified: Sun, 24 May 2026 08:34:17 GMT

max-age=600 tells caches the page is good for ten minutes. The etag is a fingerprint of the bytes; on your next visit the browser sends it back and, if nothing changed, receives a tiny 304 Not Modified instead of re-downloading the whole page.

Combine this with the daily rebuild and you get the real propagation story. An edit to my notes becomes visible only after the cron fires, the build and deploy finish, the edge cache is purged, and your own browser’s ten-minute copy expires. Staleness is bounded by roughly a day, not by anything instant. That’s the price of never running a server: I trade write-latency for read-scalability. This site can absorb a front-page traffic spike without my noticing - but a typo fix isn’t live until the next rebuild.


7. Your browser reassembles a website

The bytes arrive as gzipped text/html. Your browser parses the HTML, builds the DOM, discovers the linked stylesheet and script, fetches them over the same HTTP/2 connection, loads the fonts, and paints the page.


The shape of the whole thing

My private vault is the source of truth, encryption decides what’s public, a CI container compiles the public subset to static files once, GitHub stores them, and a CDN hands out cached copies from a machine near you.

Three decisions in this pipeline aren’t generic - pulling content from a separate encrypted repo so the writing and the renderer stay decoupled, using the encryption itself as the publish gate so secrecy is the default and exposure is the deliberate act, and polling with a daily cron because GitHub gives two repos no way to notify each other. The rest - DNS, TCP, TLS, HTTP, anycast, edge caching - is the same reliable machinery under every static site on the internet.