The Hugo project behind docs.modelplane.ai. This repo is the root of the Vercel project.
The prose is not in this repo. Content, the example manifests the pages
embed, and the API definitions the reference is generated from are all in the
modelplane repo, under docs/
and apis/. Edit pages there.
| Path | Contents |
|---|---|
build.sh |
The build. Clones each content branch and runs Hugo against it. |
hugo.toml |
Hugo config, including the mounts that read the content checkout. |
themes/geekboot/ |
Templates, SCSS, the JavaScript bundle, static files, and the version list. |
themes/geekboot/data/docversions.json |
The version list. |
utils/ |
The webpack build and the link checker. |
api/mcp.js |
The docs MCP server, deployed as a Vercel function. |
vercel.json |
Build command, output directory, and rewrites. |
flake.nix |
The preview app, so a content checkout can serve itself. |
One Vercel project serves every version. The release named by latest in the
version list is served at the root; the others are served under a prefix.
| URL | Built from | Banner |
|---|---|---|
/getting-started/ |
release-0.3, the current latest |
none |
/main/getting-started/ |
main |
"unreleased version" |
/v0.2/…, /v0.1/… |
release-0.2, release-0.1 |
"older version" |
A version's prefix depends on which release is latest, so it changes over
time. When latest becomes 0.4, the 0.4 build moves to the root and the 0.3
build moves to /v0.3/.
-
Install Hugo extended, at the version pinned near the top of
build.sh. -
Point
modelplane/at a checkout of the content repo:ln -s ~/src/modelplane modelplane -
Start the server:
hugo server
Hugo reloads on edits to the checkout, so this is the setup for writing
content. build.sh clones into its own scratch directories and leaves this
symlink alone.
bash build.sh
utils/htmltest/check.shbuild.sh downloads its own pinned Hugo on Linux and writes to public/.
check.sh checks internal links in each version and needs htmltest on PATH.
Both are what CI runs.
-
Confirm the branch exists in the modelplane repo. A missing branch fails the whole build, including the versions that would otherwise succeed.
-
Add one line to
themes/geekboot/data/docversions.json:{ "version": "0.4", "path": "v0.4", "branch": "release-0.4" } -
If the new version is the current release, set
"latest": "0.4"in the same file. -
Merge. Vercel rebuilds every version.
The version list is the only file to edit. Adding a version does not involve a content revision, a checksum, a submodule, or a separate Vercel project.
Set "latest" in the version list and merge. That moves the new release to the
root, moves the previous one to its own prefix, and adds the "older version"
banner to it. The search crawl is configured outside this repo and needs its
exclusions updated by hand - see "Search" below.
Nothing in this repo is pinned to a content revision, so each build reads the
current tip of every branch. A content merge in the modelplane repo dispatches
content-published here, and that rebuild is the publish - see "Preview a
content pull request" below. A change to the theme or the build publishes the
same way: push to main and Vercel rebuilds every version.
Everything the modelplane repo needs is here; it holds no Hugo config, no
Vercel project, and no copy of this repo. Two entry points serve it, both
building one version at the root, built as main, so a preview carries the
"unreleased version" banner:
| Entry point | Content from | Used by |
|---|---|---|
nix run github:modelplaneai/docs-site#preview |
the working directory | a writer, locally |
CONTENT_REF=<sha> bash build.sh |
a fetch of that revision | the Content workflow |
build.sh reads CONTENT_DIR (a checkout on disk) or CONTENT_REF (a
revision to fetch) in step 4a and stops there, instead of cloning the branches
in the version list. params.branch becomes the revision or branch built, so
"view page source" links land on the code under review. The version switcher
still lists every version and those links 404 on a preview that contains one.
.github/workflows/content.yml is what the modelplane repo dispatches to. It
holds the Vercel credentials and is the only thing that deploys:
| Dispatch | Effect |
|---|---|
content-preview (ref, pr) |
deploys that revision as a preview, aliased to modelplane-docs-pr-<pr>.vercel.app |
content-published |
rebuilds every version and promotes it to production |
The alias is why nothing here needs write access to the modelplane repo: the
hostname follows from the pull request number, so that repo posts the link
itself when it dispatches, before this build finishes. Deploys run on Vercel
rather than in the workflow, so vercel.json still applies - /mcp and the
per-version rewrites included.
content-published is also what publishes a content merge. Nothing here pins a
content revision, so production is a rebuild that reads the tip of every branch
in the version list.
The bundle in themes/geekboot/assets/js is committed, so the site build runs
no Node step for it. After editing anything under utils/webpack/src:
cd utils/webpack
npm ci
npm run prod
git diff ../../themes/geekboot/assets/js-
build.shreadsrepoand the version list fromthemes/geekboot/data/docversions.json. This is the only reference to the content repo; the CI workflow has none. -
The versions are built concurrently, one background job each. A job gets its own copy of
hugo.toml,postcss.config.js, andthemes/in a scratch directory, withnode_modulessymlinked, becausehugo.tomlmounts the checkout from the fixed pathmodelplane/and concurrent builds cannot share it. -
Each job clones its branch into
modelplane/inside its own scratch directory, shallow and sparse:git clone --depth 1 --single-branch --branch "$branch" \ --sparse --filter=blob:none \ "https://github.com/${repo}.git" "$src/modelplane" git -C "$src/modelplane" sparse-checkout set docs/content docs/data docs/manifests apis
--sparselimits the working tree to those four directories and--filter=blob:nonelimits the download to their blobs, so the rest of the repo is never transferred.It then logs the resolved commit. Nothing here pins a content revision, so the build log is the only record of what was deployed.
-
hugo.tomlmounts four paths out of that checkout:docs/contentas content,docs/dataas data,apis/(at the repo root, not underdocs/) as the API reference's data and assets, anddocs/manifestsas the example YAML themanifestsshortcode reads. -
Once every job finishes, the finished trees are moved into
public/: the latest release to the root, the others to their prefixes. A failure in any job prints all the logs and exits without assemblingpublic/.
The scratch directories are inside the repo, so the move in step 5 is a rename
rather than a copy, and they are removed on exit. .build-* and modelplane/
are both gitignored.
The clone is anonymous HTTPS. If the modelplane repo ever becomes private, both CI and Vercel will need a credential and that URL will need a token.
The content is written for a site at the domain root, which affects templates in two ways.
Prefixing. A root-absolute link in the prose, such as /getting-started/,
is correct for the release at the root and wrong for every other version.
partials/utils/docurl.html adds the current prefix at render time. Content on
release branches already cut therefore stays as it is. Use the partial for any
caller-supplied URL. Hugo's relURL adds the baseURL subdirectory only to a
relative input, so the partial trims the leading slash first.
Absolute URLs. The kubectl apply -f … commands are copied into a shell,
so they cannot be root-relative. .Permalink is absolute only when baseURL
is, and preview builds are root-relative on purpose (see step 3 of build.sh).
partials/utils/absurl.html builds these against params.site.
.github/workflows/ci.yml runs on pushes to main and on every pull request.
It checks out this repo, runs build.sh, then runs the link check. It does not
deploy.
On main it runs after the merge, in parallel with Vercel's build, so it
cannot block a bad deploy. Content is also not pinned, so a passing run says
nothing about what the content branches contain when Vercel builds later.
The search box is Algolia DocSearch, querying application PQSS9LVZU8, index
Modelplane Docs. The app id and the public search key are in
themes/geekboot/layouts/partials/scripts.html.
Nothing in this repo fills that index. It is crawled by Algolia's hosted
crawler, configured in the Algolia dashboard, which proves it owns the domain
by reading Algolia-Crawler-Verif out of themes/geekboot/layouts/robots.txt.
Keep that line.
The crawler therefore has to be edited in the dashboard, not here, in two
cases: when the URL layout changes, and when a release moves to a new prefix.
Only the release at the root should be indexed - a crawl that follows
/main/, /v0.2/ and /v0.1/ returns four copies of every page.