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
255 lines
8.9 KiB
Plaintext
255 lines
8.9 KiB
Plaintext
package templates
|
|
|
|
import "strconv"
|
|
|
|
// HomePage is the landing page: the 90-second read.
|
|
templ HomePage(v HomeView) {
|
|
@Layout(v.Layout) {
|
|
<section class="hero">
|
|
<p class="hero-kicker">Melbourne · twenty-five years · AI · platform · edge</p>
|
|
<h1 class="hero-title">{ v.Positioning }</h1>
|
|
<p class="hero-support">{ v.Support }</p>
|
|
<p class="hero-facts">
|
|
<span>{ strconv.Itoa(v.Areas) } areas</span>
|
|
<span>{ strconv.Itoa(v.Pages) } pages</span>
|
|
<span>{ strconv.Itoa(v.Artifacts) } linked artefacts</span>
|
|
<span>no page needs a model to render</span>
|
|
</p>
|
|
@SearchForm("", "hero-search")
|
|
</section>
|
|
|
|
<section class="pillars" aria-label="Six areas">
|
|
for _, c := range v.Cards {
|
|
@PillarCard(c)
|
|
}
|
|
</section>
|
|
|
|
<section class="honesty" aria-labelledby="how-to-read">
|
|
<h2 id="how-to-read">How to read this site</h2>
|
|
<ul>
|
|
<li><strong>Level 1</strong> is one screen per area: the claim, the numbers, the way in.</li>
|
|
<li><strong>Level 2</strong> is what is actually run, with an evidence table and the trade-offs.</li>
|
|
<li><strong>Level 3</strong> is the artefact: code, diagram, screenshot, log.</li>
|
|
<li>Every claim carries a <strong>status</strong>, and finished, running, in-progress and abandoned are all shown as they are.</li>
|
|
<li>Every artefact is at most three clicks from here, and the <a href="/evidence/">evidence index</a> lists all of them on one page.</li>
|
|
</ul>
|
|
</section>
|
|
}
|
|
}
|
|
|
|
// DocPage renders an L1, L2 or L3 document.
|
|
templ DocPage(v DocView) {
|
|
@Layout(v.Layout) {
|
|
<article class="doc">
|
|
<header class="doc-head">
|
|
<p class="doc-kicker">{ v.AreaTitle } · { v.LevelLabel }</p>
|
|
<h1 class="doc-title">{ v.Doc.Title }</h1>
|
|
<p class="doc-claim">{ v.Doc.Claim }</p>
|
|
<div class="doc-chips">
|
|
<span class={ ConfidenceClass(v.Doc.Confidence) }>{ v.Doc.Confidence }</span>
|
|
if v.Doc.Period != "" {
|
|
<span class="chip chip-plain">{ v.Doc.Period }</span>
|
|
}
|
|
for _, m := range v.Metrics {
|
|
<span class="chip">
|
|
<strong>{ m.Value }</strong>
|
|
<span>{ m.Label }</span>
|
|
</span>
|
|
}
|
|
</div>
|
|
if len(v.Tech) > 0 {
|
|
<ul class="tech" aria-label="Technology">
|
|
for _, t := range v.Tech {
|
|
<li>{ t }</li>
|
|
}
|
|
</ul>
|
|
}
|
|
</header>
|
|
|
|
<div class="prose">
|
|
@v.Body
|
|
</div>
|
|
|
|
if len(v.Evidence) > 0 {
|
|
<section class="doc-evidence" aria-labelledby="evidence-heading">
|
|
<h2 id="evidence-heading">Evidence for this page</h2>
|
|
<p class="section-note">Select a row to load the artefact into the presentation panel. Each row is also a link, so this works without JavaScript.</p>
|
|
@EvidenceTable(evidenceRows(v.Evidence))
|
|
</section>
|
|
}
|
|
|
|
if len(v.Timeline) > 0 {
|
|
<section class="doc-timeline" aria-labelledby="timeline-heading">
|
|
<h2 id="timeline-heading">In time order</h2>
|
|
@Timeline(v.Timeline)
|
|
</section>
|
|
}
|
|
|
|
if len(v.SubPages) > 0 {
|
|
<section class="doc-sub" aria-labelledby="sub-heading">
|
|
<h2 id="sub-heading">{ subHeading(v.Doc.Level) }</h2>
|
|
<ul class="drill-list">
|
|
for _, s := range v.SubPages {
|
|
<li>
|
|
<a href={ templ.SafeURL(s.Href) }>{ s.Label }</a>
|
|
<span class="drill-kind">{ s.Kind }</span>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</section>
|
|
}
|
|
|
|
if len(v.Backlinks) > 0 {
|
|
<section class="doc-back" aria-labelledby="back-heading">
|
|
<h2 id="back-heading">Referenced from</h2>
|
|
<ul class="drill-list">
|
|
for _, b := range v.Backlinks {
|
|
<li><a href={ templ.SafeURL(b.Href) }>{ b.Label }</a></li>
|
|
}
|
|
</ul>
|
|
</section>
|
|
}
|
|
|
|
<footer class="doc-foot">
|
|
<p>Status <span class={ ConfidenceClass(v.Doc.Confidence) }>{ v.Doc.Confidence }</span> — { ConfidenceNote(v.Doc.Confidence) }</p>
|
|
<p>Last verified { v.Doc.LastVerified } · id <code>{ v.Doc.ID }</code> · { v.Doc.Path }</p>
|
|
</footer>
|
|
</article>
|
|
}
|
|
}
|
|
|
|
// EvidencePage is one page listing every artefact, grouped by area. It is the
|
|
// shortest path from the landing page to any proof.
|
|
templ EvidencePage(v EvidenceView) {
|
|
@Layout(v.Layout) {
|
|
<article class="doc">
|
|
<header class="doc-head">
|
|
<p class="doc-kicker">Index</p>
|
|
<h1>Evidence index</h1>
|
|
<p class="doc-claim">Every artefact this site claims, on one page. { strconv.Itoa(v.Total) } links, each one a file in the repository.</p>
|
|
</header>
|
|
for _, g := range v.Groups {
|
|
<section class="evidence-group">
|
|
<h2><a href={ templ.SafeURL(g.Href) }>{ g.AreaTitle }</a></h2>
|
|
@EvidenceTable(g.Rows)
|
|
</section>
|
|
}
|
|
</article>
|
|
}
|
|
}
|
|
|
|
// SearchPage is the keyword search. The results block is swapped by HTMX on the
|
|
// same page; with JavaScript off the form simply reloads the page.
|
|
templ SearchPage(v SearchView) {
|
|
@Layout(v.Layout) {
|
|
<article class="doc">
|
|
<header class="doc-head">
|
|
<p class="doc-kicker">Keyword search</p>
|
|
<h1>Search { strconv.Itoa(v.Indexed) } pages</h1>
|
|
<p class="doc-claim">Full-text search over page titles and body text. Served from a local index — no model, no network call.</p>
|
|
</header>
|
|
@SearchForm(v.Query, "page-search")
|
|
@SearchResults(v)
|
|
</article>
|
|
}
|
|
}
|
|
|
|
// SearchResults is swapped into #search-results by HTMX.
|
|
templ SearchResults(v SearchView) {
|
|
<div id="search-results" aria-live="polite">
|
|
if v.Query == "" {
|
|
<p class="section-note">Try “zigbee”, “signage”, “routing”, “backups” or “guardrails”.</p>
|
|
} else if v.Failed {
|
|
<p class="search-empty">That query could not be run. Try fewer or simpler words.</p>
|
|
} else if len(v.Results) == 0 {
|
|
<p class="search-empty">No page matches <q>{ v.Query }</q>. The <a href="/evidence/">evidence index</a> lists everything by area.</p>
|
|
} else {
|
|
<p class="search-count">{ strconv.Itoa(len(v.Results)) } results for <q>{ v.Query }</q></p>
|
|
<ol class="results">
|
|
for _, r := range v.Results {
|
|
<li class="result">
|
|
<h2><a href={ templ.SafeURL(r.URL) }>{ r.Title }</a></h2>
|
|
<p class="result-meta">
|
|
<span class="kind">{ r.Area }</span>
|
|
<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>
|
|
</li>
|
|
}
|
|
</ol>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// StreamPage proves the panel can be re-pointed at a stream. The layout and the
|
|
// panel markup are the ones every other page uses; only the binding differs.
|
|
templ StreamPage(v StreamView) {
|
|
@Layout(v.Layout) {
|
|
<article class="doc">
|
|
<header class="doc-head">
|
|
<p class="doc-kicker">Design note · not a product feature</p>
|
|
<h1>The panel takes content from a driver</h1>
|
|
<p class="doc-claim">This page renders the same layout and the same panel as every other page. Its panel is bound to an event stream instead of pre-rendered files, and no layout code changed.</p>
|
|
</header>
|
|
<div class="prose">
|
|
<p>The presentation panel is a content sink. In Mode A it takes pre-rendered fragments over
|
|
HTMX: you click a row in an evidence table and one file is swapped in. On this page the same
|
|
panel is bound to <code>/api/stage/stream</code> and receives { strconv.Itoa(v.Frames) } fragments
|
|
over server-sent events, one after another.</p>
|
|
<p>Fragments are pushed, not requested. Nothing about the panel — its markup, its fragment
|
|
view, its empty state — knows the difference. That is the point: the Phase 5 console
|
|
re-points this panel at retrieval output and the layout does not change.</p>
|
|
<p class="section-note">This demonstration streams the artefacts already built for the site, with
|
|
no model in the loop. It exists to prove the transport, not to pretend an AI is answering.</p>
|
|
</div>
|
|
if len(v.Fragment) > 0 {
|
|
<section class="doc-sub">
|
|
<h2>Fragments in this stream</h2>
|
|
<ul class="drill-list">
|
|
for _, f := range v.Fragment {
|
|
<li>
|
|
<a href={ templ.SafeURL(f.Href) } hx-get={ f.HXGet } hx-target="#stage-content" hx-swap="innerHTML">{ f.Label }</a>
|
|
<span class="drill-kind">{ f.Kind }</span>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</section>
|
|
}
|
|
</article>
|
|
}
|
|
}
|
|
|
|
// NotFoundPage is the 404.
|
|
templ NotFoundPage(v NotFoundView) {
|
|
@Layout(v.Layout) {
|
|
<article class="doc">
|
|
<header class="doc-head">
|
|
<p class="doc-kicker">404</p>
|
|
<h1>That page is not here</h1>
|
|
<p class="doc-claim">The link may be from an older build. The six areas and the evidence index are the reliable ways in.</p>
|
|
</header>
|
|
<ul class="drill-list">
|
|
<li><a href="/">Landing page</a></li>
|
|
<li><a href="/evidence/">Evidence index</a></li>
|
|
<li><a href="/search">Search</a></li>
|
|
</ul>
|
|
</article>
|
|
}
|
|
}
|
|
|
|
func evidenceRows(links []LinkView) []EvidenceRow {
|
|
rows := make([]EvidenceRow, 0, len(links))
|
|
for _, l := range links {
|
|
rows = append(rows, EvidenceRow{Label: l.Label, Href: l.Href, HXGet: l.HXGet, Kind: l.Kind, Confidence: l.Confidence})
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func subHeading(level int) string {
|
|
if level == 1 {
|
|
return "Drill-downs in this area"
|
|
}
|
|
return "Artefacts behind this page"
|
|
}
|