# bztmon-site

Source for [www.bztmon.com](https://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

```bash
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`.
