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