feat(11.1-02): add the documentation theme, chroma highlighting and docs:serve
- WinterCMS-style shell: header with search and theme toggle, grouped sidebar, on-page TOC, pager, page actions, callouts, heading permalinks, footer and a 404 page - fenced code highlighted at build time by chroma/v2 into tok-* classes, with a copy button; no inline script, style or handler - vendored DM Sans/DM Mono fonts and Lucide icons with their licences - client-side search over search-index.json built with textContent only - summer docs:serve builds into a temp dir, serves on loopback by default, returns 404.html with status 404 and rebuilds on change
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path"
|
||||
"strings"
|
||||
@@ -25,6 +26,11 @@ func (s *site) url(p string) string {
|
||||
return s.base + "/" + p
|
||||
}
|
||||
|
||||
// writeIcon writes the inline Lucide SVG named name (icons.html).
|
||||
func writeIcon(w io.Writer, name string) {
|
||||
_ = pageTmpl.ExecuteTemplate(w, "icon-"+name, nil)
|
||||
}
|
||||
|
||||
type navItem struct {
|
||||
Title string
|
||||
URL string
|
||||
@@ -33,21 +39,62 @@ type navItem struct {
|
||||
|
||||
type navSection struct {
|
||||
Title string
|
||||
// API marks the API reference section, whose items are module names
|
||||
// set in DM Mono.
|
||||
API bool
|
||||
Items []navItem
|
||||
}
|
||||
|
||||
type tocItem struct {
|
||||
ID string
|
||||
Text string
|
||||
Level int
|
||||
}
|
||||
|
||||
type pagerLink struct {
|
||||
Title string
|
||||
URL string
|
||||
// Section is set only when the target is in another section.
|
||||
Section string
|
||||
}
|
||||
|
||||
// pageView is the data of page.html and 404.html.
|
||||
type pageView struct {
|
||||
DocTitle string
|
||||
Title string
|
||||
Description string
|
||||
Eyebrow string
|
||||
Content template.HTML
|
||||
CSS string
|
||||
Assets string
|
||||
HomeURL string
|
||||
SearchIndex string
|
||||
LLMS string
|
||||
LLMSFull string
|
||||
EditURL string
|
||||
MarkdownURL string
|
||||
Nav []navSection
|
||||
TOC []tocItem
|
||||
Prev, Next *pagerLink
|
||||
}
|
||||
|
||||
// minTOC is the number of H2/H3 headings a page needs for a TOC.
|
||||
const minTOC = 2
|
||||
|
||||
// baseView fills the fields every page shares.
|
||||
func (s *site) baseView(current *Page) pageView {
|
||||
return pageView{
|
||||
Assets: s.url("assets"),
|
||||
HomeURL: s.url("index.html"),
|
||||
SearchIndex: s.url("search-index.json"),
|
||||
LLMS: s.url("llms.txt"),
|
||||
LLMSFull: s.url("llms-full.txt"),
|
||||
Nav: s.nav(current),
|
||||
}
|
||||
}
|
||||
|
||||
// render renders every page and fills s.outputs with the HTML pages, their
|
||||
// Markdown siblings, llms.txt, llms-full.txt, search-index.json and assets.
|
||||
// Markdown siblings, 404.html, llms.txt, llms-full.txt, search-index.json
|
||||
// and assets.
|
||||
func (s *site) render() ([]Problem, error) {
|
||||
md := newMarkdown()
|
||||
var problems []Problem
|
||||
@@ -60,17 +107,33 @@ func (s *site) render() ([]Problem, error) {
|
||||
rendered[i] = r
|
||||
}
|
||||
for i, p := range s.pages {
|
||||
view := pageView{
|
||||
DocTitle: p.Title + " · " + s.cfg.Title + " docs",
|
||||
Title: p.Title,
|
||||
Description: p.Description,
|
||||
Content: template.HTML(rendered[i].html), //nolint:gosec // goldmark output in safe mode
|
||||
CSS: s.url("assets/site.css"),
|
||||
HomeURL: s.url("index.html"),
|
||||
Nav: s.nav(p),
|
||||
view := s.baseView(p)
|
||||
view.DocTitle = p.Title + " · " + s.cfg.Title + " docs"
|
||||
view.Title = p.Title
|
||||
view.Description = p.Description
|
||||
view.Content = template.HTML(rendered[i].html) //nolint:gosec // goldmark output in safe mode
|
||||
view.MarkdownURL = s.url(p.URL + ".md")
|
||||
if s.cfg.EditURL != "" {
|
||||
view.EditURL = strings.ReplaceAll(s.cfg.EditURL, "{path}", p.Source)
|
||||
}
|
||||
if p.Section == indexSection {
|
||||
view.DocTitle = s.cfg.Title + " documentation"
|
||||
} else {
|
||||
view.Eyebrow = s.cfg.sectionTitle(p.Section)
|
||||
}
|
||||
for _, h := range rendered[i].headings {
|
||||
if h.Level == 2 || h.Level == 3 {
|
||||
view.TOC = append(view.TOC, tocItem{ID: h.ID, Text: h.Text, Level: h.Level})
|
||||
}
|
||||
}
|
||||
if len(view.TOC) < minTOC {
|
||||
view.TOC = nil
|
||||
}
|
||||
if i > 0 {
|
||||
view.Prev = s.pagerLink(p, s.pages[i-1])
|
||||
}
|
||||
if i < len(s.pages)-1 {
|
||||
view.Next = s.pagerLink(p, s.pages[i+1])
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := pageTmpl.ExecuteTemplate(&buf, "page.html", view); err != nil {
|
||||
@@ -78,6 +141,15 @@ func (s *site) render() ([]Problem, error) {
|
||||
}
|
||||
s.outputs[p.URL+".html"] = buf.Bytes()
|
||||
}
|
||||
notFound := s.baseView(nil)
|
||||
notFound.DocTitle = "Page not found · " + s.cfg.Title + " docs"
|
||||
notFound.Title = "Page not found"
|
||||
notFound.Description = "This page does not exist or has moved."
|
||||
var buf bytes.Buffer
|
||||
if err := pageTmpl.ExecuteTemplate(&buf, "404.html", notFound); err != nil {
|
||||
return nil, fmt.Errorf("docsite: render 404.html: %w", err)
|
||||
}
|
||||
s.outputs["404.html"] = buf.Bytes()
|
||||
bodies := make([]string, len(s.pages))
|
||||
for i, p := range s.pages {
|
||||
bodies[i] = markdownBody(p, rendered[i].mdLinks)
|
||||
@@ -96,16 +168,26 @@ func (s *site) render() ([]Problem, error) {
|
||||
return problems, nil
|
||||
}
|
||||
|
||||
// pagerLink describes a prev/next target; the section line is kept only
|
||||
// when the target sits in another section (and is not the index page).
|
||||
func (s *site) pagerLink(from, to *Page) *pagerLink {
|
||||
l := &pagerLink{Title: to.Title, URL: s.url(to.URL + ".html")}
|
||||
if to.Section != from.Section && to.Section != indexSection {
|
||||
l.Section = s.cfg.sectionTitle(to.Section)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// nav builds the sidebar: every site.yaml section in order with its pages.
|
||||
func (s *site) nav(current *Page) []navSection {
|
||||
var out []navSection
|
||||
for _, sec := range s.cfg.Sections {
|
||||
ns := navSection{Title: sec.Title}
|
||||
ns := navSection{Title: sec.Title, API: sec.Name == apiSection}
|
||||
for _, p := range s.pages {
|
||||
if p.Section != sec.Name {
|
||||
continue
|
||||
}
|
||||
ns.Items = append(ns.Items, navItem{Title: p.Title, URL: s.url(p.URL + ".html"), Current: p == current})
|
||||
ns.Items = append(ns.Items, navItem{Title: p.Title, URL: s.url(p.URL + ".html"), Current: current != nil && p == current})
|
||||
}
|
||||
out = append(out, ns)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user