Skip to content

feat: multi-content + versioned docs#40

Merged
rsbh merged 55 commits intomainfrom
feat_multi_content_support
Apr 22, 2026
Merged

feat: multi-content + versioned docs#40
rsbh merged 55 commits intomainfrom
feat_multi_content_support

Conversation

@rsbh
Copy link
Copy Markdown
Member

@rsbh rsbh commented Apr 21, 2026

Summary

Adds versioned, multi-content support to Chronicle. A project can now declare multiple top-level content directories (e.g. docs/, dev/) and an ordered list of older versions (v1/, v2/), each with its own content dirs, labels, badge, and API specs. URLs are prefixed for older versions and unprefixed for latest. Search, llms.txt, sidebar, and a new version switcher are all version-aware.

Config shape

site:
  title: My Docs
  description: My Docs description

content:
  - dir: docs
    label: Docs
  - dir: dev
    label: Dev Docs

latest:
  label: "3.0"
  landing: true

versions:
  - dir: v1
    label: "1.0"
    landing: true
    badge: { label: deprecated, variant: warning }
    content:
      - dir: dev
        label: Developer Guide
      - dir: docs
        label: Docs
    api:
      - { name: REST API (v1), spec: ./v1-openapi.yaml, basePath: /apis, server: { url: https://api.example.com/v1 } }

Filesystem:

content/
├── docs/
└── dev/
versions/
└── v1/
    ├── docs/
    └── dev/

What shipped (by phase)

  • Schema + loader — new {site, content[], latest, versions[]} shape, zod strict root, uniqueness refines, lodash/uniqBy for dedup checks. site.description moved from top-level.
  • Content mirrorbuildContentMirror symlinks each (version, contentDir) into packageRoot/.content/; the vite dev/build plugin rebuilds it per run. Legacy single-symlink mirrors are replaced automatically.
  • Version-aware sourcefilterPageTreeByVersion, filterPageTreeByContentDir, filterPagesByVersion, resolveVersionFromUrl. Synthetic meta.json entries injected per content root (runtime, no filesystem writes) so sidebars render each content dir as a flat root.
  • RoutingRouteType enum + pure resolveRoute(url, config) classifier; SSR emits 302 for single-content roots, 404 for missing pages, and hands the rest off to the client. latest.landing / versions[].landing opt into a chromeless landing page with content-dir cards.
  • APIs/apis vs /<v>/apis; per-version spec loading via getApiConfigsForVersion.
  • Search + metadata/api/search?tag=<version>, per-version MiniSearch index cached by version key; canonical URLs emitted in <head> when config.url is set.
  • llms.txt/llms.txt for latest, /<v>/llms.txt for each version via a catch-all [version]/llms.txt.ts route.
  • Sidebar + switcher — Apsara DropdownMenu-based version switcher; default theme has 3 content-dir buttons + More overflow dropdown, paper theme uses stacked dropdowns in the sidebar.
  • chronicle init — scaffolds content/<dir>/, the new chronicle.yaml, and a sample index.mdx. runInit() is unit-tested.
  • Docs + examplesexamples/basic moved into content/docs/, new examples/versioned fixture with versions/v1/ + versions/v2/, docs/content/docs/configuration.mdx rewritten for the new schema. npm scripts added for each example.
  • CI.github/workflows/ci.yml runs bun run lint + bun test on PRs and main pushes.

Breaking changes

  • Top-level titlesite.title.
  • Top-level descriptionsite.description.
  • content: <string> removed; content: is now {dir, label}[].
  • Content lives under content/<dir>/ (latest) and versions/<v>/<dir>/ (old).
  • --content CLI flag removed; content location is fully config-driven.
  • Landing page is opt-in via landing: true (default false = 302 to first content dir).

Test plan

  • bun test — 79 tests pass across 7 files
  • bun run lint — 0 errors
  • Dev server boot with docs/chronicle.yaml (single-content)
  • Dev server boot with examples/versioned/chronicle.yaml (multi-content + versioned + badges)
  • Verified /, /v1, /v2 routes honour landing flag (302 vs landing render)
  • Verified per-content sidebar scoping and version-switcher navigation
  • Manual sanity check with examples/basic (API specs rendering)

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chronicle Ready Ready Preview, Comment Apr 22, 2026 4:17am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Warning

Rate limit exceeded

@rsbh has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 41 minutes and 50 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 41 minutes and 50 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1902cfde-7a3e-4b4c-9771-9975400e3586

📥 Commits

Reviewing files that changed from the base of the PR and between de0efc9 and 96f6c8b.

📒 Files selected for processing (10)
  • docs/content/docs/configuration.mdx
  • packages/chronicle/src/cli/commands/init.test.ts
  • packages/chronicle/src/cli/commands/init.ts
  • packages/chronicle/src/cli/utils/scaffold.ts
  • packages/chronicle/src/lib/config.test.ts
  • packages/chronicle/src/lib/config.ts
  • packages/chronicle/src/lib/version-source.ts
  • packages/chronicle/src/pages/DocsLayout.tsx
  • packages/chronicle/src/server/entry-client.tsx
  • packages/chronicle/src/server/entry-server.tsx
📝 Walkthrough

Walkthrough

Refactors Chronicle into a version-aware site: replaces top-level config with site + content[], adds latest/versions, implements version-aware routing, content mirroring, search/indexing, LLM endpoints, and UI components for version/content selection. Multiple CLI, server, lib, theme, and example files updated.

Changes

Cohort / File(s) Summary
Config Schema & Types
packages/chronicle/src/types/config.ts, packages/chronicle/src/lib/config.ts, packages/chronicle/src/lib/config.test.ts
Introduce Zod-driven chronicleConfigSchema with site, content[], latest, versions; add Content/Version helper types and helpers (getLatestContentRoots, getVersionContentRoots, getAllVersions, getLandingEntries, getApiConfigsForVersion); update loadConfig to schema parse; add tests.
CLI (commands & utils)
packages/chronicle/src/cli/**, packages/chronicle/src/cli/utils/config.ts, packages/chronicle/src/cli/utils/scaffold.ts, packages/chronicle/src/cli/utils/scaffold.test.ts
Remove --content flag; compute projectRoot from config; change loadCLIConfig signature/returns; replace .content symlink with content mirror (buildContentMirror); update linkContent signature; add init/runInit event model and tests; scaffold tests added.
Routing & Versioning Core
packages/chronicle/src/lib/route-resolver.ts, packages/chronicle/src/lib/version-source.ts, packages/chronicle/src/lib/route-resolver.test.ts, packages/chronicle/src/lib/version-source.test.ts
Add typed route model (RouteType, Route) and resolveRoute; add VersionContext, LATEST_CONTEXT, and filtering utilities for pages/trees; include tests for route/version behaviors.
Source, Page Context & Page APIs
packages/chronicle/src/lib/source.ts, packages/chronicle/src/lib/page-context.tsx
Add version-aware page APIs (getPagesForVersion, getPageTreeForVersion, getVersionContextForUrl); extend PageContext to include version, adjust client routing/fetch logic to use resolved route and version.
Server (SSR) & Routes
packages/chronicle/src/server/*, packages/chronicle/src/server/entry-server.tsx, packages/chronicle/src/server/entry-client.tsx, packages/chronicle/src/server/App.tsx, packages/chronicle/src/server/vite-config.ts
Switch server to resolveRoute with redirect handling; version-scoped loading for docs/apis; embed version into hydration; Vite config computes .content mirror internally; update route handlers to accept version params.
API/Search/Sitemap/Specs
packages/chronicle/src/server/api/search.ts, packages/chronicle/src/server/api/specs.ts, packages/chronicle/src/server/routes/sitemap.xml.ts, packages/chronicle/src/server/routes/llms.txt.ts, packages/chronicle/src/server/routes/[version]/llms.txt.ts
Implement version-scoped search indexing/caching and request tag handling; specs endpoint accepts version param and validates it; sitemap and llms endpoints made version-aware; llms generation delegated to helper.
LLMs Helper & Tests
packages/chronicle/src/lib/llms.ts, packages/chronicle/src/lib/llms.test.ts
Add buildLlmsTxt and LlmsPage to produce versioned llms.txt content; tests validate formatting and version labels.
Navigation & UI Helpers
packages/chronicle/src/lib/navigation.ts, packages/chronicle/src/lib/navigation.test.ts, packages/chronicle/src/components/ui/search.tsx
Add getActiveContentDir, getVersionHomeHref, splitContentButtons; Search now tags queries with version.dir.
Theme & Layout Components
packages/chronicle/src/themes/*, packages/chronicle/src/themes/default/Layout.tsx, packages/chronicle/src/themes/default/ContentDirButtons.tsx, packages/chronicle/src/themes/default/VersionSwitcher.tsx, packages/chronicle/src/themes/paper/*
Add VersionSwitcher, ContentDirButtons/Dropdown, conditional hideSidebar prop in layouts, update header/title to config.site.title, add CSS and integrate version/content controls.
Pages: Landing & Docs Layout
packages/chronicle/src/pages/LandingPage.tsx, packages/chronicle/src/pages/LandingPage.module.css, packages/chronicle/src/pages/DocsLayout.tsx, packages/chronicle/src/pages/ApiPage.tsx
Add LandingPage for docs index; DocsLayout accepts hideSidebar and filters tree by active content dir; head/title/description use config.site.*.
Examples, Docs & CI
docs/chronicle.yaml, docs/content/docs/configuration.mdx, examples/basic/chronicle.yaml, examples/versioned/**, package.json, packages/chronicle/package.json, .github/workflows/ci.yml, vercel.json
Migrate docs/examples to new config shape (site, content[], latest/versions); add example versioned site content; add npm scripts for example dev/build; add Bun test script; add CI workflow using Bun; change Vercel outputDirectory to docs/.vercel/output.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server
    participant RouteResolver as Route Resolver
    participant Config
    participant Content as Content Mirror / Source
    participant PageLoader as Page Loader

    Client->>Server: GET /v2/docs/guide
    Server->>RouteResolver: resolveRoute("/v2/docs/guide", config)
    RouteResolver->>Config: resolveVersionFromUrl("/v2/docs/guide", config)
    Config-->>RouteResolver: VersionContext(dir:"v2", urlPrefix:"/v2")
    RouteResolver-->>Server: Route{type:DocsPage, version, slug:["docs","guide"]}

    Server->>Content: getPageTreeForVersion(version)
    Content->>Config: filterPageTreeByVersion(..., version, config)
    Config-->>Content: filtered tree
    Content-->>Server: page tree

    Server->>PageLoader: getPage(slug)
    PageLoader-->>Server: page data
    Server-->>Client: Rendered HTML + embedded version context
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • rohilsurana
  • rohanchkrabrty
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: multi-content + versioned docs' clearly and concisely summarizes the main feature addition—support for multiple content directories and versioned documentation.
Description check ✅ Passed The description comprehensively explains the feature, config shape changes, filesystem layout, what shipped across phases, breaking changes, test plan, and CI additions—all directly related to the multi-content and versioned docs implementation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat_multi_content_support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@rsbh rsbh changed the title Feat multi content support feat: multi-content + versioned docs Apr 21, 2026
@rsbh rsbh marked this pull request as ready for review April 21, 2026 10:33
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@rsbh rsbh requested a review from rohilsurana April 21, 2026 10:33
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/chronicle/src/cli/utils/config.ts (1)

32-43: ⚠️ Potential issue | 🟡 Minor

Handle malformed YAML before schema validation.

parse(raw) can throw, which bypasses the formatted Invalid chronicle.yaml error path and can surface a stack trace for simple YAML syntax mistakes.

Suggested fix
 function validateConfig(raw: string, configPath: string): ChronicleConfig {
-  const parsed = parse(raw);
+  let parsed: unknown;
+  try {
+    parsed = parse(raw);
+  } catch (error) {
+    console.log(chalk.red(`Error: Invalid chronicle.yaml at '${configPath}'`));
+    console.log(chalk.gray(error instanceof Error ? error.message : String(error)));
+    process.exit(1);
+  }
+
   const result = chronicleConfigSchema.safeParse(parsed);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/cli/utils/config.ts` around lines 32 - 43, The
validateConfig function currently calls parse(raw) which can throw on malformed
YAML and bypass the friendly error handling; wrap the parse(raw) call in a
try/catch inside validateConfig, and on catch log the same formatted "Error:
Invalid chronicle.yaml at '<configPath>'" message (including the parsing error
message as context) and exit(1); then proceed to run
chronicleConfigSchema.safeParse on the parsed result if parse succeeds. Ensure
you reference the validateConfig function and the parse(raw) invocation when
making the change.
packages/chronicle/src/server/routes/sitemap.xml.ts (1)

40-43: ⚠️ Potential issue | 🟡 Minor

Include only configured landing routes in the sitemap.

Line 42 always emits ${baseUrl} but does not emit version landing URLs like ${baseUrl}/v1 when a non-latest version has landing: true. This makes version landing pages undiscoverable in the sitemap and can include a root URL even when latest landing is disabled.

Proposed fix
+  const landingPages = getAllVersions(config)
+    .filter(v => v.landing)
+    .map(v =>
+      `<url><loc>${baseUrl}${v.isLatest ? '' : `/${v.dir}`}</loc></url>`
+    );
+
   const xml = `<?xml version="1.0" encoding="UTF-8"?>
 <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
-<url><loc>${baseUrl}</loc></url>
-${[...docPages, ...apiPages].join('\n')}
+${[...landingPages, ...docPages, ...apiPages].join('\n')}
 </urlset>`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/server/routes/sitemap.xml.ts` around lines 40 - 43,
The sitemap always emits the root URL unconditionally; change the construction
of xml (the xml template string) so it does not unconditionally include
`${baseUrl}` and instead includes only configured landing routes: include
`${baseUrl}` only if the latest version has landing enabled, and append a
landing URL `${baseUrl}/${versionName}` for each version where `landing: true`;
keep concatenating `${[...docPages, ...apiPages].join('\n')}` as before. Locate
the xml template string and the version/config object used to generate pages,
remove the hard-coded `${baseUrl}` line, and generate landing entries
programmatically from your versions list (referencing the same version
identifier used elsewhere) before joining with docPages and apiPages.
🧹 Nitpick comments (10)
packages/chronicle/src/types/config.ts (1)

163-166: Minor: latest gate allows versions: [].

!cfg.versions || cfg.versions.length === 0 || !!cfg.latest intentionally treats an empty versions array as "no versions", so a user can ship versions: [] with no latest block. That's probably fine, but it does mean an empty array is a silently supported way to disable versioning despite the key being declared. If that's not desired, drop the length === 0 short-circuit so empty arrays are also required to declare latest (or better, tell users to omit the key entirely).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/types/config.ts` around lines 163 - 166, The current
Zod refinement on the schema (.refine((cfg) => !cfg.versions ||
cfg.versions.length === 0 || !!cfg.latest, { ... })) treats an empty versions
array as "no versions" and therefore doesn't require latest; remove the length
=== 0 short-circuit so the predicate becomes !cfg.versions || !!cfg.latest (or
equivalently require latest whenever cfg.versions is present), i.e., update the
.refine predicate that references cfg.versions and cfg.latest to not treat [] as
absent so empty arrays will also require a latest block.
.github/workflows/ci.yml (1)

20-25: Optional: pin Bun version and cache dependencies for reproducible, faster CI.

oven-sh/setup-bun@v2 without bun-version will pull whichever Bun is current at run time, which can cause silent drift between local and CI. Consider pinning (e.g., bun-version: 1.2.x or bun-version-file) and enabling caching for ~/.bun/install/cache to speed up bun install.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 20 - 25, Pin the Bun version in the
"Setup Bun" step by adding a bun-version or bun-version-file input (so the
oven-sh/setup-bun action uses a fixed version) and enable caching of Bun and
dependencies for the "Install dependencies" step by caching ~/.bun/install/cache
(and optionally node_modules or bun.lockb artifacts) so bun install
--frozen-lockfile runs reproducibly and faster in CI; update the action inputs
for the "Setup Bun" step and add a cache step that keys off the bun version and
lockfile to restore/save ~/.bun/install/cache before/after running bun install.
packages/chronicle/src/lib/llms.test.ts (1)

6-67: Nice coverage of the three heading paths.

The three cases (latest label present, latest absent, specific version) plus the //index.md and urlPrefix rewriting assertions give good coverage of buildLlmsTxt's formatting contract. Consider adding one more case for a page whose url already ends in a trailing slash (e.g., /docs/a/) if that's a shape the caller can produce, to lock down the .md suffixing rule.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/lib/llms.test.ts` around lines 6 - 67, Add a test case
in packages/chronicle/src/lib/llms.test.ts that calls buildLlmsTxt with a page
whose url ends with a trailing slash (e.g., '/docs/a/') and assert the output
contains the link with a .md suffix (e.g., '- [A](/docs/a.md)'); place it
alongside the other describe('buildLlmsTxt') tests and reuse LATEST_CONTEXT or a
versioned ctx as appropriate to also validate urlPrefix rewriting when
applicable.
packages/chronicle/src/server/api/search.ts (2)

53-82: Minor: redundant loadConfig() calls per request.

loadConfig() is invoked in both resolveCtx and buildApiDocs on every request. If loadConfig isn't memoized internally, this is wasteful — and if it ever becomes async/fs-bound, it'll compound. Consider accepting config as a parameter (threaded from the handler) or caching it at module scope.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/server/api/search.ts` around lines 53 - 82, The
buildApiDocs function calls loadConfig() every request; remove that redundant
call by adding a config parameter to buildApiDocs (e.g., buildApiDocs(ctx:
VersionContext, config: ConfigType)) and use that config with
getApiConfigsForVersion instead of calling loadConfig(); then update all call
sites (the request handler/resolveCtx caller that currently invokes
buildApiDocs) to loadConfig once (or use a module-scope cached config) and pass
the config into buildApiDocs; ensure the function signature, references to
getApiConfigsForVersion, and any tests or imports are updated accordingly.

18-23: Confirm cache invalidation story in dev mode.

indexCache / docsCache are module-level Maps with no eviction. In production SSR this is fine, but in dev (Vite HMR) when a page's frontmatter or a new page is added, the cached docs/index for a given version won't refresh until the process restarts. There's no file watcher or HMR handler currently clearing these caches. Consider exposing a way to bust the cache (e.g., watcher-driven cache.clear()) to improve the dev workflow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/server/api/search.ts` around lines 18 - 23, indexCache
and docsCache are global Maps that never evict, causing stale search/index state
during dev with Vite HMR; add a cache-busting mechanism and wire it to dev
file-watcher/HMR. Implement and export a small API (e.g., clearSearchCaches or
clearIndexAndDocsCache) that calls indexCache.clear() and docsCache.clear()
and/or a per-key invalidator that uses keyFor(ctx) to remove a specific entry;
then call that function from your dev watcher/HMR handler (or when
frontmatter/page files change) so updated pages rebuild their MiniSearch index
and docs array without restarting the process. Ensure the new function is
reachable where the dev server/file-watcher logic runs and only invoked in
dev/HMR scenarios.
packages/chronicle/src/themes/default/ContentDirButtons.tsx (1)

23-61: Inconsistent navigation between visible buttons and overflow items.

Visible entries use <RouterLink> (client-side SPA navigation) while overflow items in the dropdown call navigate(entry.href) via onClick. Functionally both are SPA navigations, but the <RouterLink> wrapper on visible buttons also gives users native affordances (middle-click / cmd-click to open in new tab, right-click → copy link, hover preview). Overflow items lose all of these because they are not real anchors.

Consider rendering overflow items as anchor-based items (e.g., <DropdownMenu.Item asChild><RouterLink to={entry.href}>…</RouterLink></DropdownMenu.Item>, if supported) so the behavior is consistent and keyboard/mouse users retain link semantics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/default/ContentDirButtons.tsx` around lines 23
- 61, The visible buttons use RouterLink for SPA navigation (giving anchor
semantics like middle-click, right-click, copy link) while overflow items call
navigate(entry.href) in DropdownMenu.Item onClick, losing those affordances;
change the overflow rendering to render anchor-based items by wrapping each
DropdownMenu.Item with asChild and placing a RouterLink (to={entry.href}) inside
(replace the onClick navigate usage), so overflow entries use the same
RouterLink semantics as the visible entries (refer to RouterLink,
DropdownMenu.Item, overflow, visible, and navigate in the current diff).
packages/chronicle/src/lib/llms.ts (1)

21-26: Page titles are not escaped for Markdown.

p.title is interpolated directly inside [${p.title}](${mdUrl}). Frontmatter titles legitimately containing ], [, or \ will break the link syntax (and a URL containing ) — uncommon but possible — would break the target). Consider escaping ] / \ in the title and URL-encoding the path, or at minimum documenting the constraint in LlmsPage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/lib/llms.ts` around lines 21 - 26, Escape
Markdown-sensitive characters in page titles and URL-encode the path before
building the index string: when mapping over pages (the block that computes
index from pages, using p.title and mdUrl) replace p.title with an escaped
variant that backslash-escapes backslashes and square brackets (e.g., escape
backslash '\' and ']' and '[') and replace mdUrl with a URL-encoded form (use
encodeURI/encodeURIComponent on the path or filename) so the template becomes "-
[escapedTitle](encodedMdUrl)"; update the mapping in llms.ts where index is
computed to use these escaped/encoded values.
packages/chronicle/src/themes/paper/VersionSwitcher.tsx (1)

40-54: Dropdown items navigate via onClick only — loses anchor semantics and duplicates default theme.

Same concern as ContentDirButtons: the items are not real anchors, so middle-click / cmd-click / right-click-copy-link don’t work. Also, this file is essentially a byte-for-byte duplicate of packages/chronicle/src/themes/default/VersionSwitcher.tsx apart from the width='100%' on the trigger button. Consider extracting a shared VersionSwitcher (or a shared hook/useVersionSwitcher()) in @/lib and letting each theme pass its minor visual overrides, to avoid drift as the component evolves.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/paper/VersionSwitcher.tsx` around lines 40 -
54, The DropdownMenu items in VersionSwitcher are using onClick navigation
(versions.map -> DropdownMenu.Item with onClick={() =>
navigate(getVersionHomeHref(...))}), which loses native anchor semantics and
duplicates logic from the default theme; change each DropdownMenu.Item to render
as a real anchor by passing an "as" prop or an href (use
getVersionHomeHref(config, v.dir) as the href) so
middle-click/CMD-click/right-click work, keep navigate fallback if needed for
client routing, and then extract the shared logic into a single shared
VersionSwitcher (or useVersionSwitcher() hook) in the lib so both themes import
it and only pass visual overrides (e.g., trigger button width='100%') to avoid
drift between packages/chronicle/src/themes/paper/VersionSwitcher.tsx and
packages/chronicle/src/themes/default/VersionSwitcher.tsx.
packages/chronicle/src/pages/LandingPage.module.css (1)

1-56: LGTM.

Styles are straightforward CSS-module tokens. Optional: consider a focus-visible style on .card that matches :hover — currently keyboard users get browser-default focus only, which may not be very visible on the card background.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/pages/LandingPage.module.css` around lines 1 - 56,
Keyboard users don't get a visible custom focus state on the card; add a
matching focus-visible rule so .card elements show the same visual treatment as
.card:hover. Update the CSS to include a .card:focus-visible selector (and/or
.card:focus when needed) that sets border-color and background the same as
.card:hover and also add an accessible focus indicator (e.g., outline and
outline-offset) so keyboard-only users see the card is focused; ensure this
applies to the same focusable elements using the .card class (e.g., anchor
cards).
packages/chronicle/src/cli/commands/init.ts (1)

62-73: .gitignore substring match can both over- and under-match.

existing.includes('dist') returns true for lines like distribution, build/dist, or # dist (commented), silently skipping an entry the user actually needs. Conversely, if the existing file lacks a trailing newline, an entry can be glued onto the previous line. Compare against trimmed, non-comment lines and normalize the trailing newline before appending.

♻️ Proposed refactor
   const gitignorePath = path.join(projectDir, '.gitignore');
   if (fs.existsSync(gitignorePath)) {
     const existing = fs.readFileSync(gitignorePath, 'utf-8');
-    const missing = GITIGNORE_ENTRIES.filter(e => !existing.includes(e));
+    const existingLines = new Set(
+      existing
+        .split(/\r?\n/)
+        .map(l => l.trim())
+        .filter(l => l && !l.startsWith('#')),
+    );
+    const missing = GITIGNORE_ENTRIES.filter(e => !existingLines.has(e));
     if (missing.length > 0) {
-      fs.appendFileSync(gitignorePath, `\n${missing.join('\n')}\n`);
+      const prefix = existing.length === 0 || existing.endsWith('\n') ? '' : '\n';
+      fs.appendFileSync(gitignorePath, `${prefix}${missing.join('\n')}\n`);
       events.push({ type: 'updated', path: gitignorePath, detail: missing.join(', ') });
     }
   } else {
     fs.writeFileSync(gitignorePath, `${GITIGNORE_ENTRIES.join('\n')}\n`);
     events.push({ type: 'created', path: gitignorePath });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/cli/commands/init.ts` around lines 62 - 73, The
.gitignore handling in the init flow can false-match via includes() and can
append without a trailing newline; update the logic around gitignorePath so you
read existing, split into lines, normalize by trimming whitespace and ignoring
lines that start with '#' (comments), then compute missing entries by checking
exact equality against GITIGNORE_ENTRIES (not substring includes) using the
normalized lines; ensure the file content ends with a single newline before
appending the missing entries and update events (in the same block that uses
existing, missing, and events) to record the correct detail string as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/chronicle/src/cli/commands/init.test.ts`:
- Around line 46-51: The test title says it should preserve an existing
content/docs/index.mdx but the fixture only creates content/docs/existing.mdx,
so update the test around runInit to either (a) create content/docs/index.mdx
with distinctive content before calling runInit and then assert that file still
exists and its contents were not modified (use the same path checked now and
read contents to compare), or (b) if you meant to test skipping scaffolding for
any non-empty dir, rename the test to “does not scaffold index.mdx when
content/docs is non-empty” to reflect that behavior; adjust the test setup and
assertions accordingly and keep references to runInit and the
content/docs/index.mdx path to locate the logic under test.

In `@packages/chronicle/src/cli/utils/scaffold.ts`:
- Around line 44-50: The try/catch in mirrorTree (and the similar block around
lines 63-73) is currently swallowing all errors; change them to only suppress
"not found" errors (e.g., check err.code === 'ENOENT' or err.code === 'ENOTDIR'
as appropriate) and rethrow or propagate any other errors (or surface them via
logging) so permission/read/delete issues are not silently ignored; update the
error handling in mirrorTree and the other catch blocks to inspect the caught
error object before deciding to return, referencing the mirrorTree function and
the subsequent directory-iteration/entry-processing try/catch blocks.

In `@packages/chronicle/src/lib/head.tsx`:
- Around line 16-18: The social image URLs are still rendered as relative paths;
when config.url is set you should build absolute URLs the same way as with
canonical: use config.url.replace(/\/$/, '') as the base and prepend it to the
image path (e.g. the `/og?...` image string) before assigning to the og:image
and twitter:image meta values. Update the code that computes the image URL(s)
(referencing the existing canonical computation and the variables used to render
og:image/twitter:image in this file) to avoid double slashes and return the
absolute URL when config.url is truthy, otherwise keep the relative path.

In `@packages/chronicle/src/lib/llms.ts`:
- Around line 19-28: The template always emits extra blank lines when
config.site.description is empty; update the construction around const
description, const index and the final return so the description is only
included when non-empty (e.g. build an array of sections [heading, description,
index] and filter out falsy values before joining with '\n\n'), ensuring
llms.txt does not get stray blank lines.

In `@packages/chronicle/src/lib/navigation.ts`:
- Around line 17-23: The code computes dirs by directly accessing config.content
or config.versions?.find(...).content.map(...), which bypasses the config helper
logic and can crash when a version inherits/defaults; replace this ternary with
a call to the config helper that returns the effective content roots for a
version (e.g., use the project config function that computes version content
roots) and assign its result to dirs (falling back to []). Update the code
around dirs, version.dir, config.content and config.versions to use that helper
so inherited/default content is respected and null safety is preserved.

In `@packages/chronicle/src/lib/page-context.tsx`:
- Around line 105-118: When handling API routes (route.type ===
RouteType.ApiIndex || route.type === RouteType.ApiPage) the function returns
early before clearing stale page and error state, which can leave old
page/errorStatus visible during client navigation; update the branch that
fetches /api/specs to first clear/reset page and errorStatus (call the same
state setters you use elsewhere to set page to null/empty and errorStatus to
undefined/0 as appropriate), then proceed to kick off the fetch and setApiSpecs
with cancelled.current handling unchanged, ensuring the return cleanup still
sets cancelled.current = true; reference route.type, RouteType.ApiIndex,
RouteType.ApiPage, setApiSpecs, cancelled.current, and the state setters for
page and errorStatus when making the change.

In `@packages/chronicle/src/lib/route-resolver.ts`:
- Around line 21-28: The function contentDirsFor is directly reading v?.content
instead of using the already-resolved content on the VersionContext or falling
back safely, which breaks versions that rely on helper/default content; update
contentDirsFor to first use version.content when present (map over
version.content to return dir), then if version.dir === null fall back to
config.content.map(...), and only then try to find the config version
(config.versions?.find(...)) and safely map its content (guarding against
undefined) so you never call .map on undefined; reference the function
contentDirsFor, the parameter VersionContext (version.content), and
config.versions in your change.

In `@packages/chronicle/src/pages/LandingPage.tsx`:
- Around line 20-25: The card links on LandingPage.tsx use a native <a> (inside
entries.map) causing full page reloads; replace those anchors with the app's
RouterLink component (same props: key, to={entry.href}, className etc.) and
preserve the inner spans (styles.cardLabel and styles.cardHref) so navigation
stays client-side like ContentDirButtons; ensure imports include RouterLink and
remove or update any href usage to to so the SPA routing and in-memory state are
preserved.

In `@packages/chronicle/src/server/api/search.ts`:
- Around line 107-113: resolveCtx currently returns LATEST_CONTEXT for any
unknown tag which can silently mix versions; change resolveCtx (the function
using VersionContext, loadConfig and LATEST_CONTEXT) so that when a non-null tag
is passed but no matching version is found it returns null (or a distinct
sentinel) and also emits a warning log identifying the unknown tag; then update
the caller/handler that consumes resolveCtx to detect null and return an empty
result or 404 rather than falling back to latest.

In `@packages/chronicle/src/server/entry-server.tsx`:
- Around line 27-32: The early return for RouteType.Redirect in entry-server.tsx
skips emitting the SSR telemetry hook; before returning the Response for the
redirect, call useNitroApp().hooks.callHook('chronicle:ssr-rendered', { status:
route.status, renderDuration: 0, /* include same payload fields used elsewhere
*/ }) (await the call if other code expects it) so redirects are recorded with
renderDuration = 0 and the redirect status, preserving the same payload shape as
non-redirect responses.

In `@packages/chronicle/src/server/routes/llms.txt.ts`:
- Around line 14-22: buildLlmsTxt is inserting page titles with `${p.title}` but
callers pass extractFrontmatter(p).title which can be undefined; update
buildLlmsTxt (or the caller mapping) to avoid `- [undefined](...)` by either
skipping pages without a title or providing a fallback (e.g., use page slug or
url path). Locate the pages mapping (where getPagesForVersion(LATEST_CONTEXT) is
used and pages.map(p => ({ url: p.url, title: extractFrontmatter(p).title })))
and change it so title is always a string before handing to buildLlmsTxt (e.g.,
title: extractFrontmatter(p).title ?? deriveSlugFromUrl(p.url)) or modify
buildLlmsTxt to treat undefined titles the same way and omit or substitute them.
Ensure references to buildLlmsTxt and extractFrontmatter remain intact and
tests/llms output no longer contain "undefined".

In `@packages/chronicle/src/types/config.ts`:
- Around line 82-87: dirNameSchema is too permissive: tighten it to reject
leading-dot names, surrounding/embedded whitespace, control/NUL chars, and
OS-reserved device names (e.g., CON, PRN) so mirror conflicts in
buildContentMirror are avoided; update the validation on dirNameSchema to
enforce a conservative allowed charset (letters, digits, hyphen, underscore, and
optionally a single dot not at start), trim/forbid whitespace, disallow
control/NUL characters, and explicitly reject common reserved names (CON, PRN,
AUX, NUL, COM1...COM9, LPT1...LPT9) and any name starting with '.' so invalid
values are caught at schema validation time rather than during filesystem
operations.
- Around line 127-166: chronicleConfigSchema currently allows top-level
content.dir values to collide with versions[].dir and reserved route segments
(e.g., "apis"), causing routing shadowing in resolveVersionFromUrl; add a
.refine on chronicleConfigSchema that (1) collects all top-level content dirs
and all version dirs and fails if any value appears in both, and (2) fails if
any content.dir or version.dir is one of the reserved route names (e.g.,
"apis"); provide a clear error message like 'content[].dir conflicts with
versions[].dir or reserved route segments' and set the refine path to
['content'] or ['versions'] as appropriate so the validation rejects configs
with overlapping or reserved directory names.

---

Outside diff comments:
In `@packages/chronicle/src/cli/utils/config.ts`:
- Around line 32-43: The validateConfig function currently calls parse(raw)
which can throw on malformed YAML and bypass the friendly error handling; wrap
the parse(raw) call in a try/catch inside validateConfig, and on catch log the
same formatted "Error: Invalid chronicle.yaml at '<configPath>'" message
(including the parsing error message as context) and exit(1); then proceed to
run chronicleConfigSchema.safeParse on the parsed result if parse succeeds.
Ensure you reference the validateConfig function and the parse(raw) invocation
when making the change.

In `@packages/chronicle/src/server/routes/sitemap.xml.ts`:
- Around line 40-43: The sitemap always emits the root URL unconditionally;
change the construction of xml (the xml template string) so it does not
unconditionally include `${baseUrl}` and instead includes only configured
landing routes: include `${baseUrl}` only if the latest version has landing
enabled, and append a landing URL `${baseUrl}/${versionName}` for each version
where `landing: true`; keep concatenating `${[...docPages,
...apiPages].join('\n')}` as before. Locate the xml template string and the
version/config object used to generate pages, remove the hard-coded `${baseUrl}`
line, and generate landing entries programmatically from your versions list
(referencing the same version identifier used elsewhere) before joining with
docPages and apiPages.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 20-25: Pin the Bun version in the "Setup Bun" step by adding a
bun-version or bun-version-file input (so the oven-sh/setup-bun action uses a
fixed version) and enable caching of Bun and dependencies for the "Install
dependencies" step by caching ~/.bun/install/cache (and optionally node_modules
or bun.lockb artifacts) so bun install --frozen-lockfile runs reproducibly and
faster in CI; update the action inputs for the "Setup Bun" step and add a cache
step that keys off the bun version and lockfile to restore/save
~/.bun/install/cache before/after running bun install.

In `@packages/chronicle/src/cli/commands/init.ts`:
- Around line 62-73: The .gitignore handling in the init flow can false-match
via includes() and can append without a trailing newline; update the logic
around gitignorePath so you read existing, split into lines, normalize by
trimming whitespace and ignoring lines that start with '#' (comments), then
compute missing entries by checking exact equality against GITIGNORE_ENTRIES
(not substring includes) using the normalized lines; ensure the file content
ends with a single newline before appending the missing entries and update
events (in the same block that uses existing, missing, and events) to record the
correct detail string as before.

In `@packages/chronicle/src/lib/llms.test.ts`:
- Around line 6-67: Add a test case in packages/chronicle/src/lib/llms.test.ts
that calls buildLlmsTxt with a page whose url ends with a trailing slash (e.g.,
'/docs/a/') and assert the output contains the link with a .md suffix (e.g., '-
[A](/docs/a.md)'); place it alongside the other describe('buildLlmsTxt') tests
and reuse LATEST_CONTEXT or a versioned ctx as appropriate to also validate
urlPrefix rewriting when applicable.

In `@packages/chronicle/src/lib/llms.ts`:
- Around line 21-26: Escape Markdown-sensitive characters in page titles and
URL-encode the path before building the index string: when mapping over pages
(the block that computes index from pages, using p.title and mdUrl) replace
p.title with an escaped variant that backslash-escapes backslashes and square
brackets (e.g., escape backslash '\' and ']' and '[') and replace mdUrl with a
URL-encoded form (use encodeURI/encodeURIComponent on the path or filename) so
the template becomes "- [escapedTitle](encodedMdUrl)"; update the mapping in
llms.ts where index is computed to use these escaped/encoded values.

In `@packages/chronicle/src/pages/LandingPage.module.css`:
- Around line 1-56: Keyboard users don't get a visible custom focus state on the
card; add a matching focus-visible rule so .card elements show the same visual
treatment as .card:hover. Update the CSS to include a .card:focus-visible
selector (and/or .card:focus when needed) that sets border-color and background
the same as .card:hover and also add an accessible focus indicator (e.g.,
outline and outline-offset) so keyboard-only users see the card is focused;
ensure this applies to the same focusable elements using the .card class (e.g.,
anchor cards).

In `@packages/chronicle/src/server/api/search.ts`:
- Around line 53-82: The buildApiDocs function calls loadConfig() every request;
remove that redundant call by adding a config parameter to buildApiDocs (e.g.,
buildApiDocs(ctx: VersionContext, config: ConfigType)) and use that config with
getApiConfigsForVersion instead of calling loadConfig(); then update all call
sites (the request handler/resolveCtx caller that currently invokes
buildApiDocs) to loadConfig once (or use a module-scope cached config) and pass
the config into buildApiDocs; ensure the function signature, references to
getApiConfigsForVersion, and any tests or imports are updated accordingly.
- Around line 18-23: indexCache and docsCache are global Maps that never evict,
causing stale search/index state during dev with Vite HMR; add a cache-busting
mechanism and wire it to dev file-watcher/HMR. Implement and export a small API
(e.g., clearSearchCaches or clearIndexAndDocsCache) that calls
indexCache.clear() and docsCache.clear() and/or a per-key invalidator that uses
keyFor(ctx) to remove a specific entry; then call that function from your dev
watcher/HMR handler (or when frontmatter/page files change) so updated pages
rebuild their MiniSearch index and docs array without restarting the process.
Ensure the new function is reachable where the dev server/file-watcher logic
runs and only invoked in dev/HMR scenarios.

In `@packages/chronicle/src/themes/default/ContentDirButtons.tsx`:
- Around line 23-61: The visible buttons use RouterLink for SPA navigation
(giving anchor semantics like middle-click, right-click, copy link) while
overflow items call navigate(entry.href) in DropdownMenu.Item onClick, losing
those affordances; change the overflow rendering to render anchor-based items by
wrapping each DropdownMenu.Item with asChild and placing a RouterLink
(to={entry.href}) inside (replace the onClick navigate usage), so overflow
entries use the same RouterLink semantics as the visible entries (refer to
RouterLink, DropdownMenu.Item, overflow, visible, and navigate in the current
diff).

In `@packages/chronicle/src/themes/paper/VersionSwitcher.tsx`:
- Around line 40-54: The DropdownMenu items in VersionSwitcher are using onClick
navigation (versions.map -> DropdownMenu.Item with onClick={() =>
navigate(getVersionHomeHref(...))}), which loses native anchor semantics and
duplicates logic from the default theme; change each DropdownMenu.Item to render
as a real anchor by passing an "as" prop or an href (use
getVersionHomeHref(config, v.dir) as the href) so
middle-click/CMD-click/right-click work, keep navigate fallback if needed for
client routing, and then extract the shared logic into a single shared
VersionSwitcher (or useVersionSwitcher() hook) in the lib so both themes import
it and only pass visual overrides (e.g., trigger button width='100%') to avoid
drift between packages/chronicle/src/themes/paper/VersionSwitcher.tsx and
packages/chronicle/src/themes/default/VersionSwitcher.tsx.

In `@packages/chronicle/src/types/config.ts`:
- Around line 163-166: The current Zod refinement on the schema (.refine((cfg)
=> !cfg.versions || cfg.versions.length === 0 || !!cfg.latest, { ... })) treats
an empty versions array as "no versions" and therefore doesn't require latest;
remove the length === 0 short-circuit so the predicate becomes !cfg.versions ||
!!cfg.latest (or equivalently require latest whenever cfg.versions is present),
i.e., update the .refine predicate that references cfg.versions and cfg.latest
to not treat [] as absent so empty arrays will also require a latest block.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9f7afa2f-581a-48ee-af4c-9ca7a0882bdd

📥 Commits

Reviewing files that changed from the base of the PR and between e8d8f04 and fa3ebdb.

📒 Files selected for processing (74)
  • .github/workflows/ci.yml
  • docs/chronicle.yaml
  • docs/content/docs/cli.mdx
  • docs/content/docs/components.mdx
  • docs/content/docs/configuration.mdx
  • docs/content/docs/docker.mdx
  • docs/content/docs/frontmatter.mdx
  • docs/content/docs/index.mdx
  • docs/content/docs/themes.mdx
  • examples/basic/chronicle.yaml
  • examples/basic/content/docs/api/endpoints.mdx
  • examples/basic/content/docs/api/overview.mdx
  • examples/basic/content/docs/getting-started.mdx
  • examples/basic/content/docs/guides/configuration.mdx
  • examples/basic/content/docs/guides/installation.mdx
  • examples/basic/content/docs/index.mdx
  • examples/versioned/chronicle.yaml
  • examples/versioned/content/dev/api.mdx
  • examples/versioned/content/dev/index.mdx
  • examples/versioned/content/docs/guide.mdx
  • examples/versioned/content/docs/index.mdx
  • examples/versioned/versions/v1/dev/index.mdx
  • examples/versioned/versions/v1/docs/index.mdx
  • examples/versioned/versions/v2/docs/guide.mdx
  • examples/versioned/versions/v2/docs/index.mdx
  • package.json
  • packages/chronicle/package.json
  • packages/chronicle/src/cli/commands/build.ts
  • packages/chronicle/src/cli/commands/dev.ts
  • packages/chronicle/src/cli/commands/init.test.ts
  • packages/chronicle/src/cli/commands/init.ts
  • packages/chronicle/src/cli/commands/serve.ts
  • packages/chronicle/src/cli/commands/start.ts
  • packages/chronicle/src/cli/utils/config.ts
  • packages/chronicle/src/cli/utils/scaffold.test.ts
  • packages/chronicle/src/cli/utils/scaffold.ts
  • packages/chronicle/src/components/ui/search.tsx
  • packages/chronicle/src/lib/config.test.ts
  • packages/chronicle/src/lib/config.ts
  • packages/chronicle/src/lib/head.tsx
  • packages/chronicle/src/lib/llms.test.ts
  • packages/chronicle/src/lib/llms.ts
  • packages/chronicle/src/lib/navigation.test.ts
  • packages/chronicle/src/lib/navigation.ts
  • packages/chronicle/src/lib/page-context.tsx
  • packages/chronicle/src/lib/route-resolver.test.ts
  • packages/chronicle/src/lib/route-resolver.ts
  • packages/chronicle/src/lib/source.ts
  • packages/chronicle/src/lib/version-source.test.ts
  • packages/chronicle/src/lib/version-source.ts
  • packages/chronicle/src/pages/ApiPage.tsx
  • packages/chronicle/src/pages/DocsLayout.tsx
  • packages/chronicle/src/pages/LandingPage.module.css
  • packages/chronicle/src/pages/LandingPage.tsx
  • packages/chronicle/src/server/App.tsx
  • packages/chronicle/src/server/api/search.ts
  • packages/chronicle/src/server/api/specs.ts
  • packages/chronicle/src/server/entry-client.tsx
  • packages/chronicle/src/server/entry-server.tsx
  • packages/chronicle/src/server/routes/[version]/llms.txt.ts
  • packages/chronicle/src/server/routes/llms.txt.ts
  • packages/chronicle/src/server/routes/og.tsx
  • packages/chronicle/src/server/routes/sitemap.xml.ts
  • packages/chronicle/src/server/vite-config.ts
  • packages/chronicle/src/themes/default/ContentDirButtons.tsx
  • packages/chronicle/src/themes/default/Layout.tsx
  • packages/chronicle/src/themes/default/VersionSwitcher.tsx
  • packages/chronicle/src/themes/paper/ContentDirDropdown.tsx
  • packages/chronicle/src/themes/paper/Layout.module.css
  • packages/chronicle/src/themes/paper/Layout.tsx
  • packages/chronicle/src/themes/paper/VersionSwitcher.tsx
  • packages/chronicle/src/types/config.ts
  • packages/chronicle/src/types/theme.ts
  • vercel.json

Comment thread packages/chronicle/src/cli/commands/init.test.ts
Comment thread packages/chronicle/src/cli/utils/scaffold.ts
Comment thread packages/chronicle/src/lib/head.tsx Outdated
Comment thread packages/chronicle/src/lib/llms.ts Outdated
Comment thread packages/chronicle/src/lib/navigation.ts
Comment thread packages/chronicle/src/server/api/search.ts
Comment thread packages/chronicle/src/server/entry-server.tsx
Comment thread packages/chronicle/src/server/routes/llms.txt.ts
Comment thread packages/chronicle/src/types/config.ts
Comment thread packages/chronicle/src/types/config.ts
rsbh and others added 18 commits April 21, 2026 18:04
Rewrite chronicleConfigSchema for multi-content + versioning:
- site.title replaces top-level title
- content is now {dir,label}[] (single string form removed)
- latest + versions[] with per-version content, api, and badge
- badge variant maps to Apsara Badge color prop
- strict root + dir-uniqueness refines

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- Validate via chronicleConfigSchema.parse instead of ad-hoc spread
- Default config uses new {site, content[]} shape
- Add helpers: getLatestContentRoots, getVersionContentRoots, getAllVersions
- ContentRoot resolves fs path (content/<dir> or versions/<v>/<dir>) and URL prefix

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- Validates parse rules: required site, non-empty content[], strict root
- Rejects legacy title, content:string, versions-without-latest, duplicate dirs
- Covers getLatestContentRoots, getVersionContentRoots, getAllVersions order
- Covers loadConfig fallback + yaml parsing via __CHRONICLE_CONFIG_RAW__

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- loadCLIConfig returns projectRoot (configPath's dirname) and drops
  resolveContentDir now that content is an array of {dir,label}
- Commands drop --content flag; content path is config-driven
- Vite define __CHRONICLE_CONTENT_DIR__ points at packageRoot/.content
  mirror so downstream routes remain stable as the mirror grows to
  include versioned subtrees
- linkContent still called with a bridge path (projectRoot/content);
  scaffold gets its real multi-root rewrite next commit

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
linkContent now takes (projectRoot, config) and rebuilds
packageRoot/.content to mirror the configured layout:

  .content/<contentDir>           → <projectRoot>/content/<contentDir>
  .content/<versionDir>/<contentDir> → <projectRoot>/versions/<v>/<contentDir>

The mirror is wiped on each run (handles legacy single-symlink and
stale entries), then rebuilt from getLatestContentRoots and
getVersionContentRoots. CLI commands pass config through and rename
vite's return to viteConfig to avoid shadowing.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Pure tree/page filters live in src/lib/version-source.ts, keyed off
the config's versions[]. source.ts exposes thin wrappers:

- getPageTreeForVersion(ctx) returns a subtree scoped to the version
- getPagesForVersion(ctx)   returns pages filtered to the version
- getVersionContextForUrl   resolves URL -> VersionContext

Latest (ctx.dir === null) excludes anything under /<versionDir>/*,
while a version returns only its subtree.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Extract buildContentMirror(mirrorRoot, projectRoot, config) as the pure
unit; linkContent stays a thin wrapper binding PACKAGE_ROOT/.content.

Tests use tmpdir to exercise:
- single and multi content latest layouts
- nested versioned layout
- idempotency on re-run
- stale entries wiped when config shrinks
- legacy single-symlink mirror replaced by directory + child symlinks

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- resolveVersionFromUrl: matches versions exactly, falls back to latest
  (no false positive on substring overlap e.g. /v1 vs /v1beta)
- filterPagesByVersion: latest excludes versioned pages, version scopes
  to its prefix
- filterPageTreeByVersion: latest strips version folders, version unwraps
  its folder, absent version returns empty

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
resolveRoute(pathname, config) classifies a URL into:

- redirect      single-content root -> /<dir> or /<v>/<dir>
- docs-index    multi-content root (latest or versioned) for landing
- docs-page     slug is the full URL slug incl. version prefix
                (fumadocs page URLs already include /<v>/ so the
                mirror + loader handle lookup without stripping)
- api-index / api-page  /apis or /<v>/apis routes

Resolver stays classifier-only; invalid content dirs fall through
to docs-page and let page lookup return 404 downstream.

Tests cover single/multi/versioned configs, trailing slash, and
version-shaped-but-unknown prefixes.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Replace string literal types with a `RouteType` const-object so
callers can reference RouteType.DocsPage etc. instead of repeating
hyphenated strings.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- entry-server.tsx uses resolveRoute:
  - RouteType.Redirect returns 302 with Location (single-content
    root -> /<dir>; version root -> /<v>/<dir>)
  - RouteType.DocsPage loads page + version-scoped tree via
    getPageTreeForVersion; missing page -> 404
  - RouteType.DocsIndex returns 404 today (multi-content landing
    lands in phase 3B)
  - API routes load specs only when the URL is actually an API
    route instead of on every request
- PageProvider now takes initialVersion and exposes it via context;
  client nav re-runs resolveRoute on pathname changes so the version
  ctx stays in sync
- App.tsx delegates to the shared resolver and updates Head/JSON-LD
  references to read config.site.title (schema change from phase 1)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- entry-client now forwards embedded.version into PageProvider and
  uses the shared resolveRoute to decide when to fetch /api/specs
  (previously hard-coded to pathname.startsWith('/apis'), which
  missed /<v>/apis)
- PageProvider only pushes version state when the route actually
  carries one (Redirect has no version payload)
- Default config fallback matches the new {site, content[]} shape

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- getLandingEntries(config, versionDir) returns {label, href,
  contentDir} per content root (null = latest) so both UI and
  tests share the same source of truth
- LandingPage renders a grid of cards from getLandingEntries; it
  reads version via usePageContext so a versioned /<v> root
  (multi-content) shows that version's dirs + labels
- App.tsx routes RouteType.DocsIndex to LandingPage inside
  DocsLayout; entry-server returns 200 and PageProvider stops
  forcing a 404 for docs-index
- Tests: getLandingEntries covers latest, versioned, and unknown
  version cases

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- getApiConfigsForVersion(config, dir|null) picks config.api for
  latest or versions[].api for a version; tests cover both
- /api/specs accepts ?version=<dir>; entry-server and entry-client
  pass the active version so /apis and /<v>/apis resolve to their
  own spec set
- page-context always refetches on api-nav so switching versions
  from the api page updates the spec list

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- buildLlmsTxt(config, pages, version) centralises the llms.txt
  rendering so /llms.txt and /<v>/llms.txt share one codepath
- /llms.txt now scopes to LATEST_CONTEXT via getPagesForVersion
- New /<version>/llms.txt handler looks up the version from config,
  404s for unknown, and emits pages from getPagesForVersion(ctx)
- sitemap.xml iterates getAllVersions and emits /<v>/apis/... for
  each version's api specs (latest stays unprefixed)
- og.tsx reads config.site.title (schema change from phase 1)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- head.tsx, default/paper Layout.tsx, ApiPage.tsx read config.site.title
- init.ts defaultConfig matches the new {site, content[]} schema and
  scaffolds content/<dir>/index.mdx so chronicle dev finds the mirror
  entry; drops --content flag that no longer maps to the config shape
- [version]/llms.txt.ts reads the version param via h3's
  getRouterParam (nitro doesn't re-export it)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
rsbh and others added 8 commits April 21, 2026 18:04
Vite's build-time import.meta.glob doesn't descend into symlinked
directories, so the previous dir-level symlinks made the production
bundle ship an empty page tree (/docs etc. 404). Replace
buildContentMirror's dir symlinks with a recursive walk that mkdirs
each subfolder in the mirror and symlinks files individually; dev
live-reload is preserved, and vite build now walks real dirs.

Tests updated to assert on real dirs + per-file symlinks.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The start command lost --config in the phase-2 CLI refactor; pass
the user's chronicle.yaml path through loadCLIConfig so
npm-script workflows like `chronicle start --config docs/...`
resolve the right config.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Content + version dir names now fail schema validation unless they
are simple folder names. Rejects '.', '..', and anything containing
'/' or '\\' so neither fsPath nor urlPrefix can resolve to a path
traversal or produce a broken URL like '/./.'.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- versions[].dir can no longer collide with a top-level content[].dir;
  the URL segment would otherwise shadow the content root
- 'apis' (and future RESERVED_ROUTE_SEGMENTS entries) are rejected as
  content or version dir names to avoid colliding with built-in routes
- Tests cover both refines

Addresses coderabbit review on types/config.ts:166.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- LlmsPage.title is now optional; buildLlmsTxt falls back to the
  page URL when title is missing or whitespace-only (extractFrontmatter
  defaults to 'Untitled' today but the helper should defend itself)
- Empty description no longer produces a stray blank line between
  the heading and the index
- Tests cover both cases

Addresses coderabbit review on routes/llms.txt.ts:22.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- scaffold.ts: mirrorTree/removeMirror only swallow ENOENT now;
  other fs errors (permission, read failures) propagate
- navigation.ts + route-resolver.ts: resolve content dirs through
  getLatestContentRoots / getVersionContentRoots so the mirror,
  source, nav, and resolver all agree on how content roots are
  derived
- page-context.tsx: clear stale page + errorStatus when entering
  an API route so a prior 404 doesn't linger on the API screen
- LandingPage.tsx: switch content-dir cards from <a> to RouterLink
  to keep navigation SPA-internal and preserve hydrated state

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Bun 1.3.13 rolled out mid-PR and resolves the lockfile slightly
differently from 1.3.9, causing --frozen-lockfile to fail even
without any dep change. Pin the setup-bun action to 1.3.9 so CI
matches the lockfile that was committed.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Main picked up fumadocs-core/mdx bumps (#39) while this branch was
in flight; regenerate bun.lock against the current package.json so
bun install --frozen-lockfile passes on CI's merge-ref checkout.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- init.test.ts: the "preserve existing index.mdx" case now actually
  writes an index.mdx and asserts its contents are untouched
- head.tsx: og:image + twitter:image are absolute URLs when config.url
  is set (crawlers require absolute); relative fallback otherwise
- api/search.ts: unknown ?tag=<x> now returns HTTP 400 instead of
  silently folding into LATEST_CONTEXT — easier to spot client bugs
- entry-server.tsx: the chronicle:ssr-rendered hook now fires on
  302 redirects too so analytics/metrics don't under-count them
- types/config.ts: dirNameSchema accepts only /^[a-zA-Z0-9][\w.-]*$/
  so hidden (".git"), whitespace, and control-char names are rejected

Tests cover the new accept/reject sets.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/chronicle/src/cli/utils/scaffold.ts`:
- Around line 33-41: The current linkContent function uses a package-global
mirror path (PACKAGE_ROOT/.content) which causes cross-project clobbering;
change linkContent to compute a project-scoped mirror directory (for example
based on projectRoot or a unique namespaced temp path) and call
buildContentMirror with that project-specific path instead of
PACKAGE_ROOT/.content, and ensure the resulting mirror path is propagated to any
Vite/SSR bootstrap or consumers that expect the mirror (references: linkContent,
buildContentMirror, PACKAGE_ROOT/.content).

In `@packages/chronicle/src/types/config.ts`:
- Around line 77-80: The siteSchema's title currently uses z.string() which
permits empty strings; update siteSchema to require a non-empty title by
changing the title validator to z.string().min(1) (matching the sibling label
validators like contentEntrySchema.label, latestSchema.label,
versionSchema.label, badgeSchema.label) so empty titles are rejected at
validation time; locate the title property in the siteSchema object and replace
its validator accordingly and run/adjust any related tests or callers that
assume a non-empty title.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7aef190a-ba30-4600-88ad-46a9213f6a80

📥 Commits

Reviewing files that changed from the base of the PR and between fa3ebdb and a3ab4a6.

📒 Files selected for processing (9)
  • packages/chronicle/src/cli/utils/scaffold.ts
  • packages/chronicle/src/lib/config.test.ts
  • packages/chronicle/src/lib/llms.test.ts
  • packages/chronicle/src/lib/llms.ts
  • packages/chronicle/src/lib/navigation.ts
  • packages/chronicle/src/lib/page-context.tsx
  • packages/chronicle/src/lib/route-resolver.ts
  • packages/chronicle/src/pages/LandingPage.tsx
  • packages/chronicle/src/types/config.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/chronicle/src/lib/navigation.ts
  • packages/chronicle/src/lib/llms.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/chronicle/src/lib/llms.test.ts
  • packages/chronicle/src/pages/LandingPage.tsx

Comment thread packages/chronicle/src/cli/utils/scaffold.ts
Comment thread packages/chronicle/src/types/config.ts
@rohilsurana rohilsurana requested a review from Copilot April 21, 2026 18:51
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds versioned + multi-content documentation support to Chronicle, making routing, search, APIs, llms.txt, and themes version-aware under a new strict config schema.

Changes:

  • Introduces new chronicle.yaml schema (site, content[], latest, versions[]) plus helpers and validation.
  • Adds version-aware routing/source utilities (route resolver, version scoping, content-dir scoping) and a landing page mode.
  • Updates server/CLI/theme layers to use version context (API specs, search tagging, content mirror, version/content switchers) and adds fixtures/docs/CI updates.

Reviewed changes

Copilot reviewed 62 out of 75 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
vercel.json Points Vercel output to docs build output directory.
packages/chronicle/src/types/theme.ts Adds hideSidebar to theme layout props.
packages/chronicle/src/types/config.ts Defines new strict config schema with content/version structures and validation refinements.
packages/chronicle/src/themes/paper/VersionSwitcher.tsx Adds version switcher UI for paper theme.
packages/chronicle/src/themes/paper/Layout.tsx Adds version/content-dir controls; supports hideSidebar.
packages/chronicle/src/themes/paper/Layout.module.css Styles nav container for new controls.
packages/chronicle/src/themes/paper/ContentDirDropdown.tsx Adds content-dir dropdown for paper theme.
packages/chronicle/src/themes/default/VersionSwitcher.tsx Adds version switcher UI for default theme.
packages/chronicle/src/themes/default/Layout.tsx Adds version/content controls; supports hideSidebar.
packages/chronicle/src/themes/default/ContentDirButtons.tsx Adds visible + overflow content-dir buttons.
packages/chronicle/src/server/vite-config.ts Switches dev FS allowlist/content define to .content mirror.
packages/chronicle/src/server/routes/sitemap.xml.ts Makes sitemap API routes version-aware.
packages/chronicle/src/server/routes/og.tsx Uses config.site.title for OG rendering.
packages/chronicle/src/server/routes/llms.txt.ts Switches llms.txt generation to helper + latest version scoping.
packages/chronicle/src/server/routes/[version]/llms.txt.ts Adds per-version llms.txt route.
packages/chronicle/src/server/entry-server.tsx Integrates route resolver, version-aware tree/pages/specs, and SSR redirects.
packages/chronicle/src/server/entry-client.tsx Hydrates with version context; makes API specs fetching version-aware.
packages/chronicle/src/server/api/specs.ts Adds version query param to load per-version API specs.
packages/chronicle/src/server/api/search.ts Makes search index + results version-aware via tag and per-version caching.
packages/chronicle/src/server/App.tsx Switches to centralized route resolver; adds landing page rendering mode.
packages/chronicle/src/pages/LandingPage.tsx Adds landing page listing content roots for a version.
packages/chronicle/src/pages/LandingPage.module.css Styles the landing page layout/cards.
packages/chronicle/src/pages/DocsLayout.tsx Scopes sidebar tree to active content dir and version; supports hiding sidebar.
packages/chronicle/src/pages/ApiPage.tsx Uses config.site.title in metadata.
packages/chronicle/src/lib/version-source.ts Adds version context + filtering helpers for pages and page trees.
packages/chronicle/src/lib/version-source.test.ts Tests version context resolution and filtering behavior.
packages/chronicle/src/lib/source.ts Injects synthetic meta roots; adds version-scoped page/tree APIs.
packages/chronicle/src/lib/route-resolver.ts Adds pure URL classifier with redirect/index/page/api routing.
packages/chronicle/src/lib/route-resolver.test.ts Tests route classification and redirects across versions/content layouts.
packages/chronicle/src/lib/page-context.tsx Threads version through context; makes client nav version-aware.
packages/chronicle/src/lib/navigation.ts Adds helpers for active content dir, version home href, and content button splitting.
packages/chronicle/src/lib/navigation.test.ts Tests navigation helpers.
packages/chronicle/src/lib/llms.ts Adds shared llms.txt builder (version-aware heading + index).
packages/chronicle/src/lib/llms.test.ts Tests llms.txt formatting/version heading behavior.
packages/chronicle/src/lib/head.tsx Adds canonical URL + absolute OG image URL support when config.url is set.
packages/chronicle/src/lib/config.ts Implements schema-based config loading and content/version helper APIs.
packages/chronicle/src/lib/config.test.ts Comprehensive tests for schema + helper functions + config loading.
packages/chronicle/src/components/ui/search.tsx Tags search queries by active version.
packages/chronicle/src/cli/utils/scaffold.ts Replaces single symlink with full multi-root .content mirror builder.
packages/chronicle/src/cli/utils/scaffold.test.ts Tests content mirror building across latest/multi/versions and idempotency.
packages/chronicle/src/cli/utils/config.ts Removes --content resolution; switches CLI config to projectRoot.
packages/chronicle/src/cli/commands/start.ts Removes --content; uses config-driven mirror + project root.
packages/chronicle/src/cli/commands/serve.ts Removes --content; uses config-driven mirror + project root.
packages/chronicle/src/cli/commands/init.ts Updates init scaffold to new schema/layout; adds testable runInit().
packages/chronicle/src/cli/commands/init.test.ts Adds tests for runInit() scaffolding behavior.
packages/chronicle/src/cli/commands/dev.ts Removes --content; uses config-driven mirror + project root.
packages/chronicle/src/cli/commands/build.ts Removes --content; uses config-driven mirror + project root.
packages/chronicle/package.json Adds bun test script for the package.
package.json Adds scripts to run/build example projects using --config.
examples/versioned/versions/v2/docs/index.mdx Adds versioned example content (v2).
examples/versioned/versions/v2/docs/guide.mdx Adds versioned example content (v2).
examples/versioned/versions/v1/docs/index.mdx Adds versioned example content (v1).
examples/versioned/versions/v1/dev/index.mdx Adds versioned example content (v1).
examples/versioned/content/docs/index.mdx Adds latest example content (docs).
examples/versioned/content/docs/guide.mdx Adds latest example content (docs).
examples/versioned/content/dev/index.mdx Adds latest example content (dev).
examples/versioned/content/dev/api.mdx Adds latest example content (dev).
examples/versioned/chronicle.yaml Adds versioned example config using new schema.
examples/basic/content/docs/index.mdx Adds basic example docs content.
examples/basic/content/docs/guides/installation.mdx Adds basic example guide.
examples/basic/content/docs/guides/configuration.mdx Adds basic example config guide content.
examples/basic/content/docs/getting-started.mdx Adds basic example getting started content.
examples/basic/content/docs/api/overview.mdx Adds basic example API overview content.
examples/basic/content/docs/api/endpoints.mdx Adds basic example CLI/API endpoints content.
examples/basic/chronicle.yaml Migrates basic example config to new schema.
docs/content/docs/themes.mdx Adds docs page describing built-in themes.
docs/content/docs/index.mdx Adds docs getting-started page for Chronicle docs site.
docs/content/docs/frontmatter.mdx Adds docs page for MDX frontmatter.
docs/content/docs/docker.mdx Adds docs page for Docker usage.
docs/content/docs/configuration.mdx Rewrites configuration docs for new schema/versioning.
docs/content/docs/components.mdx Adds docs page for supported MDX components.
docs/content/docs/cli.mdx Adds docs page for CLI commands.
docs/chronicle.yaml Migrates Chronicle docs site config to new schema/preset.
bun.lock Updates dependencies (fumadocs, esbuild, etc.).
.github/workflows/ci.yml Adds CI to run lint + tests with Bun.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/chronicle/src/server/entry-client.tsx
Comment thread packages/chronicle/src/server/App.tsx
Comment thread packages/chronicle/src/types/config.ts Outdated
Comment thread packages/chronicle/src/server/api/specs.ts
Comment thread packages/chronicle/src/types/config.ts Outdated
- config.ts: RESERVED_ROUTE_SEGMENTS now includes every top-level
  server-owned route (api, apis, og, llms.txt, robots.txt,
  sitemap.xml), and the reserved-segment check uses superRefine so
  zod points at the offending path (content[i].dir,
  versions[vi].dir, or versions[vi].content[ci].dir) instead of a
  generic 'content' path
- api/specs.ts: unknown ?version=<x> returns HTTP 400 to match the
  search endpoint's behaviour
- entry-client.tsx: api spec resolution + specsUrl now read
  route.version.dir so versioned URLs fetch their own specs even
  when embedded data is missing; embedded.version still wins for
  initialVersion when present
- App.tsx: RouteType.Redirect renders react-router's <Navigate>
  so client nav (e.g. clicking the title to /) follows the same
  302 target the server would emit

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/chronicle/src/server/api/search.ts (1)

39-50: ⚠️ Potential issue | 🟠 Major

Add a title fallback before indexing pages.

A page without a frontmatter title can produce an undefined title, and the client assumes result content is a string. Use a stable fallback before caching/indexing.

🛡️ Proposed fix
   return pages.map(p => {
     const fm = extractFrontmatter(p);
+    const title = fm.title?.trim() || p.url;
     return {
       id: p.url,
       url: p.url,
-      title: fm.title,
-      content: fm.description ?? '',
+      title,
+      content: fm.description ?? title,
       type: 'page' as const
     };
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/server/api/search.ts` around lines 39 - 50,
scanContent currently maps pages to SearchDocument using extractFrontmatter but
can set title to undefined; update the mapping in scanContent (inside pages.map)
to provide a stable string fallback for title (e.g., fm.title ?? fm.description
?? p.title ?? p.url) and ensure content remains a string (keep fm.description ??
''). Modify the object returned in scanContent so id, url, title and content are
always strings before caching/indexing.
packages/chronicle/src/themes/default/Layout.tsx (1)

79-87: ⚠️ Potential issue | 🟠 Major

Use version-scoped API configs for navbar API links.

config.api is latest-only now. On /v1/..., this can link users to /apis for the latest API, or hide versions[].api links entirely.

🧭 Proposed fix
 import { MethodBadge } from '@/components/api/method-badge';
 import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher';
 import { Footer } from '@/components/ui/footer';
 import { Search } from '@/components/ui/search';
+import { getApiConfigsForVersion } from '@/lib/config';
+import { usePageContext } from '@/lib/page-context';
...
   const { pathname } = useLocation();
+  const { version } = usePageContext();
+  const apiConfigs = getApiConfigsForVersion(config, version.dir);
   const scrollRef = useRef<HTMLDivElement>(null);
...
-            {config.api?.map(api => (
+            {apiConfigs.map(api => (
               <RouterLink
-                key={api.basePath}
-                to={api.basePath}
+                key={`${version.dir ?? 'latest'}:${api.basePath}`}
+                to={`${version.urlPrefix}${api.basePath}`}
                 className={styles.navButton}
               >
                 {api.name} API
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/themes/default/Layout.tsx` around lines 79 - 87,
Navbar links currently iterate config.api (which is latest-only) causing wrong
targets on versioned pages; update the Layout component to resolve a
version-scoped API list first: detect the active version from the current route
(e.g., from window.location.pathname or router location), find the matching
config.versions entry (e.g., config.versions.find(...)) and use that entry's
.api array if present, otherwise fall back to config.api; then map that
resolvedApi list when rendering the RouterLink elements (keep keys like
api.basePath and className={styles.navButton}). Ensure this logic is applied
where config.api is currently referenced so versioned pages link to
versions[].api.
packages/chronicle/src/components/ui/search.tsx (1)

113-133: ⚠️ Potential issue | 🟡 Minor

Handle versioned API result URLs when rendering icons.

Versioned API search results now come back as /<version>/apis/..., so Line 201 misses them and shows document/heading icons instead of MethodBadge.

🎨 Proposed fix
-                          {getResultIcon(result)}
+                          {getResultIcon(result, version.dir)}
...
-                      {getResultIcon(result)}
+                      {getResultIcon(result, version.dir)}
...
-function getResultIcon(result: SortedResult): React.ReactNode {
-  if (!result.url.startsWith('/apis/')) {
+function getResultIcon(
+  result: SortedResult,
+  versionDir: string | null
+): React.ReactNode {
+  const apiPrefix = versionDir ? `/${versionDir}/apis/` : '/apis/';
+  if (!result.url.startsWith(apiPrefix)) {
     return result.type === 'page' ? (
       <DocumentIcon className={styles.icon} />
     ) : (

Also applies to: 200-209

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/components/ui/search.tsx` around lines 113 - 133,
getResultIcon is missing versioned API paths (e.g. "/v1/apis/...") so API
results render wrong icons; update the icon-detection logic inside getResultIcon
(and any duplicate checks used in the render loop) to recognize API URLs that
contain "/apis/" even when prefixed by a version segment — e.g. match either a
"/apis/" substring or use a regex that allows an initial "/<version>/" before
"apis" (such as /(^\/[^\/]+\/apis\/)|\/apis\//) and return MethodBadge for those
matches instead of the document/heading icons.
♻️ Duplicate comments (2)
packages/chronicle/src/cli/utils/scaffold.ts (1)

33-41: ⚠️ Potential issue | 🟠 Major

Avoid the package-global .content mirror.

linkContent() still rebuilds PACKAGE_ROOT/.content, so parallel builds/dev servers or multiple projects sharing one Chronicle install can delete and replace each other’s mirror. This needs a project-scoped mirror path propagated to Vite/SSR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/cli/utils/scaffold.ts` around lines 33 - 41,
linkContent currently forces a global mirror at PACKAGE_ROOT/.content which
causes interference between projects; change linkContent(projectRoot, config) to
compute a project-scoped mirror path (e.g., derive from projectRoot such as
path.join(projectRoot, '.chronicle', 'content')) and pass that path into
buildContentMirror instead of PACKAGE_ROOT/.content, then propagate this new
mirror path through the ChronicleConfig or the return value so the Vite and SSR
setup (where buildContentMirror output is consumed) use the project-scoped
mirror rather than PACKAGE_ROOT/.content (update call sites that assume
PACKAGE_ROOT/.content accordingly).
packages/chronicle/src/types/config.ts (1)

77-80: ⚠️ Potential issue | 🟡 Minor

Require a non-empty site.title.

z.string() still accepts '', unlike the sibling user-facing labels that use .min(1).

🛡️ Proposed fix
 const siteSchema = z.object({
-  title: z.string(),
+  title: z.string().min(1),
   description: z.string().optional(),
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/types/config.ts` around lines 77 - 80, The siteSchema
currently allows an empty title because title is defined as z.string(); update
the siteSchema's title validator to require a non-empty string (e.g., change the
title schema on siteSchema to use z.string().min(1) or z.string().nonempty()) so
empty titles are rejected by validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/content/docs/configuration.mdx`:
- Line 27: Update the sentence describing how content dirs are resolved to
explicitly include the content directory segment so users know to place files
under a subdirectory (e.g. versions/<version-dir>/<dir>/) rather than directly
under versions/<version-dir> — modify the line that references `content:` and
`versions[].content[]` to show the full resolved paths including the content
directory segment (for example: "Content dirs declared in `content:` are
resolved under `content/` for the latest version; each `versions[].content[]`
entry is resolved under `versions/<version-dir>/<content-dir>/").

In `@packages/chronicle/src/cli/commands/init.ts`:
- Around line 62-68: The current check uses existing.includes(e) which matches
substrings; change to compare entries by line: read the .gitignore into lines
(split on /\r?\n/), trim each line and treat blank/comment lines appropriately,
then compute missing = GITIGNORE_ENTRIES.filter(e => !linesSet.has(e)) so only
exact line matches count; update the append/events logic (symbols:
gitignorePath, existing, GITIGNORE_ENTRIES, missing) to use this lines-based
membership test.

In `@packages/chronicle/src/cli/utils/scaffold.ts`:
- Around line 15-29: The code currently continues silently when a configured
content root path is missing, producing a partial mirror; update the scaffold
logic that iterates getLatestContentRoots and getVersionContentRoots so it fails
fast on missing source paths: before calling mirrorTree(source, dest) (in both
the latest-content loop and the versions loop handling versionMirror), check
that the resolved source path exists (e.g., fs.stat or fs.existsSync) and if it
does not, throw a clear error that includes the offending configured value (use
root.fsPath and/or root.contentDir and the computed source) so the caller gets a
config validation error instead of silently building an empty/partial mirror;
alternatively, catch ENOENT from mirrorTree and rethrow a descriptive error
mentioning root.fsPath/root.contentDir and version.dir.

In `@packages/chronicle/src/lib/config.ts`:
- Around line 16-24: The current loadConfig function parses the raw YAML and
returns it directly, which drops any missing optional fields from defaultConfig;
change loadConfig to parse the raw YAML into an object, deep-merge that parsed
object onto defaultConfig (so defaultConfig values remain for omitted keys),
then pass the merged result into chronicleConfigSchema.parse before returning.
Use the existing symbols loadConfig, defaultConfig, chronicleConfigSchema.parse
and parse(raw) to locate the logic and ensure the merge is deep (not shallow) so
nested fields like search.enabled and theme are preserved.

In `@packages/chronicle/src/lib/page-context.tsx`:
- Around line 87-101: The page tree is stuck on initialTree because tree is
initialized from initialTree and never updated on cross-version navigations;
update the logic in page-context.tsx around the useState for tree and the effect
that runs on pathname changes (resolveRoute, setVersion, setCurrentPath) so that
when the resolved route.version differs from the current version you either (a)
fetch and set a version-scoped tree via setTree (replace the fixed const [tree]
= useState<Root>(initialTree) with a mutable state and call setTree with the
SSR-provided or server-fetched tree for that version) or (b) trigger a full
document navigation for cross-version switches instead of client-only route
handling; ensure the check uses route.version vs version/currentPath to decide
which approach to take and update any other places (e.g., the effect around
lines 160-163) that assume a static initialTree.

In `@packages/chronicle/src/lib/route-resolver.ts`:
- Around line 59-69: The current root-handling always redirects to dirs[0] when
remainder is empty, which hides other top-level content; change the logic in
route-resolver where remainder is checked (use isLandingEnabled, contentDirsFor
and RouteType cases) so that you only emit a RouteType.Redirect when dirs.length
=== 1 (use version.urlPrefix + dirs[0]); keep returning RouteType.DocsIndex for
dirs.length === 0 and for dirs.length > 1 so multi-content sites show the index
instead of auto-redirecting to the first content dir.

In `@packages/chronicle/src/lib/version-source.ts`:
- Around line 94-98: The folder-matching predicate used to compute match can
wrongly accept empty folders because nodeUrls(n).every(...) returns true for []
— update the predicate inside the tree.children.find call (the one assigning
match) to additionally require nodeUrls(n).length > 0 (or an equivalent
non-empty check) before calling isUnderPrefix on its urls; reference the Folder
type, nodeUrls(n) call, and isUnderPrefix(u, expectedPrefix) to ensure only
folders with at least one URL are considered matches so the correct contentDir
folder is selected.

In `@packages/chronicle/src/server/entry-client.tsx`:
- Around line 58-61: The current logic forces routeVersion to LATEST_CONTEXT
when route.type === RouteType.Redirect, which loses the intended version for
redirect-to-version-root hits; change the assignment in entry-client.tsx so
routeVersion uses route.version when present and only falls back to
LATEST_CONTEXT if route.version is undefined (e.g., replace the ternary that
sets LATEST_CONTEXT for RouteType.Redirect with something like routeVersion =
route.version ?? LATEST_CONTEXT), keeping the final version = embedded?.version
?? routeVersion unchanged.

In `@packages/chronicle/src/server/entry-server.tsx`:
- Around line 39-44: The current logic silences failures when loading API specs
by using loadApiSpecs(...).catch(() => []), causing APIs with broken specs to
still return 200; change the logic in entry-server.tsx so that when
apiConfigs.length > 0 you await loadApiSpecs(apiConfigs) without swallowing
errors (or catch and rethrow a new Error including context) so failures
propagate as server errors; update the two occurrences around
apiConfigs/apiSpecs (the block using isApiRoute,
getApiConfigsForVersion(route.version.dir), and the later duplicate at lines
~117-124) to either let loadApiSpecs throw or throw an explicit error with the
original error attached rather than returning an empty array.

In `@packages/chronicle/src/themes/default/VersionSwitcher.tsx`:
- Around line 12-17: The versions list currently omits the unprefixed "latest"
when config.latest is absent, so update VersionSwitcher to ensure a latest entry
is present: after calling getAllVersions(config) (or inside getAllVersions if
you prefer), detect if no version has dir === null (or isLatest === true) and
synthesize a latest version object (isLatest: true, dir: null, label from
config.latest || infer from content/first item) and unshift/push it into
versions so the dropdown can switch back to the unprefixed docs; keep the
existing active-finding logic (active = versions.find(...)) unchanged so it will
pick the synthesized latest.

---

Outside diff comments:
In `@packages/chronicle/src/components/ui/search.tsx`:
- Around line 113-133: getResultIcon is missing versioned API paths (e.g.
"/v1/apis/...") so API results render wrong icons; update the icon-detection
logic inside getResultIcon (and any duplicate checks used in the render loop) to
recognize API URLs that contain "/apis/" even when prefixed by a version segment
— e.g. match either a "/apis/" substring or use a regex that allows an initial
"/<version>/" before "apis" (such as /(^\/[^\/]+\/apis\/)|\/apis\//) and return
MethodBadge for those matches instead of the document/heading icons.

In `@packages/chronicle/src/server/api/search.ts`:
- Around line 39-50: scanContent currently maps pages to SearchDocument using
extractFrontmatter but can set title to undefined; update the mapping in
scanContent (inside pages.map) to provide a stable string fallback for title
(e.g., fm.title ?? fm.description ?? p.title ?? p.url) and ensure content
remains a string (keep fm.description ?? ''). Modify the object returned in
scanContent so id, url, title and content are always strings before
caching/indexing.

In `@packages/chronicle/src/themes/default/Layout.tsx`:
- Around line 79-87: Navbar links currently iterate config.api (which is
latest-only) causing wrong targets on versioned pages; update the Layout
component to resolve a version-scoped API list first: detect the active version
from the current route (e.g., from window.location.pathname or router location),
find the matching config.versions entry (e.g., config.versions.find(...)) and
use that entry's .api array if present, otherwise fall back to config.api; then
map that resolvedApi list when rendering the RouterLink elements (keep keys like
api.basePath and className={styles.navButton}). Ensure this logic is applied
where config.api is currently referenced so versioned pages link to
versions[].api.

---

Duplicate comments:
In `@packages/chronicle/src/cli/utils/scaffold.ts`:
- Around line 33-41: linkContent currently forces a global mirror at
PACKAGE_ROOT/.content which causes interference between projects; change
linkContent(projectRoot, config) to compute a project-scoped mirror path (e.g.,
derive from projectRoot such as path.join(projectRoot, '.chronicle', 'content'))
and pass that path into buildContentMirror instead of PACKAGE_ROOT/.content,
then propagate this new mirror path through the ChronicleConfig or the return
value so the Vite and SSR setup (where buildContentMirror output is consumed)
use the project-scoped mirror rather than PACKAGE_ROOT/.content (update call
sites that assume PACKAGE_ROOT/.content accordingly).

In `@packages/chronicle/src/types/config.ts`:
- Around line 77-80: The siteSchema currently allows an empty title because
title is defined as z.string(); update the siteSchema's title validator to
require a non-empty string (e.g., change the title schema on siteSchema to use
z.string().min(1) or z.string().nonempty()) so empty titles are rejected by
validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e5ee1d72-d5f6-42ac-a5d0-9364b28d02b7

📥 Commits

Reviewing files that changed from the base of the PR and between a3ab4a6 and de0efc9.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (74)
  • .github/workflows/ci.yml
  • docs/chronicle.yaml
  • docs/content/docs/cli.mdx
  • docs/content/docs/components.mdx
  • docs/content/docs/configuration.mdx
  • docs/content/docs/docker.mdx
  • docs/content/docs/frontmatter.mdx
  • docs/content/docs/index.mdx
  • docs/content/docs/themes.mdx
  • examples/basic/chronicle.yaml
  • examples/basic/content/docs/api/endpoints.mdx
  • examples/basic/content/docs/api/overview.mdx
  • examples/basic/content/docs/getting-started.mdx
  • examples/basic/content/docs/guides/configuration.mdx
  • examples/basic/content/docs/guides/installation.mdx
  • examples/basic/content/docs/index.mdx
  • examples/versioned/chronicle.yaml
  • examples/versioned/content/dev/api.mdx
  • examples/versioned/content/dev/index.mdx
  • examples/versioned/content/docs/guide.mdx
  • examples/versioned/content/docs/index.mdx
  • examples/versioned/versions/v1/dev/index.mdx
  • examples/versioned/versions/v1/docs/index.mdx
  • examples/versioned/versions/v2/docs/guide.mdx
  • examples/versioned/versions/v2/docs/index.mdx
  • package.json
  • packages/chronicle/package.json
  • packages/chronicle/src/cli/commands/build.ts
  • packages/chronicle/src/cli/commands/dev.ts
  • packages/chronicle/src/cli/commands/init.test.ts
  • packages/chronicle/src/cli/commands/init.ts
  • packages/chronicle/src/cli/commands/serve.ts
  • packages/chronicle/src/cli/commands/start.ts
  • packages/chronicle/src/cli/utils/config.ts
  • packages/chronicle/src/cli/utils/scaffold.test.ts
  • packages/chronicle/src/cli/utils/scaffold.ts
  • packages/chronicle/src/components/ui/search.tsx
  • packages/chronicle/src/lib/config.test.ts
  • packages/chronicle/src/lib/config.ts
  • packages/chronicle/src/lib/head.tsx
  • packages/chronicle/src/lib/llms.test.ts
  • packages/chronicle/src/lib/llms.ts
  • packages/chronicle/src/lib/navigation.test.ts
  • packages/chronicle/src/lib/navigation.ts
  • packages/chronicle/src/lib/page-context.tsx
  • packages/chronicle/src/lib/route-resolver.test.ts
  • packages/chronicle/src/lib/route-resolver.ts
  • packages/chronicle/src/lib/source.ts
  • packages/chronicle/src/lib/version-source.test.ts
  • packages/chronicle/src/lib/version-source.ts
  • packages/chronicle/src/pages/ApiPage.tsx
  • packages/chronicle/src/pages/DocsLayout.tsx
  • packages/chronicle/src/pages/LandingPage.module.css
  • packages/chronicle/src/pages/LandingPage.tsx
  • packages/chronicle/src/server/App.tsx
  • packages/chronicle/src/server/api/search.ts
  • packages/chronicle/src/server/api/specs.ts
  • packages/chronicle/src/server/entry-client.tsx
  • packages/chronicle/src/server/entry-server.tsx
  • packages/chronicle/src/server/routes/[version]/llms.txt.ts
  • packages/chronicle/src/server/routes/llms.txt.ts
  • packages/chronicle/src/server/routes/og.tsx
  • packages/chronicle/src/server/routes/sitemap.xml.ts
  • packages/chronicle/src/server/vite-config.ts
  • packages/chronicle/src/themes/default/ContentDirButtons.tsx
  • packages/chronicle/src/themes/default/Layout.tsx
  • packages/chronicle/src/themes/default/VersionSwitcher.tsx
  • packages/chronicle/src/themes/paper/ContentDirDropdown.tsx
  • packages/chronicle/src/themes/paper/Layout.module.css
  • packages/chronicle/src/themes/paper/Layout.tsx
  • packages/chronicle/src/themes/paper/VersionSwitcher.tsx
  • packages/chronicle/src/types/config.ts
  • packages/chronicle/src/types/theme.ts
  • vercel.json
✅ Files skipped from review due to trivial changes (26)
  • examples/versioned/content/dev/api.mdx
  • examples/versioned/versions/v2/docs/guide.mdx
  • examples/versioned/versions/v2/docs/index.mdx
  • examples/versioned/content/docs/index.mdx
  • packages/chronicle/src/pages/ApiPage.tsx
  • .github/workflows/ci.yml
  • examples/versioned/content/docs/guide.mdx
  • examples/versioned/versions/v1/dev/index.mdx
  • packages/chronicle/src/types/theme.ts
  • examples/versioned/chronicle.yaml
  • vercel.json
  • packages/chronicle/src/server/routes/og.tsx
  • packages/chronicle/src/themes/paper/Layout.module.css
  • examples/versioned/content/dev/index.mdx
  • packages/chronicle/package.json
  • docs/chronicle.yaml
  • examples/versioned/versions/v1/docs/index.mdx
  • packages/chronicle/src/server/routes/[version]/llms.txt.ts
  • packages/chronicle/src/lib/llms.test.ts
  • packages/chronicle/src/lib/navigation.test.ts
  • examples/basic/chronicle.yaml
  • packages/chronicle/src/pages/LandingPage.module.css
  • packages/chronicle/src/lib/llms.ts
  • packages/chronicle/src/cli/commands/init.test.ts
  • packages/chronicle/src/lib/navigation.ts
  • package.json
🚧 Files skipped from review as they are similar to previous changes (15)
  • packages/chronicle/src/server/vite-config.ts
  • packages/chronicle/src/pages/DocsLayout.tsx
  • packages/chronicle/src/server/api/specs.ts
  • packages/chronicle/src/themes/paper/ContentDirDropdown.tsx
  • packages/chronicle/src/themes/default/ContentDirButtons.tsx
  • packages/chronicle/src/cli/commands/dev.ts
  • packages/chronicle/src/themes/paper/VersionSwitcher.tsx
  • packages/chronicle/src/server/App.tsx
  • packages/chronicle/src/lib/route-resolver.test.ts
  • packages/chronicle/src/cli/commands/build.ts
  • packages/chronicle/src/themes/paper/Layout.tsx
  • packages/chronicle/src/lib/source.ts
  • packages/chronicle/src/cli/utils/scaffold.test.ts
  • packages/chronicle/src/pages/LandingPage.tsx
  • packages/chronicle/src/cli/commands/start.ts

Comment thread docs/content/docs/configuration.mdx Outdated
Comment thread packages/chronicle/src/cli/commands/init.ts
Comment thread packages/chronicle/src/cli/utils/scaffold.ts
Comment thread packages/chronicle/src/lib/config.ts Outdated
Comment thread packages/chronicle/src/lib/config.ts Outdated
Comment thread packages/chronicle/src/lib/route-resolver.ts
Comment thread packages/chronicle/src/lib/version-source.ts Outdated
Comment thread packages/chronicle/src/server/entry-client.tsx
Comment thread packages/chronicle/src/server/entry-server.tsx Outdated
Comment thread packages/chronicle/src/themes/default/VersionSwitcher.tsx
rsbh and others added 5 commits April 22, 2026 09:46
- scaffold.mirrorTree now rethrows ENOENT as 'Content directory not
  found: <path>' so a config that points at a non-existent dir errors
  out instead of silently building an empty mirror
- filterPageTreeByContentDir requires the candidate folder to carry
  at least one URL before checking the prefix, otherwise an empty
  top-level folder's vacuous every() match would shadow the actual
  content folder

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- loadConfig shallow-merges user config over defaultConfig so an
  omitted theme or search still falls back to the defaults that
  defaultConfig defines (previously dropped on any user config load)
- getAllVersions always emits the latest entry (even when config.latest
  is absent and versions[] is empty), so consumers like sitemap.xml
  don't miss /apis when there are no explicit versions

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Substring match treats 'dist' as already present when the existing
.gitignore only has 'distribution'. Split on newlines and compare
trimmed lines exactly; added regression test covering that case.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Switching versions client-side (e.g. via VersionSwitcher) previously
kept the SSR's version-scoped tree in PageContext, leaving the
sidebar showing the wrong version's pages. Now:

- entry-server.tsx emits the full unfiltered pageTree
- DocsLayout runs filterPageTreeByVersion followed by
  filterPageTreeByContentDir per render, so nav across versions
  re-derives the sidebar from pathname + active context
- entry-client.tsx resolves routeVersion via resolveVersionFromUrl
  (not LATEST_CONTEXT) so a direct client hit to a redirect target
  like /v1 hydrates with the correct version ctx

Also: entry-server no longer catches loadApiSpecs failures — broken
API specs now surface as server errors instead of rendering an empty
API page with a 200.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Make it explicit that versions[].content[].dir is rooted at
versions/<version-dir>/<dir>/, not directly under versions/<v>/.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@rsbh rsbh merged commit 88286c7 into main Apr 22, 2026
4 checks passed
@rsbh rsbh deleted the feat_multi_content_support branch April 22, 2026 05:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants