Skip to content

Commit b4dda33

Browse files
rsbhclaude
andcommitted
fix: pass TOC data from compiled MDX modules to page components
- loadPageModule returns both default component and toc array - PageData includes toc field (TableOfContents) - entry-client and page-context extract toc from import.meta.glob modules - DocsPage uses page.toc instead of hardcoded empty array Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bd7f9af commit b4dda33

5 files changed

Lines changed: 26 additions & 22 deletions

File tree

packages/chronicle/src/lib/page-context.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import React, {
88
import { useLocation } from 'react-router';
99
import { mdxComponents } from '@/components/mdx';
1010
import type { ApiSpec } from '@/lib/openapi';
11-
import type { ChronicleConfig, Frontmatter, Root } from '@/types';
11+
import type { ChronicleConfig, Frontmatter, Root, TableOfContents } from '@/types';
1212

1313
interface PageData {
1414
slug: string[];
1515
frontmatter: Frontmatter;
1616
content: ReactNode;
17+
toc: TableOfContents;
1718
}
1819

1920
interface PageContextValue {
@@ -47,21 +48,22 @@ interface PageProviderProps {
4748
children: ReactNode;
4849
}
4950

50-
const contentModules = import.meta.glob<{ default?: React.ComponentType<any> }>(
51+
const contentModules = import.meta.glob<{ default?: React.ComponentType<any>; toc?: TableOfContents }>(
5152
'../../.content/**/*.{mdx,md}'
5253
);
5354

54-
async function loadMdxComponent(relativePath: string): Promise<ReactNode> {
55+
async function loadMdxModule(relativePath: string): Promise<{ content: ReactNode; toc: TableOfContents }> {
5556
const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
5657
const key = relativePath.endsWith('.md')
5758
? `../../.content/${withoutExt}.md`
5859
: `../../.content/${withoutExt}.mdx`;
5960
const loader = contentModules[key];
60-
if (!loader) return null;
61+
if (!loader) return { content: null, toc: [] };
6162
const mod = await loader();
62-
return mod.default
63+
const content = mod.default
6364
? React.createElement(mod.default, { components: mdxComponents })
6465
: null;
66+
return { content, toc: mod.toc ?? [] };
6567
}
6668

6769
export function PageProvider({
@@ -105,9 +107,9 @@ export function PageProvider({
105107
.then(res => res.json())
106108
.then(async (data: { frontmatter: Frontmatter; relativePath: string }) => {
107109
if (cancelled.current) return;
108-
const content = await loadMdxComponent(data.relativePath);
110+
const { content, toc } = await loadMdxModule(data.relativePath);
109111
if (cancelled.current) return;
110-
setPage({ slug, frontmatter: data.frontmatter, content });
112+
setPage({ slug, frontmatter: data.frontmatter, content, toc });
111113
})
112114
.catch(() => {});
113115

packages/chronicle/src/lib/source.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { loader } from 'fumadocs-core/source';
44
import type { Root, Node, Folder } from 'fumadocs-core/page-tree';
55
import matter from 'gray-matter';
66
import type { MDXContent } from 'mdx/types';
7+
import type { TableOfContents } from 'fumadocs-core/toc';
78

89
function getContentDir(): string {
910
return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
@@ -120,20 +121,20 @@ export async function getPage(slugs?: string[]) {
120121
return s.getPage(slugs);
121122
}
122123

123-
export async function loadPageComponent(
124+
export async function loadPageModule(
124125
relativePath: string
125-
): Promise<MDXContent | null> {
126-
if (!relativePath) return null;
126+
): Promise<{ default: MDXContent | null; toc: TableOfContents }> {
127+
if (!relativePath) return { default: null, toc: [] };
127128
const contentDir = getContentDir();
128129
const fullPath = path.join(contentDir, relativePath);
129130
try {
130131
await fs.access(fullPath);
131132
} catch {
132-
return null;
133+
return { default: null, toc: [] };
133134
}
134135
const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
135136
const mod = relativePath.endsWith('.md')
136137
? await import(`../../.content/${withoutExt}.md`)
137138
: await import(`../../.content/${withoutExt}.mdx`);
138-
return mod.default;
139+
return { default: mod.default ?? null, toc: mod.toc ?? [] };
139140
}

packages/chronicle/src/pages/DocsPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function DocsPage({ slug }: DocsPageProps) {
3333
slug,
3434
frontmatter: page.frontmatter,
3535
content: page.content,
36-
toc: []
36+
toc: page.toc
3737
}}
3838
config={config}
3939
tree={tree}

packages/chronicle/src/server/entry-client.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { BrowserRouter } from 'react-router';
55
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
66
import { mdxComponents } from '@/components/mdx';
77
import { PageProvider } from '@/lib/page-context';
8-
import type { ChronicleConfig, Frontmatter, Root } from '@/types';
8+
import type { ChronicleConfig, Frontmatter, Root, TableOfContents } from '@/types';
99
import type { ApiSpec } from '@/lib/openapi';
1010
import type { ReactNode } from 'react';
1111
import { App } from './App';
@@ -60,13 +60,13 @@ async function hydrate() {
6060
}
6161
}
6262

63-
const contentModules = import.meta.glob<{ default?: React.ComponentType<any> }>(
63+
const contentModules = import.meta.glob<{ default?: React.ComponentType<any>; toc?: TableOfContents }>(
6464
'../../.content/**/*.{mdx,md}'
6565
);
6666

6767
async function loadPage(
6868
embedded: EmbeddedData
69-
): Promise<{ slug: string[]; frontmatter: Frontmatter; content: ReactNode }> {
69+
): Promise<{ slug: string[]; frontmatter: Frontmatter; content: ReactNode; toc: TableOfContents }> {
7070
const withoutExt = embedded.relativePath.replace(/\.(mdx|md)$/, '');
7171
const key = embedded.relativePath.endsWith('.md')
7272
? `../../.content/${withoutExt}.md`
@@ -76,7 +76,7 @@ async function loadPage(
7676
const content = mod?.default
7777
? React.createElement(mod.default, { components: mdxComponents })
7878
: null;
79-
return { slug: embedded.slug, frontmatter: embedded.frontmatter, content };
79+
return { slug: embedded.slug, frontmatter: embedded.frontmatter, content, toc: mod?.toc ?? [] };
8080
}
8181

8282
hydrate();

packages/chronicle/src/server/entry-server.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { mdxComponents } from '@/components/mdx';
88
import { loadConfig } from '@/lib/config';
99
import { loadApiSpecs } from '@/lib/openapi';
1010
import { PageProvider } from '@/lib/page-context';
11-
import { getPageTree, getPage, loadPageComponent } from '@/lib/source';
11+
import { getPageTree, getPage, loadPageModule } from '@/lib/source';
1212
import { App } from './App';
1313

1414
// @ts-expect-error virtual import from Nitro
@@ -35,6 +35,8 @@ export default {
3535
const data = page?.data as Record<string, unknown> | undefined;
3636
const relativePath = (data?._relativePath as string) ?? null;
3737

38+
const mdxModule = relativePath ? await loadPageModule(relativePath) : null;
39+
3840
const pageData = page
3941
? {
4042
slug,
@@ -45,11 +47,10 @@ export default {
4547
icon: data?.icon as string | undefined,
4648
lastModified: data?.lastModified as string | undefined,
4749
},
48-
content: relativePath
49-
? await loadPageComponent(relativePath).then(component =>
50-
component ? React.createElement(component, { components: mdxComponents }) : null
51-
)
50+
content: mdxModule?.default
51+
? React.createElement(mdxModule.default, { components: mdxComponents })
5252
: null,
53+
toc: mdxModule?.toc ?? [],
5354
}
5455
: null;
5556

0 commit comments

Comments
 (0)