Tags: raystack/chronicle
Tags
fix: bump fumadocs-core to 16.15.15 so bold text does not overflow th… …e stack (#186) mdast-util-to-markdown 2.1.3 serialises strong and emphasis through containerPhrasing, which reads an `attention` property off the handler. fumadocs-core's stringifier wraps every handler in a new function and, before 16.15.15, dropped that property. The strong handler then calls containerPhrasing, which calls the wrapped strong handler again, until the build fails with "Maximum call stack size exceeded" on any page with bold or italic text. fumadocs-core pins mdast-util-to-markdown with a caret, so every fresh install of chronicle picks up 2.1.3 and breaks. 16.15.15 copies the handler's properties onto the wrapper. The lockfile moves mdast-util-to-markdown to 2.1.3 as well, so CI builds against the version a fresh install resolves.
docs: rebuild the documentation (#185) * fix: keep navigation inside the section it belongs to A content directory is a section: its own folder, its own URL prefix, its own navigation. Four things were leaking across those boundaries or losing them. `filterPageTreeByContentDir` was called twice on the same tree — once by `entry-server` before serialising, once by `DocsLayout` on what it was handed — and the second pass had no way to tell an already narrowed tree from a wide one. It looked for a folder whose urls all start with the prefix, which on a narrowed tree matches the first sub-folder instead. So the docs site rendered an empty sidebar, the basic example showed only its `guides` folder, and a versioned site showed only its first content directory. Telling the two apart needs three signals, because url shapes alone are ambiguous: `root → [folder Docs (/docs/*)]` and `root → [folder guides (/docs/guides/*)]` look identical to a prefix test. A folder is the content directory when every url inside it belongs to the directory, when it holds every url in the tree that belongs to the directory, and when it has a page directly below the prefix rather than only pages nested deeper. A page sitting at the prefix settles it earlier: that is the directory's own page, so the tree is already its contents. `filterPageTreeByVersion` had the same double-call problem and now recognises an already narrowed tree too. Previous and next were chained across the whole site, so the last page of Docs offered the first page of Ops Guide — walking a reader out of the section they chose. Both implementations now chain per section, sharing `contentSectionPrefixes` and `sectionOf` so the server and the static build cannot drift apart. The fanfold header printed "DOCS / DOCS / GETTING STARTED": `getBreadcrumbItems` starts at the tree root, whose name is the section's own label. Any name that repeats the one before it is dropped, which also covers a folder whose index page carries the folder's title. Its workaround for the scoping bug goes with it — the filters are safe to apply twice now. * fix: give every page one title `RootHead` rendered the site title and every route then rendered its own, so two `<title>` tags reached the document. A browser reads the first one, and `RootHead` renders before the page — so every tab in the site said "Chronicle" no matter which page was open. Dropped the one in `RootHead`, which keeps the site-level JSON-LD and nothing else. Every real route already renders `<Head>`; the two that did not were the 404 and the render-error page, which showed whatever title the previous page had left behind. Both name themselves now. * fix: build sites that declare versions `chronicle build` failed on any site with a `versions:` key, so no versioned site could ship. Dev mode was fine, which is why it went unnoticed — the error only appears when the production bundle is assembled. `remarkResolveImages` located a page's content root by looking for `/content/` in its path and returned when it found none. Versioned pages live under `versions/v1/docs/`, so every one of them took that path and returned before setting `file.data.images`. The mdx config names `images` in `valueToExport`, so the build then died with a missing-export error on the first versioned page it reached. It now recognises `versions/` as well. The path below the marker is also the path under `/_content/` — `content/docs/` mirrors as `docs/` and `versions/v1/docs/` as `v1/docs/` — so versioned pages get working image resolution rather than only a build that finishes. `images` is also initialised before any early return. A page that resolves to neither root still exports an empty list, so a path shape nobody anticipated cannot break a build again. * feat: honour theme.colors `theme.colors` has been in the config schema, documented as "custom color overrides", and read by nothing. A site could set it, pass validation, and see no change. The alternative was deleting the key, but the schema is `.strict()` — a config that sets it would go from silently ignored to failing at startup. Better to make the promise true. Keys name an Apsara colour token and take the `--rs-color-` prefix automatically; a key written as a full custom property is used as-is, which is how a theme's own variables like `--paper-ink` are reached. The declarations go into the head after the stylesheets so they win, and under `:root` plus both `[data-theme]` selectors, because Apsara defines its dark values behind that attribute and a bare `:root` would lose to them once a reader picks a theme. One value covers both themes, since the config holds one value per token. A token that needs to differ is better left to the theme. Values are checked against a colour-shaped pattern before they reach the stylesheet. This string comes from a config file and is written into CSS, so anything that could close a declaration or open a rule has to be impossible rather than unlikely. * feat: give every theme a logo, and put a folder's index in the sidebar Four things in the sidebars, all of them about what a reader could not see. A site that set no `logo` got a book icon in the default theme and nothing at all in paper or fanfold. The Raystack mark is the fallback now, in all three, drawn from the brand file in `raystack/website` with `currentColor` so it works on either ground. `SidebarLogo` moves to `components/ui/logo.tsx` and resolves `logo.light` / `logo.dark` for every theme, so a site that sets one gets it everywhere rather than only where a theme happened to look. In fanfold the mark sits above the site name rather than beside it. The rail is a fixed 240px and the display face is wide, so sharing a line started breaking titles mid-word. A folder's index page never appeared in the sidebar. `SidebarNode` and `ChapterNav` render a folder's children and its group label is a collapse trigger rather than a link, so the page existed with no route into it. Both render it as the group's first row now — which is what fanfold's `Nav` already did. Top-level groups were never collapsible, because `collapsible={depth >= 1}` was written when the content-root wrapper made every real group a level deeper. Every group can be collapsed now, with top-level ones open on arrival and nested ones open only when they hold the page being read. Two spacing corrections fall out of the above. `.navGroup[data-depth='0']` set a top margin on every top-level group including the first, which doubled the padding `.sidebarMain` already applies — 48px above the first label. Apsara guards its own nav-group margin with `:not(:first-child)`; that guard is restored and the rest of the rule is dropped, since Apsara's spacing was winning on source order anyway. The content-directory links then needed a margin of their own: they are a separate block above the tree, and the tree may start with a plain page carrying no margin, so the two ran together. * docs: rebuild the documentation around reader tasks The docs were organised around the framework's own parts. Nine pages, each named after a thing — Configuration, Frontmatter, Components, Themes — so a reader who arrived with a job had to guess which noun held the answer. Twenty-four pages in six groups now, named after the jobs: start here, writing docs, guides, themes, reference, deploy and operate. `features.mdx` is gone. It was doing three jobs at once: a marketing bullet list, a feature index, and the only documentation in the whole site for five topics. Sorting and `meta.json` went to Navigation, redirects to Links and redirects, markdown URLs and `llms.txt` to Generated routes, the playground to API reference, health checks to Monitoring. What was left became an Introduction. Versioning, API references and deployment each had a config key and a bullet and no guide, despite being three of the strongest reasons to pick Chronicle. Deployment was the sharpest case: the last step of the job was three words in a config reference. They have guides now, along with search, multiple content sections, and moving an existing docs site over. `configuration.mdx` was 468 lines, nearly a third of the site, and was where people were forced to learn versioning and API specs because there was nowhere else. It is a reference again, with a link to the guide for each large key. The frontmatter page leads with a table, and its `draft` section is no longer pasted into the middle of `authors`, which had split that field's prose in half. Writing the guides meant reading the source, which turned up four things the docs had wrong. Only three deploy presets were listed where the code has eight, four of them static. Search was called "powered by Fumadocs" on one page and "SQLite FTS5" on another; it is SQLite full-text search on a server build and a downloaded index in the browser on a static one, which decides how large a static site can sensibly get. A static build is a single-page app rather than pre-rendered HTML, so a host has to rewrite unknown paths to `index.html` — undocumented, and the usual reason a deep link 404s. And it optimises images at build time, which the old page denied. Seven permanent redirects cover every URL that moved, so no existing link breaks. `url` is set, which the sitemap, canonical tags and social cards all needed and none of them had. Content elsewhere gets the same treatment as the pages: every file opened with an `# H1` repeating its own frontmatter title, so both examples and the `init` scaffold showed the same words twice. Removed, and the rule is written down under `title` in the frontmatter reference. The one exception is the basic example's heading-levels demo, which now starts at `##` and says why. * fix: address review findings, and trim the commentary CodeRabbit flagged eight things on #185. Seven were real. The serious one: identifying a content directory by the shape of its urls broke a section that holds only sub-folders. No index page and no page directly under the prefix meant the wrapper failed the depth check, and with a sibling section present the tree came back empty — an empty sidebar. The heuristic is gone. `buildFiles` already marks every content root with `root: true`, and fumadocs carries that onto the folder node; it was only missing because `compactTree` dropped it before the tree reached a layout. Keeping it on folders makes the check explicit, and the three-signal guess with it. `KEEP_FIELDS` gains `root` for folders only — it means nothing on a page or a separator, and a test already pinned that. `splitContentRoot` took the first marker in array order rather than the deepest in the path, so a project inside a directory named `versions` resolved its images against the wrong root. Sidebar groups used `defaultOpen`, which is read once on mount. Client-side navigation does not remount the tree, so a group the reader collapsed stayed shut over the page they then opened. Controlled now, reopening when it holds the active page and otherwise leaving the reader's choice alone. `Logo` gained a `labelled` option. Fanfold and paper render the site name beside it, where announcing it again is noise — and the fallback is Raystack's mark, which should not speak for a site that is not theirs. Four documentation corrections, all mine. The request tester's proxy means an auth token reaches the docs server, so "stays in their browser" was wrong and worth being plain about. The search index is built on the first readiness or search request, not at startup — nothing calls `ensureIndex` before then. A page without `title` renders as `Untitled` rather than failing, so "required" was overstating it in three places. And pages do not sort before folders: they share one scale, as the same page said two sections later. The eighth finding misread the mark's label as the site title. Skipped. Comments throughout this branch were doing too much explaining. Cut back to what the code cannot say for itself. * feat: fall back to the site's initial, and use the logo as the favicon The fallback mark was Raystack's. Fine for a Raystack site, wrong for anyone else's — a project that forgot to set `logo` shipped someone else's branding. It is now the site's first letter in a tinted box: plain enough to read as unfinished rather than borrowed. Codepoint-safe, so an emoji or a non-Latin title survives being taken apart. Two places ignored `logo` entirely. The favicon was hardcoded to `/favicon.ico` and `/favicon.svg`, so a site that set a logo still had to supply favicon files separately, and one that did neither got no favicon at all. A configured logo is now the icon; the static files stay as the fallback, so an explicit favicon still works. Social cards read `logo.dark` alone. A site that set only `light` got a card with no mark even though its sidebar showed one. Both variants resolve now, and a site with no logo gets the same initial the sidebar falls back to. Drawing the box from `currentColor` looked right but was not: Apsara colours its text components rather than their containers, so the inherited colour is black in both themes and the letter vanished on a dark ground. It takes explicit tokens instead. `.brandLogo` in fanfold was setting `display: block` for an svg, which killed the flex centring — block-level `flex` keeps both. * docs: use the Raystack mark on Chronicle's own site Chronicle is a Raystack project, so its docs should carry the Raystack mark — but through `chronicle.yaml` like any other site would, not baked into a theme. The themes stay neutral and fall back to the site's initial. The brand file draws a flat navy through a single-stop gradient, which reads as near-black on a dark page. Flattened to a plain fill, with a light variant for `logo.dark`. Same path data either way. This also becomes the favicon and the mark on social cards, which the config now covers. * docs: draw the mark in greyscale Navy pulled the eye to the one thing on the page that is not type. Greyscale sits with the rest of the sidebar instead. Matched to the site's own ink rather than picked by eye: `--rs-color-foreground-base-primary` is oklch(0.2435 0 0) in light and oklch(0.9491 0 0) in dark, which are #202020 and #EEEEEE.
feat: let a page state its own identifiers, and prefer short labels i… …n the trail The fanfold header was designed to carry what a page is: the standard it implements, the package, the command. The theme cannot know any of that, so it was filling the two lines under the trail with the only facts it had — the site title, the section, and the URL. A page can now name them itself. `identifiers` is a list of lines printed verbatim under the trail, and a page without it keeps the lines the theme can work out on its own, so nothing else changes. Frontmatter reaches a theme through one allowlist in `extractFrontmatter`, so that is the only place a new field can enter — and the only edit needed here beyond the theme. Non-string entries are dropped rather than rendered: this comes straight from a content file, where one mistyped entry should not arrive as `[object Object]`. Named `identifiers` rather than `meta` because `meta.json` already means a directory's metadata and the two would read as the same thing. The trail now prefers a `short` where the tree carries one, so it reads "… / SPP" rather than "… / Space Packet Protocol". `getBreadcrumbItems` is fumadocs' own and returns names only, so the lookup is built here from the `short` that `attachShortNames` puts on every page node and each folder index. Kept local to this theme: the other two render their breadcrumbs from the shared helper and never use `short`.
feat: add the fanfold theme, and a `short` frontmatter field(#181) Fanfold is a third built-in theme: continuous-form line printer paper, with tractor-feed strips down both edges, faint zebra banding behind the sheet and monospace type throughout. It was translated from a design file, so its colours and metrics come from there rather than from a style guide. Two changes reach outside the theme. `short` frontmatter gives navigation a shorter label than the page title: title: Space Packet Protocol short: SPP The rail shows `SPP` and keeps the full title on the link's tooltip; headings, breadcrumbs, the browser tab and search all keep using `title`. `source.ts` copies it onto the page tree and `tree-utils.ts` exposes a `shortName` reader, so any theme can pick it up — today only fanfold does, since only its rail is narrow enough to need it. Note `short` had to be added to `KEEP_FIELDS`: that allowlist strips unknown fields when the tree is serialised for the client, so without it the value never reached the browser. Themes may now supply their own landing page through an optional `Landing` slot. The shared `LandingPage` still resolves config, `<Head>` tags and the version label, so a theme's `Landing` is presentation only. Themes that leave it out keep the existing layout. `LandingEntry` moved to `types/content.ts` so `types/theme.ts` can name it without closing an import cycle. Smaller shared changes: - `useSearch` is exported, so a theme can build its own search trigger instead of the stock icon button. - Departure Mono is declared once in `themes/fonts/` and shared by the paper and fanfold themes. Both previously declared the same family from their own copy of a byte-identical file, so the build shipped 22KB twice and the two `@font-face` rules collided on family name. - Fanfold requests its web fonts from a `<link>` rather than a stylesheet `@import`. `registry.ts` imports every theme statically, so an `@import` was hoisted into the one bundled stylesheet and every site fetched Doto and Geist Mono — including sites running a different theme that never renders them.
feat: add configurable links to the sidebar footer (#176) * feat: add configurable links to the sidebar footer Adds a top-level `links:` key to chronicle.yaml, surfaced from the sidebar footer in both the default and paper themes. On desktop the links sit behind a `?` icon button that opens a menu upward, matching the design. In the default theme's mobile hamburger menu they render inline as nav items instead, above the version switcher — no dropdown. Each destination is tagged with a `ref` query param carrying the full URL of the page the link was clicked from. External links open with `noopener` (not `noreferrer`) so the destination also receives a Referer header; under the default referrer policy that header carries only the origin, so `ref` is what identifies the specific page. Existing query strings on the href are preserved, and non-web schemes such as mailto: are left untouched. The sidebar footer previously rendered only when versions were configured; it now renders when either versions or links are present. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: show latest.label as a static footer label when unversioned `latest` is valid without `versions` — the schema only requires it in the other direction — but both VersionSwitchers returned null whenever no versions were configured, so a configured `latest.label` was silently dropped. It now renders as static text in the sidebar footer. Deliberately not a dropdown: with nothing to switch to, a menu holding a single option would imply a navigation that does not exist. The footer visibility check now also accounts for a latest-only config. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: add a mobile layout to the paper theme (#178) The paper theme had no small-screen branch at all — none of its four CSS modules contained a single media query. At 390px it rendered the full desktop layout: the 262px sidebar ate two thirds of the viewport, and `.content`'s `margin-left: calc(-1 * var(--paper-sidebar-width))` dragged the article underneath it, clipping headings to "e (3.0)", "st" and "ed". There was no header, no hamburger and no way to reach the navigation. Add a mobile layout mirroring the default theme's, at the same 768px breakpoint: - a sticky mobile header carrying the existing SidebarHeader, the theme switcher and a hamburger toggle - a full-screen menu holding the chapter nav, a divider, the configured links as inline items (SidebarLinks variant='list') and the version switcher - the desktop sidebar hidden and `.content`'s negative offset neutralised, so the article is full-width and unclipped - the menu closes on navigation The reading-progress rail is a fixed 200px column pinned to the right edge, so it is dropped below the breakpoint rather than left overlapping the article, and the page navbar parks below the header instead of colliding with it. Two narrow-viewport defects surfaced once the content became full-width: a long breadcrumb could not shrink past its intrinsic width and stretched the page 7px wider than the viewport, and a long site title wrapped out of the fixed-height header. Both are corrected inside the media query. The mobile chrome only renders when the sidebar does, so it composes with reader mode rather than fighting it. Desktop rendering is unchanged — screenshots at 1440x900 before and after are byte-identical. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat: add author pages listing everything an author wrote (#172) * feat: add author pages listing everything an author wrote Adds an optional `authors` registry to chronicle.yaml holding each author's name, bio, avatar, url, and email. A frontmatter string that matches a registry key picks up that profile; anything else still reads as a plain name, so occasional contributors need no config. /authors lists everyone found in the content, and /authors/<slug> shows one author's profile followed by their pages grouped by content dir. The index is served from /api/authors, written to /data/authors.json for static builds, and embedded in the SSR payload so both routes render without JavaScript. A content dir named `authors` keeps its own pages. Bylines now use the registry avatar and profile link when present. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: add breathing room above author page content Also gives the paper-theme example an authors registry entry, so both themes demonstrate a full profile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: give author pages the paper theme's sheet and fonts Author pages bypass each theme's Page component, so in paper they sat flat on the neutral backdrop while every other page rendered on a sheet. Tag the page root with the active theme and, for paper, apply the same sheet treatment its content uses — base background, side borders, soft shadow — with the theme's Hanuman body face and Departure Mono for the meta text. The profile header now stacks as avatar and name on one line, the bio below it, then the links. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: put the paper byline in the meta line beside reading time Paper stacked the avatar byline under the page description. Move the authors up into the meta line as `1min Read | JANE DOE`, inheriting that line's mono face and tertiary color, with round avatars. Bylines now show two authors at most and collapse the rest into a `+N` counter that names them on hover. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: link byline names to the author's page The byline linked out to an author's `url`, or their email, which sent readers off-site while the author page it should reach went unlinked. Names now always go to `/authors/<slug>`, keeping the version prefix and falling back to plain text when a content dir owns that segment. An author's url and email stay on their page. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: hide the AI menu on author pages Author pages aren't MDX, so `/authors/jane.md` 404s and every item in the menu — copy, view, and both hand-offs — fails silently. Hide it there. Adds an isAuthorRoute predicate and uses it at the four places that were each spelling the check out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix: render unknown code fence languages as plain text (#169) Code fences using a language Shiki does not bundle threw at build time, for example "Language `logql` not found, you may need to load it first". Shiki's rehype integration only loads a language when it is in the bundle, and otherwise falls through to `fallbackLanguage`. That option was unset, so the unknown language reached `codeToHast` and threw. Set it to `text` so those blocks render unhighlighted instead of failing the page. Fumadocs' defaults are spread because the config type is not `Partial`; the highlighter factory merges them anyway, so themes, notation transformers and meta parsing are unaffected. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix: keep every frame when optimizing animated images (#168) sharp decodes only the first frame unless constructed with `{ animated: true }`, so animated GIFs were served as still images by both image pipelines — the /api/image handler (dev and server builds) and the static build's .webp generation, which MDXImage always prefers via <picture><source>. Detect multi-frame sources with a header-only metadata probe, gated on the two formats that can animate, and pass the flag through to sharp. AVIF output is skipped for animated input because sharp flattens it; those requests get animated WebP instead, which every AVIF-capable browser decodes. The flag is folded into the cache key and ETag only when set, so still-image keys stay stable while stale single-frame entries are busted. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
chore: bump bun, typescript 6, and update dependencies (#167) * chore: bump bun to 1.3.14 in CI and typescript to 6.0.3 TypeScript 6 removes the baseUrl option; drop it from tsconfig — the existing paths mapping is already relative to the tsconfig directory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: update dependencies to latest in-range versions bun update across the workspace plus two capped bumps: fumadocs-core 16.8.1 → 16.12.1 and @tanstack/react-query 5.100.10 → 5.101.4. Notable: react/react-dom 19.2.8, react-router 7.18.1, mermaid 11.16, zod 4.4.3, biome 2.5.5. nitro and vite were already at latest; major bumps (react-router 8, apsara 1.2, fumadocs-mdx 15, commander 15) deferred. - Restore @base-ui/react <1.6.0 upper bound that bun update rewrote - Drop stale nested @codemirror/state@6.5.4 lockfile resolutions so a single 6.7.1 instance is installed (duplicates break CodeMirror) - Widen two test casts for fumadocs-core 16.12 narrowed Folder type Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
PreviousNext