No description
Find a file
2026-06-03 16:38:38 +10:00
.forgejo/workflows fix(ci): isolate DOCKER_CONFIG so login skips the host pass credstore 2026-06-02 23:53:18 +10:00
docs feat(deploy): self-hosted CI/CD + dedicated edge-proxy topology 2026-06-02 15:31:16 +10:00
public/assets initial commit 2026-06-01 19:35:56 +10:00
src chore(particle-field): visual tuning — tighter glow, dimmer field, cursor brightness boost 2026-06-03 16:38:38 +10:00
.dockerignore feat(deploy): self-hosted CI/CD + dedicated edge-proxy topology 2026-06-02 15:31:16 +10:00
.gitignore initial commit 2026-06-01 19:35:56 +10:00
astro.config.mjs initial commit 2026-06-01 19:35:56 +10:00
Caddyfile feat(deploy): self-hosted CI/CD + dedicated edge-proxy topology 2026-06-02 15:31:16 +10:00
compose.dev.yml feat(deploy): self-hosted CI/CD + dedicated edge-proxy topology 2026-06-02 15:31:16 +10:00
compose.yml fix(deploy): default REGISTRY_OWNER to joshbairstow (actual Forgejo owner) 2026-06-02 23:46:56 +10:00
Dockerfile feat(deploy): self-hosted CI/CD + dedicated edge-proxy topology 2026-06-02 15:31:16 +10:00
package-lock.json chore(blog-search): add astro/check dep, plan, and follow-up note 2026-06-02 01:54:46 +10:00
package.json chore(blog-search): add astro/check dep, plan, and follow-up note 2026-06-02 01:54:46 +10:00
README.md feat(deploy): self-hosted CI/CD + dedicated edge-proxy topology 2026-06-02 15:31:16 +10:00
tsconfig.json initial commit 2026-06-01 19:35:56 +10:00

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.