This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
All commands use bun (not npm/npx/pnpm — blocked by hook).
# Build CLI (required after any source changes before testing)
bun run build:cli
# Dev server with examples
bun run dev:examples:basic # paper theme, port 3000
bun run dev:examples:versioned # versioned docs
bun run dev:docs # project docs site
# Build examples
bun run build:examples:basic
# Lint (from packages/chronicle/)
cd packages/chronicle && bunx biome lint src/
# Tests (from packages/chronicle/)
cd packages/chronicle && bun test
# Type check
bunx tsc --noEmit --project packages/chronicle/tsconfig.jsonCritical: CLI runs from dist/, not source. Always bun run build:cli after changing anything in src/ before testing with the dev server.
Monorepo: packages/chronicle (main package) + examples/* (reference sites).
- CLI (
src/cli/) — Commander.js commands:init,dev,build,start,serve - CLI loads
chronicle.yaml(Zod-validated insrc/lib/config.ts), symlinks project content to.content/ - Vite + Nitro (
src/server/vite-config.ts) — dev server and SSR build - SSR handler (
src/server/entry-server.tsx) — renders React app per request, injects__PAGE_DATA__JSON into HTML - Client hydration (
src/server/entry-client.tsx) — hydrates from__PAGE_DATA__, handles subsequent navigation client-side
- Project content is symlinked to
packages/chronicle/.content/at dev/build time src/lib/source.tsusesimport.meta.glob('../../.content/**/*.{mdx,md}')to build page tree viafumadocs-core/sourceloaderreadme.mdxfiles are treated asindex.mdx- Frontmatter fields:
title,description,order(sort),icon,lastModified - Directory metadata via
meta.jsonfiles;{ root: true }creates a content root boundary - Versioning:
src/lib/version-source.tsfilters tree/pages by version prefix in URL
Remark plugins (configured in vite-config.ts): directives, admonitions, image resolution, link resolution, mermaid, reading time.
src/server/App.tsx is a single component (no React Router <Route> elements). It calls resolveRoute(pathname, config) from src/lib/route-resolver.ts which returns a discriminated union: DocsPage | DocsIndex | ApiPage | ApiIndex | Redirect.
src/lib/page-context.tsx provides PageProvider — holds tree (static after SSR), page (fetched on client nav), API specs, and version context.
Two themes registered in src/themes/registry.ts:
- default — sidebar + TOC layout
- paper — book-style single-column with reading progress
Each theme exports { Layout, Page } conforming to ThemeLayoutProps / ThemePageProps from src/types/theme.ts. Theme is selected via theme.name in chronicle.yaml.
OpenAPI specs declared in chronicle.yaml → loaded by src/lib/openapi.ts → rendered by components in src/components/api/. The API "Try it out" panel uses CodeMirror and a CORS proxy (src/server/api/apis-proxy.ts).
src/server/api/: health, page (metadata), search (MiniSearch), specs (OpenAPI aggregation), apis-proxy.
src/server/routes/: [...slug].md, llms.txt, robots.txt, sitemap.xml, og.tsx (satori).
@raystack/apsarais the UI component library. For component docs/props, fetchhttps://apsara.raystack.org/llms.txt- Path alias:
@/*→./src/*(configured in tsconfig + vite) - CSS: use CSS modules with Apsara design tokens (
--rs-color-*,--rs-font-*,--rs-space-*). Use--rs-color-border-base-primary(not--rs-color-border-base). [data-theme=dark]via:global()for dark mode overrides in CSS modules- Shiki syntax highlighting needs explicit CSS:
.pre code span { color: var(--shiki-light) }+ dark variant remark-directivemangles URLs with ports (:5173) —remark-unused-directivesplugin works around this