serve a bundle at its permalink
-site (or KHOSRA_SITE) opens the site root through content.OpenSite, so every
read keeps the os.Root guarantee. A path is a bundle key: /{section}/{slug}/
serves, the slashless form redirects permanently to it (ADR-0008), anything
unknown is 404. Render failure logs and returns a bare 500 rather than leaking a
template or filesystem detail.
internal/render holds goldmark plus the embedded reference theme (ADR-0026):
base.html with a redefinable "main" block, and one stylesheet inlined through
.Style. Serving it at an asset route would have been a second routing case for no
gain, and static serving belongs to a later entry.
Evidence beyond the tests: the binary against a real site root returns 200 with
<h1>About</h1> and the rendered body, 301 from /pages/about to /pages/about/, and
404 for /nope/. A Bengali variant is scanned but not yet reachable — that is the
next entry.
theme-contract.md gains a "Live today" section listing the six fields and two
named templates a theme may now rely on; the rest stays marked as shape.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
// Package render turns a bundle into bytes: Markdown to HTML, then a template set. It knows content and
|
||||
// nothing about HTTP.
|
||||
//
|
||||
// The embedded templates and stylesheet are the reference theme (ADR-0026) — a demonstration of
|
||||
// docs/theme-contract.md, not a design. Fields a template may rely on are listed there.
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
|
||||
"khosra/internal/content"
|
||||
)
|
||||
|
||||
//go:embed templates
|
||||
var themeFS embed.FS
|
||||
|
||||
// Page is what a template receives. Absence is the zero value: a template reads what exists and never
|
||||
// fails on a missing field (invariant 1).
|
||||
type Page struct {
|
||||
// Title may be empty; whether that is legal depends on a type, which nothing decides yet.
|
||||
Title string
|
||||
// Lang is the locale this variant is written in.
|
||||
Lang string
|
||||
// Key is the bundle's identity, useful for building links.
|
||||
Key string
|
||||
// HTML is the rendered body, already escaped by the Markdown renderer.
|
||||
HTML template.HTML
|
||||
// Extra carries every frontmatter key the parser does not name (ADR-0002).
|
||||
Extra map[string]any
|
||||
// Style is the reference theme's stylesheet, inlined so a bare site root needs no asset route.
|
||||
Style template.CSS
|
||||
}
|
||||
|
||||
// Renderer holds the parsed template set and the Markdown converter. Templates are parsed once, never
|
||||
// per request (conventions.md).
|
||||
type Renderer struct {
|
||||
tmpl *template.Template
|
||||
md goldmark.Markdown
|
||||
style template.CSS
|
||||
}
|
||||
|
||||
// New parses the reference theme and prepares the Markdown converter.
|
||||
//
|
||||
// A malformed embedded template is a programming error caught at startup, not at request time, so this
|
||||
// returns an error and the caller is expected to treat it as fatal.
|
||||
func New() (*Renderer, error) {
|
||||
tmpl, err := template.ParseFS(themeFS, "templates/*.html")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse reference theme: %w", err)
|
||||
}
|
||||
css, err := themeFS.ReadFile("templates/theme.css")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read reference stylesheet: %w", err)
|
||||
}
|
||||
return &Renderer{tmpl: tmpl, md: goldmark.New(), style: template.CSS(css)}, nil
|
||||
}
|
||||
|
||||
// Bundle renders one bundle into a complete page.
|
||||
func (r *Renderer) Bundle(b content.Bundle) ([]byte, error) {
|
||||
var body bytes.Buffer
|
||||
if err := r.md.Convert(b.Body, &body); err != nil {
|
||||
return nil, fmt.Errorf("markdown %s: %w", b.Path, err)
|
||||
}
|
||||
p := Page{
|
||||
Title: b.Title,
|
||||
Lang: b.Lang,
|
||||
Key: b.Key,
|
||||
HTML: template.HTML(body.String()),
|
||||
Extra: b.Extra,
|
||||
Style: r.style,
|
||||
}
|
||||
var out bytes.Buffer
|
||||
if err := r.tmpl.ExecuteTemplate(&out, "base", p); err != nil {
|
||||
return nil, fmt.Errorf("template %s: %w", b.Key, err)
|
||||
}
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user