Development
Guide for contributing to moat or understanding the codebase.
Prerequisites
- Go 1.24+ —
go version - Node.js 18+ — only for e2e tests, not required for building moat itself
- Chromium with
--remote-debugging-port=9222— for running Playwright e2e tests
Build from source
git clone https://github.com/oddship/moat.git
cd moat
go build -o moat .
./moat version
Run tests
Go unit tests
go test ./...
Covers search index generation, config parsing, and build output. No browser needed.
Playwright e2e tests
The e2e tests connect to an existing Chromium browser via CDP — they don't launch their own. This means they test against the real browser the user is running.
# 1. Start Chromium with remote debugging (in a separate terminal)
chromium --remote-debugging-port=9222
# 2. Build the docs site without base_path for local testing
moat build docs/ _site/ --site-name moat
# 3. Serve locally
moat serve _site/
# 4. Install test dependencies and run (in another terminal)
npm install
npx playwright test --workers=1
Tests cover layout structure, search dialog, keyboard shortcuts, theme toggle, and accessibility attributes.
Project structure
moat/
├── main.go # CLI entrypoint (build, serve, init, version)
├── build.go # Build pipeline: walk → parse → render → write
├── config.go # Config types and TOML parsing
├── search.go # Search index generation from rendered HTML
├── nav.go # Sidebar navigation tree + HTML rendering
├── layouts.go # Template loading (built-in + custom)
├── markdown.go # Goldmark markdown → HTML rendering
├── frontmatter.go # YAML frontmatter parsing
├── shortcodes.go # Shortcode template processing
├── defaults.go # Title/filename conventions (strip prefixes)
├── serve.go # Simple static file server
├── search_test.go # Go unit tests
├── embed/ # Built-in templates (embedded via go:embed)
│ ├── _layout.html # Base layout (oat sidebar + topnav)
│ ├── _layout.landing.html # Landing page variant
│ └── config.toml # Default config scaffold
├── e2e/ # Playwright e2e tests
│ ├── fixtures.js # CDP connection fixture
│ ├── layout.spec.js
│ ├── search.spec.js
│ └── theme.spec.js
├── docs/ # moat's own documentation (built with moat)
└── .github/workflows/ # CI: build docs, deploy to Pages, releases
Build pipeline
The Build() function in build.go runs this pipeline:
- Walk source directory for
.mdfiles (skip_and.prefixed paths) - Parse frontmatter and store raw markdown body on each
Page - Build navigation tree from directory structure
- Generate syntax highlighting CSS (light + dark themes via Chroma)
- Render each page:
- Process shortcodes (expand
{{</* name */>}}templates) - Render markdown to HTML via Goldmark
- Store rendered HTML on
Page.HTML - Execute layout template with
TemplateData - Write output HTML file
- Process shortcodes (expand
- Generate search index from rendered HTML (strip tags, cap at 2000 chars)
- Copy
_static/directory as-is
Search indexing
The search index (_search.json) is built after the render loop so it indexes the final rendered HTML, including shortcode output. extractSearchText() strips HTML tags with a single regex and caps text at 2000 characters per entry.
The built-in layout includes inline JavaScript that:
- Lazily fetches the search index on first keystroke
- Scores matches: title (100 pts) > description (25 pts) > body (10 pts)
- Renders top 8 results in a
<dialog>modal - Supports
/keyboard shortcut to open search
Layout system
Layouts are Go html/template files. The built-in layout is embedded via go:embed and used when no _layout.html exists in the source directory.
Custom layouts override the built-in by providing _layout.html. Named variants (_layout.wide.html) are selected via layout: wide in frontmatter.
Template functions available in layouts:
| Function | Description |
|---|---|
safeHTML |
Render a string as raw HTML |
formatDate |
Format a date string as "January 2, 2006" |
linkIcon |
Return built-in SVG icon by name (e.g. "github") |
Adding a built-in icon
Icons for [[links]] and [[topnav]] config are stored in nav.go:
var builtinIcons = map[string]string{
"github": `<svg ...>...</svg>`,
}
To add a new icon, add an entry to the builtinIcons map with the SVG markup. Keep SVGs at 16×16, using fill="currentColor" and aria-hidden="true".
Style guidelines
- No inline styles in the built-in layout. Use oat utility classes (
hstack,gap-2,justify-end,mt-6, etc.) or component patterns (role="search",data-topnav, etc.). - No new CSS framework or build pipeline. The layout uses oat via CDN.
- No new Go dependencies for HTML processing. Use regex-based extraction for search text.
- Conventional commits for commit messages (
feat:,fix:,test:,docs:).
Releasing
Releases are automated via GitHub Actions. Push a v* tag to trigger:
git tag v0.3.0
git push origin v0.3.0
This builds binaries for linux/darwin × amd64/arm64 and creates a GitHub Release.