A laptop and notebook on a desk
building 8 min read

Building michaelbrant.com with Jekyll and Claude Code

A static Jekyll site, a Linux sandbox, Claude Code as the build agent. The whole pipeline.

Stack
Jekyll Sass Docker Cloudflare Claude Code

This site is the build I’m working on right now. It runs on Jekyll, hosted on a personal AlmaLinux sandbox, deployed via Docker, with Claude Code doing most of the actual file editing.

The architecture

The site lives in three folders, each with one job.

/home/almalinux/Sites/michaelbrant.com/ is the Jekyll source — markdown content, Sass partials, Liquid layouts, the lot. This is the only folder I git-commit. It’s also the only folder Claude Code edits.

/opt/stacks/michaelbrant/ is the runtime stack — Dockerfile, docker-compose.yml, nginx.conf. The compose file mounts the source folder as the build context, runs bundle exec jekyll build inside a multi-stage container, and serves the resulting _site/ via nginx behind Traefik. This folder is intentionally separate from the source so I can change deploy config without polluting the content repo.

/home/almalinux/agent-stack/mb-jekyll-build/ is the build orchestration layer — task prompts, build reports, manifests. When I want a new feature, I drop a markdown file describing the work into the prompts folder. A watcher script picks it up, hands it to Claude Code, and the resulting changes land in the source folder. The reports folder accumulates a record of every build pass.

How Claude Code fits in

The pattern is simple and it took longer to invent than it should have.

A bash watcher script monitors /shared/queue/ for new files. When one appears, it shells out to claude --print --dangerously-skip-permissions <prompt>. Claude reads the file, executes whatever the prompt asks for (file edits, builds, Docker rebuilds, screenshots), writes a report back to the reports folder, and exits.

The “skip permissions” flag is the part that makes this fast. Without it, every file edit prompts a confirmation. With it, Claude operates as a build engineer who has root in the source folder. That’s an acceptable trade-off in a sandboxed environment with a real user reviewing the resulting commits.

What I get out of this: the entire site — design system, layouts, includes, content, deploy config — was built in roughly a hundred sequential prompts over a few days. Each prompt is a small focused chunk of work with a clear acceptance criterion. The build reports are a paper trail.

The Docker stack

The runtime image is a simple multi-stage build:

FROM ruby:3.3-alpine AS builder
WORKDIR /site
COPY Gemfile Gemfile.lock ./
RUN bundle install --jobs 4 --retry 3
COPY . .
RUN bundle exec jekyll build

FROM nginx:alpine
COPY --from=builder /site/_site /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf

bundle install runs in a layer that gets cached as long as Gemfile.lock doesn’t change. The actual Jekyll build is fast — under two seconds for a site with sixty-odd posts.

The compose file routes the container behind Traefik with a Host rule, and Traefik handles TLS via Cloudflare’s API. From the outside, all you see is https://michaelbrant.com resolving to a static site that loads in under a second.

services:
  michaelbrant:
    build:
      context: /home/almalinux/Sites/michaelbrant.com
      dockerfile: /opt/stacks/michaelbrant/Dockerfile
    container_name: michaelbrant
    ports:
      - "8083:80"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.michaelbrant.rule=Host(`michaelbrant.com`)"

What’s next

The WordPress migration just landed — sixty real posts and a dozen recipes from two old WP installs, parsed and converted to markdown with xml2js + turndown. Next up: real photography to replace the Unsplash placeholders, GHL form activation for newsletter and contact, and probably a deploy hook so commits to main build and ship without me running docker compose build by hand.

The whole pipeline is a single text file away from any change I want to make. That’s the part I keep coming back to.