pi-docsite
The docs site you are reading is generated by pi-docsite, a package in this
monorepo. It is also a standalone product — you can use it in any project that keeps
its documentation as Markdown.
It renders a directory of *.md into a static site: sidebar navigation, full-text
search, llms.txt / llms-full.txt for LLM consumption, and a search index. No config
file, no build framework, no build step to learn.
Run it
No install step — uvx runs it in an ephemeral environment:
uvx pi-docsite --docs-dir docs --tagline "One line describing this project"
From this repository, where it is a workspace package:
uv run --group docs pi-docsite --docs-dir docs --tagline "..."
To add it to a project that depends on it:
uv add pi-docsite
Every value is a flag with a derived default, so the same command works in any repo:
| Flag | Default |
|---|---|
--docs-dir |
docs/ next to the script |
--project-name |
the git remote's repo name |
--repo-url |
git remote origin |
--tagline |
empty |
--badge-label |
pi-docsite |
Identity follows --docs-dir rather than the script's own location, so pointing it at
another project's docs directory renders that project's site with that project's name.
--badge-label is the header badge text. It names the generator, not the project being
documented, so a vendored site never shows this repo's branding. The suffix after the first
- keeps the accent styling; a label without a - renders unaccented.
Regenerating this site
uv run --group docs pi-docsite --docs-dir docs --tagline "Agent orchestration for pi: ..."
The output is committed. Edit the Markdown and rebuild — never hand-edit the generated
.html, llms.txt, llms-full.txt, or search-index.json.
Adding a page
Write the .md file, add one line to docs/nav.json, rebuild. Nothing in the
generator needs editing, and a page cannot be silently invisible.
Each page needs exactly one H1 — that is its title and its sidebar entry. Headings inside fenced code blocks do not count.
Layout: docs/nav.json
The sidebar structure lives in one reviewable file. Array order is display order, for groups and for pages within a group:
{
"groups": [
{ "name": "Getting Started", "pages": ["quickstart", "configuration"] },
{ "name": "Reference", "pages": ["cli-reference", "safety-enforcements"] }
]
}
Two rules make it safe to let an LLM drive this:
-
A slug in
nav.jsonwith no.mdbehind it is a build error. That is a renamed or deleted page, and ignoring it would leave a dead sidebar link. -
A
.mdnot listed innav.jsonis still built and listed under a fallback group, and the generator prints a note naming it. A forgotten page is visible, never missing.
Reorder by reordering the array. Renaming files to reorder churns git history and breaks links to the old path.
Per-page override
Frontmatter overrides the layout for a single page:
---
nav_group: Reference
nav_order: 30
---
# My Page Title
Precedence, highest first: frontmatter, nav.json, then the fallback group.
What gets generated
Written into --docs-dir next to the sources:
| Output | Purpose |
|---|---|
<slug>.html |
one page per .md |
index.html |
landing page with the grouped sidebar |
search-index.json |
full page text for search, fetched once by every page |
llms.txt |
short page index for LLM consumption |
llms-full.txt |
full page text for LLM consumption |
assets/ |
CSS and JS |
Stale <slug>.html files with no matching .md are removed. *.md sources and
.nojekyll are never touched.
Determinism
The same Markdown always produces the same bytes. Rendering twice and diffing is empty, so a docs change is reviewable and unrelated churn is a reliable signal that something actually changed — not a timestamp or a reordering.
Search works offline
The search index is written once and fetched by every page rather than inlined,
which keeps each page small. Browsers block fetch() of a local file over file://, so
serve the site over HTTP for search to work.
The error message follows the cause. A page opened over file:// is told to serve the site
over HTTP — the one case where that is the remedy. Over HTTP the three failures stay
distinct: a 404 means the index is missing (rebuild), a 5xx says the server failed and
the index may be fine (retry), and a network or parse failure is reported as such. The
underlying error goes to the browser console in every case.
Ask your LLM
A ready-made skill ships inside the package at skill/SKILL.md, with the commands,
the nav.json contract, and the failure modes.
The generator tells you when it is missing. Every build prints one extra line while no agent has it installed:
pi-docsite: generator skill not installed for any agent. To expose pi-docsite guidance to
your LLM, copy .../pi_docsite/skill/SKILL.md into your agent's skills directory (for
example ~/.pi/agent/skills/pi-docsite/SKILL.md).
That line is why an LLM driving a rebuild can offer the install instead of silently working
from guesswork. pip has no post-install hook, so this build-output notice is the only
channel that reaches whoever is actually running the tool. It disappears once
SKILL.md exists under ~/.pi/agent/skills/, ~/.claude/skills/, ~/.cursor/skills/, or
~/.gemini/skills/.
To install it yourself:
uv run --package pi-docsite python -c "
import pi_docsite, pathlib, shutil
src = pathlib.Path(pi_docsite.__file__).parent / 'skill' / 'SKILL.md'
dst = pathlib.Path.home() / '.pi/agent/skills/pi-docsite'
dst.mkdir(parents=True, exist_ok=True)
shutil.copy(src, dst / 'SKILL.md')
print('installed to', dst)
"
Keeping generated files out of AI review
The generated site is committed, so a repo with an AI reviewer configured — Qodo Merge /
PR-Agent, .pr_agent.toml — gets findings against thousands of lines of machine-written
HTML. The consuming repo cannot act on them: the source is the generator.
The generator names the fix. While a .pr_agent.toml exists and its [ignore] glob
does not cover the files this build wrote, every build prints one line with the exact
patterns to add:
pi-docsite: 13 generated file(s) under docs/ are not excluded from AI review, and the
reviewer will report findings in output this generator owns. Add to .pr_agent.toml (keep
the .md and nav.json sources in scope):
[ignore]
glob = ["docs/*.html", "docs/search-index.json", "docs/llms.txt", "docs/llms-full.txt", "docs/assets/callouts.js", ...]
When the config already has an [ignore] table, the notice says to merge the patterns
into the existing glob list instead — a second [ignore] table is invalid TOML. The
config is looked up from the docs directory upwards, so website/docs and a symlinked
docs/ both find the repo's file, and the printed globs stay repo-relative.
The list is deliberately narrow. docs/*.md and nav.json are the repo's own work and
stay in review, and so does anything the generator did not write: a hand-written
404.html or a verification page is preserved on every build, so when one exists the
suggestion names the generated pages individually instead of suggesting docs/*.html.
The notice disappears once the config covers the generated files, and a repo with no
.pr_agent.toml never sees it. An LLM driving a build should ask before editing the
review config — review scope is the repo owner's call.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
nav.json lists 'x' but docs/x.md does not exist |
The page was renamed or deleted. Fix nav.json. |
note: N page(s) not in nav.json |
The page built fine but has no curated sidebar position. Add it to nav.json. |
docs/<slug>.md has N H1 headings, expected 1 |
Add exactly one real H1, or close the code fence swallowing it. |
| Search returns nothing | Open the browser console. search index unavailable on a file:// page means the fetch was blocked. Serve it over HTTP. |
| Search index unavailable (HTTP 500) | The server failed, not the file. Retry; check the deployment if it persists. |
| Search says the index could not be loaded | The request failed before a response arrived, or the body was not JSON. Check the console and the network. |
| Rebuild shows unrelated diffs | That is a determinism bug, not a stale build. |
Requirements
Python 3.14+, with markdown, jinja2, and pygments.