Files
ai-resume/cmd/build/main.go
Sam Rolfe 84fc585c91 Phase 2: Mode A static site — build, serve, search, stage panel
One Go binary serves a pre-rendered site. No Node, no bundler, no node_modules:
htmx and the SSE extension are vendored files, Markdown and code highlighting
run at build time (goldmark + chroma), and the server only writes files.

Implements docs/phase-2-static-site.md:

  2.1 service skeleton   cmd/build (content -> dist), cmd/server (dist -> HTTP),
                         env-configured, 127.0.0.1:8096, /healthz with separate
                         static / ai / search readiness, --health-check probe,
                         deploy/compose.yml + deploy/Dockerfile at 120m
  2.2 rendering          templ components (Layout, Nav, PillarCard, Stage,
                         EvidenceTable, CodeBlock, Timeline, Footer);
                         `make generate` is a build step and is byte-stable
  2.3 navigation         six-area rail, <details> disclosure under 900px,
                         breadcrumbs, aria-current, L1/L2/L3 with artefact pages
  2.4 stage panel        internal/stage owns the contract; RouteDriver (fragment
                         swap) and SSEDriver (event stream) differ only in the
                         Binding, never in layout. fragment ids live in the
                         contract so route files, links and streamed frames agree
  2.5 search             SQLite FTS5 over titles and body, rebuilt by the build;
                         one handler returns a page without HTMX, a fragment with
                         it. Visitors' FTS operators are never honoured
  2.6 quality floor      print stylesheet, JS-off navigation, focus states,
                         semantic landmarks, dark default; security headers

Definition of Done evidence (see issue AUD-142):
  A1  all pages 200 in a network namespace with no external route
  one binary serves the site; make dist writes 45 pages, 86 fragments, 40 docs
  templ generate is deterministic (md5 identical across runs)
  6 L1 + 6 L2 pages, artefact pages reachable in <= 3 clicks
  FTS5 returns sensible hits for zigbee / signage / routing (tested)
  Lighthouse 100 performance and 100 accessibility, mobile and desktop
  first byte well inside 100ms over loopback (test)
  panel renders from a route driver and re-points at the stream untouched
  print legible: /edge/ renders to a 3-page PDF with the print stylesheet

Also fixes found while finishing the build:
  - reject "." / ".." path segments before ServeMux redirects them into a file
    outside the asset directory (/static/../build.json now 404, was 307->200)
  - make test is the real gate: go test ./... plus the validator self-test
2026-10-26 20:42:52 +11:00

59 lines
1.9 KiB
Go

// Command build pre-renders the whole site into a directory.
//
// It reads content/ and writes dist/: every page as HTML, one file per stage
// fragment, the fragment manifest, the SQLite FTS5 index, the assets and the
// static files. No network call is made, and the output is what the server
// serves — Mode A never renders a page per request.
//
// Configuration is from the environment, so the same binary is used by
// `make build`, by CI and by the container image:
//
// RESUME_ROOT repository root (default ".")
// RESUME_CONTENT content directory (default $RESUME_ROOT/content)
// RESUME_DIST output directory (default $RESUME_ROOT/dist)
package main
import (
"flag"
"fmt"
"os"
"gitea.lab.audasmedia.com.au/sam/ai-resume/internal/site"
)
func main() {
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "usage: build [--root DIR] [--content DIR] [--dist DIR]\n")
fmt.Fprintf(flag.CommandLine.Output(), "environment: RESUME_ROOT, RESUME_CONTENT, RESUME_DIST\n")
}
root := flag.String("root", env("RESUME_ROOT", "."), "repository root")
contentDir := flag.String("content", env("RESUME_CONTENT", ""), "content directory")
distDir := flag.String("dist", env("RESUME_DIST", ""), "output directory")
flag.Parse()
cfg := site.Config{Root: *root, ContentDir: *contentDir, OutDir: *distDir}
s, err := site.New(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "build: %v\n", err)
os.Exit(1)
}
if err := s.Build(); err != nil {
fmt.Fprintf(os.Stderr, "build: %v\n", err)
os.Exit(1)
}
info, err := site.LoadBuildInfo(s.Config.OutDir)
if err != nil {
fmt.Fprintf(os.Stderr, "build: %v\n", err)
os.Exit(1)
}
fmt.Printf("built %d pages, %d stage fragments, %d indexed documents into %s\n",
info.Pages, info.Fragments, info.Indexed, s.Config.OutDir)
}
func env(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}