Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (10)
📝 WalkthroughWalkthroughRefactors Chronicle into a version-aware site: replaces top-level config with Changes
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
There was a problem hiding this comment.
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 | 🟡 MinorHandle malformed YAML before schema validation.
parse(raw)can throw, which bypasses the formattedInvalid chronicle.yamlerror 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 | 🟡 MinorInclude only configured landing routes in the sitemap.
Line 42 always emits
${baseUrl}but does not emit version landing URLs like${baseUrl}/v1when a non-latest version haslanding: 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:latestgate allowsversions: [].
!cfg.versions || cfg.versions.length === 0 || !!cfg.latestintentionally treats an emptyversionsarray as "no versions", so a user can shipversions: []with nolatestblock. 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 thelength === 0short-circuit so empty arrays are also required to declarelatest(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@v2withoutbun-versionwill 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.xorbun-version-file) and enabling caching for~/.bun/install/cacheto speed upbun 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.mdandurlPrefixrewriting assertions give good coverage ofbuildLlmsTxt's formatting contract. Consider adding one more case for a page whoseurlalready ends in a trailing slash (e.g.,/docs/a/) if that's a shape the caller can produce, to lock down the.mdsuffixing 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: redundantloadConfig()calls per request.
loadConfig()is invoked in bothresolveCtxandbuildApiDocson every request. IfloadConfigisn't memoized internally, this is wasteful — and if it ever becomes async/fs-bound, it'll compound. Consider acceptingconfigas 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/docsCacheare module-levelMaps with no eviction. In production SSR this is fine, but indev(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-drivencache.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 callnavigate(entry.href)viaonClick. 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.titleis 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 inLlmsPage.🤖 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 viaonClickonly — 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 ofpackages/chronicle/src/themes/default/VersionSwitcher.tsxapart from thewidth='100%'on the trigger button. Consider extracting a sharedVersionSwitcher(or a shared hook/useVersionSwitcher()) in@/liband 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-visiblestyle on.cardthat 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:.gitignoresubstring match can both over- and under-match.
existing.includes('dist')returns true for lines likedistribution,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
📒 Files selected for processing (74)
.github/workflows/ci.ymldocs/chronicle.yamldocs/content/docs/cli.mdxdocs/content/docs/components.mdxdocs/content/docs/configuration.mdxdocs/content/docs/docker.mdxdocs/content/docs/frontmatter.mdxdocs/content/docs/index.mdxdocs/content/docs/themes.mdxexamples/basic/chronicle.yamlexamples/basic/content/docs/api/endpoints.mdxexamples/basic/content/docs/api/overview.mdxexamples/basic/content/docs/getting-started.mdxexamples/basic/content/docs/guides/configuration.mdxexamples/basic/content/docs/guides/installation.mdxexamples/basic/content/docs/index.mdxexamples/versioned/chronicle.yamlexamples/versioned/content/dev/api.mdxexamples/versioned/content/dev/index.mdxexamples/versioned/content/docs/guide.mdxexamples/versioned/content/docs/index.mdxexamples/versioned/versions/v1/dev/index.mdxexamples/versioned/versions/v1/docs/index.mdxexamples/versioned/versions/v2/docs/guide.mdxexamples/versioned/versions/v2/docs/index.mdxpackage.jsonpackages/chronicle/package.jsonpackages/chronicle/src/cli/commands/build.tspackages/chronicle/src/cli/commands/dev.tspackages/chronicle/src/cli/commands/init.test.tspackages/chronicle/src/cli/commands/init.tspackages/chronicle/src/cli/commands/serve.tspackages/chronicle/src/cli/commands/start.tspackages/chronicle/src/cli/utils/config.tspackages/chronicle/src/cli/utils/scaffold.test.tspackages/chronicle/src/cli/utils/scaffold.tspackages/chronicle/src/components/ui/search.tsxpackages/chronicle/src/lib/config.test.tspackages/chronicle/src/lib/config.tspackages/chronicle/src/lib/head.tsxpackages/chronicle/src/lib/llms.test.tspackages/chronicle/src/lib/llms.tspackages/chronicle/src/lib/navigation.test.tspackages/chronicle/src/lib/navigation.tspackages/chronicle/src/lib/page-context.tsxpackages/chronicle/src/lib/route-resolver.test.tspackages/chronicle/src/lib/route-resolver.tspackages/chronicle/src/lib/source.tspackages/chronicle/src/lib/version-source.test.tspackages/chronicle/src/lib/version-source.tspackages/chronicle/src/pages/ApiPage.tsxpackages/chronicle/src/pages/DocsLayout.tsxpackages/chronicle/src/pages/LandingPage.module.csspackages/chronicle/src/pages/LandingPage.tsxpackages/chronicle/src/server/App.tsxpackages/chronicle/src/server/api/search.tspackages/chronicle/src/server/api/specs.tspackages/chronicle/src/server/entry-client.tsxpackages/chronicle/src/server/entry-server.tsxpackages/chronicle/src/server/routes/[version]/llms.txt.tspackages/chronicle/src/server/routes/llms.txt.tspackages/chronicle/src/server/routes/og.tsxpackages/chronicle/src/server/routes/sitemap.xml.tspackages/chronicle/src/server/vite-config.tspackages/chronicle/src/themes/default/ContentDirButtons.tsxpackages/chronicle/src/themes/default/Layout.tsxpackages/chronicle/src/themes/default/VersionSwitcher.tsxpackages/chronicle/src/themes/paper/ContentDirDropdown.tsxpackages/chronicle/src/themes/paper/Layout.module.csspackages/chronicle/src/themes/paper/Layout.tsxpackages/chronicle/src/themes/paper/VersionSwitcher.tsxpackages/chronicle/src/types/config.tspackages/chronicle/src/types/theme.tsvercel.json
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]>
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]>
638525f to
c223ba8
Compare
- 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]>
There was a problem hiding this comment.
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
📒 Files selected for processing (9)
packages/chronicle/src/cli/utils/scaffold.tspackages/chronicle/src/lib/config.test.tspackages/chronicle/src/lib/llms.test.tspackages/chronicle/src/lib/llms.tspackages/chronicle/src/lib/navigation.tspackages/chronicle/src/lib/page-context.tsxpackages/chronicle/src/lib/route-resolver.tspackages/chronicle/src/pages/LandingPage.tsxpackages/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
There was a problem hiding this comment.
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.yamlschema (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.
- 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]>
There was a problem hiding this comment.
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 | 🟠 MajorAdd a title fallback before indexing pages.
A page without a frontmatter title can produce an undefined
title, and the client assumes resultcontentis 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 | 🟠 MajorUse version-scoped API configs for navbar API links.
config.apiis latest-only now. On/v1/..., this can link users to/apisfor the latest API, or hideversions[].apilinks 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 | 🟡 MinorHandle 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 ofMethodBadge.🎨 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 | 🟠 MajorAvoid the package-global
.contentmirror.
linkContent()still rebuildsPACKAGE_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 | 🟡 MinorRequire 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
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (74)
.github/workflows/ci.ymldocs/chronicle.yamldocs/content/docs/cli.mdxdocs/content/docs/components.mdxdocs/content/docs/configuration.mdxdocs/content/docs/docker.mdxdocs/content/docs/frontmatter.mdxdocs/content/docs/index.mdxdocs/content/docs/themes.mdxexamples/basic/chronicle.yamlexamples/basic/content/docs/api/endpoints.mdxexamples/basic/content/docs/api/overview.mdxexamples/basic/content/docs/getting-started.mdxexamples/basic/content/docs/guides/configuration.mdxexamples/basic/content/docs/guides/installation.mdxexamples/basic/content/docs/index.mdxexamples/versioned/chronicle.yamlexamples/versioned/content/dev/api.mdxexamples/versioned/content/dev/index.mdxexamples/versioned/content/docs/guide.mdxexamples/versioned/content/docs/index.mdxexamples/versioned/versions/v1/dev/index.mdxexamples/versioned/versions/v1/docs/index.mdxexamples/versioned/versions/v2/docs/guide.mdxexamples/versioned/versions/v2/docs/index.mdxpackage.jsonpackages/chronicle/package.jsonpackages/chronicle/src/cli/commands/build.tspackages/chronicle/src/cli/commands/dev.tspackages/chronicle/src/cli/commands/init.test.tspackages/chronicle/src/cli/commands/init.tspackages/chronicle/src/cli/commands/serve.tspackages/chronicle/src/cli/commands/start.tspackages/chronicle/src/cli/utils/config.tspackages/chronicle/src/cli/utils/scaffold.test.tspackages/chronicle/src/cli/utils/scaffold.tspackages/chronicle/src/components/ui/search.tsxpackages/chronicle/src/lib/config.test.tspackages/chronicle/src/lib/config.tspackages/chronicle/src/lib/head.tsxpackages/chronicle/src/lib/llms.test.tspackages/chronicle/src/lib/llms.tspackages/chronicle/src/lib/navigation.test.tspackages/chronicle/src/lib/navigation.tspackages/chronicle/src/lib/page-context.tsxpackages/chronicle/src/lib/route-resolver.test.tspackages/chronicle/src/lib/route-resolver.tspackages/chronicle/src/lib/source.tspackages/chronicle/src/lib/version-source.test.tspackages/chronicle/src/lib/version-source.tspackages/chronicle/src/pages/ApiPage.tsxpackages/chronicle/src/pages/DocsLayout.tsxpackages/chronicle/src/pages/LandingPage.module.csspackages/chronicle/src/pages/LandingPage.tsxpackages/chronicle/src/server/App.tsxpackages/chronicle/src/server/api/search.tspackages/chronicle/src/server/api/specs.tspackages/chronicle/src/server/entry-client.tsxpackages/chronicle/src/server/entry-server.tsxpackages/chronicle/src/server/routes/[version]/llms.txt.tspackages/chronicle/src/server/routes/llms.txt.tspackages/chronicle/src/server/routes/og.tsxpackages/chronicle/src/server/routes/sitemap.xml.tspackages/chronicle/src/server/vite-config.tspackages/chronicle/src/themes/default/ContentDirButtons.tsxpackages/chronicle/src/themes/default/Layout.tsxpackages/chronicle/src/themes/default/VersionSwitcher.tsxpackages/chronicle/src/themes/paper/ContentDirDropdown.tsxpackages/chronicle/src/themes/paper/Layout.module.csspackages/chronicle/src/themes/paper/Layout.tsxpackages/chronicle/src/themes/paper/VersionSwitcher.tsxpackages/chronicle/src/types/config.tspackages/chronicle/src/types/theme.tsvercel.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
- 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]>
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
Filesystem:
What shipped (by phase)
{site, content[], latest, versions[]}shape, zod strict root, uniqueness refines,lodash/uniqByfor dedup checks.site.descriptionmoved from top-level.buildContentMirrorsymlinks each(version, contentDir)intopackageRoot/.content/; the vite dev/build plugin rebuilds it per run. Legacy single-symlink mirrors are replaced automatically.filterPageTreeByVersion,filterPageTreeByContentDir,filterPagesByVersion,resolveVersionFromUrl. Syntheticmeta.jsonentries injected per content root (runtime, no filesystem writes) so sidebars render each content dir as a flat root.RouteTypeenum + pureresolveRoute(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[].landingopt into a chromeless landing page with content-dir cards./apisvs/<v>/apis; per-version spec loading viagetApiConfigsForVersion./api/search?tag=<version>, per-version MiniSearch index cached by version key; canonical URLs emitted in<head>whenconfig.urlis set./llms.txtfor latest,/<v>/llms.txtfor each version via a catch-all[version]/llms.txt.tsroute.chronicle init— scaffoldscontent/<dir>/, the newchronicle.yaml, and a sampleindex.mdx.runInit()is unit-tested.examples/basicmoved intocontent/docs/, newexamples/versionedfixture withversions/v1/+versions/v2/,docs/content/docs/configuration.mdxrewritten for the new schema.npmscripts added for each example..github/workflows/ci.ymlrunsbun run lint+bun teston PRs and main pushes.Breaking changes
title→site.title.description→site.description.content: <string>removed;content:is now{dir, label}[].content/<dir>/(latest) andversions/<v>/<dir>/(old).--contentCLI flag removed; content location is fully config-driven.landing: true(defaultfalse= 302 to first content dir).Test plan
bun test— 79 tests pass across 7 filesbun run lint— 0 errorsdocs/chronicle.yaml(single-content)examples/versioned/chronicle.yaml(multi-content + versioned + badges)/,/v1,/v2routes honourlandingflag (302 vs landing render)examples/basic(API specs rendering)🤖 Generated with Claude Code