Files
claude-timemachine aa36b2905a initial: zero-trust markdown tutorials site
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>
2026-05-13 00:52:53 +02:00

4.5 KiB

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:

    ---
    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).

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

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.