// Package site assembles the Mode A static site out of content/ and knows how // to write it to disk. // // One assembler, two callers: cmd/build writes the whole site to a directory, // and cmd/server uses the same view models for the two things that cannot be // pre-rendered — a keyword search with a query in it, and the streamed stage // demonstration. Everything else in the running server is a file on disk, so a // page is never rendered per request and no page needs an AI service. package site import ( "path/filepath" "time" "github.com/a-h/templ" "gitea.lab.audasmedia.com.au/sam/ai-resume/internal/content" "gitea.lab.audasmedia.com.au/sam/ai-resume/internal/search" "gitea.lab.audasmedia.com.au/sam/ai-resume/internal/stage" "gitea.lab.audasmedia.com.au/sam/ai-resume/internal/web/templates" ) // Config is where the site is read from and written to. type Config struct { // Root is the repository root. Static assets are read from it. Root string // ContentDir is the Markdown source of truth. Defaults to Root/content. ContentDir string // OutDir is where Build writes the pre-rendered site. Defaults to Root/dist. OutDir string } func (c *Config) defaults() { if c.Root == "" { c.Root = "." } if c.ContentDir == "" { c.ContentDir = filepath.Join(c.Root, "content") } if c.OutDir == "" { c.OutDir = filepath.Join(c.Root, "dist") } } // Shell is the chrome every page shares. It needs no content tree, so the // running server can render a search page with only dist/ on disk. type Shell struct { // Year is stamped in the footer. Year int } // NewShell returns the chrome for the current year. func NewShell() Shell { return Shell{Year: year()} } // Nav builds the six-area navigation, marking area as current. func (Shell) Nav(area string) []templates.NavItem { items := make([]templates.NavItem, 0, len(content.Areas)) for _, a := range content.Areas { items = append(items, templates.NavItem{ Title: a.Title, Href: "/" + a.Key + "/", Gutter: a.Gutter, Current: a.Key == area, }) } return items } // RouteBinding is the Mode A driver: one pre-rendered fragment per click. func (Shell) RouteBinding() stage.Binding { return stage.RouteDriver{BaseURL: "/stage"}.Bind() } // StreamBinding is the Mode B driver: fragments pushed over SSE. func (Shell) StreamBinding(endpoint string) stage.Binding { return stage.SSEDriver{Endpoint: endpoint}.Bind() } // LayoutOpts are the per-page chrome values. type LayoutOpts struct { Title string Description string Area string Crumbs []templates.Crumb Binding stage.Binding } // Layout builds the chrome for one page. It never looks at the stage driver // beyond handing the binding to the template. func (sh Shell) Layout(o LayoutOpts) templates.LayoutView { return templates.LayoutView{ Title: o.Title, Description: o.Description, Area: o.Area, Nav: sh.Nav(o.Area), Crumbs: o.Crumbs, Stage: o.Binding, Year: sh.Year, } } // SearchView builds the search page. Search is the one Mode A page that reads a // query, so the server renders it; with JavaScript off the form posts and the // same page comes back with results. func (sh Shell) SearchView(indexed int, q string, results []search.Result, failed bool) templates.SearchView { return templates.SearchView{ Layout: sh.Layout(LayoutOpts{ Title: "Search — Sam Rolfe", Description: "Keyword search across every page of this site. No model, no network call.", Crumbs: []templates.Crumb{{Label: "Home", Href: "/"}, {Label: "Search"}}, Binding: sh.RouteBinding(), }), Query: q, Results: results, Indexed: indexed, Failed: failed, } } // NotFoundPage is the 404, pre-rendered at build time and also served live. func (sh Shell) NotFoundPage() templates.NotFoundView { return templates.NotFoundView{ Layout: sh.Layout(LayoutOpts{ Title: "Page not found — Sam Rolfe", Description: "That page is not here. The six areas and the evidence index are the reliable ways in.", Crumbs: []templates.Crumb{{Label: "Home", Href: "/"}, {Label: "Not found"}}, Binding: sh.RouteBinding(), }), } } // Site is the content tree plus everything derived from it. type Site struct { Shell Config Config Content *content.Set Renderer *content.Renderer // Fragments are the pre-rendered stage fragments, in manifest order. The // first len(content.Areas) are the six area landing pages, which is the // default stream. Fragments []*Fragment // Manifest indexes the same fragments the way the server sees them. Manifest *stage.Manifest // FragmentsByDoc lists fragment ids per document, in reading order. FragmentsByDoc map[string][]string fragByID map[string]*Fragment } // New loads content/ and renders every page and stage fragment in memory. It // touches no network and starts no service. func New(cfg Config) (*Site, error) { cfg.defaults() set, err := content.Load(cfg.ContentDir) if err != nil { return nil, err } r := content.NewRenderer(set) if err := r.Render(); err != nil { return nil, err } s := &Site{ Shell: NewShell(), Config: cfg, Content: set, Renderer: r, Manifest: &stage.Manifest{}, FragmentsByDoc: map[string][]string{}, fragByID: map[string]*Fragment{}, } if err := s.buildFragments(); err != nil { return nil, err } return s, nil } // PublicDocs returns the published documents: public: true, in navigation order. func (s *Site) PublicDocs() []*content.Document { out := make([]*content.Document, 0, len(s.Content.Docs)) for _, d := range s.Content.Docs { if d.Public { out = append(out, d) } } return out } // PublicInArea returns the published documents of one area, L1 first. func (s *Site) PublicInArea(area string) []*content.Document { var out []*content.Document for _, d := range s.PublicDocs() { if d.Area == area { out = append(out, d) } } return out } // pageCount counts published pages in an area. func (s *Site) pageCount(area string) int { return len(s.PublicInArea(area)) } // artefactCount counts every resolved artefact on the site. func (s *Site) artefactCount() int { n := 0 for _, d := range s.PublicDocs() { n += len(d.Artefacts) } return n } // Home is the landing page view: the ninety-second read. func (s *Site) Home() templates.HomeView { cardArea := func(a content.Area) templates.PillarCardView { card := templates.PillarCardView{ Area: a, Title: a.Title, Claim: a.Blurb, Href: "/" + a.Key + "/", Level: 1, Pages: s.pageCount(a.Key), Metrics: []templates.MetricView{}, Tech: []string{}, } if d, ok := s.Content.AreaLanding(a.Key); ok { card.Claim = d.Claim card.Confidence = d.Confidence card.Title = d.Title for _, m := range d.Metrics { card.Metrics = append(card.Metrics, templates.MetricView{Label: m.Label, Value: content.MetricValue(m.Value)}) } card.Tech = d.Tech } return card } cards := make([]templates.PillarCardView, 0, len(content.Areas)) for _, a := range content.Areas { cards = append(cards, cardArea(a)) } return templates.HomeView{ Layout: s.Layout(LayoutOpts{ Title: "Sam Rolfe — AI systems, the platforms under them, the hardware at the edge", Description: content.Positioning + " " + content.SupportLine, Crumbs: []templates.Crumb{{Label: "Home"}}, Binding: s.RouteBinding(), }), Positioning: content.Positioning, Support: content.SupportLine, Location: content.Location, Cards: cards, Artifacts: s.artefactCount(), Pages: len(s.PublicDocs()), Areas: len(content.Areas), } } // Doc builds the view for one document at any depth. func (s *Site) Doc(d *content.Document) templates.DocView { area, _ := content.AreaByKey(d.Area) levelLabel := templates.LevelLabel(d.Level) view := templates.DocView{ Layout: s.Layout(LayoutOpts{ Title: d.Title + " — Sam Rolfe", Description: content.Truncate(d.Claim, 160), Area: d.Area, Crumbs: s.crumbs(d), Binding: s.RouteBinding(), }), Doc: d, AreaTitle: area.Title, Role: area.Role, Body: templates.MarkdownBody(d.HTML), Metrics: make([]templates.MetricView, 0, len(d.Metrics)), Tech: d.Tech, Evidence: s.evidence(d), Backlinks: s.backlinks(d), SubPages: s.subPages(d), LevelLabel: levelLabel, } for _, m := range d.Metrics { view.Metrics = append(view.Metrics, templates.MetricView{Label: m.Label, Value: content.MetricValue(m.Value)}) } if d.Area == "history" && d.Level <= 2 { view.Timeline = s.timeline(d.Area) } return view } func (s *Site) crumbs(d *content.Document) []templates.Crumb { area, _ := content.AreaByKey(d.Area) out := []templates.Crumb{ {Label: "Home", Href: "/"}, {Label: area.Title, Href: "/" + d.Area + "/"}, } if d.Level > 1 { out = append(out, templates.Crumb{Label: d.Title, Href: d.URL}) } return out } // evidence turns each artefact into a row that loads the artefact into the // stage panel and is also a working link. func (s *Site) evidence(d *content.Document) []templates.LinkView { out := make([]templates.LinkView, 0, len(d.Artefacts)) ids := s.FragmentsByDoc[d.ID] for _, a := range d.Artefacts { ra, err := s.Content.Resolve(d, a) if err != nil { // Load already rejected dangling artefacts; keep going rather than // render a broken link. continue } conf := d.Confidence if ra.Target != nil { conf = ra.Target.Confidence } link := templates.LinkView{ Label: a.Label, Href: ra.Href, Kind: ra.Kind, Confidence: conf, } for _, id := range ids { if f, ok := s.fragByID[id]; ok && f.ArtefactURL == a.URL { link.HXGet = s.RouteFragmentURL(id) } } out = append(out, link) } return out } func (s *Site) backlinks(d *content.Document) []templates.LinkView { out := make([]templates.LinkView, 0, len(d.Backlinks)) for _, b := range d.Backlinks { out = append(out, templates.LinkView{Label: b.Title, Href: b.URL, Kind: templates.LevelLabel(b.Level)}) } return out } // subPages is the drill-down list under a page. L1 lists the capability pages // in its area; every other level lists artefacts in the area that this page // does not already cite, so no page can become unreachable. func (s *Site) subPages(d *content.Document) []templates.LinkView { cited := map[string]bool{} for _, a := range d.Artefacts { if ra, err := s.Content.Resolve(d, a); err == nil && ra.Target != nil { cited[ra.Target.ID] = true } } var out []templates.LinkView for _, other := range s.PublicInArea(d.Area) { if other.ID == d.ID || cited[other.ID] { continue } if d.Level == 1 && other.Level != 2 { continue } if d.Level > 1 && other.Level < 3 { continue } out = append(out, templates.LinkView{ Label: other.Title, Href: other.URL, Kind: other.Confidence, }) } return out } // timeline orders an area's deeper pages by period, newest first. func (s *Site) timeline(area string) []templates.TimelineItem { var items []templates.TimelineItem for _, d := range s.PublicInArea(area) { if d.Level < 2 { continue } items = append(items, templates.TimelineItem{ Period: d.Period, Title: d.Title, Href: d.URL, Confidence: d.Confidence, Claim: d.Claim, }) } return items } // Evidence is the one-page index of every artefact on the site. func (s *Site) Evidence() templates.EvidenceView { var groups []templates.EvidenceGroup total := 0 for _, a := range content.Areas { group := templates.EvidenceGroup{AreaTitle: a.Title, Href: "/" + a.Key + "/"} for _, d := range s.PublicInArea(a.Key) { for _, l := range s.evidence(d) { group.Rows = append(group.Rows, templates.EvidenceRow{ Label: d.Title + " · " + l.Label, Href: l.Href, HXGet: l.HXGet, Kind: l.Kind, Confidence: l.Confidence, }) total++ } } if len(group.Rows) > 0 { groups = append(groups, group) } } return templates.EvidenceView{ Layout: s.Layout(LayoutOpts{ Title: "Evidence index — Sam Rolfe", Description: "Every artefact this site claims, on one page.", Crumbs: []templates.Crumb{{Label: "Home", Href: "/"}, {Label: "Evidence"}}, Binding: s.RouteBinding(), }), Groups: groups, Total: total, } } // StreamKeys is the fragment sequence the streaming panel demonstrates: the six // area landing fragments, in navigation order. No model is involved. func (s *Site) StreamKeys() []string { n := len(content.Areas) if n > len(s.Fragments) { n = len(s.Fragments) } keys := make([]string, 0, n) for _, f := range s.Fragments[:n] { keys = append(keys, f.ID) } return keys } // Stream is the page that re-points the panel at an event stream. The layout is // the same as every other page; only the binding differs. func (s *Site) Stream(endpoint string) templates.StreamView { keys := s.StreamKeys() links := make([]templates.LinkView, 0, len(keys)) for _, id := range keys { if f, ok := s.fragByID[id]; ok { links = append(links, templates.LinkView{ Label: f.Title, Href: s.DocURL(f.DocID), HXGet: s.RouteFragmentURL(id), Kind: f.Kind, }) } } return templates.StreamView{ Layout: s.Layout(LayoutOpts{ Title: "Streaming panel — Sam Rolfe", Description: "The presentation panel bound to an event stream instead of pre-rendered files.", Crumbs: []templates.Crumb{{Label: "Home", Href: "/"}, {Label: "Streaming panel"}}, Binding: s.StreamBinding(endpoint), }), Frames: len(links), Fragment: links, } } // DocURL is the published URL for a document id. func (s *Site) DocURL(id string) string { if d, ok := s.Content.ByID(id); ok { return d.URL } return "/" } // RouteFragmentURL is where the route driver's fragment file is served from. func (s *Site) RouteFragmentURL(id string) string { return stage.RouteDriver{BaseURL: "/stage"}.FragmentURL(id) } // Fragment is one pre-rendered stage fragment. type Fragment struct { ID string DocID string Kind string Title string // ArtefactURL is the artefact this fragment was built from, if any. It is // how an evidence row finds the fragment that belongs to it. ArtefactURL string // File is relative to the published root, e.g. stage/doc-edge-overview.html. File string // HTML is the rendered fragment markup, written to File at build time. HTML string subtitle string body templ.Component code *stage.Code src string alt string links []stage.Link confidence string docURL string docTitle string } // Fragment returns a pre-rendered fragment by id. func (s *Site) Fragment(id string) (*Fragment, bool) { f, ok := s.fragByID[id] return f, ok } func year() int { return time.Now().Year() }