|
All checks were successful
deploy / build-and-deploy (push) Successful in 11s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| docs | ||
| public/assets | ||
| src | ||
| .dockerignore | ||
| .gitignore | ||
| astro.config.mjs | ||
| Caddyfile | ||
| compose.dev.yml | ||
| compose.yml | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
jb-website
The first-draft live site for joshbairstow.com — Astro 5 + React islands, styled to a quiet-luxury brand system in warm browns and ink. Serves the apex and three personal subdomains (blog., art., code.) from a single build via host-aware reverse-proxy routing.
The planning doc — including tech rationale, architecture, decisions log, and open questions — lives at docs/planning.md. Read that first if you want context. The canonical brand spec lives in docs/design-system/ (imported from a Claude Design handoff bundle); the "one base + one accent" rule and the visit-one / visit-three litmus test are load-bearing for design decisions.
What's here
src/
├── pages/
│ ├── index.astro apex home (stack layout)
│ ├── 404.astro on-brand missing-page
│ ├── blog/
│ │ ├── index.astro writing archive
│ │ └── posts/[...slug].astro post detail
│ ├── art/index.astro generative-art focus view
│ └── code/index.astro workshop placeholder
├── layouts/
│ ├── BaseLayout.astro topbar + grain + sprinkled marks + footer
│ └── PostLayout.astro editorial reading template
├── components/
│ ├── atoms/ Wordmark, Eyebrow, Mark, InkArrow
│ ├── molecules/ IndexRow, LatestLine, PostRow, YearHead
│ ├── organisms/ TopBar, SiteFoot
│ └── islands/ HeroZone, TideStudyCanvas (React)
├── content/posts/ markdown blog posts (Astro content collection)
├── styles/ colors_and_type.css + site.css
└── lib/urls.ts cross-subdomain URL helpers
Dev
npm install
npm run dev # http://localhost:4321
The Astro dev server collapses subdomain links to relative paths so a single localhost preview shows the cross-subdomain navigation working. In a production build the same helpers emit absolute https://blog.joshbairstow.com/... URLs.
Build
npm run build # outputs static site to dist/
npm run preview # serve the build locally
Local container preview
npm run dev is the primary local loop. To eyeball the built container (real
Caddy + static output) without the production edge proxy:
docker compose -f compose.dev.yml up --build # http://localhost:8080
localhost:8080 falls through to the apex build; subdomain Host-matching only
fires for the real hostnames (use npm run dev, or hosts-file entries, to
exercise cross-subdomain routing locally).
Deploy (CI/CD → Vultr VPS behind a shared edge proxy)
Deployment is automated. The site no longer terminates its own TLS — a
dedicated caddy-docker-proxy edge container (in the separate
jb-infra repo) owns 80/443, does automatic HTTPS for every
host, and routes to this container over the shared edge Docker network using
the labels in compose.yml. This container is now an internal
plaintext static server that still maps host → path prefix in its Caddyfile:
joshbairstow.com→/srv/(www.→ 301 apex)blog.joshbairstow.com→/srv/blog/art.joshbairstow.com→/srv/art/code.joshbairstow.com→/srv/code/
Shared assets (/_astro/*, /assets/*) are re-rooted back to /srv so they
resolve from any subdomain.
Pipeline (.forgejo/workflows/deploy.yml):
push to main → the home-desktop Forgejo Actions runner builds the image,
pushes it to the Forgejo container registry (git.joshbairstow.com), then SSHes
to the VPS to docker compose pull && up -d. The VPS never builds.
See jb-infra/README.md for the one-time bootstrap
(DNS, edge proxy + Forgejo, runner registration, secrets) and the required
Forgejo variables/secrets.
To add another in-repo subdomain: add a directory under src/pages/, a @host
matcher block in the Caddyfile, and the hostname to the caddy label in
compose.yml.
Writing a blog post
Add a .md file under src/content/posts/. Frontmatter schema (validated by Zod in src/content.config.ts):
---
title: A short title
date: 2026-03-12
tag: Field notes # optional
series: No. 19 # optional
standfirst: One sentence in italic display type, under the title.
description: SEO/OG fallback when standfirst is absent.
readingMinutes: 6 # optional
draft: false # excludes from list when true
---
Lead with {.lead} after a paragraph to apply the small-caps lead-in styling. The Astro markdown renderer handles the rest against PostLayout.astro.
What's deferred
- Blog search (Pagefind wiring) — see planning doc step 8.
- Auth middleware — needed when access-gated pages land.
- RSS, OG image generation, additional art pieces.
- Content for
code.(currently a workshop placeholder). - Side-hustle subdomains (signage, keycaps) — out of scope; live in their own containers behind the same proxy.
License
All rights reserved unless stated otherwise. Brand marks, type pairings, and prose are personal; please don't redistribute.