aa36b2905a
Single-binary Go service that renders markdown pages from a runtime volume mount. Targeted at public, no-auth, no-WAF deployment behind a TLS ingress; security posture is defense-in-depth at every layer: - goldmark with no WithUnsafe — raw HTML in author markdown is stripped - CSP without 'unsafe-inline', plus HSTS, COOP, CORP, Permissions-Policy - static handler rejects non-GET/HEAD, directory listings, dotfiles, traversal - content loader rejects symlinks that escape the content root, dotfiles, and .md files larger than 1 MiB - per-page template trees (cloned from layout) so define-blocks don't collide between home/category/page - SIGHUP triggers atomic library swap — live edits on volume, no rebuild Locale layout content/<locale>/<category>/<slug>.md. Categories without _index.md still appear on the home page with a humanized name. Search is a ~70-line vanilla JS scan over /search.json?lang=<locale>; swap for a real indexer if the corpus ever balloons. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
93 lines
4.5 KiB
Markdown
93 lines
4.5 KiB
Markdown
# automc-tutorials
|
|
|
|
Public-facing tutorials site for the automc Minecraft platform. Renders markdown from a mounted volume as a localized static-ish web app. No auth, no database, no state — exposed to the open internet behind TLS, hardened for zero-trust.
|
|
|
|
## Stack
|
|
|
|
- **Go** + `net/http` + `html/template` — single binary
|
|
- **goldmark** — markdown → HTML (GFM + footnotes + auto heading IDs)
|
|
- **Plain CSS** — no framework, dark mode via `prefers-color-scheme`
|
|
- **Vanilla JS** — ~70-line client-side substring search, no dependency
|
|
- Content lives on a mounted volume; reload via SIGHUP
|
|
|
|
## Layout
|
|
|
|
```
|
|
cmd/automc-tutorials/main.go — entry: flags, signals, SIGHUP reload
|
|
internal/content/ — loader + path-traversal guard for content/<locale>/<category>/<slug>.md
|
|
internal/render/ — goldmark wrapper (HTML escaping on)
|
|
internal/server/ — mux, handlers, embedded templates + /static/ bundle
|
|
content/<locale>/<category>/ — markdown source. _index.md is the category landing page (optional).
|
|
```
|
|
|
|
## Adding or editing a tutorial
|
|
|
|
The image carries only the binary. Markdown sits on a volume mounted at `/content` (default in the image; override with `CONTENT_DIR`). Workflow:
|
|
|
|
1. Open an admin shell on the host that owns the PVC (or shell into a pod that mounts the same RWX volume).
|
|
2. Drop / edit `.md` files under `<volume>/<locale>/<category>/`.
|
|
3. Add YAML frontmatter:
|
|
|
|
```yaml
|
|
---
|
|
title: Page title
|
|
summary: One-sentence preview shown on category list.
|
|
order: 1
|
|
---
|
|
```
|
|
|
|
4. Write markdown. GFM tables, fenced code, footnotes, autolinks, task lists all work. Raw HTML in markdown is **escaped** — don't try to embed `<script>` or `<iframe>`, they'll render as text.
|
|
5. Reload without restart: `kill -HUP $(pidof automc-tutorials)` inside the container, or `kubectl exec deploy/automc-tutorials -- kill -HUP 1`.
|
|
|
|
No rebuild, no rollout. The atomic library swap means in-flight requests keep serving the old library; new requests see the new one.
|
|
|
|
## Localization
|
|
|
|
Each top-level directory under the content root is a locale. Pages are independent per locale — translations are not auto-mapped. The header has a locale switcher that just changes `/<locale>/` in the URL.
|
|
|
|
If a page exists in `en` but not `cs`, switching to `cs` lands on the `cs` home (no automatic redirect, no fallback render).
|
|
|
|
## Search
|
|
|
|
Client-side substring search over `title`, `summary`, and `body`. Index served at `GET /search.json?lang=<locale>` — flat array of `{path, title, summary, body}`. `static/search.js` does a case-insensitive linear scan with crude scoring (title hit = 100, summary = 10, body = 1) and shows the top 8.
|
|
|
|
This is fine for a few hundred pages. If the corpus ever balloons, swap the in-browser scan for a real indexer — the endpoint contract is stable.
|
|
|
|
## Configuration
|
|
|
|
| Var | Default | Purpose |
|
|
|---|---|---|
|
|
| `ADDR` | `:8080` | HTTP listen address |
|
|
| `CONTENT_DIR` | `/content` | Path to content root (volume mount in production, `./content` in dev) |
|
|
| `DEFAULT_LOCALE` | `en` | Locale used when none in URL (`/` → `/<locale>/`) |
|
|
|
|
## Zero-trust posture
|
|
|
|
The service is intended to sit behind TLS (k8s ingress) on the open internet, with no auth and no WAF. Hardening:
|
|
|
|
- **No HTML in markdown.** `goldmark.WithUnsafe()` is not enabled, so raw HTML in `.md` is escaped.
|
|
- **CSP** restricts everything to `'self'`; `'unsafe-inline'` is absent (templates carry no inline styles or scripts). Frame ancestors blocked, base/form-action pinned to self.
|
|
- **HSTS, COOP, CORP, Permissions-Policy** all set conservatively.
|
|
- **No directory listing** on `/static/`; only GET/HEAD; hidden files (`/.foo`) blocked.
|
|
- **Content loader** rejects symlinks that escape the content root, hidden files, and `.md` files larger than 1 MiB.
|
|
- **HTTP timeouts** are set on read header, read body, write, and idle.
|
|
- **Distroless static, runs as 65532:65532**, no shell, no package manager.
|
|
- **No outbound calls** — the service never reaches out to anything.
|
|
|
|
## Build
|
|
|
|
```fish
|
|
make build # binary
|
|
make dev # go run, no build artifact
|
|
make test # go test ./...
|
|
make docker # Docker image
|
|
```
|
|
|
|
## Deployment
|
|
|
|
Single binary, distroless image, port 8080, no persistent state. Deploy as a normal k8s `Deployment` + `Service` + `Ingress` for `tutorials.timemachine.center`. Mount the content PVC read-only at `/content`. No DB, no auth, no secrets.
|
|
|
|
## Repo
|
|
|
|
Standalone Gitea repo at `git.timemachine.center/Timemachine/automc-tutorials`. Sibling of the automc orchestration repo.
|