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.json with no .md behind it is a build error. That is a renamed or deleted page, and ignoring it would leave a dead sidebar link.

  • A .md not listed in nav.json is 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.