Generalize the docs mount past umbra, so a second project can mount at all #136

Open
opened 2026-08-27 08:21:56 +00:00 by coilyco-ops · 1 comment
Collaborator

Design Engineer lane. Handoff from coilysiren/website#135, which built the sync and left the render layer umbra-shaped.

The sync half is done and is config-driven: adding a project is one entry in src/data/docs-mounts.json, and scripts/sync-project-docs.py vendors it with no code change. The render half cannot take a second project without work.

The five places that name umbra

  • eleventy.config.js - the mountedDocLinks transform returns early unless the URL starts with /projects/umbra/docs/, resolves against a hardcoded UMBRA_SOURCE, and builds its mounted set from umbraDocsFlat. A second mount needs project, source URL, and slug set resolved per page rather than closed over once.
  • src/_includes/layouts/docs.njk - reads the umbraDocs global directly for the sidebar, the shelves, and the snapshot stamp, and hardcodes docs--umbra as the layout class.
  • src/data/umbra-docs.js and src/_data/umbraDocs.js - the manifest and the data join, both named for the project.
  • src/projects/umbra-docs-index.njk - the front door, a bespoke template.
  • src/build-output.test.ts and src/docs-mount.test.ts - DOCS_ROUTES derives from umbra's manifest, and the mount test asserts umbra by name.

None of it is hard. It is one project's worth of naming that wants to become a parameter.

What good looks like

Adding agent-compose or mcp-beaver should be: one entry in docs-mounts.json, one manifest file, and nothing else. The transform, the layout, the front door, and the tests should all read the project from data.

Worth preserving while doing it: the stamp is generated into src/data/docs-mount-source.json and keyed by project already, so that part generalizes for free. src/docs-mount.test.ts fails when the vendored files and the manifest disagree, and that check should survive as a per-project loop rather than being dropped.

What this does not cover

The manifests themselves. Neither agent-compose nor mcp-beaver has a docs/index.md, which is where umbra's titles and blurbs were lifted from, so 62 pages need copy written before either can mount. That is the sibling issue in the Developer Advocate lane, and it is the longer pole. This issue is the code that makes the copy usable when it exists.

Why it is worth doing

coilysiren/website#133 opens on the defect that every project card sends readers to github.com at the moment of highest interest. The agent-compose and mcp-beaver project pages still link their docs off-domain. Mounting is what fixes that, and this is the half that has to exist first.

**Design Engineer lane. Handoff from coilysiren/website#135, which built the sync and left the render layer umbra-shaped.** The sync half is done and is config-driven: adding a project is one entry in `src/data/docs-mounts.json`, and `scripts/sync-project-docs.py` vendors it with no code change. The render half cannot take a second project without work. ## The five places that name umbra * **`eleventy.config.js`** - the `mountedDocLinks` transform returns early unless the URL starts with `/projects/umbra/docs/`, resolves against a hardcoded `UMBRA_SOURCE`, and builds its `mounted` set from `umbraDocsFlat`. A second mount needs project, source URL, and slug set resolved per page rather than closed over once. * **`src/_includes/layouts/docs.njk`** - reads the `umbraDocs` global directly for the sidebar, the shelves, and the snapshot stamp, and hardcodes `docs--umbra` as the layout class. * **`src/data/umbra-docs.js` and `src/_data/umbraDocs.js`** - the manifest and the data join, both named for the project. * **`src/projects/umbra-docs-index.njk`** - the front door, a bespoke template. * **`src/build-output.test.ts` and `src/docs-mount.test.ts`** - `DOCS_ROUTES` derives from umbra's manifest, and the mount test asserts umbra by name. None of it is hard. It is one project's worth of naming that wants to become a parameter. ## What good looks like Adding agent-compose or mcp-beaver should be: one entry in `docs-mounts.json`, one manifest file, and nothing else. The transform, the layout, the front door, and the tests should all read the project from data. Worth preserving while doing it: the stamp is generated into `src/data/docs-mount-source.json` and keyed by project already, so that part generalizes for free. `src/docs-mount.test.ts` fails when the vendored files and the manifest disagree, and that check should survive as a per-project loop rather than being dropped. ## What this does not cover The manifests themselves. Neither agent-compose nor mcp-beaver has a `docs/index.md`, which is where umbra's titles and blurbs were lifted from, so 62 pages need copy written before either can mount. That is the sibling issue in the Developer Advocate lane, and it is the longer pole. This issue is the code that makes the copy usable when it exists. ## Why it is worth doing `coilysiren/website#133` opens on the defect that every project card sends readers to `github.com` at the moment of highest interest. The agent-compose and mcp-beaver project pages still link their docs off-domain. Mounting is what fixes that, and this is the half that has to exist first.
Author
Collaborator

Done, on main at 602180b. Nothing in the render layer names a project.

Adding a mount is now three steps: an entry in docs-mounts.json, a docs-manifest-<project>.js, and one import line in docs-mount-routes.js. src/docs-mount.test.ts fails if a declared mount has no manifest, so a missed step is a test failure rather than a broken page.

The five, plus a sixth

  • eleventy.config.js - the transform reads the project out of the page URL, then resolves source repo, branch, docs dir, and slug set from that mount.
  • docs.njk - reads mount from data, and the layout class is docs--{{ project }}.
  • The manifest and its data join - src/data/docs-manifest-umbra.js, resolved by name from config through docs-mount-loader.js. No registry to update on the Node side.
  • The front door - was a bespoke template, is now one virtual template per mount sharing _includes/components/docs-front.njk. Its written copy moved into the manifest as a front block, since a template cannot derive a headline.
  • The tests - both loop config.mounts. Routes derive from the manifests, so cypress/routes.ts cannot fall behind a sync.
  • src/llms.njk - not on the list, and it hardcoded umbra's shelves. A second mount would have built fine and been silently absent from llms.txt.

The vendored trees moved under src/projects/docs/<project>/, so one directory data file serves every mount and reads the project from the path. Eleventy cascades directory data into subdirectories, which is what removes the per-project boilerplate.

Two things the build found that reading would not

I mounted mcp-beaver in a scratch copy to check the three steps were really the three steps. 23 routes emitted correctly, and two defects surfaced that would otherwise have landed on whoever mounted the second project.

Pagination for the front doors was the obvious shape and the wrong one. Only its first page reaches collections.all, so mcp-beaver's front door built correctly, carried the right canonical, and was missing from the sitemap. Virtual templates via addTemplate have no such gap.

The route module cannot use top-level await. cypress/routes.ts pulls it into a browser bundle whose bundler rejects it, so manifests there are imported statically rather than resolved by name. That is the one line of code per mount, and it is why the count is three steps rather than two.

Both are written up in the new docs/project-docs-render.md.

Verification

89 Cypress specs, 15 unit tests, typecheck, and the full hook suite are green. The umbra pages render pixel-identically, and just sync-project-docs still reproduces the vendored tree byte-for-byte.

The stamp moved to umbra 1935291 as a side effect of that check: upstream HEAD advanced without touching docs content.

Not covered, as the issue says

The manifests for agent-compose and mcp-beaver. Neither repo has a docs/index.md, so 62 pages need titles and blurbs written before either can mount for real. That is the Developer Advocate lane and the longer pole.

Done, on `main` at `602180b`. Nothing in the render layer names a project. Adding a mount is now three steps: an entry in `docs-mounts.json`, a `docs-manifest-<project>.js`, and one import line in `docs-mount-routes.js`. `src/docs-mount.test.ts` fails if a declared mount has no manifest, so a missed step is a test failure rather than a broken page. ## The five, plus a sixth * **`eleventy.config.js`** - the transform reads the project out of the page URL, then resolves source repo, branch, docs dir, and slug set from that mount. * **`docs.njk`** - reads `mount` from data, and the layout class is `docs--{{ project }}`. * **The manifest and its data join** - `src/data/docs-manifest-umbra.js`, resolved by name from config through `docs-mount-loader.js`. No registry to update on the Node side. * **The front door** - was a bespoke template, is now one virtual template per mount sharing `_includes/components/docs-front.njk`. Its written copy moved into the manifest as a `front` block, since a template cannot derive a headline. * **The tests** - both loop `config.mounts`. Routes derive from the manifests, so `cypress/routes.ts` cannot fall behind a sync. * **`src/llms.njk`** - not on the list, and it hardcoded umbra's shelves. A second mount would have built fine and been silently absent from `llms.txt`. The vendored trees moved under `src/projects/docs/<project>/`, so one directory data file serves every mount and reads the project from the path. Eleventy cascades directory data into subdirectories, which is what removes the per-project boilerplate. ## Two things the build found that reading would not I mounted mcp-beaver in a scratch copy to check the three steps were really the three steps. 23 routes emitted correctly, and two defects surfaced that would otherwise have landed on whoever mounted the second project. **Pagination for the front doors was the obvious shape and the wrong one.** Only its first page reaches `collections.all`, so mcp-beaver's front door built correctly, carried the right canonical, and was missing from the sitemap. Virtual templates via `addTemplate` have no such gap. **The route module cannot use top-level await.** `cypress/routes.ts` pulls it into a browser bundle whose bundler rejects it, so manifests there are imported statically rather than resolved by name. That is the one line of code per mount, and it is why the count is three steps rather than two. Both are written up in the new `docs/project-docs-render.md`. ## Verification 89 Cypress specs, 15 unit tests, typecheck, and the full hook suite are green. The umbra pages render pixel-identically, and `just sync-project-docs` still reproduces the vendored tree byte-for-byte. The stamp moved to umbra `1935291` as a side effect of that check: upstream HEAD advanced without touching docs content. ## Not covered, as the issue says The manifests for agent-compose and mcp-beaver. Neither repo has a `docs/index.md`, so 62 pages need titles and blurbs written before either can mount for real. That is the Developer Advocate lane and the longer pole.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilysiren/website#136
No description provided.