Static Site Generator that builds a project's documentation site directly from its source code, so the docs can never drift from the code they describe, with SEO/AEO, first-class blog, search, and cross-project linking built in. It is for maintainers who want a repository's documentation built from the repository itself, so API references, CLI help, schemas and tests are written by the code rather than beside it.
Extractors ship for Go, Python, TypeScript/JavaScript, Svelte, Zig, Dart, Kotlin, Swift and SQL. selfdoc is one Go binary with no runtime to install: stylesheets, scripts, themes and the word list are compiled into it.
go install github.com/stricttools/selfdoc@v0
On a machine with no Go toolchain, download the archive for your platform from the latest GitHub Release -- prebuilt binaries are published for Linux, macOS and Windows on amd64 and arm64 -- and put selfdoc on your PATH.
Two optional dependencies, needed only by the features that use them: Pagefind for the search index, and python3 for custom directives (a .py directive script is the only thing selfdoc runs an interpreter for; every built-in extractor, the Python one included, parses in process).
# Initialize in an existing project (auto-detects language)
selfdoc init --base-url https://myproject.pages.dev
# Auto-generate API and CLI reference pages
selfdoc gen
# Edit .stricttools/docs/ pages -- add directives referencing your code
# Build HTML output
selfdoc build
# Validate directives, coverage, and SEO lint
selfdoc check
# Serve locally with live reload
selfdoc serveYour selfdoc.json needs versions and locales -- even for a single-version, single-locale project. Each source entry names its own path and language:
{
"source": [{"path": "internal/", "language": "go"}],
"base_url": "https://my-project.example.com",
"versions": [{"version": "1.0.0"}],
"locales": [{"code": "en", "label": "English", "default": true}]
}- Directive syntax -- embed live API references, schemas, tests, and CLI help directly from source code (
:-:,:<:,:>:) - Auto-generated pages -- API reference and CLI docs from source code structure (
selfdoc gen) - Multi-version docs -- build from git tags, cached builds, version picker UI
- Localization -- parallel locale directories, hreflang tags, locale picker, per-locale sitemaps
- Monorepo support -- unified site builder combines multiple projects into one docs site
- Blog posts --
selfdoc blog postfor authoring, listing pages, feeds, and a local editor app - Faceted search -- key=value filter syntax, 7 dimensions, chip UI, auto-injected version default
- Sandboxed data generation -- run scripts in bubblewrap isolation (
selfdoc gen-data) - Theming -- dark mode, accent colors, custom CSS overrides
- Search -- Pagefind, indexed at build time, no network at read time
- SEO -- lint rules, WCAG contrast validation, JSON-LD structured data, sitemaps
- Coverage tracking -- per-symbol documentation coverage with a configurable threshold
- Syntax highlighting -- build-time highlighting via chroma, code tabs, sortable tables
- Performance -- CSS/JS/HTML minification, critical CSS inlining, gzip and Brotli pre-compression
- Feeds and AI -- Atom feed,
robots.txtwith AI crawler controls,llms.txt/llms-full.txt - Landing page -- hero section, tagline, and feature cards
- Live reload -- SSE-based dev server
- Auto-commit -- generated files committed automatically (prefers safegit)
Directives are inline blocks in your Markdown templates. They get replaced with content extracted from your source code at build time.
:-: directive-name path="arg"
Self-closing directives use :-:. Block directives that wrap a body use :<: to open, :>: to close, with :=: and ::: to delimit sections inside. Directives inside fenced code blocks are ignored.
| Directive | Description |
|---|---|
callout-danger |
Styled danger callout block |
callout-important |
Styled important callout block |
callout-note |
Styled note callout block |
callout-tip |
Styled tip callout block |
callout-warning |
Styled warning callout block |
code-help |
Extract CLI help/usage text and flag definitions |
code-test |
Embed test source code (whole file or specific function) |
cv |
Render a curriculum vitae declared in a TOML document, plus the Person it states |
list-crawlers |
List of the crawlers the generated robots.txt allows |
list-glossary |
Definition list from Term: Definition lines |
list-modules |
List source modules with file paths and docstring summaries |
list-tree |
File/directory tree listing |
prose-desc |
Extract module/package docstring as prose text |
ref |
Extract module docstring, exported functions, and classes |
table-commands |
CLI command summary table from strictcli structure |
table-config |
Render a config file (JSON/TOML) as a key-value table |
table-config-schema |
Configuration field reference table from schema |
table-dep |
Dependencies table from pyproject.toml |
table-directives |
Table of all core built-in directives |
table-endpoint |
REST API endpoint table from OpenAPI spec |
table-lints |
Table of every lint code selfdoc can emit, with its severity |
table-schema |
Extract dataclass/struct fields as a markdown table |
var |
Interpolate project metadata value |
Example -- embed the API docs for a package:
## API Reference
:-: ref path="internal/config"Example -- show a struct's fields as a table:
:-: table-schema path="internal/config.Field"Register custom directives in selfdoc.json under the directives key. Each entry maps a directive name to a Python script (relative to project root) that defines a resolve(attrs, config, body) function returning a Markdown string.
{
"directives": {
"changelog": "scripts/changelog_directive.py"
}
}Script interface:
def resolve(attrs: dict, config: dict, body: list) -> str:
"""Return Markdown string to replace the directive block.
attrs -- directive attributes as str->str dict (e.g. {"path": "v1.0.0"})
config -- the full selfdoc.json config dict
body -- body lines from the directive block (empty list for one-liners)
"""
version = attrs.get("path")
...Use in templates:
:-: changelog path="v1.0.0"The script runs out of process: selfdoc hands an embedded driver to python3, passes the script's path, and sends attrs, config and body as one JSON object on standard input. What the script prints on standard output replaces the directive. A script that will not load, one with no callable resolve, one that raises, and a machine with no python3 are each a hard error that stops the build -- never a note on the published page.
Dispatch order is content directives, then custom directives, then the language extractors -- so a custom name overrides a code-extraction directive such as ref, but not a content directive such as callout-note.
selfdoc.json at the project root:
{
"source": [{"path": "internal/", "language": "go"}],
"docs": ".stricttools/docs/",
"output": ".stricttools/docs-cache/build/",
"base_url": "https://my-project.example.com",
"versions": [{"version": "1.0.0"}],
"locales": [{"code": "en", "label": "English", "default": true}],
"deploy": {
"provider": "cloudflare-pages",
"project": "my-docs"
},
"directives": {}
}| Field | Required | Description |
|---|---|---|
source |
no | List of source entries to extract documentation from. |
base_url |
yes | Base URL of the generated site, used for canonical links and SEO. |
version |
no | Project version. When present, used by deploy instead of reading from the project manifest (VERSION, pyproject.toml or package.json). |
docs |
no | Directory containing the handwritten Markdown documentation templates. It lives inside the tool-state directory, and a value outside it is refused as the layout selfdoc used before. |
output |
no | Output directory for generated HTML files. It lives inside the tool-state directory, in the uncommitted cache. |
changelog |
no | Path to the changelog document published as the site's changelog page, relative to the project root. Absent means the project root's CHANGELOG.md is used if it exists; declare it when that file is not this site's changelog. |
theme |
no | Visual theme for the generated site. One of the themes selfdoc ships -- 'minimal', 'clean' or 'tinymoon'. A build's --theme flag overrides this for that build only, without writing anything back here. |
repo |
no | GitHub repository URL shown in the site header. |
lang |
no | BCP 47 language tag for the site content (e.g. 'en', 'pt-BR'). |
name |
no | Explicit project name. Used as the single source of truth for the manifest name and the auto-generated API reference index description. When absent, the name is derived heuristically (single-source basename or project directory basename). |
description |
no | Short description of the project, used in meta tags and SEO. |
branch |
no | Git branch used for source links in the generated site. |
search |
no | Search UI mode: icon button, full bar, or hidden. |
search_engine |
yes | Search engine that answers this site's search UI. Required and never inferred: every site builds a search UI, so the engine behind it is declared, not defaulted. |
code_icons |
no | Style of language icons shown on code blocks. |
line_numbers |
no | Show line numbers in code blocks. |
run_button |
no | Show a run button on code blocks for supported languages. |
page_nav |
no | Show previous/next navigation links between pages. |
page_progress |
no | Show a reading progress bar at the top of each page. |
glossary |
no | Auto-generate a glossary page from dfn terms. |
coverage_threshold |
no | Minimum fraction of public symbols that must be documented for selfdoc check to pass (0.0-1.0). Default 1.0 requires 100% coverage. |
feed_max_entries |
no | Maximum number of entries in the Atom feed, sorted by most recent. |
lint_ignore |
no | List of warning-severity lint rule IDs to suppress (e.g. 'SEO007', 'SEO008'). Error-severity codes cannot be suppressed and are refused at load. |
root_files |
no | List of underscore-prefixed template paths in docs/ for root file generation. |
redirects |
no | Page-level redirects expanded across all locale/version combos. |
deploy |
no | Deployment configuration for publishing the generated site. |
directives |
no | Custom directive mappings from directive name to source file path. |
examples |
no | Validator command templates keyed by code-block language, used by 'selfdoc check' to execute fenced blocks marked 'validate'. Each template must contain the '{file}' placeholder. Absent means example validation is off. |
author |
yes | The site's author: one Person, named in every page's structured data. Required -- there is no inferred author. |
twitter |
no | Twitter/X handle (starts with @) for the twitter:site meta tag. |
feedback |
no | Feedback collection configuration (at least one of webhook or ga required). |
branding |
no | Landing page branding and call-to-action configuration. |
auto_detect |
no | Automatic content detection settings for step guides and API entries. |
gen |
no | Configuration for the gen command. |
gen_data |
no | Configuration for the gen-data command. |
schema_types |
no | Mapping from page type to schema.org @type (e.g. guide -> TechArticle). |
versions |
no | List of documentation versions to build. |
unversioned |
no | Declares that this project has no public version -- a personal site or portfolio that publishes no artifact. It replaces the 'versions' array (declaring both is an error) and is refused for a project that declares 'source', because code is the thing that gets released and therefore carries a version. An unversioned project's pages show no version badge, offer no version search filter and no version picker. |
locales |
no | List of locales for multi-language documentation. |
unified |
no | Configuration for unified multi-project documentation. |
posts |
no | Blog post configuration. |
topology |
no | Deployment topology for multi-project unified sites. |
assembly |
no | Assembly configuration for unified site deployment. |
selfdoc init auto-detects language and source paths from project files (go.mod, pyproject.toml, tsconfig.json, package.json), and takes the site's own address as --base-url. A project with no detectable language is initialized as a codeless project: no source key, and no code-extraction directive in the starter page.
| Command | Description |
|---|---|
init |
Initialize selfdoc configuration and starter docs template |
build |
Build the documentation site from templates and source code |
serve |
Serve the documentation site locally with live reload |
deploy |
Deploy the built documentation site to the configured provider |
check |
Check documentation coverage, directive resolution, and lint rules -- and write: it advances the content and description baseline of every page it does not report stale or drifted in .stricttools/docs-state/hashes/hashes.json and commits the store, which is why check is a mutating command and not a read-only one |
gen |
Auto-generate documentation pages from project structure |
gen-data |
Generate data files by running sandboxed scripts via bwrap |
spell-corpus |
Spell-check the docs of every selfdoc project beside this one, using the same engine 'selfdoc check' runs (SPELL001) and the shared accept list. Read-only over every project it visits |
quality |
Show documentation quality tier and metrics for the current project |
| baseline | Manage the content and description hash baselines that drive staleness (STALE001) and source-drift (DRIFT001) detection during selfdoc check |
baseline accept |
Accept a reviewed staleness or drift dead-end by advancing a page's stored content and description hash baseline to its current values. Use this only after a human has confirmed the page's content changed but its existing frontmatter description was reviewed and is still accurate. Each named page must currently be reporting a STALE001 or DRIFT001 error; accepting clears that error so selfdoc check passes without rewriting an already-correct description. |
| layout | Inspect and check the per-repository directories selfdoc owns under .stricttools/ |
layout dump |
Print selfdoc's layout declaration: every directory it claims, whether the directory is handwritten or generated, whether the repository commits it, the manifest.toml that grants it and what that file must hold, and the paths it replaced |
layout validate |
Check this repository's .stricttools/ directory: every directory carries a manifest.toml naming a tool this machine has, every directory selfdoc claims names selfdoc, every directory selfdoc owns holds only what its side allows, nothing inside starts with a dot except the derived ignore file, and that ignore file is what selfdoc's declaration renders |
| assembly | Manage the unified multi-project documentation assembly and deployment |
assembly init |
Create and initialize the assembly GitHub repository with workflow and configuration files. Creates a private GitHub repo, pushes initial files via the Contents API, creates a Cloudflare Pages project if credentials are available, and sets GitHub secrets for deployment authentication. |
assembly push |
Dispatch a GitHub Actions workflow to rebuild this project in the documentation assembly. Detects the source repository, resolves the latest git tag as the version reference, and sends a repository dispatch event to the assembly repo with the project slug, version, and commit SHA. |
assembly status |
Show the status of recent assembly build workflow runs on GitHub. Queries the assembly repository for recent workflow runs using the GitHub CLI and displays their status, conclusion, and timing information for monitoring deployment progress. |
assembly rebuild |
Dispatch rebuild workflows for every project registered in the assembly. Fetches the projects.json manifest from the assembly repository, then sends a separate GitHub Actions repository dispatch event for each registered project to trigger a full documentation rebuild. |
assembly retire |
Retire a project from the unified assembly: remove its [[project]] block from the roster and, in the same commit, delete its whole site subtree, all of its manifests and its membership record, then dispatch a shared-only rebuild so the listing, feed, sitemap and search index stop naming it. |
assembly redirects |
Generate a Cloudflare Pages _redirects file for this project that redirects standalone documentation URLs to the corresponding paths on the unified assembly site. Requires a project slug and assembly base URL as inputs, prints the redirect rules to stdout. |
assembly generate-shared |
Generate the shared cross-project elements for the assembled documentation site. Reads per-project manifest JSON files, merges post overlays, and produces a homepage, blog index, navigation JSON, RSS feed, XML sitemap, robots.txt, a site-wide llms.txt linking to each project's own, a root 404 page and a security headers file in the site output directory. It also deletes the redirect worker a deploy made before the worker was retired left at the site root. |
assembly integrate |
Integrate one dispatched project into the assembly repository checkout and push the result. Builds the cloned source project, replaces its subtree under site/, refreshes its manifest and membership record, regenerates the shared cross-project elements, rebuilds the search index, then commits and pushes with a re-sync retry loop so concurrent deploys converge instead of clobbering each other. This is the whole body of the generated deploy workflow. |
assembly verify |
Assert every property a built assembly tree has to have before it is deployed: that the roster, the site subtrees and the manifests name the same projects, that each manifest's pages and posts were actually emitted, that the shared cross-project artifacts exist and parse, that every internal reference, sitemap entry, feed link and cross-project link resolves, that every page has a title and a canonical, and that no unresolved directive or per-project routing file survived. The deploy runs this itself before it pushes; this command is how you run the same assertions by hand against a checkout. |
assembly preview |
Assemble every named local checkout into a preview tree and serve it on loopback. Builds each project with the toolchain running this command, grafts the output exactly as the deploy does -- the home project at the site root, everybody else under their slug -- writes the roster, membership record and manifests the assembly keeps, generates the shared cross-project files and the site chrome, rebuilds the search index, runs the real pre-deploy verification and prints its report, then serves the result with a working 404. Nothing leaves the machine and nothing is published: this is the look-before-you-ship step. |
assembly sync-workflow |
Regenerate the assembly repository's deploy workflow from this project's configuration and push it. The deployed workflow is a generated artifact like any other: without this it stays frozen at whatever the template said when 'assembly init' ran. Pushes only when the content actually differs. |
| blog | Blog posts, the authoring app, and publishing this project's documentation to the unified site |
blog publish-docs |
Publish this project's documentation to the assembly without a release. Builds the docs locally, pushes the built site, its manifest and its membership record into the assembly repo via the Git Data API -- deleting the pages this project published before and no longer produces -- then dispatches a shared-only workflow to regenerate cross-project elements. |
Blog posts and the unified multi-project documentation assembly are part of the same binary. selfdoc blog post new|list|generate|publish manages posts, selfdoc blog editor serve runs the local authoring app, and selfdoc assembly ... initializes, pushes, rebuilds and verifies an assembly that mounts every project under its own slug.
Requires the Wrangler CLI installed and authenticated.
{
"deploy": {
"provider": "cloudflare-pages",
"project": "my-docs-project"
}
}selfdoc build && selfdoc deployPushes the output directory to the gh-pages branch via force-push.
{
"deploy": {
"provider": "github-pages"
}
}Enable GitHub Pages in your repo settings (source: gh-pages branch).
When rlsbl detects a selfdoc.json in the project, it can trigger selfdoc build and selfdoc deploy as part of the release lifecycle via the .rlsbl/hooks/post-release.sh hook.
Full documentation at selfdoc.smmh.dev.
MIT