2026-06-17 17:20:54 +10:00
2026-06-17 17:20:54 +10:00
2026-06-17 17:20:54 +10:00

bztmon-site

The source for www.bztmon.com — Jonathon Wright's portfolio / résumé site. A fast, animated, security-hardened static site for a platform / infrastructure engineer.

This repo is public. It lives on a self-hosted public Gitea (git.bztmon.com), isolated from the private homelab GitOps. Never commit secrets — the static site needs none.

Stack

  • Astro (static output) + TypeScript + Tailwind v4
  • Zero JS by default; tiny islands for the theme toggle + scroll reveals
  • Content & config are data-driven (src/data/) — adding a project never touches a component

Develop

npm install
npm run dev        # http://localhost:4321
npm run check      # astro check (types + diagnostics)
npm run build      # static build → dist/
npm run preview    # serve the build locally
npm run gen:og     # regenerate the social-preview image (public/og.png)

Project layout

src/
  data/          site.ts, socials.ts, skills.ts, projects.ts, experience.ts
  components/     Hero, Nav, ThemeToggle, ProjectCard, SkillGroup, ...
  layouts/        Layout.astro (SEO/OG, theme bootstrap)
  pages/          index.astro, projects/, 404.astro
  styles/         tokens.css (theme), global.css
  lib/            build-time helpers (cv detection)
scripts/          gen-og.mjs, build-image.sh
nginx/            default.conf (security headers, caching) baked into the image
Dockerfile        Debian build stage → nginx-unprivileged runtime

Content TODOs (Jonathon)

  • Drop a real CV at public/cv.pdf — the Download CV button appears automatically.
  • Fill the TODO(Jonathon) markers in src/data/experience.ts, projects.ts, socials.ts (employer names, dates, GitHub/LinkedIn handles).

Publishing a post

A post is just a Markdown file in src/content/blog/. Write one by hand, or generate a schema-valid one with the publish helper (this is the seam an IaC/CI step calls):

# from flags
node scripts/new-post.mjs --title "My post" --summary "One line" \
     --tags "kubernetes,gpu" [--draft] [--bodyFile notes.md]

# from a JSON event (e.g. an Ansible/AWX deploy summary)
echo '{"title":"...","summary":"...","tags":["x"],"body":"## Hi\n..."}' \
  | node scripts/new-post.mjs --stdin

Commit the file to main → CI rebuilds and ships. A malformed post fails the build (frontmatter is zod-validated), so a bad pipeline event never reaches production.

CI/CD

.gitea/workflows/deploy.yml runs on a self-hosted runner (a dedicated unprivileged user on the bastion):

npm ci → astro check → audit-ci (high/critical gate) → build → scan dist →
SBOM (CycloneDX) → buildah build+push → open a digest-bump PR to home-ops

The PR is never auto-mergedhome-ops main is branch-protected; merging it is what triggers the ArgoCD rollout. The runner holds only least-privilege creds (a home-ops-scoped deploy key + a PR token + a registry push token).

  • npm run scan — build-time gate: no secrets, no inline scripts, no third-party origins.
  • .audit-ci.json — fails on high/critical advisories. One allowlisted: GHSA-gv7w-rqvm-qjhr (esbuild install-integrity; build-time only, mitigated by the committed lockfile + trusted registry).
  • renovate.json — keeps npm deps and the digest-pinned base images current.

Deploy

Built into a container image, served by nginx-unprivileged on a homelab Kubernetes cluster, exposed via Cloudflare Tunnel. The image is pinned by digest in the private home-ops repo and rolled out by ArgoCD. Manual/bootstrap build: scripts/build-image.sh push. See SECURITY.md for the full security posture.

S
Description
Source for www.bztmon.com
Readme 497 KiB
Languages
Astro 53.3%
TypeScript 14.7%
JavaScript 11.3%
CSS 9.8%
Shell 9.6%
Other 1.3%