Architecture
This page describes how Braised Docs is structured internally. It is intended for contributors and developers building on the codebase.
For the full design spec including goals, non-goals, and resolved design decisions, see ssg-spec.md in the repository root.
Binaries
Two binaries. No other runtime dependencies.
braised ← core build tool (Go, single binary)
tailwindcss ← CSS compilation (Tailwind v4 standalone CLI)
The braised binary embeds at compile time:
- Built-in block templates
- Built-in layout templates
- Built-in theme CSS (
braised:theme/) - Alpine.js
Repository layout
/
├── braised.yaml ← this site's config (dogfooding)
├── nav.yaml ← this site's navigation
├── docs/ ← this site's source content
├── main.go
├── cmd/
│ ├── root.go ← cobra root
│ ├── build.go ← braised build
│ ├── init.go ← braised init (project scaffold)
│ ├── serve.go ← braised serve (dev server)
│ ├── inspect.go ← braised inspect (single-file diagnostic)
│ ├── mcp.go ← braised mcp (stdio schema server)
│ └── mcp_serve.go ← braised mcp serve (HTTP artifact server)
├── internal/
│ ├── artifact/ ← structured JSON artifact writer
│ ├── block/ ← :::directive parser, renderer, built-in templates
│ ├── build/ ← build orchestration
│ ├── bundle/ ← JS bundle (Alpine.js + block components)
│ ├── chunk/ ← AST-based semantic chunker
│ ├── config/ ← braised.yaml → SiteConfig
│ ├── css/ ← Tailwind CSS pipeline
│ ├── errs/ ← thread-safe error/warning collector
│ ├── indexstate/ ← content hash + chunk ID tracking
│ ├── linkcheck/ ← internal link validation
│ ├── llmstxt/ ← llms.txt generation
│ ├── nav/ ← nav.yaml → NavTree
│ ├── parser/ ← Goldmark setup, ParsedPage, :::include
│ ├── pipeline/ ← post-build pipeline runner (command, HTTP)
│ ├── ref/ ← cross-reference index (braised:ref/)
│ ├── render/ ← layout template execution, file output
│ ├── serve/ ← dev server with live reload
│ └── testutil/ ← shared test helpers
└── ssg-spec.md ← full product and technical spec
Build pipeline
braised build
│
├─ 1. Load braised.yaml → SiteConfig
├─ 2. Parse nav.yaml → NavTree
├─ 3. Walk docs/ → list of .md files
├─ 3b. Load index state + hash source files (incremental manifest delta)
├─ 3c. Scan frontmatter on all files → build ref index (braised:ref/)
├─ 4. Create renderer pool (one renderer per CPU)
├─ 5. Parse + render each file (parallel, errors collected, never aborted)
│ Resolve :::include directives
│ Parse Markdown → ParsedPage (Goldmark + :::directive extension)
│ Execute block templates → assembled HTML
│ Execute layout template → final HTML
│ Write dist/{url}/index.html
├─ 6. Write content manifest (new/changed chunks) + save index state
├─ 7. Write JS bundle (Alpine.js + block components)
├─ 8. CSS pipeline (Tailwind v4 CLI)
├─ 8b. Pass 2 — resolve block-contributed braised:ref/ sentinels
├─ 9. Link validation (internal links across all pages)
├─ 9a. Copy static/ files verbatim to dist/
├─ 9b. Write llms.txt
├─ 10. Run post-build pipeline (command or HTTP webhook)
├─ 11. Write structured artifacts → artifacts/nav-structure.json, artifacts/frontmatter-index.json, artifacts/sources-index.json
├─ 11b. Write sitemap.xml (when site.url is set)
└─ 12. Write .braised/last_build.json
Error handling
No single page failure stops the build. All pages are always attempted. Errors and warnings are accumulated in a thread-safe errs.Collector and printed together at the end.
Exit codes:
0— clean build1— build completed with one or more errors
Key packages
internal/config
Loads and validates braised.yaml. Defines SiteConfig and all sub-structs. Fields for future stages (versioning) are present in the struct even when not yet implemented — this keeps the config schema stable as features ship.
internal/nav
Parses nav.yaml into a NavTree. Uses yaml.Node low-level parsing to handle nav.yaml's irregular structure (sections vs. leaf entries vs. !include tags). Resolves !include directives recursively. MarkActive() walks the tree to mark the current page and its ancestors for sidebar highlighting.
internal/parser
Configures Goldmark with GFM extensions and auto-heading IDs. Registers the :::directive block parser extension and the :::include preprocessor. Extracts frontmatter via goldmark-meta and headings via AST walking. Retains the raw AST on ParsedPage for use by the chunker.
internal/block
Implements the :::directive block system. The block parser (braisedBlockParser) handles fence-open, named-close, and simple-close lines. The renderer executes Go html/template templates against a BlockContext and assembles the final HTML. Built-in block templates are embedded in the binary. Local site templates in blocks/ take precedence.
internal/render
Executes the page layout template against a PageContext and writes output files. The built-in template is embedded in the binary via Go's embed package. Uses Go's html/template {{block}} system for per-region overrides without copying the whole template.
internal/ref
In-memory cross-reference index. Populated during the pre-render scan (step 3c) from frontmatter id and aliases fields, and augmented during rendering by block id= props. Resolves braised:ref/{id} links. A two-pass approach (step 8b) handles forward references where a block on a later page contributes a label that an earlier page links to.
internal/chunk
AST-based semantic chunker. Walks the Goldmark AST and splits pages into context-preserving chunks for RAG indexing. Each chunk carries inherited heading context so embedding models receive meaningful fragments rather than raw document slices.
internal/bundle
Writes dist/blocks.js — Alpine.js concatenated with braised's built-in Alpine components (braisedTabs, braisedPillstrip, etc.). Always written so any block on any page can reference these components.
internal/css
Invokes the Tailwind v4 standalone CLI to compile the site's CSS entry file into dist/assets/braised.css. Resolves braised:theme/ imports from the embedded theme.
internal/serve
Dev server. Runs an initial full build, then watches source files with fsnotify and schedules debounced rebuilds (300 ms) on any change. Connected browsers are notified via Server-Sent Events and reload automatically. Ignores writes to dist/, hidden files, and editor temp files.
internal/pipeline
Post-build pipeline runner. Executes a configured command or http webhook after a successful build, passing the path to manifest.jsonl. Used to trigger embedding pipelines (Nomic, OpenAI, etc.).
internal/linkcheck
Validates internal links across all rendered pages. Broken internal links are recorded as warnings — the build still succeeds.
internal/indexstate
Tracks a content hash and chunk IDs for each source file between builds. On each build, only new and changed chunks are written to the manifest, so embedding pipelines re-process only what actually changed.
internal/llmstxt
Generates dist/llms.txt — a structured Markdown index of the site following the llmstxt.org spec. Populated from nav structure and per-page metadata collected during rendering. Supports llm_description and llm_exclude frontmatter fields and the llms_txt.preamble config key.
internal/artifact
Writes structured JSON artifact files for agent audiences at the end of every full build. Built-in artifacts: nav-structure.json (full nav tree), frontmatter-index.json (per-page metadata), and sources-index.json (provenance index from :::sources blocks). Also writes .braised/last_build.json as a build state marker for braised mcp serve.
internal/errs
Thread-safe error and warning collector. Errors accumulate across concurrent page builds; no single page failure aborts the build.
Block rendering philosophy
The core rule:
Child templates are data providers. Parent templates own all structural markup.
When a parent template (e.g. tabs.html) iterates .ChildBlocks, each entry's .Rendered field contains the full output of the child's template — not raw inner content. Child templates for structural children (e.g. tab.html) should emit {{.Children}} and nothing else. The parent template is responsible for the surrounding elements: tab buttons, panel divs, <tr> wrappers, and so on.
Independent leaf blocks (callout, details, synopsis, rawhtml) always execute their own templates and are never data providers to a parent.
Implication for custom block authors: if your block type is always a child of a specific structural parent, keep its template minimal ({{.Children}}). Let the parent template own the wrapper markup. The parent can read any props it needs from .ChildBlocks[i].Props.
Inspiration
Braised Docs draws from Hugo and Sphinx. Where patterns are borrowed, code comments acknowledge the lineage.
Roadmap
| Stage | Scope |
|---|---|
| 1 ✓ | Core build pipeline: config, nav, Markdown → HTML |
| 2 ✓ | Block system: :::directive syntax, Go template blocks, Alpine.js |
| 3 ✓ | CSS pipeline: Tailwind v4 CLI integration, braised:theme/ |
| 4 ✓ | Chunking + manifest: AST-based semantic chunker, manifest.jsonl |
| 5 ✓ | RAG pipelines: command and HTTP webhook integration |
| 6 ✓ | llms.txt generation (llm_description/llm_exclude/llms_txt.preamble), :::include, :::sources + provenance index, dev server, braised inspect/--agent, braised mcp (stdio schema server), braised mcp serve (HTTP artifact server), artifact writer (nav-structure.json, frontmatter-index.json, sources-index.json, last_build.json), sitemap.xml |
| 7 | Versioning: version switcher, URL prefix support |