Skip to content

Commit f960bfb

Browse files
rsbhclaude
andcommitted
refactor: extract shared mdx-loader and frontmatter helpers
- Create lib/mdx-loader.ts — single import.meta.glob + loadMdxModule() - Remove duplicate glob/loader from page-context.tsx and entry-client.tsx - Add extractFrontmatter() and getRelativePath() to source.ts - Remove duplicate frontmatter reconstruction from entry-server and api/page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b4dda33 commit f960bfb

6 files changed

Lines changed: 52 additions & 67 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React, { type ReactNode } from 'react';
2+
import type { TableOfContents } from 'fumadocs-core/toc';
3+
import { mdxComponents } from '@/components/mdx';
4+
5+
const contentModules = import.meta.glob<{ default?: React.ComponentType<any>; toc?: TableOfContents }>(
6+
'../../.content/**/*.{mdx,md}'
7+
);
8+
9+
export async function loadMdxModule(relativePath: string): Promise<{ content: ReactNode; toc: TableOfContents }> {
10+
const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
11+
const key = relativePath.endsWith('.md')
12+
? `../../.content/${withoutExt}.md`
13+
: `../../.content/${withoutExt}.mdx`;
14+
const loader = contentModules[key];
15+
if (!loader) return { content: null, toc: [] };
16+
const mod = await loader();
17+
const content = mod.default
18+
? React.createElement(mod.default, { components: mdxComponents })
19+
: null;
20+
return { content, toc: mod.toc ?? [] };
21+
}

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

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import React, {
1+
import {
22
createContext,
33
type ReactNode,
44
useContext,
55
useEffect,
66
useState
77
} from 'react';
88
import { useLocation } from 'react-router';
9-
import { mdxComponents } from '@/components/mdx';
9+
import { loadMdxModule } from '@/lib/mdx-loader';
1010
import type { ApiSpec } from '@/lib/openapi';
1111
import type { ChronicleConfig, Frontmatter, Root, TableOfContents } from '@/types';
1212

@@ -48,24 +48,6 @@ interface PageProviderProps {
4848
children: ReactNode;
4949
}
5050

51-
const contentModules = import.meta.glob<{ default?: React.ComponentType<any>; toc?: TableOfContents }>(
52-
'../../.content/**/*.{mdx,md}'
53-
);
54-
55-
async function loadMdxModule(relativePath: string): Promise<{ content: ReactNode; toc: TableOfContents }> {
56-
const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
57-
const key = relativePath.endsWith('.md')
58-
? `../../.content/${withoutExt}.md`
59-
: `../../.content/${withoutExt}.mdx`;
60-
const loader = contentModules[key];
61-
if (!loader) return { content: null, toc: [] };
62-
const mod = await loader();
63-
const content = mod.default
64-
? React.createElement(mod.default, { components: mdxComponents })
65-
: null;
66-
return { content, toc: mod.toc ?? [] };
67-
}
68-
6951
export function PageProvider({
7052
initialConfig,
7153
initialTree,

packages/chronicle/src/lib/source.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Root, Node, Folder } from 'fumadocs-core/page-tree';
55
import matter from 'gray-matter';
66
import type { MDXContent } from 'mdx/types';
77
import type { TableOfContents } from 'fumadocs-core/toc';
8+
import type { Frontmatter } from '@/types';
89

910
function getContentDir(): string {
1011
return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
@@ -121,6 +122,21 @@ export async function getPage(slugs?: string[]) {
121122
return s.getPage(slugs);
122123
}
123124

125+
export function extractFrontmatter(page: { data: unknown }, fallbackTitle?: string): Frontmatter {
126+
const d = page.data as Record<string, unknown>;
127+
return {
128+
title: (d.title as string) ?? fallbackTitle ?? 'Untitled',
129+
description: d.description as string | undefined,
130+
order: d.order as number | undefined,
131+
icon: d.icon as string | undefined,
132+
lastModified: d.lastModified as string | undefined,
133+
};
134+
}
135+
136+
export function getRelativePath(page: { data: unknown }): string {
137+
return ((page.data as Record<string, unknown>)._relativePath as string) ?? '';
138+
}
139+
124140
export async function loadPageModule(
125141
relativePath: string
126142
): Promise<{ default: MDXContent | null; toc: TableOfContents }> {
Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineHandler, HTTPError } from 'nitro';
2-
import { getPage } from '@/lib/source';
2+
import { getPage, extractFrontmatter, getRelativePath } from '@/lib/source';
33

44
export default defineHandler(async event => {
55
const slugParam = event.context.params?.slug ?? '';
@@ -10,17 +10,8 @@ export default defineHandler(async event => {
1010
throw new HTTPError({ status: 404, message: 'Page not found' });
1111
}
1212

13-
const data = page.data as Record<string, unknown>;
14-
const relativePath = (data._relativePath as string) ?? '';
15-
1613
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,
14+
frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
15+
relativePath: getRelativePath(page),
2516
};
2617
});

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

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import '@vitejs/plugin-react/preamble';
2-
import React from 'react';
32
import { hydrateRoot } from 'react-dom/client';
43
import { BrowserRouter } from 'react-router';
54
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
6-
import { mdxComponents } from '@/components/mdx';
5+
import { loadMdxModule } from '@/lib/mdx-loader';
76
import { PageProvider } from '@/lib/page-context';
8-
import type { ChronicleConfig, Frontmatter, Root, TableOfContents } from '@/types';
7+
import type { ChronicleConfig, Frontmatter, Root } from '@/types';
98
import type { ApiSpec } from '@/lib/openapi';
10-
import type { ReactNode } from 'react';
119
import { App } from './App';
1210

1311
interface EmbeddedData {
@@ -37,7 +35,11 @@ async function hydrate() {
3735
: [];
3836

3937
const page = embedded?.relativePath
40-
? await loadPage(embedded)
38+
? {
39+
slug: embedded.slug,
40+
frontmatter: embedded.frontmatter,
41+
...(await loadMdxModule(embedded.relativePath)),
42+
}
4143
: null;
4244

4345
hydrateRoot(
@@ -60,23 +62,4 @@ async function hydrate() {
6062
}
6163
}
6264

63-
const contentModules = import.meta.glob<{ default?: React.ComponentType<any>; toc?: TableOfContents }>(
64-
'../../.content/**/*.{mdx,md}'
65-
);
66-
67-
async function loadPage(
68-
embedded: EmbeddedData
69-
): Promise<{ slug: string[]; frontmatter: Frontmatter; content: ReactNode; toc: TableOfContents }> {
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
77-
? React.createElement(mod.default, { components: mdxComponents })
78-
: null;
79-
return { slug: embedded.slug, frontmatter: embedded.frontmatter, content, toc: mod?.toc ?? [] };
80-
}
81-
8265
hydrate();

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

Lines changed: 3 additions & 11 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, loadPageModule } from '@/lib/source';
11+
import { getPageTree, getPage, loadPageModule, extractFrontmatter, getRelativePath } from '@/lib/source';
1212
import { App } from './App';
1313

1414
// @ts-expect-error virtual import from Nitro
@@ -32,21 +32,13 @@ export default {
3232
getPage(slug),
3333
]);
3434

35-
const data = page?.data as Record<string, unknown> | undefined;
36-
const relativePath = (data?._relativePath as string) ?? null;
37-
35+
const relativePath = page ? getRelativePath(page) : null;
3836
const mdxModule = relativePath ? await loadPageModule(relativePath) : null;
3937

4038
const pageData = page
4139
? {
4240
slug,
43-
frontmatter: {
44-
title: (data?.title as string) ?? slug[slug.length - 1] ?? 'Untitled',
45-
description: data?.description as string | undefined,
46-
order: data?.order as number | undefined,
47-
icon: data?.icon as string | undefined,
48-
lastModified: data?.lastModified as string | undefined,
49-
},
41+
frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
5042
content: mdxModule?.default
5143
? React.createElement(mdxModule.default, { components: mdxComponents })
5244
: null,

0 commit comments

Comments
 (0)