@raystack/chronicle 0.1.0-canary.ac60f9f → 0.1.0-canary.bd7f9af

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -16,15 +16,43 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
18
18
 
19
+ // src/lib/remark-unused-directives.ts
20
+ import { visit } from "unist-util-visit";
21
+ var remarkUnusedDirectives = () => {
22
+ return (tree) => {
23
+ visit(tree, ["textDirective"], (node) => {
24
+ const directive = node;
25
+ if (!directive.data) {
26
+ const hasAttributes = directive.attributes && Object.keys(directive.attributes).length > 0;
27
+ const hasChildren = directive.children && directive.children.length > 0;
28
+ if (!hasAttributes && !hasChildren) {
29
+ const name = directive.name;
30
+ if (!name)
31
+ return;
32
+ Object.keys(directive).forEach((key) => delete directive[key]);
33
+ directive.type = "text";
34
+ directive.value = `:${name}`;
35
+ }
36
+ }
37
+ });
38
+ };
39
+ }, remark_unused_directives_default;
40
+ var init_remark_unused_directives = __esm(() => {
41
+ remark_unused_directives_default = remarkUnusedDirectives;
42
+ });
43
+
19
44
  // src/server/vite-config.ts
20
45
  var exports_vite_config = {};
21
46
  __export(exports_vite_config, {
22
47
  createViteConfig: () => createViteConfig
23
48
  });
24
49
  import react from "@vitejs/plugin-react";
50
+ import { remarkDirectiveAdmonition, remarkMdxMermaid } from "fumadocs-core/mdx-plugins";
51
+ import { defineConfig as defineFumadocsConfig } from "fumadocs-mdx/config";
25
52
  import mdx from "fumadocs-mdx/vite";
26
53
  import { nitro } from "nitro/vite";
27
54
  import path4 from "node:path";
55
+ import remarkDirective from "remark-directive";
28
56
  async function createViteConfig(options) {
29
57
  const { packageRoot, projectRoot, contentDir, preset } = options;
30
58
  return {
@@ -35,13 +63,39 @@ async function createViteConfig(options) {
35
63
  serverDir: path4.resolve(packageRoot, "src/server"),
36
64
  ...preset && { preset }
37
65
  }),
38
- mdx({}, { index: false }),
66
+ mdx({
67
+ default: defineFumadocsConfig({
68
+ mdxOptions: {
69
+ remarkPlugins: [
70
+ remarkDirective,
71
+ [remarkDirectiveAdmonition, {
72
+ tags: {
73
+ CalloutContainer: "Callout",
74
+ CalloutTitle: "CalloutTitle",
75
+ CalloutDescription: "CalloutDescription"
76
+ },
77
+ types: {
78
+ note: "accent",
79
+ tip: "accent",
80
+ info: "accent",
81
+ warn: "attention",
82
+ warning: "attention",
83
+ danger: "alert",
84
+ caution: "alert",
85
+ success: "success"
86
+ }
87
+ }],
88
+ remark_unused_directives_default,
89
+ remarkMdxMermaid
90
+ ]
91
+ }
92
+ })
93
+ }, { index: false }),
39
94
  react()
40
95
  ],
41
96
  resolve: {
42
97
  alias: {
43
- "@": path4.resolve(packageRoot, "src"),
44
- "@content": path4.resolve(packageRoot, ".content")
98
+ "@": path4.resolve(packageRoot, "src")
45
99
  },
46
100
  conditions: ["module-sync", "import", "node"],
47
101
  dedupe: [
@@ -59,8 +113,7 @@ async function createViteConfig(options) {
59
113
  },
60
114
  define: {
61
115
  __CHRONICLE_CONTENT_DIR__: JSON.stringify(contentDir),
62
- __CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot),
63
- __CHRONICLE_PACKAGE_ROOT__: JSON.stringify(packageRoot)
116
+ __CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot)
64
117
  },
65
118
  css: {
66
119
  modules: {
@@ -81,7 +134,9 @@ async function createViteConfig(options) {
81
134
  }
82
135
  };
83
136
  }
84
- var init_vite_config = () => {};
137
+ var init_vite_config = __esm(() => {
138
+ init_remark_unused_directives();
139
+ });
85
140
 
86
141
  // src/cli/index.ts
87
142
  import { Command as Command6 } from "commander";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raystack/chronicle",
3
- "version": "0.1.0-canary.ac60f9f",
3
+ "version": "0.1.0-canary.bd7f9af",
4
4
  "description": "Config-driven documentation framework",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
- import type { ComponentProps } from 'react'
3
+ import { type ComponentProps, isValidElement, Children } from 'react'
4
+ import { Mermaid } from './mermaid'
4
5
  import styles from './code.module.css'
5
6
 
6
7
  type PreProps = ComponentProps<'pre'> & {
@@ -16,6 +17,14 @@ export function MdxCode({ children, className, ...props }: ComponentProps<'code'
16
17
  }
17
18
 
18
19
  export function MdxPre({ children, title, className, ...props }: PreProps) {
20
+ // Detect mermaid code blocks
21
+ if (isValidElement(children)) {
22
+ const childProps = children.props as { className?: string; children?: string }
23
+ if (childProps.className?.includes('language-mermaid') && typeof childProps.children === 'string') {
24
+ return <Mermaid chart={childProps.children} />
25
+ }
26
+ }
27
+
19
28
  return (
20
29
  <div className={styles.codeBlock}>
21
30
  {title && <div className={styles.codeHeader}>{title}</div>}
@@ -1,35 +1,10 @@
1
1
  .details {
2
- border: 1px solid var(--rs-color-border-base-primary);
3
- border-radius: var(--rs-radius-2);
4
2
  margin: var(--rs-space-5) 0;
5
3
  }
6
4
 
7
- .summary {
8
- padding: var(--rs-space-4) var(--rs-space-5);
9
- cursor: pointer;
5
+ .trigger {
10
6
  font-weight: 500;
11
7
  font-size: var(--rs-font-size-small);
12
- color: var(--rs-color-text-base-primary);
13
- background: var(--rs-color-background-base-secondary);
14
- list-style: none;
15
- display: flex;
16
- align-items: center;
17
- gap: var(--rs-space-3);
18
- }
19
-
20
- .summary::-webkit-details-marker {
21
- display: none;
22
- }
23
-
24
- .summary::before {
25
- content: '▶';
26
- font-size: 10px;
27
- transition: transform 0.2s ease;
28
- color: var(--rs-color-text-base-secondary);
29
- }
30
-
31
- .details[open] > .summary::before {
32
- transform: rotate(90deg);
33
8
  }
34
9
 
35
10
  .content {
@@ -1,9 +1,8 @@
1
1
  import type { ComponentProps } from 'react'
2
- import styles from './details.module.css'
3
2
 
4
3
  export function MdxDetails({ children, className, ...props }: ComponentProps<'details'>) {
5
4
  return (
6
- <details className={`${styles.details} ${className ?? ''}`} {...props}>
5
+ <details className={className} {...props}>
7
6
  {children}
8
7
  </details>
9
8
  )
@@ -11,7 +10,7 @@ export function MdxDetails({ children, className, ...props }: ComponentProps<'de
11
10
 
12
11
  export function MdxSummary({ children, className, ...props }: ComponentProps<'summary'>) {
13
12
  return (
14
- <summary className={`${styles.summary} ${className ?? ''}`} {...props}>
13
+ <summary className={className} {...props}>
15
14
  {children}
16
15
  </summary>
17
16
  )
@@ -1,53 +1,19 @@
1
1
  'use client'
2
2
 
3
3
  import { Breadcrumb } from '@raystack/apsara'
4
- import type { PageTree, PageTreeItem } from '@/types'
4
+ import { getBreadcrumbItems } from 'fumadocs-core/breadcrumb'
5
+ import type { Root } from 'fumadocs-core/page-tree'
5
6
 
6
7
  interface BreadcrumbsProps {
7
8
  slug: string[]
8
- tree: PageTree
9
- }
10
-
11
- function findInTree(items: PageTreeItem[], targetPath: string): PageTreeItem | undefined {
12
- for (const item of items) {
13
- const itemUrl = item.url || `/${item.name.toLowerCase().replace(/\s+/g, '-')}`
14
- if (itemUrl === targetPath || itemUrl === `/${targetPath}`) {
15
- return item
16
- }
17
- if (item.children) {
18
- const found = findInTree(item.children, targetPath)
19
- if (found) return found
20
- }
21
- }
22
- return undefined
23
- }
24
-
25
- function getFirstPageUrl(item: PageTreeItem): string | undefined {
26
- if (item.type === 'page' && item.url) {
27
- return item.url
28
- }
29
- if (item.children) {
30
- for (const child of item.children) {
31
- const url = getFirstPageUrl(child)
32
- if (url) return url
33
- }
34
- }
35
- return undefined
9
+ tree: Root
36
10
  }
37
11
 
38
12
  export function Breadcrumbs({ slug, tree }: BreadcrumbsProps) {
39
- const items: { label: string; href: string }[] = []
13
+ const url = slug.length === 0 ? '/' : `/${slug.join('/')}`
14
+ const items = getBreadcrumbItems(url, tree, { includePage: true })
40
15
 
41
- for (let i = 0; i < slug.length; i++) {
42
- const currentPath = `/${slug.slice(0, i + 1).join('/')}`
43
- const node = findInTree(tree.children, currentPath)
44
- const href = node?.url || (node && getFirstPageUrl(node)) || currentPath
45
- const label = node?.name ?? slug[i]
46
- items.push({
47
- label: label.charAt(0).toUpperCase() + label.slice(1),
48
- href,
49
- })
50
- }
16
+ if (items.length === 0) return null
51
17
 
52
18
  return (
53
19
  <Breadcrumb size="small">
@@ -55,10 +21,10 @@ export function Breadcrumbs({ slug, tree }: BreadcrumbsProps) {
55
21
  const breadcrumbItem = (
56
22
  <Breadcrumb.Item
57
23
  key={`item-${index}`}
58
- href={item.href}
24
+ href={item.url}
59
25
  current={index === items.length - 1}
60
26
  >
61
- {item.label}
27
+ {item.name}
62
28
  </Breadcrumb.Item>
63
29
  )
64
30
  if (index === 0) return [breadcrumbItem]
@@ -1,6 +1,6 @@
1
1
  import type { OpenAPIV3 } from 'openapi-types'
2
2
  import slugify from 'slugify'
3
- import type { PageTree, PageTreeItem } from '@/types/content'
3
+ import type { Root, Node, Item, Folder } from 'fumadocs-core/page-tree'
4
4
  import type { ApiSpec } from './openapi'
5
5
 
6
6
  export function getSpecSlug(spec: ApiSpec): string {
@@ -56,16 +56,15 @@ export function findApiOperation(specs: ApiSpec[], slug: string[]): ApiRouteMatc
56
56
  return null
57
57
  }
58
58
 
59
- export function buildApiPageTree(specs: ApiSpec[]): PageTree {
60
- const children: PageTreeItem[] = []
59
+ export function buildApiPageTree(specs: ApiSpec[]): Root {
60
+ const children: Node[] = []
61
61
 
62
62
  for (const spec of specs) {
63
63
  const specSlug = getSpecSlug(spec)
64
64
  const paths = spec.document.paths ?? {}
65
65
  const tags = spec.document.tags ?? []
66
66
 
67
- // Group operations by tag (case-insensitive to avoid duplicates)
68
- const opsByTag = new Map<string, PageTreeItem[]>()
67
+ const opsByTag = new Map<string, Item[]>()
69
68
  const tagDisplayName = new Map<string, string>()
70
69
 
71
70
  for (const [, pathItem] of Object.entries(paths)) {
@@ -90,7 +89,6 @@ export function buildApiPageTree(specs: ApiSpec[]): PageTree {
90
89
  }
91
90
  }
92
91
 
93
- // Use doc.tags display names where available
94
92
  for (const t of tags) {
95
93
  const key = t.name.toLowerCase()
96
94
  if (opsByTag.has(key)) {
@@ -98,7 +96,7 @@ export function buildApiPageTree(specs: ApiSpec[]): PageTree {
98
96
  }
99
97
  }
100
98
 
101
- const tagFolders: PageTreeItem[] = Array.from(opsByTag.entries()).map(([key, ops]) => ({
99
+ const tagFolders: Folder[] = Array.from(opsByTag.entries()).map(([key, ops]) => ({
102
100
  type: 'folder' as const,
103
101
  name: tagDisplayName.get(key) ?? key,
104
102
  icon: 'rectangle-stack',
@@ -110,7 +108,7 @@ export function buildApiPageTree(specs: ApiSpec[]): PageTree {
110
108
  type: 'folder',
111
109
  name: spec.name,
112
110
  children: tagFolders,
113
- })
111
+ } as Folder)
114
112
  } else {
115
113
  children.push(...tagFolders)
116
114
  }
@@ -8,7 +8,7 @@ import React, {
8
8
  import { useLocation } from 'react-router';
9
9
  import { mdxComponents } from '@/components/mdx';
10
10
  import type { ApiSpec } from '@/lib/openapi';
11
- import type { ChronicleConfig, Frontmatter, PageTree } from '@/types';
11
+ import type { ChronicleConfig, Frontmatter, Root } from '@/types';
12
12
 
13
13
  interface PageData {
14
14
  slug: string[];
@@ -18,7 +18,7 @@ interface PageData {
18
18
 
19
19
  interface PageContextValue {
20
20
  config: ChronicleConfig;
21
- tree: PageTree;
21
+ tree: Root;
22
22
  page: PageData | null;
23
23
  apiSpecs: ApiSpec[];
24
24
  }
@@ -31,7 +31,7 @@ export function usePageContext(): PageContextValue {
31
31
  console.error('usePageContext: no context found!');
32
32
  return {
33
33
  config: { title: 'Documentation' },
34
- tree: { name: 'root', children: [] },
34
+ tree: { name: 'root', children: [] } as Root,
35
35
  page: null,
36
36
  apiSpecs: []
37
37
  };
@@ -41,14 +41,24 @@ export function usePageContext(): PageContextValue {
41
41
 
42
42
  interface PageProviderProps {
43
43
  initialConfig: ChronicleConfig;
44
- initialTree: PageTree;
44
+ initialTree: Root;
45
45
  initialPage: PageData | null;
46
46
  initialApiSpecs: ApiSpec[];
47
47
  children: ReactNode;
48
48
  }
49
49
 
50
+ const contentModules = import.meta.glob<{ default?: React.ComponentType<any> }>(
51
+ '../../.content/**/*.{mdx,md}'
52
+ );
53
+
50
54
  async function loadMdxComponent(relativePath: string): Promise<ReactNode> {
51
- const mod = await import(/* @vite-ignore */ `/.content/${relativePath}`);
55
+ const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
56
+ const key = relativePath.endsWith('.md')
57
+ ? `../../.content/${withoutExt}.md`
58
+ : `../../.content/${withoutExt}.mdx`;
59
+ const loader = contentModules[key];
60
+ if (!loader) return null;
61
+ const mod = await loader();
52
62
  return mod.default
53
63
  ? React.createElement(mod.default, { components: mdxComponents })
54
64
  : null;
@@ -62,7 +72,7 @@ export function PageProvider({
62
72
  children
63
73
  }: PageProviderProps) {
64
74
  const { pathname } = useLocation();
65
- const [tree] = useState<PageTree>(initialTree);
75
+ const [tree] = useState<Root>(initialTree);
66
76
  const [page, setPage] = useState<PageData | null>(initialPage);
67
77
  const [apiSpecs, setApiSpecs] = useState<ApiSpec[]>(initialApiSpecs);
68
78
  const [currentPath, setCurrentPath] = useState(pathname);
package/src/lib/source.ts CHANGED
@@ -1,16 +1,9 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { loader } from 'fumadocs-core/source';
4
+ import type { Root, Node, Folder } from 'fumadocs-core/page-tree';
4
5
  import matter from 'gray-matter';
5
6
  import type { MDXContent } from 'mdx/types';
6
- import type { Frontmatter, PageTree, PageTreeItem } from '@/types';
7
-
8
- export interface SourcePage {
9
- url: string;
10
- slugs: string[];
11
- filePath: string;
12
- frontmatter: Frontmatter;
13
- }
14
7
 
15
8
  function getContentDir(): string {
16
9
  return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
@@ -43,7 +36,7 @@ async function scanFiles(contentDir: string) {
43
36
  files.push({
44
37
  type: 'page',
45
38
  path: relativePath,
46
- data: { ...data, _absolutePath: fullPath }
39
+ data: { ...data, _relativePath: relativePath }
47
40
  });
48
41
  } else if (entry.name === 'meta.json' || entry.name === 'meta.yaml') {
49
42
  const raw = await fs.readFile(fullPath, 'utf-8');
@@ -63,7 +56,6 @@ async function scanFiles(contentDir: string) {
63
56
  }
64
57
 
65
58
  let cachedSource: ReturnType<typeof loader> | null = null;
66
- let cachedPages: SourcePage[] | null = null;
67
59
 
68
60
  async function getSource() {
69
61
  if (cachedSource) return cachedSource;
@@ -76,111 +68,72 @@ async function getSource() {
76
68
  return cachedSource;
77
69
  }
78
70
 
71
+ export { getSource as source };
72
+
79
73
  export function invalidate() {
80
74
  cachedSource = null;
81
- cachedPages = null;
82
75
  }
83
76
 
84
- export async function getPages(): Promise<SourcePage[]> {
85
- if (cachedPages) return cachedPages;
77
+ function getOrder(node: Node, orderMap: Map<string, number>): number | undefined {
78
+ if (node.type === 'page') return orderMap.get(node.url);
79
+ if (node.type === 'folder' && node.index) return orderMap.get(node.index.url);
80
+ return undefined;
81
+ }
86
82
 
83
+ function sortNodes(nodes: Node[], orderMap: Map<string, number>): Node[] {
84
+ return [...nodes]
85
+ .map(n =>
86
+ n.type === 'folder'
87
+ ? ({ ...n, children: sortNodes(n.children, orderMap) } as Folder)
88
+ : n
89
+ )
90
+ .sort(
91
+ (a, b) =>
92
+ (getOrder(a, orderMap) ?? Number.MAX_SAFE_INTEGER) -
93
+ (getOrder(b, orderMap) ?? Number.MAX_SAFE_INTEGER)
94
+ );
95
+ }
96
+
97
+ function sortTreeByOrder(tree: Root, pages: { url: string; data: unknown }[]): Root {
98
+ const orderMap = new Map<string, number>();
99
+ for (const page of pages) {
100
+ const d = page.data as Record<string, unknown>;
101
+ const order = d.order as number | undefined;
102
+ if (order !== undefined) orderMap.set(page.url, order);
103
+ if (page.url === '/') orderMap.set('/', order ?? 0);
104
+ }
105
+ return { ...tree, children: sortNodes(tree.children, orderMap) };
106
+ }
107
+
108
+ export async function getPageTree(): Promise<Root> {
87
109
  const s = await getSource();
88
- cachedPages = s.getPages().map(page => {
89
- const data = page.data as Record<string, unknown>;
90
- return {
91
- url: page.url,
92
- slugs: page.slugs,
93
- filePath: (data._absolutePath as string) ?? '',
94
- frontmatter: {
95
- title:
96
- (data.title as string) ??
97
- page.slugs[page.slugs.length - 1] ??
98
- 'Untitled',
99
- description: data.description as string | undefined,
100
- order: data.order as number | undefined,
101
- icon: data.icon as string | undefined,
102
- lastModified: data.lastModified as string | undefined
103
- }
104
- };
105
- });
110
+ return sortTreeByOrder(s.pageTree as Root, s.getPages());
111
+ }
106
112
 
107
- return cachedPages;
113
+ export async function getPages() {
114
+ const s = await getSource();
115
+ return s.getPages();
108
116
  }
109
117
 
110
- export async function getPage(slug?: string[]): Promise<SourcePage | null> {
111
- const pages = await getPages();
112
- const targetUrl = !slug || slug.length === 0 ? '/' : `/${slug.join('/')}`;
113
- return pages.find(p => p.url === targetUrl) ?? null;
118
+ export async function getPage(slugs?: string[]) {
119
+ const s = await getSource();
120
+ return s.getPage(slugs);
114
121
  }
115
122
 
116
123
  export async function loadPageComponent(
117
- page: SourcePage
124
+ relativePath: string
118
125
  ): Promise<MDXContent | null> {
119
- if (!page.filePath) return null;
126
+ if (!relativePath) return null;
127
+ const contentDir = getContentDir();
128
+ const fullPath = path.join(contentDir, relativePath);
120
129
  try {
121
- await fs.access(page.filePath);
130
+ await fs.access(fullPath);
122
131
  } catch {
123
132
  return null;
124
133
  }
125
- const contentDir = getContentDir();
126
- const relativePath = path.relative(contentDir, page.filePath);
127
134
  const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
128
135
  const mod = relativePath.endsWith('.md')
129
- ? await import(`@content/${withoutExt}.md`)
130
- : await import(`@content/${withoutExt}.mdx`);
136
+ ? await import(`../../.content/${withoutExt}.md`)
137
+ : await import(`../../.content/${withoutExt}.mdx`);
131
138
  return mod.default;
132
139
  }
133
-
134
- export async function buildPageTree(): Promise<PageTree> {
135
- const s = await getSource();
136
- const pages = s.getPages();
137
- const folders = new Map<string, PageTreeItem[]>();
138
- const rootPages: PageTreeItem[] = [];
139
-
140
- for (const page of pages) {
141
- const data = page.data as Record<string, unknown>;
142
- const isIndex = page.url === '/';
143
- const item: PageTreeItem = {
144
- type: 'page',
145
- name: (data.title as string) ?? page.slugs.join('/') ?? 'Untitled',
146
- url: page.url,
147
- order: (data.order as number | undefined) ?? (isIndex ? 0 : undefined)
148
- };
149
-
150
- if (page.slugs.length > 1) {
151
- const folder = page.slugs[0];
152
- if (!folders.has(folder)) {
153
- folders.set(folder, []);
154
- }
155
- folders.get(folder)?.push(item);
156
- } else {
157
- rootPages.push(item);
158
- }
159
- }
160
-
161
- const sortByOrder = (items: PageTreeItem[]) =>
162
- items.sort(
163
- (a, b) =>
164
- (a.order ?? Number.MAX_SAFE_INTEGER) -
165
- (b.order ?? Number.MAX_SAFE_INTEGER)
166
- );
167
-
168
- const children: PageTreeItem[] = sortByOrder(rootPages);
169
-
170
- const folderItems: PageTreeItem[] = [];
171
- for (const [folder, items] of folders) {
172
- const sorted = sortByOrder(items);
173
- const indexPage = items.find(item => item.url === `/${folder}`);
174
- const folderOrder = indexPage?.order ?? sorted[0]?.order;
175
- folderItems.push({
176
- type: 'folder',
177
- name: `${folder.charAt(0).toUpperCase()}${folder.slice(1)}`,
178
- order: folderOrder,
179
- children: sorted
180
- });
181
- }
182
-
183
- children.push(...sortByOrder(folderItems));
184
-
185
- return { name: 'root', children };
186
- }
@@ -1,4 +1,3 @@
1
- import path from 'node:path';
2
1
  import { defineHandler, HTTPError } from 'nitro';
3
2
  import { getPage } from '@/lib/source';
4
3
 
@@ -11,8 +10,17 @@ export default defineHandler(async event => {
11
10
  throw new HTTPError({ status: 404, message: 'Page not found' });
12
11
  }
13
12
 
14
- const contentDir = __CHRONICLE_CONTENT_DIR__;
15
- const relativePath = path.relative(contentDir, page.filePath);
13
+ const data = page.data as Record<string, unknown>;
14
+ const relativePath = (data._relativePath as string) ?? '';
16
15
 
17
- return { frontmatter: page.frontmatter, relativePath };
16
+ return {
17
+ frontmatter: {
18
+ title: (data.title as string) ?? slug[slug.length - 1] ?? 'Untitled',
19
+ description: data.description as string | undefined,
20
+ order: data.order as number | undefined,
21
+ icon: data.icon as string | undefined,
22
+ lastModified: data.lastModified as string | undefined,
23
+ },
24
+ relativePath,
25
+ };
18
26
  });
@@ -2,16 +2,17 @@ import '@vitejs/plugin-react/preamble';
2
2
  import React from 'react';
3
3
  import { hydrateRoot } from 'react-dom/client';
4
4
  import { BrowserRouter } from 'react-router';
5
+ import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
5
6
  import { mdxComponents } from '@/components/mdx';
6
7
  import { PageProvider } from '@/lib/page-context';
7
- import type { ChronicleConfig, Frontmatter, PageTree } from '@/types';
8
+ import type { ChronicleConfig, Frontmatter, Root } from '@/types';
8
9
  import type { ApiSpec } from '@/lib/openapi';
9
10
  import type { ReactNode } from 'react';
10
11
  import { App } from './App';
11
12
 
12
13
  interface EmbeddedData {
13
14
  config: ChronicleConfig;
14
- tree: PageTree;
15
+ tree: Root;
15
16
  slug: string[];
16
17
  frontmatter: Frontmatter;
17
18
  relativePath: string;
@@ -26,7 +27,7 @@ async function hydrate() {
26
27
  const config: ChronicleConfig = embedded?.config ?? {
27
28
  title: 'Documentation'
28
29
  };
29
- const tree: PageTree = embedded?.tree ?? { name: 'root', children: [] };
30
+ const tree: Root = embedded?.tree ?? { name: 'root', children: [] };
30
31
  const isApiPage =
31
32
  window.location.pathname.startsWith('/apis') && !!config.api?.length;
32
33
  const apiSpecs: ApiSpec[] = isApiPage
@@ -42,14 +43,16 @@ async function hydrate() {
42
43
  hydrateRoot(
43
44
  document.getElementById('root') as HTMLElement,
44
45
  <BrowserRouter>
45
- <PageProvider
46
- initialConfig={config}
47
- initialTree={tree}
48
- initialPage={page}
49
- initialApiSpecs={apiSpecs}
50
- >
51
- <App />
52
- </PageProvider>
46
+ <ReactRouterProvider>
47
+ <PageProvider
48
+ initialConfig={config}
49
+ initialTree={tree}
50
+ initialPage={page}
51
+ initialApiSpecs={apiSpecs}
52
+ >
53
+ <App />
54
+ </PageProvider>
55
+ </ReactRouterProvider>
53
56
  </BrowserRouter>
54
57
  );
55
58
  } catch (err) {
@@ -57,11 +60,20 @@ async function hydrate() {
57
60
  }
58
61
  }
59
62
 
63
+ const contentModules = import.meta.glob<{ default?: React.ComponentType<any> }>(
64
+ '../../.content/**/*.{mdx,md}'
65
+ );
66
+
60
67
  async function loadPage(
61
68
  embedded: EmbeddedData
62
69
  ): Promise<{ slug: string[]; frontmatter: Frontmatter; content: ReactNode }> {
63
- const mod = await import(/* @vite-ignore */ `/.content/${embedded.relativePath}`);
64
- const content = mod.default
70
+ const withoutExt = embedded.relativePath.replace(/\.(mdx|md)$/, '');
71
+ const key = embedded.relativePath.endsWith('.md')
72
+ ? `../../.content/${withoutExt}.md`
73
+ : `../../.content/${withoutExt}.mdx`;
74
+ const loader = contentModules[key];
75
+ const mod = loader ? await loader() : null;
76
+ const content = mod?.default
65
77
  ? React.createElement(mod.default, { components: mdxComponents })
66
78
  : null;
67
79
  return { slug: embedded.slug, frontmatter: embedded.frontmatter, content };
@@ -1,14 +1,14 @@
1
1
  import '@raystack/apsara/normalize.css';
2
2
  import '@raystack/apsara/style.css';
3
- import path from 'node:path';
4
3
  import React from 'react';
5
4
  import { renderToReadableStream } from 'react-dom/server.edge';
6
5
  import { StaticRouter } from 'react-router';
6
+ import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
7
7
  import { mdxComponents } from '@/components/mdx';
8
8
  import { loadConfig } from '@/lib/config';
9
9
  import { loadApiSpecs } from '@/lib/openapi';
10
10
  import { PageProvider } from '@/lib/page-context';
11
- import { buildPageTree, getPage, loadPageComponent } from '@/lib/source';
11
+ import { getPageTree, getPage, loadPageComponent } from '@/lib/source';
12
12
  import { App } from './App';
13
13
 
14
14
  // @ts-expect-error virtual import from Nitro
@@ -27,25 +27,32 @@ export default {
27
27
  ? await loadApiSpecs(config.api).catch(() => [])
28
28
  : [];
29
29
 
30
- const [tree, sourcePage] = await Promise.all([
31
- buildPageTree(),
30
+ const [tree, page] = await Promise.all([
31
+ getPageTree(),
32
32
  getPage(slug),
33
33
  ]);
34
34
 
35
- const pageData = sourcePage
35
+ const data = page?.data as Record<string, unknown> | undefined;
36
+ const relativePath = (data?._relativePath as string) ?? null;
37
+
38
+ const pageData = page
36
39
  ? {
37
40
  slug,
38
- frontmatter: sourcePage.frontmatter,
39
- content: await loadPageComponent(sourcePage).then(component =>
40
- component ? React.createElement(component, { components: mdxComponents }) : null
41
- ),
41
+ frontmatter: {
42
+ title: (data?.title as string) ?? slug[slug.length - 1] ?? 'Untitled',
43
+ description: data?.description as string | undefined,
44
+ order: data?.order as number | undefined,
45
+ icon: data?.icon as string | undefined,
46
+ lastModified: data?.lastModified as string | undefined,
47
+ },
48
+ content: relativePath
49
+ ? await loadPageComponent(relativePath).then(component =>
50
+ component ? React.createElement(component, { components: mdxComponents }) : null
51
+ )
52
+ : null,
42
53
  }
43
54
  : null;
44
55
 
45
- const relativePath = sourcePage
46
- ? path.relative(__CHRONICLE_CONTENT_DIR__, sourcePage.filePath)
47
- : null;
48
-
49
56
  const embeddedData = {
50
57
  config,
51
58
  tree,
@@ -74,14 +81,16 @@ export default {
74
81
  <body>
75
82
  <div id="root">
76
83
  <StaticRouter location={pathname}>
77
- <PageProvider
78
- initialConfig={config}
79
- initialTree={tree}
80
- initialPage={pageData}
81
- initialApiSpecs={apiSpecs}
82
- >
83
- <App />
84
- </PageProvider>
84
+ <ReactRouterProvider>
85
+ <PageProvider
86
+ initialConfig={config}
87
+ initialTree={tree}
88
+ initialPage={pageData}
89
+ initialApiSpecs={apiSpecs}
90
+ >
91
+ <App />
92
+ </PageProvider>
93
+ </ReactRouterProvider>
85
94
  </StaticRouter>
86
95
  </div>
87
96
  </body>
@@ -16,8 +16,9 @@ export default defineHandler(async event => {
16
16
 
17
17
  const pages = await getPages();
18
18
  const docPages = pages.map(page => {
19
- const lastmod = page.frontmatter.lastModified
20
- ? `<lastmod>${new Date(page.frontmatter.lastModified).toISOString()}</lastmod>`
19
+ const data = page.data as Record<string, unknown>;
20
+ const lastmod = data.lastModified
21
+ ? `<lastmod>${new Date(data.lastModified as string).toISOString()}</lastmod>`
21
22
  : '';
22
23
  return `<url><loc>${baseUrl}/${page.slugs.join('/')}</loc>${lastmod}</url>`;
23
24
  });
@@ -1,8 +1,12 @@
1
1
  import react from '@vitejs/plugin-react';
2
+ import { remarkDirectiveAdmonition, remarkMdxMermaid } from 'fumadocs-core/mdx-plugins';
3
+ import { defineConfig as defineFumadocsConfig } from 'fumadocs-mdx/config';
2
4
  import mdx from 'fumadocs-mdx/vite';
3
5
  import { nitro } from 'nitro/vite';
4
6
  import path from 'node:path';
7
+ import remarkDirective from 'remark-directive';
5
8
  import { type InlineConfig } from 'vite';
9
+ import remarkUnusedDirectives from '../lib/remark-unused-directives';
6
10
 
7
11
  export interface ViteConfigOptions {
8
12
  packageRoot: string;
@@ -24,13 +28,39 @@ export async function createViteConfig(
24
28
  serverDir: path.resolve(packageRoot, 'src/server'),
25
29
  ...(preset && { preset }),
26
30
  }),
27
- mdx({}, { index: false }),
31
+ mdx({
32
+ default: defineFumadocsConfig({
33
+ mdxOptions: {
34
+ remarkPlugins: [
35
+ remarkDirective,
36
+ [remarkDirectiveAdmonition, {
37
+ tags: {
38
+ CalloutContainer: 'Callout',
39
+ CalloutTitle: 'CalloutTitle',
40
+ CalloutDescription: 'CalloutDescription',
41
+ },
42
+ types: {
43
+ note: 'accent',
44
+ tip: 'accent',
45
+ info: 'accent',
46
+ warn: 'attention',
47
+ warning: 'attention',
48
+ danger: 'alert',
49
+ caution: 'alert',
50
+ success: 'success',
51
+ },
52
+ }],
53
+ remarkUnusedDirectives,
54
+ remarkMdxMermaid,
55
+ ],
56
+ },
57
+ }),
58
+ }, { index: false }),
28
59
  react()
29
60
  ],
30
61
  resolve: {
31
62
  alias: {
32
63
  '@': path.resolve(packageRoot, 'src'),
33
- '@content': path.resolve(packageRoot, '.content'),
34
64
  },
35
65
  conditions: ['module-sync', 'import', 'node'],
36
66
  dedupe: [
@@ -48,8 +78,7 @@ export async function createViteConfig(
48
78
  },
49
79
  define: {
50
80
  __CHRONICLE_CONTENT_DIR__: JSON.stringify(contentDir),
51
- __CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot),
52
- __CHRONICLE_PACKAGE_ROOT__: JSON.stringify(packageRoot)
81
+ __CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot)
53
82
  },
54
83
  css: {
55
84
  modules: {
@@ -14,7 +14,8 @@ import { MethodBadge } from '@/components/api/method-badge';
14
14
  import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher';
15
15
  import { Footer } from '@/components/ui/footer';
16
16
  import { Search } from '@/components/ui/search';
17
- import type { PageTreeItem, ThemeLayoutProps } from '@/types';
17
+ import type { Node } from 'fumadocs-core/page-tree';
18
+ import type { ThemeLayoutProps } from '@/types';
18
19
  import styles from './Layout.module.css';
19
20
 
20
21
  const iconMap: Record<string, React.ReactNode> = {
@@ -96,9 +97,9 @@ export function Layout({
96
97
  className={cx(styles.sidebar, classNames?.sidebar)}
97
98
  >
98
99
  <Sidebar.Main ref={scrollRef}>
99
- {tree.children.map(item => (
100
+ {tree.children.map((item, i) => (
100
101
  <SidebarNode
101
- key={item.url ?? item.name}
102
+ key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
102
103
  item={item}
103
104
  pathname={pathname}
104
105
  />
@@ -118,23 +119,24 @@ function SidebarNode({
118
119
  item,
119
120
  pathname
120
121
  }: {
121
- item: PageTreeItem;
122
+ item: Node;
122
123
  pathname: string;
123
124
  }) {
124
125
  if (item.type === 'separator') {
125
126
  return null;
126
127
  }
127
128
 
128
- if (item.type === 'folder' && item.children) {
129
+ if (item.type === 'folder') {
130
+ const icon = typeof item.icon === 'string' ? iconMap[item.icon] : item.icon;
129
131
  return (
130
132
  <Sidebar.Group
131
- label={item.name}
132
- leadingIcon={item.icon ? iconMap[item.icon] : undefined}
133
+ label={item.name?.toString() ?? ''}
134
+ leadingIcon={icon ?? undefined}
133
135
  classNames={{ items: styles.groupItems }}
134
136
  >
135
- {item.children.map(child => (
137
+ {item.children.map((child, i) => (
136
138
  <SidebarNode
137
- key={child.url ?? child.name}
139
+ key={child.type === 'page' ? child.url : (child.name?.toString() ?? i)}
138
140
  item={child}
139
141
  pathname={pathname}
140
142
  />
@@ -145,13 +147,14 @@ function SidebarNode({
145
147
 
146
148
  const isActive = pathname === item.url;
147
149
  const href = item.url ?? '#';
150
+ const icon = typeof item.icon === 'string' ? iconMap[item.icon] : item.icon;
148
151
  const link = useMemo(() => <RouterLink to={href} />, [href]);
149
152
 
150
153
  return (
151
154
  <Sidebar.Item
152
155
  href={href}
153
156
  active={isActive}
154
- leadingIcon={item.icon ? iconMap[item.icon] : undefined}
157
+ leadingIcon={icon ?? undefined}
155
158
  as={link}
156
159
  >
157
160
  {item.name}
@@ -38,9 +38,53 @@
38
38
  margin-bottom: var(--rs-space-3);
39
39
  }
40
40
 
41
+ .content img {
42
+ max-width: 100%;
43
+ height: auto;
44
+ }
45
+
41
46
  .content table {
42
47
  display: block;
43
48
  max-width: 100%;
44
49
  overflow-x: auto;
45
50
  margin-bottom: var(--rs-space-5);
46
51
  }
52
+
53
+ .content details {
54
+ border: 1px solid var(--rs-color-border-base-primary);
55
+ border-radius: var(--rs-radius-2);
56
+ margin: var(--rs-space-5) 0;
57
+ overflow: hidden;
58
+ }
59
+
60
+ .content details summary {
61
+ padding: var(--rs-space-4) var(--rs-space-5);
62
+ cursor: pointer;
63
+ font-weight: 500;
64
+ font-size: var(--rs-font-size-small);
65
+ color: var(--rs-color-text-base-primary);
66
+ background: var(--rs-color-background-base-secondary);
67
+ list-style: none;
68
+ display: flex;
69
+ align-items: center;
70
+ gap: var(--rs-space-3);
71
+ }
72
+
73
+ .content details summary::-webkit-details-marker {
74
+ display: none;
75
+ }
76
+
77
+ .content details summary::before {
78
+ content: '▶';
79
+ font-size: 10px;
80
+ transition: transform 0.2s ease;
81
+ color: var(--rs-color-text-base-secondary);
82
+ }
83
+
84
+ .content details[open] > summary::before {
85
+ transform: rotate(90deg);
86
+ }
87
+
88
+ .content details > :not(summary) {
89
+ padding: var(--rs-space-4) var(--rs-space-5);
90
+ }
@@ -1,46 +1,30 @@
1
1
  'use client';
2
2
 
3
3
  import { Text } from '@raystack/apsara';
4
- import { useEffect, useState } from 'react';
5
- import type { TocItem } from '@/types';
4
+ import { AnchorProvider, useActiveAnchor } from 'fumadocs-core/toc';
5
+ import type { TableOfContents, TOCItemType } from 'fumadocs-core/toc';
6
6
  import styles from './Toc.module.css';
7
7
 
8
8
  interface TocProps {
9
- items: TocItem[];
9
+ items: TableOfContents;
10
10
  }
11
11
 
12
12
  export function Toc({ items }: TocProps) {
13
- const [activeId, setActiveId] = useState<string>('');
14
-
15
- // Filter to only show h2 and h3 headings
16
13
  const filteredItems = items.filter(
17
14
  item => item.depth >= 2 && item.depth <= 3
18
15
  );
19
16
 
20
- useEffect(() => {
21
- const headingIds = filteredItems.map(item => item.url.replace('#', ''));
22
-
23
- const observer = new IntersectionObserver(
24
- entries => {
25
- entries.forEach(entry => {
26
- if (entry.isIntersecting) {
27
- setActiveId(entry.target.id);
28
- }
29
- });
30
- },
31
- // -80px top: offset for fixed header, -80% bottom: trigger when heading is in top 20% of viewport
32
- { rootMargin: '-80px 0px -80% 0px' }
33
- );
34
-
35
- headingIds.forEach(id => {
36
- const element = document.getElementById(id);
37
- if (element) observer.observe(element);
38
- });
17
+ if (filteredItems.length === 0) return null;
39
18
 
40
- return () => observer.disconnect();
41
- }, [filteredItems]);
19
+ return (
20
+ <AnchorProvider toc={filteredItems} single>
21
+ <TocContent items={filteredItems} />
22
+ </AnchorProvider>
23
+ );
24
+ }
42
25
 
43
- if (filteredItems.length === 0) return null;
26
+ function TocContent({ items }: { items: TOCItemType[] }) {
27
+ const activeAnchor = useActiveAnchor();
44
28
 
45
29
  return (
46
30
  <aside className={styles.toc}>
@@ -48,9 +32,9 @@ export function Toc({ items }: TocProps) {
48
32
  On this page
49
33
  </Text>
50
34
  <nav className={styles.nav}>
51
- {filteredItems.map(item => {
35
+ {items.map(item => {
52
36
  const id = item.url.replace('#', '');
53
- const isActive = activeId === id;
37
+ const isActive = activeAnchor === id;
54
38
  const isNested = item.depth > 2;
55
39
  return (
56
40
  <a
@@ -1,6 +1,6 @@
1
1
  import { Link as RouterLink, useLocation } from 'react-router';
2
2
  import { MethodBadge } from '@/components/api/method-badge';
3
- import type { PageTree, PageTreeItem } from '@/types';
3
+ import type { Root, Node } from 'fumadocs-core/page-tree';
4
4
  import styles from './ChapterNav.module.css';
5
5
 
6
6
  const iconMap: Record<string, React.ReactNode> = {
@@ -12,16 +12,16 @@ const iconMap: Record<string, React.ReactNode> = {
12
12
  };
13
13
 
14
14
  interface ChapterNavProps {
15
- tree: PageTree;
15
+ tree: Root;
16
16
  }
17
17
 
18
18
  function buildChapterIndices(
19
- children: PageTreeItem[]
20
- ): Map<PageTreeItem, number> {
21
- const indices = new Map<PageTreeItem, number>();
19
+ children: Node[]
20
+ ): Map<Node, number> {
21
+ const indices = new Map<Node, number>();
22
22
  let index = 0;
23
23
  for (const item of children) {
24
- if (item.type === 'folder' && item.children) {
24
+ if (item.type === 'folder') {
25
25
  index++;
26
26
  indices.set(item, index);
27
27
  }
@@ -39,17 +39,17 @@ export function ChapterNav({ tree }: ChapterNavProps) {
39
39
  {tree.children.map(item => {
40
40
  if (item.type === 'separator') return null;
41
41
 
42
- if (item.type === 'folder' && item.children) {
42
+ if (item.type === 'folder') {
43
43
  const chapterIndex = chapterIndices.get(item) ?? 0;
44
44
  return (
45
- <li key={item.name} className={styles.chapter}>
45
+ <li key={item.name?.toString()} className={styles.chapter}>
46
46
  <span className={styles.chapterLabel}>
47
47
  {String(chapterIndex).padStart(2, '0')}. {item.name}
48
48
  </span>
49
49
  <ul className={styles.chapterItems}>
50
50
  {item.children.map(child => (
51
51
  <ChapterItem
52
- key={child.url ?? child.name}
52
+ key={child.type === 'page' ? child.url : (child.name?.toString() ?? '')}
53
53
  item={child}
54
54
  pathname={pathname}
55
55
  />
@@ -61,7 +61,7 @@ export function ChapterNav({ tree }: ChapterNavProps) {
61
61
 
62
62
  return (
63
63
  <ChapterItem
64
- key={item.url ?? item.name}
64
+ key={item.url ?? item.name?.toString() ?? ''}
65
65
  item={item}
66
66
  pathname={pathname}
67
67
  />
@@ -76,19 +76,19 @@ function ChapterItem({
76
76
  item,
77
77
  pathname
78
78
  }: {
79
- item: PageTreeItem;
79
+ item: Node;
80
80
  pathname: string;
81
81
  }) {
82
82
  if (item.type === 'separator') return null;
83
83
 
84
- if (item.type === 'folder' && item.children) {
84
+ if (item.type === 'folder') {
85
85
  return (
86
86
  <li>
87
87
  <span className={styles.subLabel}>{item.name}</span>
88
88
  <ul className={styles.chapterItems}>
89
89
  {item.children.map(child => (
90
90
  <ChapterItem
91
- key={child.url ?? child.name}
91
+ key={child.type === 'page' ? child.url : (child.name?.toString() ?? '')}
92
92
  item={child}
93
93
  pathname={pathname}
94
94
  />
@@ -99,7 +99,7 @@ function ChapterItem({
99
99
  }
100
100
 
101
101
  const isActive = pathname === item.url;
102
- const icon = item.icon ? iconMap[item.icon] : null;
102
+ const icon = typeof item.icon === 'string' ? iconMap[item.icon] : item.icon;
103
103
 
104
104
  return (
105
105
  <li>
@@ -174,6 +174,11 @@
174
174
  margin-bottom: var(--rs-space-3);
175
175
  }
176
176
 
177
+ .content img {
178
+ max-width: 100%;
179
+ height: auto;
180
+ }
181
+
177
182
  .content blockquote {
178
183
  margin: 1rem 0;
179
184
  padding-left: 1rem;
@@ -2,59 +2,33 @@ import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
2
2
  import { Flex } from '@raystack/apsara';
3
3
  import { useMemo } from 'react';
4
4
  import { Link as RouterLink, useLocation } from 'react-router';
5
+ import { getBreadcrumbItems } from 'fumadocs-core/breadcrumb';
6
+ import { flattenTree } from 'fumadocs-core/page-tree';
5
7
  import { Search } from '@/components/ui/search';
6
- import type { PageTreeItem, ThemePageProps } from '@/types';
8
+ import type { ThemePageProps } from '@/types';
7
9
  import styles from './Page.module.css';
8
10
  import { ReadingProgress } from './ReadingProgress';
9
11
 
10
- function flattenTree(items: PageTreeItem[]): PageTreeItem[] {
11
- const result: PageTreeItem[] = [];
12
- for (const item of items) {
13
- if (item.type === 'page' && item.url) result.push(item);
14
- if (item.children) result.push(...flattenTree(item.children));
15
- }
16
- return result;
17
- }
18
-
19
- function findBreadcrumb(
20
- items: PageTreeItem[],
21
- slug: string[]
22
- ): { label: string; href: string }[] {
23
- const result: { label: string; href: string }[] = [];
24
- for (let i = 0; i < slug.length; i++) {
25
- const path = '/' + slug.slice(0, i + 1).join('/');
26
- const found = findInTree(items, path);
27
- result.push({ label: found?.name ?? slug[i], href: path });
28
- }
29
- return result;
30
- }
31
-
32
- function findInTree(
33
- items: PageTreeItem[],
34
- path: string
35
- ): PageTreeItem | undefined {
36
- for (const item of items) {
37
- if (item.url === path) return item;
38
- if (item.children) {
39
- const found = findInTree(item.children, path);
40
- if (found) return found;
41
- }
42
- }
43
- return undefined;
44
- }
45
-
46
12
  export function Page({ page, config, tree }: ThemePageProps) {
47
13
  const { pathname } = useLocation();
48
14
 
49
15
  const { prev, next, crumbs } = useMemo(() => {
50
16
  const pages = flattenTree(tree.children);
51
17
  const currentIndex = pages.findIndex(p => p.url === pathname);
18
+ const breadcrumbItems = getBreadcrumbItems(
19
+ pathname,
20
+ tree,
21
+ { includePage: true }
22
+ );
52
23
  return {
53
24
  prev: currentIndex > 0 ? pages[currentIndex - 1] : null,
54
25
  next: currentIndex < pages.length - 1 ? pages[currentIndex + 1] : null,
55
- crumbs: findBreadcrumb(tree.children, page.slug)
26
+ crumbs: breadcrumbItems.map(item => ({
27
+ label: item.name,
28
+ href: item.url ?? pathname,
29
+ })),
56
30
  };
57
- }, [tree, pathname, page.slug]);
31
+ }, [tree, pathname]);
58
32
 
59
33
  return (
60
34
  <>
@@ -63,7 +37,7 @@ export function Page({ page, config, tree }: ThemePageProps) {
63
37
  <Flex align='center' gap='small' className={styles.navLeft}>
64
38
  {prev ? (
65
39
  <RouterLink
66
- to={prev.url!}
40
+ to={prev.url}
67
41
  className={styles.arrow}
68
42
  aria-label='Previous page'
69
43
  >
@@ -80,7 +54,7 @@ export function Page({ page, config, tree }: ThemePageProps) {
80
54
  )}
81
55
  {next ? (
82
56
  <RouterLink
83
- to={next.url!}
57
+ to={next.url}
84
58
  className={styles.arrow}
85
59
  aria-label='Next page'
86
60
  >
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { cx } from 'class-variance-authority';
4
4
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
- import type { TocItem } from '@/types';
5
+ import type { TOCItemType } from 'fumadocs-core/toc';
6
6
  import styles from './ReadingProgress.module.css';
7
7
 
8
8
  interface Heading {
@@ -68,7 +68,7 @@ function resolveOverlaps(headings: Heading[], maxPosition: number): Heading[] {
68
68
  }
69
69
 
70
70
  interface ReadingProgressProps {
71
- items: TocItem[];
71
+ items: TOCItemType[];
72
72
  }
73
73
 
74
74
  export function ReadingProgress({ items }: ReadingProgressProps) {
@@ -1,4 +1,8 @@
1
1
  import type { ReactNode } from 'react'
2
+ import type { TableOfContents } from 'fumadocs-core/toc'
3
+
4
+ export type { Root, Node, Item, Folder, Separator } from 'fumadocs-core/page-tree'
5
+ export type { TOCItemType, TableOfContents } from 'fumadocs-core/toc'
2
6
 
3
7
  export interface Frontmatter {
4
8
  title: string
@@ -12,25 +16,5 @@ export interface Page {
12
16
  slug: string[]
13
17
  frontmatter: Frontmatter
14
18
  content: ReactNode
15
- toc: TocItem[]
16
- }
17
-
18
- export interface TocItem {
19
- title: string
20
- url: string
21
- depth: number
22
- }
23
-
24
- export interface PageTreeItem {
25
- type: 'page' | 'folder' | 'separator'
26
- name: string
27
- url?: string
28
- order?: number
29
- icon?: string
30
- children?: PageTreeItem[]
31
- }
32
-
33
- export interface PageTree {
34
- name: string
35
- children: PageTreeItem[]
19
+ toc: TableOfContents
36
20
  }
@@ -1,4 +1,3 @@
1
1
  // Vite build-time constants (injected via define in vite-config.ts)
2
2
  declare const __CHRONICLE_CONTENT_DIR__: string
3
3
  declare const __CHRONICLE_PROJECT_ROOT__: string
4
- declare const __CHRONICLE_PACKAGE_ROOT__: string
@@ -1,18 +1,19 @@
1
1
  import type { ReactNode } from 'react'
2
+ import type { Root } from 'fumadocs-core/page-tree'
2
3
  import type { ChronicleConfig } from './config'
3
- import type { Page, PageTree } from './content'
4
+ import type { Page } from './content'
4
5
 
5
6
  export interface ThemeLayoutProps {
6
7
  children: ReactNode
7
8
  config: ChronicleConfig
8
- tree: PageTree
9
+ tree: Root
9
10
  classNames?: { layout?: string; body?: string; sidebar?: string; content?: string }
10
11
  }
11
12
 
12
13
  export interface ThemePageProps {
13
14
  page: Page
14
15
  config: ChronicleConfig
15
- tree: PageTree
16
+ tree: Root
16
17
  }
17
18
 
18
19
  export interface Theme {