83 lines
3.3 KiB
Markdown
83 lines
3.3 KiB
Markdown
# 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
|
|
- Static output with one self-hosted client bundle (~74 KB: Motion for scroll reveals, plus a
|
|
small pre-paint theme script). No inline script anywhere, so `script-src 'self'` holds.
|
|
- Content is visible with JavaScript disabled: reveal styles are gated behind an `html.js`
|
|
class the script adds, and `prefers-reduced-motion` forces the visible state
|
|
- 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 Argo CD 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 Argo CD. Manual bootstrap build:
|
|
`scripts/build-image.sh push`. Full posture in `SECURITY.md`.
|