← blog

Building This Site

#astro#docker#devlog

Every developer eventually ends up building their own site. Here’s how I built this one.

The Stack

I wanted something that would be easy to maintain long-term. Blog posts and project entries should just be Markdown files — no CMS, no database. The site should be static by default.

The choices:

  • Astro 5 with output: 'static' — generates plain HTML, Markdown content collections built in, zero JS shipped by default
  • Tailwind v4 — CSS-first config via @theme directive, no config file needed
  • nginx in production, Astro dev server in development
  • Docker + Traefik — matches the rest of my local dev setup

Content Collections

Astro 5’s content collections let me define a schema in TypeScript and then write posts as Markdown files:

src/content/
├── blog/
│   └── building-this-site.md
└── projects/
    └── board-game-tracker.md

Type-safe frontmatter, automatic slug generation from filename, and a dead-simple query API:

const posts = await getCollection('blog', ({ data }) => !data.draft);

Local Dev Setup

The site runs on https://jsamackay.local.com locally using mkcert-generated wildcard certs handled by Traefik. Two compose files:

  • docker-compose.yml — production nginx build
  • docker-compose.dev.yml — dev override with Astro’s hot-reload server and volume mounts
# Dev (hot reload)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d

# Prod build
docker compose up -d --build

Tailwind v4

The biggest change from v3: no more tailwind.config.js. Design tokens live in CSS:

@import "tailwindcss";

@theme {
  --color-accent: #00ff41;
  --font-family-mono: 'JetBrains Mono', monospace;
}

These automatically become utility classes: text-accent, bg-accent, font-mono. Clean.

What’s Next

  • Add more projects
  • Write up some of the experiments I’ve been running
  • Maybe add an RSS feed