One site for every teenygrad project's documentation, at docs.teenygrad.org.
It replaces two separately-built, separately-styled sites — docs.teenygrad.org
(built in the teenygrad repo's docs-site/) and vision-rs.teenygrad.org (the
same pattern duplicated in vision-rs) — with a single Nuxt site that shares the
design system of teenygrad.org.
| Books | /teenygrad, /kernels, /vision-rs, /cargo-teeny |
| API reference | /api/<project>/ — generated rustdoc, served as-is |
| Stack | Nuxt 3 · Tailwind v4 · IBM Plex — the same as the marketing site |
| Output | fully static (nuxt generate), served by nginx |
It lives beside the code it documents, in each project's own repo, and stays
valid mdbook input — book.toml is untouched and mdbook serve still works
there for local authoring. This site is a second renderer of the same files.
teenygrad/books/teenygrad/src/**.md ─┐
teenygrad/books/kernels/src/**.md ─┼─→ scripts/fetch-books.mjs → .books/
vision-rs/book/src/**.md ─┤ (sparse git clone)
cargo-teeny/README.md ─┘
│
scripts/build-books.mjs
│
site/generated/ + public/search-index.json
│
nuxt generate
books.config.mjs is the registry of what gets published. Everything else reads
it.
Add an entry to books.config.mjs. Point bookDir at the directory holding
book.toml and src/, nested or not; if the project has no book yet, point
readme at its README instead and the site publishes that as a one-page book
until a real one exists (this is how cargo-teeny is published today — swap the
field over and nothing else changes).
Entries are keyed by slug, not by repo, so one repo can publish more than one
book — /teenygrad (books/teenygrad) and /kernels (books/kernels) are both
teenygrad/teenygrad.
Everything the books actually use: SUMMARY.md (parts, nesting, draft
chapters, prefix/suffix chapters), mermaid, fence annotations (rust,ignore),
mdbook's hidden-line convention (# in a Rust block), relative .md
cross-links, and mdbook's own heading-id scheme — so anchors published on the
old site still resolve.
{{#include}} and {{#rustdoc_include}} are expanded before parsing, with
mdbook's anchor (file.rs:name) and line-range (file.rs:10:20) forms — the
kernels book keeps every code sample in a file its CI compiles and pulls it in
by reference. Included paths may not climb out of the project's own checkout,
and a missing file or anchor fails the build rather than publishing a hole.
Not supported: {{#playground}}, which needs a runtime this static site does
not have. A build that meets one warns and leaves it as written.
The build also reports two classes of problem mdbook was silent about: a .md
file that no SUMMARY.md lists, and an internal link that points at no chapter.
npm install
npm run dev # fetches the books on first run, then starts Nuxtnpm run books re-fetches and recompiles on its own. Fetches are cached in
.books/; node scripts/fetch-books.mjs --force re-pulls.
To work against unpushed book edits in sibling checkouts (../teenygrad,
../vision-rs), either set it per-command:
BOOKS_FROM_SIBLINGS=1 npm run booksor put it in a .env at the repo root, which npm run books and npm run dev
both read (it is gitignored):
# .env
BOOKS_FROM_SIBLINGS=1Switching between the two invalidates the fetch cache automatically — the mode
is recorded in each .books/<slug>/.fetch-stamp.json, so turning siblings on
re-pulls rather than reporting "up to date" and quietly serving the GitHub copy.
To build exactly what deploys:
npm run generate # → .output/public
npm run serve # serve it, the way nginx willnpm run serve is a plain static file server over .output/public, which is
what production actually does — nuxt preview runs it through Nitro instead, so
serve is the closer approximation.
It binds to 0.0.0.0:3001, so the build is reachable by anything that can
route to the machine, with no authentication. That default exists because these
builds usually get checked from another machine; override it when you would
rather not:
HOST=127.0.0.1 npm run serve # this machine only
PORT=8080 npm run servenpm run generate does not build rustdoc — that needs each project's full
Rust toolchain, and teenygrad's needs the CUDA headers teeny-cuda's build.rs
requires. Without it, /api/<project>/ gets a placeholder page and the rest of
the site builds normally.
To include a real one, drop cargo doc output at api-docs/<project>/ (or set
API_DOCS_TEENYGRAD / API_DOCS_VISION_RS to a directory).
npm run dev mounts the same thing into public/api/ (gitignored, and excluded
from the Docker build context) so the API links in the header and sidebar
resolve in dev instead of 404ing. The built site mounts rustdoc straight into
.output/public after nuxt generate rather than through public/, because
Nitro enumerates public assets and a workspace's worth of generated HTML does
not belong in that manifest.
GitHub Actions → Docker → Kamal, onto the same host as the other sites.
.github/workflows/deploy.yml builds each project's rustdoc in its own job,
then builds and deploys the site.
The rustdoc jobs are continue-on-error. If a project's workspace stops
compiling, the books still ship — that project's API reference falls back to a
placeholder rather than blocking the deploy. The run is marked with a warning
when this happens.
Because the books live elsewhere, a push to this repo is not the only reason to
rebuild. The workflow also runs on a daily schedule, on workflow_dispatch, and
on a docs-updated repository_dispatch — see Remaining migration steps.
The site is complete and deployable, but these have to happen outside this repo. Until the first two are done, two Kamal services claim the same hostnames.
-
Retire the old services, from a machine with Kamal SSH access:
# in teenygrad/ (service: teenygrad-docs, host: docs.teenygrad.org) kamal app remove # in vision-rs/ (service: vision-rs-docs, host: vision-rs.teenygrad.org) kamal app remove
Then deploy this repo, so kamal-proxy routes both hostnames here.
-
Delete the superseded build machinery in those repos:
docs-site/and.github/workflows/deploy-docs.ymlin both teenygrad and vision-rs, and theconfig/deploy.ymlthat names the retired service. Keep the book sources — this site builds from them. -
Trigger rebuilds on book changes. Add to each project repo a workflow that fires when its book changes:
on: push: branches: [main] paths: ["book/**"] # teenygrad watches books/** instead jobs: rebuild-docs: runs-on: ubuntu-latest steps: - run: | curl -X POST -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer ${{ secrets.DOCS_DISPATCH_TOKEN }}" \ https://api.github.com/repos/teenygrad/docs/dispatches \ -d '{"event_type":"docs-updated"}'
Without it the daily schedule still picks the change up, just later.
-
Secrets on this repo:
KAMAL_REGISTRY_PASSWORDandKAMAL_SSH_PRIVATE_KEY, the same values the other two deploys use. -
DNS needs no change. Both hostnames already point at the same host, and
vision-rs.teenygrad.orgkeeps resolving — nginx redirects each of its URLs to the matching page here rather than dropping readers on the front page.
Handled in nginx.conf, and rewritten in the book markdown at build time so
cross-book links resolve in-site rather than through a redirect:
| Old | New |
|---|---|
docs.teenygrad.org/book/x/y.html |
/teenygrad/x/y |
docs.teenygrad.org/api/teeny-core/ |
/api/teenygrad/teeny-core/ |
vision-rs.teenygrad.org/book/x/y.html |
docs.teenygrad.org/vision-rs/x/y |
vision-rs.teenygrad.org/api/… |
docs.teenygrad.org/api/vision-rs/… |
books.config.mjs what is published, and where it comes from
nginx.conf routing, legacy redirects, cache headers
scripts/
fetch-books.mjs sparse-clones each book's source directory
build-books.mjs compiles books → site/generated/ (+ search index)
summary.mjs mdbook SUMMARY.md parser
includes.mjs {{#include}} / {{#rustdoc_include}} expansion
markdown.mjs markdown → HTML (Shiki, anchors, link rewriting)
mount-api-docs.mjs places rustdoc under /api/<project>/
site/ the Nuxt app (srcDir)
assets/css/main.css design system — shared half is kept in sync with
teenygrad/website's own main.css