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 indist/.audit-ci.json- fails the build on high/critical advisoriesrenovate.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.