How this blog works (and why it’s built this way)

2026-03-02

A blog is trivial.

Until you take the entire path seriously.

This post describes the system as it exists today, and why each piece is there.

Not to run a blog.

To minimise the cost of change.


Architecture at a glance

flowchart TD

  USER["👤 Reader"]
  INTERNET["🌐 Internet + DNS + TLS"]
  CLOUDRUN["☁️ Cloud Run service<br/>(container running Axum app)"]
  APP["🦀 Rust application<br/>(Axum router + handlers)"]
  CONTENT["📝 content/*<br/>(Markdown + layout fragments)"]

  USER --> INTERNET --> CLOUDRUN --> APP --> CONTENT

Users here means actual readers — someone opening the site in a browser.

They do not interact with GitHub.
They do not know about CI.

They send an HTTP request.
The container responds with HTML.

Everything else exists to support that moment.


Developer loop (local)

flowchart TD

  EDIT["✍️ Edit src/* or content/*"]
  RUN["🦀 cargo run<br/>(RUST_ENV=development)"]
  WATCH["👀 File watcher"]
  WS["🔌 Websocket reload signal"]
  BROWSER["💻 Local browser refresh"]

  EDIT --> RUN
  RUN --> WATCH
  WATCH --> WS
  WS --> BROWSER

In development mode:

Edit. Save. See the change.

No manual restart. No rebuild for content tweaks.

Feedback loops stay short.


Runtime responsibilities

At startup, the application:

There is no database.

Content lives in git.
State lives in files.

A post is simply:

---
title: "Post title"
date: 2026-03-02
slug: 2026-03-02-example
---

Markdown body here.

Rendering is deliberately simple:

No ORM.
No CMS.
No runtime mutation.

Constraint keeps surface area small.


Development loop (production)

flowchart TD

  PUSH["📤 git push to main"]
  GHA["⚙️ GitHub Actions"]
  AUTH["🔐 OIDC → Workload Identity Federation"]
  BUILD["🐳 Docker build"]
  AR["📦 Artifact Registry"]
  DEPLOY["🚀 Cloud Run deploy"]
  USERS["👥 Readers receive new version"]

  PUSH --> GHA
  GHA --> AUTH
  AUTH --> BUILD
  BUILD --> AR
  AR --> DEPLOY
  DEPLOY --> USERS

Push to main.

That is the release process.

GitHub Actions:

  1. Authenticates to Google Cloud (GCP) using OIDC (no stored service account keys)

  2. Detects change type:

    • Full (code + content)
    • Content-only
  3. Builds accordingly:

    Full build

    • Compile Rust
    • Build runtime image
    • Tag with commit SHA

    Content-only build

    • Overlay updated content/
    • Tag with commit SHA
  4. Deploys to Cloud Run

Content changes ship without recompiling the binary.

That is deliberate.

Content is production change.
It should not pay the full rebuild tax.


Infrastructure

Infrastructure is declared in OpenTofu.

It provisions:

There are no manual console steps in steady state.

If something exists, it is declared.

If it is not declared, it does not exist.


Security posture

The container can serve traffic.

It cannot mutate infrastructure.

Boundaries matter.


Why this design?

Most blogs optimise for features.

This one optimises for:

The interesting problem is not publishing text.

It is reducing the cost of safe change.

This system demonstrates:


What this actually signals

The blog itself is not the point.

The system is.

It shows how I think about:

When implementation becomes commoditised, leverage shifts to architecture.

This is a small system.

Small systems reveal principles clearly.

And principles scale.

Whispering Pines in Myriad Valleys (宋李唐萬壑松風圖 軸), Li Tang
Figure. Collection image from Li Tang's "Whispering Pines in Myriad Valleys" (宋李唐萬壑松風圖 軸). Source: Wikimedia Commons (public domain).