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
This commit is contained in:
2026-10-26 20:42:52 +11:00
parent fd9974d07e
commit 84fc585c91
23 changed files with 4373 additions and 34 deletions

12
internal/web/assets.go Normal file
View File

@@ -0,0 +1,12 @@
// Package web embeds the browser-side assets: htmx, the SSE extension and the
// stylesheet. They are compiled into the binary, so the built site has no
// dependency on a bundler, a CDN or the working directory.
package web
import "embed"
// Static holds everything served from /static/. The build publishes it
// verbatim, and adds a chroma.css generated from the Markdown renderer's style.
//
//go:embed static
var Static embed.FS

1360
internal/web/static/site.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,6 @@ package templates
import (
"strconv"
"github.com/a-h/templ"
"gitea.lab.audasmedia.com.au/sam/ai-resume/internal/stage"
)
@@ -154,7 +152,7 @@ templ FragmentView(f stage.Fragment) {
</header>
switch f.Kind {
case stage.KindText:
<div class="frag-text">{ f.Body }</div>
<div class="frag-text">@f.Body</div>
case stage.KindCode:
if f.Code != nil {
<figure class="code">
@@ -299,7 +297,7 @@ templ Timeline(items []TimelineItem) {
templ CodeBlock(c stage.Code) {
<figure class="code">
<figcaption>{ c.Label }<span class="code-lang">{ c.Language }</span></figcaption>
{ c.HTML }
@c.HTML
</figure>
}

View File

@@ -1,10 +1,6 @@
package templates
import (
"strconv"
"github.com/a-h/templ"
)
import "strconv"
// HomePage is the landing page: the 90-second read.
templ HomePage(v HomeView) {
@@ -55,10 +51,10 @@ templ DocPage(v DocView) {
<span class="chip chip-plain">{ v.Doc.Period }</span>
}
for _, m := range v.Metrics {
<li class="chip">
<span class="chip">
<strong>{ m.Value }</strong>
<span>{ m.Label }</span>
</li>
</span>
}
</div>
if len(v.Tech) > 0 {
@@ -178,7 +174,7 @@ templ SearchResults(v SearchView) {
<span class="kind">L{ strconv.Itoa(r.Level) }</span>
<span class={ ConfidenceClass(r.Confidence) }>{ r.Confidence }</span>
</p>
<p class="result-snippet">{ SafeSnippet(r.Snippet) }</p>
<p class="result-snippet">@SafeSnippet(r.Snippet)</p>
</li>
}
</ol>

View File

@@ -1,5 +1,11 @@
// Package templates holds every templ component and the view models they read.
// Layout never branches on the stage driver: it renders the binding it is given.
//
// The generated *_templ.go files are not committed; `make generate` writes them
// from the repository root before every build. There is deliberately no
// //go:generate directive: templ records the .templ path in its error messages,
// so generating from a package directory and generating from the root produce
// different files. One entry point keeps the output byte-identical.
package templates
import (
@@ -91,15 +97,15 @@ type TimelineItem struct {
// PillarCardView is one of the six landing cards.
type PillarCardView struct {
Area content.Area
Title string
Claim string
Href string
Metrics []MetricView
Tech []string
Area content.Area
Title string
Claim string
Href string
Metrics []MetricView
Tech []string
Confidence string
Level int
Pages int
Level int
Pages int
}
// Layout of each page kind.
@@ -151,9 +157,9 @@ type (
// StreamView is the SSE-driven stage demonstration page.
StreamView struct {
Layout LayoutView
Frames int
Fragment []LinkView
Layout LayoutView
Frames int
Fragment []LinkView
}
// NotFoundView is the 404 page.