# 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`](docs/planning.md). Read that first if you want context. The canonical brand spec lives in [`docs/design-system/`](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 ```sh 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 ```sh 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: ```sh 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`](../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`](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`](.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`](../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`): ```yaml --- 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.