2026-08-18 01:12:54 +10:00
2026-08-05 02:02:11 +10:00

bztmon-site

Source for www.bztmon.com - portfolio and blog for a platform / infrastructure engineer. Static output, no server runtime, hardened by default.

This repo is public and lives on a self-hosted Gitea (git.bztmon.com), isolated from the private GitOps repos. The static site needs no secrets; none are committed.

Stack

  • Astro (static output), TypeScript, Tailwind v4
  • Zero JS by default; small islands for the theme toggle and scroll reveals
  • Content and config are data-driven (src/data/, src/content/) - adding a post or 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)

Layout

src/
  content/blog/   posts (Markdown, zod-validated frontmatter)
  content/projects/ project dossiers
  data/           site.ts, series.ts, skills.ts, projects.ts, experience.ts
  components/     Hero, Nav, PostList, ProjectCard, ...
  layouts/        Layout.astro (SEO/OG, theme bootstrap)
  pages/          index, blog/, projects/, 404
  styles/         tokens.css (theme), global.css
scripts/          gen-og.mjs, build-image.sh, new-post.mjs
public/           heroes (blog/), diagrams (diagrams/), og.png, cv
nginx/            default.conf (security headers, caching) baked into the image
Dockerfile        Debian build stage -> nginx-unprivileged runtime

Posts

A post is a Markdown file in src/content/blog/ with zod-validated frontmatter; a malformed post fails the build, so a bad file never reaches production. scripts/new-post.mjs scaffolds one from flags or a JSON event on stdin - the seam a CI step can call. Each post belongs to one numbered series (src/data/series.ts), which drives the badge on the index cards.

CI/CD

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

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-merged - the deploy repo's main is branch-protected, and merging the PR is what triggers the ArgoCD rollout. The runner holds least-privilege credentials only: a scoped deploy key, a PR token, and a registry push token.

  • npm run scan - build-time gate: no secrets, no inline scripts, no third-party origins in dist/
  • .audit-ci.json - fails the build on high/critical advisories
  • renovate.json - keeps npm deps and the digest-pinned base images current

Deploy

Built into a container image, served by nginx-unprivileged on Kubernetes, exposed outbound-only via Cloudflare Tunnel. The image is pinned by digest in the private deploy repo and rolled out by ArgoCD. Manual bootstrap build: scripts/build-image.sh push. Full posture in SECURITY.md.

S
Description
Source for www.bztmon.com
Readme 8.2 MiB
Languages
Astro 57%
TypeScript 14.8%
CSS 11.8%
JavaScript 10.3%
Shell 5.1%
Other 1%