Refactor the site container from a TLS-owning, port-binding proxy into an internal plaintext static server that sits behind a dedicated edge reverse proxy, and add an automated build/deploy pipeline. - Caddyfile: single :80 listener (auto_https off), Host-matcher routing for apex/blog/art/code with /_astro+/assets re-root and www->apex redirect - Dockerfile: EXPOSE 80 only (TLS now handled by the edge proxy) - compose.yml: pull registry image, join external 'edge' network, declare routing via caddy-docker-proxy labels; drop build/ports/cert volumes - compose.dev.yml: local container preview on :8080 - .forgejo/workflows/deploy.yml: push-to-main -> desktop runner builds, pushes to the Forgejo registry, SSH-deploys to the VPS - docs + .dockerignore updated for the new topology Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
5.2 KiB
Markdown
120 lines
5.2 KiB
Markdown
# 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.
|