Sample content from fixtures. Replace it in Payload before launch.

How this site is built: a static blog on a homelab

  • Mursalin Kabir
  • Tech
  • 1 min read

Series: Building smkabir.com, Part 1

A monitor and a laptop showing code on a desk
Photo: Pakata Goh on Unsplash
On this page

This site is a static Astro build. The words live in a headless CMS, but readers never touch it: they get plain files from nginx (or from a mirror in object storage when the homelab is down).

The shape of it

Publishing in the CMS triggers a build. The build produces a dist/ folder, uploads it as a numbered release, and moves a pointer. The pods notice the new pointer and swap their content without a rollout.

Diagram
Diagram source
direction: right
author: You { shape: person }
cms: Payload CMS
ci: GitHub Actions
r2: Object storage { shape: cylinder }
pods: nginx pods
author -> cms: publish
cms -> ci: dispatch
ci -> r2: upload release
pods -> r2: poll LATEST

How releases are laid out:

  • Static output, one folder per release
    1. Pointer file names the live release
    2. Rollback rewrites the pointer
  • Content-sync sidecar pulls the new release

Where the site build stands:

  • Done:Type check clean
  • Done:Pagination tried with a small page size
  • To do:First real post written

Why not just render pages from the CMS?

  • Availability: a static file keeps working when the database does not.
  • Cost: no application runtime on the read path.
  • Security: the CMS is only reachable behind an access gate.

Note

A static site on a single-house homelab is strictly less available than a hosted static platform. The point of running it here is to learn, which is why a failover mirror exists.

Typed content, end to end

The schema is code. Types generated from it are imported by the site, so a schema change that breaks a page fails the type check before it fails a reader.

src/lib/posts.ts
import type { Post } from '~/lib/types';
export function hasPage(post: Post, locale: Locale): boolean {
return post._status === 'published' && post.publishedIn[locale] === true;
}

Building it

A local build is two commands:

Terminal window
pnpm install --frozen-lockfile
pnpm build
i18n: 96 keys present in all locales
[build] 24 page(s) built in 3.1s
[build] Complete!
Published URLs are permanent. Changing a slug without a redirect is a bug.

More in the next part, where the CMS goes into the cluster. See the homelab write-up for the platform.

An interactive demo

Posts can include small interactive pieces written as plain HTML and JavaScript. They run in a sandboxed frame, so they cannot touch the site around them or send anything anywhere.

This frame is author-written HTML with its own script, shown in an isolated frame.