site: publish the platform guides at /platforms/ and link them from an About page
(cherry picked from commit 408d6cdfd76d13945db1d9727b58feca23d1e48b)
This commit is contained in:
@@ -100,6 +100,13 @@ func (s *Site) Build() error {
|
||||
if err := writePage("/404.html", templates.NotFoundPage(s.NotFoundPage())); err != nil {
|
||||
return err
|
||||
}
|
||||
// The platform guides are configuration and public copy at the same time.
|
||||
// Rendering them here keeps the page and the pipeline's include/exclude rules
|
||||
// from drifting: both read platforms/*.yml.
|
||||
if _, err := RenderPlatforms(s.Config.Root, s.Config.OutDir); err != nil {
|
||||
return fmt.Errorf("platform guides: %w", err)
|
||||
}
|
||||
pages++
|
||||
|
||||
for _, f := range s.Fragments {
|
||||
if err := writeFile(filepath.Join(s.Config.OutDir, filepath.FromSlash(f.File)), []byte(f.HTML)); err != nil {
|
||||
|
||||
128
internal/site/platforms.go
Normal file
128
internal/site/platforms.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package site
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// PlatformGuide is one `platforms/<name>.yml` file: configuration and public
|
||||
// copy at the same time. The pipeline reads include/exclude from here; the site
|
||||
// shows what, where it lives, and what is read.
|
||||
type PlatformGuide struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
What string `yaml:"what" json:"what"`
|
||||
Fits []string `yaml:"fits" json:"fits"`
|
||||
Base string `yaml:"base" json:"base"`
|
||||
Read []string `yaml:"read" json:"read"`
|
||||
Public bool `yaml:"public" json:"public"`
|
||||
Include struct {
|
||||
Repos any `yaml:"repos" json:"repos"`
|
||||
Paths []string `yaml:"paths" json:"paths"`
|
||||
} `yaml:"include" json:"include"`
|
||||
Exclude struct {
|
||||
Repos []string `yaml:"repos" json:"repos"`
|
||||
Paths []string `yaml:"paths" json:"paths"`
|
||||
} `yaml:"exclude" json:"exclude"`
|
||||
}
|
||||
|
||||
// LoadPlatforms reads every guide. A file that does not parse is an error: the
|
||||
// site must not silently show a different list from the one the pipeline reads.
|
||||
func LoadPlatforms(root string) ([]PlatformGuide, error) {
|
||||
dir := filepath.Join(root, "platforms")
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []PlatformGuide
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !(strings.HasSuffix(entry.Name(), ".yml") || strings.HasSuffix(entry.Name(), ".yaml")) {
|
||||
continue
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(dir, entry.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var guide PlatformGuide
|
||||
if err := yaml.Unmarshal(raw, &guide); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", entry.Name(), err)
|
||||
}
|
||||
if guide.Name == "" || guide.What == "" {
|
||||
return nil, fmt.Errorf("%s: name and what are required", entry.Name())
|
||||
}
|
||||
out = append(out, guide)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// RenderPlatforms writes /platforms/index.html and /platforms/index.json.
|
||||
//
|
||||
// The page is generated outside the templ layout because templ needs a Node-free
|
||||
// generator on PATH and the build host has no network requirement. It uses the
|
||||
// same stylesheet, the same navigation and the same wording as the rest of the
|
||||
// site; the only thing it does not have is the presentation panel, which has no
|
||||
// meaning on a reference page.
|
||||
func RenderPlatforms(root, outDir string) (int, error) {
|
||||
guides, err := LoadPlatforms(root)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
target := filepath.Join(outDir, "platforms")
|
||||
if err := os.MkdirAll(target, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var body strings.Builder
|
||||
body.WriteString(`<p class="lede">The assistant reads four sources. Three are harvested from the estate; the fourth is the curated text this site is built from. Everything below is the same configuration the pipeline runs.</p>`)
|
||||
for _, guide := range guides {
|
||||
state := "enabled"
|
||||
if !guide.Enabled {
|
||||
state = "disabled"
|
||||
}
|
||||
body.WriteString(`<section class="platform">`)
|
||||
fmt.Fprintf(&body, `<h2>%s <span class="conf">%s</span></h2>`, template.HTMLEscapeString(guide.Name), state)
|
||||
fmt.Fprintf(&body, `<p>%s</p>`, template.HTMLEscapeString(strings.TrimSpace(guide.What)))
|
||||
fmt.Fprintf(&body, `<dl><dt>Where it lives</dt><dd>%s</dd>`, template.HTMLEscapeString(guide.Base))
|
||||
fmt.Fprintf(&body, `<dt>Feeds</dt><dd>%s</dd>`, template.HTMLEscapeString(strings.Join(guide.Fits, ", ")))
|
||||
fmt.Fprintf(&body, `<dt>Reads</dt><dd>%s</dd>`, template.HTMLEscapeString(strings.Join(guide.Read, ", ")))
|
||||
if len(guide.Include.Paths) > 0 {
|
||||
fmt.Fprintf(&body, `<dt>Includes</dt><dd>%s</dd>`, template.HTMLEscapeString(strings.Join(guide.Include.Paths, ", ")))
|
||||
}
|
||||
if len(guide.Exclude.Paths) > 0 {
|
||||
fmt.Fprintf(&body, `<dt>Excludes</dt><dd>%s</dd>`, template.HTMLEscapeString(strings.Join(guide.Exclude.Paths, ", ")))
|
||||
}
|
||||
body.WriteString(`</dl></section>`)
|
||||
}
|
||||
|
||||
page, err := template.New("platforms").Parse(platformsPage)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
file, err := os.Create(filepath.Join(target, "index.html"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer file.Close()
|
||||
if err := page.Execute(file, map[string]any{"Body": template.HTML(body.String())}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
raw, err := json.MarshalIndent(map[string]any{"version": 1, "platforms": guides}, "", " ")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(target, "index.json"), append(raw, '\n'), 0o644); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(guides), nil
|
||||
}
|
||||
|
||||
const platformsPage = `<!doctype html><html lang="en-AU"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sources and platform guides — Sam Rolfe</title><meta name="description" content="Every source the assistant reads, with the include and exclude rules, shown as configuration and as public copy."><meta name="color-scheme" content="dark"><link rel="stylesheet" href="/static/site.css"></head><body><a class="skip-link" href="#main">Skip to content</a><header class="topbar"><a class="brand" href="/"><span class="brand-mark">SR</span> <span class="brand-text">Sam Rolfe <span class="brand-sub">AI · Platform · Edge</span></span></a><nav class="nav nav-rail" aria-label="Sections"><a href="/ai/" class="nav-link"><span class="nav-gutter" aria-hidden="true">AI</span> <span class="nav-title">AI & Agentic Systems</span></a><a href="/platform/" class="nav-link"><span class="nav-gutter" aria-hidden="true">PL</span> <span class="nav-title">Platform & Infrastructure</span></a><a href="/edge/" class="nav-link"><span class="nav-gutter" aria-hidden="true">ED</span> <span class="nav-title">Edge & Devices</span></a><a href="/products/" class="nav-link"><span class="nav-gutter" aria-hidden="true">PR</span> <span class="nav-title">Products</span></a><a href="/history/" class="nav-link"><span class="nav-gutter" aria-hidden="true">HI</span> <span class="nav-title">History</span></a><a href="/about/" class="nav-link"><span class="nav-gutter" aria-hidden="true">AB</span> <span class="nav-title">About</span></a><a href="/evidence/" class="nav-link"><span class="nav-gutter" aria-hidden="true">EV</span> <span class="nav-title">Evidence index</span></a></nav></header><div class="shell"><main id="main" class="main"><nav class="crumbs" aria-label="Breadcrumb"><ol><li><a href="/">Home</a></li><li><span aria-current="page">Sources</span></li></ol></nav><article class="doc"><header class="doc-head"><p class="doc-kicker">AI overlay</p><h1>What the assistant reads</h1><p class="doc-claim">Four sources, one pool. Content is tier 1: a curated claim outranks harvested text when the two disagree.</p></header>{{.Body}}<p class="doc-foot"><a href="/evidence/">Every artefact</a> · <a href="/search">Search the site</a></p></article></main></div><footer class="footer"><p class="footer-line">Twenty-five years shipping software · Melbourne · remote · hybrid · office · travel</p><p class="footer-links"><a href="/evidence/">Evidence index</a> <a href="/search">Search</a></p></footer></body></html>`
|
||||
42
internal/site/platforms_test.go
Normal file
42
internal/site/platforms_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package site
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRenderPlatformsShowsTheGuides(t *testing.T) {
|
||||
root, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := t.TempDir()
|
||||
count, err := RenderPlatforms(root, out)
|
||||
if err != nil {
|
||||
t.Fatalf("RenderPlatforms: %v", err)
|
||||
}
|
||||
if count != 3 {
|
||||
t.Fatalf("got %d guides, want 3", count)
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(out, "platforms", "index.html"))
|
||||
if err != nil {
|
||||
t.Fatalf("read page: %v", err)
|
||||
}
|
||||
page := strings.ToLower(string(raw))
|
||||
for _, want := range []string{
|
||||
"what the assistant reads",
|
||||
"self-hosted git",
|
||||
"personal knowledge base",
|
||||
"knowledge graph over the notes",
|
||||
"node_modules",
|
||||
} {
|
||||
if !strings.Contains(page, want) {
|
||||
t.Errorf("page does not contain %q", want)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(out, "platforms", "index.json")); err != nil {
|
||||
t.Errorf("index.json missing: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user