Docs Sidebar Navigation
Design Log #04 — Docs Sidebar Navigation
Written for AI agents. See Log Methodology Note below for details.
Background
The docs section (DL#03) has a landing page at /docs and 69 guide pages across 5 role sub-routes (/docs/{role}/[slug]). The landing page has role cards with links, but once you're reading a guide there's no navigation — you have to go back to /docs to find another guide. Standard documentation sites (Stripe, Next.js, Tailwind) solve this with a persistent sidebar.
Related
- DL#03 — Agent Kit documentation pages (established the docs route structure and sync script)
Problem
- On role sub-pages, there's no way to navigate between guides without returning to the landing page.
- The role links on the landing page are below the fold — readers don't see available roles immediately.
- Readers can't see the scope of available guides (how many, what topics) without clicking into each role.
Questions and Answers
Should the landing page have a sidebar? No. The landing page has a rich multi-section layout with alternating full-width backgrounds (hero, overview, "What's in the Kit", "How Agents Use It", role cards). A sidebar would break this visual structure. Most doc sites don't sidebar their index page — only content pages. The landing page already serves as the discovery/orientation experience.
Static HTML or dynamic data-driven sidebar? Generated by the sync script. The guide list only changes when sync runs. The sync script already reads all guide files — generating the sidebar HTML, contract, and component logic from the same data is natural.
How to get guide titles for the sidebar?
Extract the first # heading from each markdown file. The sync script already reads every file. Entry-point files (INSTRUCTIONS.md, GUIDE.md) get short fixed titles ("Instructions", "Guide") since their actual headings are long and role-prefixed.
How to handle active state? SSR with variant tags — no client JS needed. The sync script generates:
- A contract with a variant enum tag listing every page (e.g.,
designer-instructions | designer-accordion | ...) - Class expressions on each link:
class="{activePage === designer-instructions ? active}" - Conditional
openon each<details>:open="activePage===designer-instructions || activePage===designer-accordion || ..."
The component receives an activePage prop from the page template and maps it to the variant tag in withFastRender. Active state is resolved server-side — the correct classes and open attributes are baked into the rendered HTML.
Design
Component: docs-sidebar
A headfull component at src/components/docs-sidebar/ — all three files generated by the sync script:
docs-sidebar.jay-contract — GENERATED: variant enum tag with all page identifiers, activePage prop
docs-sidebar.ts — GENERATED: maps activePage prop to variant tag
docs-sidebar.jay-html — GENERATED: nav HTML with class expressions and conditional open
Generated contract
name: docs-sidebar
description: Documentation sidebar navigation. Generated by sync-agent-kit-docs.cjs.
tags:
- tag: activePage
type: variant
dataType: enum (designer-instructions | designer-accordion | ... | contracts-guide | ...)
phase: fast
props:
- name: role
kind: optional
- name: slug
kind: optional
Each page across all roles gets a unique enum value: {role}-{slug}.
Param forwarding: page.ts → ViewState → component prop
Route params don't flow automatically to headfull components. The page receives them and must forward explicitly:
- Each role sub-page gets a
page.tsthat receives theslugroute param and computesactivePage:
// src/pages/docs/designer/[slug]/page.ts
.withFastRender(async (props) => {
return phaseOutput({ activePage: `designer-${props.slug}` }, {});
});
The page contract adds an
activePagetag (string, fast phase).The template binds it to the sidebar prop:
<jay:DocsSidebar activePage="{activePage}" />
Each role template hardcodes its own role prefix in page.ts. The sidebar component receives the combined {role}-{slug} value.
Generated component logic
export const DocsSidebar = makeJayStackComponent<DocsSidebarContract>()
.withProps<{ activePage?: string }>()
.withFastRender(async (props) => {
return phaseOutput({ activePage: props.activePage ?? '' }, {});
});
Generated sidebar HTML
<details open="activePage===designer-instructions || activePage===designer-accordion || ...">
<summary>
<img src="[designer spider]" alt="" width="20" height="20">
Designer
<span class="sidebar-count">25</span>
</summary>
<ul>
<li><a href="/docs/designer/instructions" class="{activePage === designer-instructions ? active}">Instructions</a></li>
<li><a href="/docs/designer/accordion" class="{activePage === designer-accordion ? active}">Accordion</a></li>
...
</ul>
</details>
<!-- repeat for developer, plugin, devops, contracts -->
The open attribute on each <details> is a boolean expression — true when activePage matches any page in that role section. The class expression on each link adds active only for the current page. Both resolve server-side — zero client JS.
Sync script changes
scripts/sync-agent-kit-docs.cjs adds a generateSidebarComponent() function:
- For each role, reads all
.mdfiles fromcontent/docs/{role}/ - Extracts title from first
#heading - Sorts: entry point first, then alphabetical by title
- Generates all three component files:
.jay-contract,.ts,.jay-html
Layout
Role sub-page templates include the sidebar component with activePage bound from page ViewState:
<jay:SiteHeader />
<jay:DocsSidebar activePage="{activePage}" />
<main class="docs">
<div class="md">{post.content}</div>
</main>
<jay:SiteFooter />
CSS strategy
| Breakpoint | Sidebar | Content |
|---|---|---|
| Desktop (≥1024px) | position: fixed, 280px, left edge, full height, scrollable |
padding-left: 280px |
| Tablet (768–1023px) | Same, 240px | padding-left: 240px |
| Mobile (<768px) | Hidden; checkbox-toggled overlay, z-index: 40 (below header at 50) |
Full width |
Sidebar background: var(--color-surface-lowest) with right border.
Role headers: Sora 14px/600 with spider icon.
Guide links: JetBrains Mono 13px, muted, primary on hover/active.
Active link: color: var(--color-primary) + 2px left border.
Pages modified
All 5 role sub-page directories get:
page.ts(new) — forwardsslugparam asactivePagewith role prefixpage.jay-contract(updated) — addsactivePagedata tagpage.jay-html(updated) — registers sidebar component, adds<jay:DocsSidebar activePage="{activePage}" />, adds layout CSS
src/pages/docs/designer/[slug]/
src/pages/docs/developer/[slug]/
src/pages/docs/plugin/[slug]/
src/pages/docs/devops/[slug]/
src/pages/docs/contracts/[slug]/
Each page.ts differs only in the hardcoded role prefix (designer-, developer-, etc.).
Guide ordering per role
- Entry point always first (INSTRUCTIONS.md → "Instructions", GUIDE.md → "Guide")
- Remaining guides sorted alphabetically by extracted title
Trade-offs
Generated component files in src/: All three sidebar component files are generated code living alongside authored code. Mitigated by <!-- GENERATED --> comments and the fact that they only change when the sync script runs.
No sidebar on landing page: Means the landing page and role pages feel slightly different. This is the standard docs pattern and actually helps — the landing page is orientation, role pages are reference.
Static guide list: If someone adds a guide to agent-kit/ without running the sync script, the sidebar won't include it. This is already true for the content files themselves — the sync script is the single pipeline.
Large variant enum: With 69 guides, the enum has 69 values. This is verbose but generated automatically and doesn't affect runtime — the variant resolves to a single value per render.
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.