internal/site/site.go
Ref: Size: 7.2 KiB History
package site
import (
"fmt"
"html/template"
"os"
"path/filepath"
"strings"
)
// pages is the published doc set: how-to material for people running or using
// a fleet. The rest of docs/ (architecture, ethos, decisions, the shape
// diagram, superpowers/) is for people working on eitri and never publishes.
// Build hard-fails if any listed doc is missing.
var pages = []string{
"quickstart",
"joining",
"connecting",
"networking",
"volumes",
"self-hosting",
"byo-idp",
"mcp",
"upgrade",
"cert-rotation",
"credential-revocation",
"faq",
"releases",
}
// Config locates the generator's inputs and output.
type Config struct {
DocsDir string // repo docs/ — markdown sources for the pages list
SiteDir string // site/ — index.md, docs.md, template.html, style.css
DistDir string // optional dist/<version> with release artifacts; "" renders a docs-only preview
OutDir string // webroot to emit
BaseURL string // where the site is served from; "" leaves preview links relative
}
// abs makes a site path absolute against BaseURL. Link-preview scrapers do
// not resolve relative og:image or og:url against the page they fetched, so
// a published site must name itself.
func (cfg Config) abs(path string) string {
if cfg.BaseURL == "" {
return path
}
return strings.TrimSuffix(cfg.BaseURL, "/") + path
}
type pageData struct {
Title string
Section string // "home" | "docs" | "dl" — which nav entry is active
Content template.HTML
Description string // the page's own summary, for search and link previews
URL string // canonical location
Image string // the link-preview card drawn for this page
}
// Build renders the whole site into cfg.OutDir.
func Build(cfg Config) error {
tmplSrc, err := os.ReadFile(filepath.Join(cfg.SiteDir, "template.html"))
if err != nil {
return err
}
tmpl, err := template.New("page").Parse(string(tmplSrc))
if err != nil {
return err
}
targets := linkTargets()
page := func(outPath string, d pageData) error {
if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
return err
}
f, err := os.Create(outPath)
if err != nil {
return err
}
if err := tmpl.Execute(f, d); err != nil {
f.Close()
return err
}
return f.Close()
}
// renderFile returns a page's HTML and the summary drawn from its own
// opening paragraph — one source of truth for what the page is about.
renderFile := func(src string) (template.HTML, string, error) {
b, err := os.ReadFile(src)
if err != nil {
return "", "", err
}
h, secs, err := render(b, targets)
if err != nil {
return "", "", fmt.Errorf("%s: %w", src, err)
}
return template.HTML(withTOC(h, secs)), summary(b), nil
}
// preview draws the page's link-preview card and returns the canonical
// URL and image to advertise. label names the page on the card; empty
// leaves the landing page unlabelled.
preview := func(name, sitePath, label string) (url, image string, err error) {
raw, err := renderCard(label)
if err != nil {
return "", "", fmt.Errorf("card %q: %w", name, err)
}
dir := filepath.Join(cfg.OutDir, "og")
if err := os.MkdirAll(dir, 0o755); err != nil {
return "", "", err
}
if err := os.WriteFile(filepath.Join(dir, name+".png"), raw, 0o644); err != nil {
return "", "", err
}
return cfg.abs(sitePath), cfg.abs("/og/" + name + ".png"), nil
}
// Landing.
content, desc, err := renderFile(filepath.Join(cfg.SiteDir, "index.md"))
if err != nil {
return err
}
url, image, err := preview("home", "/", "")
if err != nil {
return err
}
if err := page(filepath.Join(cfg.OutDir, "index.html"), pageData{
Title: "eitri", Section: "home", Content: content,
Description: desc, URL: url, Image: image,
}); err != nil {
return err
}
// Docs index (site-owned: the published set is a curated subset of docs/,
// so the repo's own docs/README.md would dangle links here) + one page per
// published doc + the roadmap.
if content, desc, err = renderFile(filepath.Join(cfg.SiteDir, "docs.md")); err != nil {
return err
}
if url, image, err = preview("docs", "/docs/", "docs"); err != nil {
return err
}
if err := page(filepath.Join(cfg.OutDir, "docs", "index.html"), pageData{
Title: "eitri – docs", Section: "docs", Content: content,
Description: desc, URL: url, Image: image,
}); err != nil {
return err
}
for _, slug := range pages {
if content, desc, err = renderFile(filepath.Join(cfg.DocsDir, slug+".md")); err != nil {
return err
}
label := strings.ReplaceAll(slug, "-", " ")
if url, image, err = preview(slug, "/docs/"+slug+"/", label); err != nil {
return err
}
if err := page(filepath.Join(cfg.OutDir, "docs", slug, "index.html"), pageData{
Title: "eitri – " + slug, Section: "docs", Content: content,
Description: desc, URL: url, Image: image,
}); err != nil {
return err
}
}
if content, desc, err = renderFile(filepath.Join(cfg.DocsDir, "..", "ROADMAP.md")); err != nil {
return err
}
if url, image, err = preview("roadmap", "/docs/roadmap/", "roadmap"); err != nil {
return err
}
if err := page(filepath.Join(cfg.OutDir, "docs", "roadmap", "index.html"), pageData{
Title: "eitri – roadmap", Section: "docs", Content: content,
Description: desc, URL: url, Image: image,
}); err != nil {
return err
}
// Downloads page.
dlMD, err := downloadsMarkdown(cfg.DistDir)
if err != nil {
return err
}
h, _, err := render([]byte(dlMD), targets)
if err != nil {
return err
}
if url, image, err = preview("dl", "/dl/", "downloads"); err != nil {
return err
}
if err := page(filepath.Join(cfg.OutDir, "dl", "index.html"), pageData{
Title: "eitri – downloads", Section: "dl", Content: template.HTML(h),
Description: summary([]byte(dlMD)), URL: url, Image: image,
}); err != nil {
return err
}
// The API contract publishes verbatim at the site root when the docs
// tree carries it (site-check enforces presence in the real tree;
// synthetic docs trees may omit it).
spec, err := os.ReadFile(filepath.Join(cfg.DocsDir, "openapi.json"))
switch {
case err == nil:
if err := os.WriteFile(filepath.Join(cfg.OutDir, "openapi.json"), spec, 0o644); err != nil {
return err
}
case !os.IsNotExist(err):
return err
}
// The one stylesheet.
css, err := os.ReadFile(filepath.Join(cfg.SiteDir, "style.css"))
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(cfg.OutDir, "style.css"), css, 0o644); err != nil {
return err
}
// The tab icon, drawn from the font rather than kept as a source file so
// it cannot drift from the mark on the page.
iconFont, err := parseBDF(gohu11)
if err != nil {
return err
}
iconMark, err := markBitmap(iconFont)
if err != nil {
return err
}
icon, err := faviconSVG(iconMark, faviconBox)
if err != nil {
return err
}
return os.WriteFile(filepath.Join(cfg.OutDir, "favicon.svg"), []byte(icon), 0o644)
}
// linkTargets maps markdown link destinations (as written in the sources) to
// published site paths — the vocabulary the link rewriter validates against.
// A doc outside the published set is deliberately absent: linking it from a
// published page is a build failure, not a dangling link.
func linkTargets() map[string]string {
t := map[string]string{"../ROADMAP.md": "/docs/roadmap/"}
for _, s := range pages {
t[s+".md"] = "/docs/" + s + "/"
}
return t
}