@raystack/chronicle 0.1.0-canary.a638730 → 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.
Files changed (87) hide show
  1. package/dist/cli/index.js +204 -530
  2. package/package.json +13 -9
  3. package/src/cli/commands/build.ts +30 -63
  4. package/src/cli/commands/dev.ts +24 -13
  5. package/src/cli/commands/init.ts +38 -123
  6. package/src/cli/commands/serve.ts +35 -50
  7. package/src/cli/commands/start.ts +20 -16
  8. package/src/cli/index.ts +14 -14
  9. package/src/cli/utils/config.ts +25 -26
  10. package/src/cli/utils/index.ts +3 -2
  11. package/src/cli/utils/resolve.ts +7 -3
  12. package/src/cli/utils/scaffold.ts +14 -16
  13. package/src/components/mdx/details.module.css +0 -2
  14. package/src/components/mdx/image.tsx +5 -20
  15. package/src/components/mdx/index.tsx +3 -3
  16. package/src/components/mdx/link.tsx +24 -20
  17. package/src/components/ui/breadcrumbs.tsx +8 -42
  18. package/src/components/ui/footer.tsx +2 -3
  19. package/src/components/ui/search.tsx +116 -71
  20. package/src/lib/api-routes.ts +6 -8
  21. package/src/lib/config.ts +31 -29
  22. package/src/lib/get-llm-text.ts +10 -0
  23. package/src/lib/head.tsx +26 -22
  24. package/src/lib/openapi.ts +8 -8
  25. package/src/lib/page-context.tsx +87 -58
  26. package/src/lib/source.ts +117 -116
  27. package/src/pages/ApiLayout.tsx +22 -18
  28. package/src/pages/ApiPage.tsx +32 -27
  29. package/src/pages/DocsLayout.tsx +7 -7
  30. package/src/pages/DocsPage.tsx +11 -11
  31. package/src/pages/NotFound.tsx +11 -4
  32. package/src/server/App.tsx +35 -27
  33. package/src/server/api/apis-proxy.ts +69 -0
  34. package/src/server/api/health.ts +5 -0
  35. package/src/server/api/page/[...slug].ts +26 -0
  36. package/src/server/api/search.ts +170 -0
  37. package/src/server/api/specs.ts +9 -0
  38. package/src/server/build-search-index.ts +117 -0
  39. package/src/server/entry-client.tsx +66 -58
  40. package/src/server/entry-server.tsx +104 -35
  41. package/src/server/routes/llms.txt.ts +61 -0
  42. package/src/server/routes/og.tsx +75 -0
  43. package/src/server/routes/robots.txt.ts +11 -0
  44. package/src/server/routes/sitemap.xml.ts +40 -0
  45. package/src/server/utils/safe-path.ts +17 -0
  46. package/src/server/vite-config.ts +79 -49
  47. package/src/themes/default/Layout.tsx +78 -47
  48. package/src/themes/default/Page.module.css +0 -16
  49. package/src/themes/default/Page.tsx +9 -11
  50. package/src/themes/default/Toc.tsx +25 -39
  51. package/src/themes/default/index.ts +7 -9
  52. package/src/themes/paper/ChapterNav.tsx +63 -43
  53. package/src/themes/paper/Layout.module.css +1 -1
  54. package/src/themes/paper/Layout.tsx +24 -12
  55. package/src/themes/paper/Page.module.css +16 -4
  56. package/src/themes/paper/Page.tsx +56 -62
  57. package/src/themes/paper/ReadingProgress.tsx +160 -139
  58. package/src/themes/paper/index.ts +5 -5
  59. package/src/themes/registry.ts +7 -7
  60. package/src/types/content.ts +5 -21
  61. package/src/types/globals.d.ts +3 -0
  62. package/src/types/theme.ts +4 -3
  63. package/src/cli/__tests__/config.test.ts +0 -25
  64. package/src/cli/__tests__/scaffold.test.ts +0 -10
  65. package/src/pages/__tests__/head.test.tsx +0 -57
  66. package/src/server/__tests__/entry-server.test.tsx +0 -35
  67. package/src/server/__tests__/handlers.test.ts +0 -77
  68. package/src/server/__tests__/og.test.ts +0 -23
  69. package/src/server/__tests__/router.test.ts +0 -72
  70. package/src/server/__tests__/vite-config.test.ts +0 -25
  71. package/src/server/adapters/vercel.ts +0 -133
  72. package/src/server/dev.ts +0 -156
  73. package/src/server/entry-prod.ts +0 -97
  74. package/src/server/entry-vercel.ts +0 -26
  75. package/src/server/handlers/apis-proxy.ts +0 -52
  76. package/src/server/handlers/health.ts +0 -3
  77. package/src/server/handlers/llms.ts +0 -58
  78. package/src/server/handlers/og.ts +0 -87
  79. package/src/server/handlers/robots.ts +0 -11
  80. package/src/server/handlers/search.ts +0 -140
  81. package/src/server/handlers/sitemap.ts +0 -39
  82. package/src/server/handlers/specs.ts +0 -9
  83. package/src/server/index.html +0 -12
  84. package/src/server/prod.ts +0 -18
  85. package/src/server/request-handler.ts +0 -63
  86. package/src/server/router.ts +0 -42
  87. package/src/themes/default/font.ts +0 -4
@@ -0,0 +1,10 @@
1
+ import { source } from '@/lib/source'
2
+ import type { InferPageType } from 'fumadocs-core/source'
3
+
4
+ export async function getLLMText(page: InferPageType<typeof source>) {
5
+ const processed = await page.data.getText('processed')
6
+
7
+ return `# ${page.data.title} (${page.url})
8
+
9
+ ${processed}`
10
+ }
package/src/lib/head.tsx CHANGED
@@ -1,45 +1,49 @@
1
- import type { ChronicleConfig } from '@/types'
1
+ import type { ChronicleConfig } from '@/types';
2
2
 
3
3
  export interface HeadProps {
4
- title: string
5
- description?: string
6
- config: ChronicleConfig
7
- jsonLd?: Record<string, unknown>
4
+ title: string;
5
+ description?: string;
6
+ config: ChronicleConfig;
7
+ jsonLd?: Record<string, unknown>;
8
8
  }
9
9
 
10
10
  export function Head({ title, description, config, jsonLd }: HeadProps) {
11
- const fullTitle = `${title} | ${config.title}`
12
- const ogParams = new URLSearchParams({ title })
13
- if (description) ogParams.set('description', description)
11
+ const fullTitle = `${title} | ${config.title}`;
12
+ const ogParams = new URLSearchParams({ title });
13
+ if (description) ogParams.set('description', description);
14
14
 
15
15
  return (
16
16
  <>
17
17
  <title>{fullTitle}</title>
18
- {description && <meta name="description" content={description} />}
18
+ {description && <meta name='description' content={description} />}
19
19
 
20
20
  {config.url && (
21
21
  <>
22
- <meta property="og:title" content={title} />
23
- {description && <meta property="og:description" content={description} />}
24
- <meta property="og:site_name" content={config.title} />
25
- <meta property="og:type" content="website" />
26
- <meta property="og:image" content={`/og?${ogParams.toString()}`} />
27
- <meta property="og:image:width" content="1200" />
28
- <meta property="og:image:height" content="630" />
22
+ <meta property='og:title' content={title} />
23
+ {description && (
24
+ <meta property='og:description' content={description} />
25
+ )}
26
+ <meta property='og:site_name' content={config.title} />
27
+ <meta property='og:type' content='website' />
28
+ <meta property='og:image' content={`/og?${ogParams.toString()}`} />
29
+ <meta property='og:image:width' content='1200' />
30
+ <meta property='og:image:height' content='630' />
29
31
 
30
- <meta name="twitter:card" content="summary_large_image" />
31
- <meta name="twitter:title" content={title} />
32
- {description && <meta name="twitter:description" content={description} />}
33
- <meta name="twitter:image" content={`/og?${ogParams.toString()}`} />
32
+ <meta name='twitter:card' content='summary_large_image' />
33
+ <meta name='twitter:title' content={title} />
34
+ {description && (
35
+ <meta name='twitter:description' content={description} />
36
+ )}
37
+ <meta name='twitter:image' content={`/og?${ogParams.toString()}`} />
34
38
  </>
35
39
  )}
36
40
 
37
41
  {jsonLd && (
38
42
  <script
39
- type="application/ld+json"
43
+ type='application/ld+json'
40
44
  dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd, null, 2) }}
41
45
  />
42
46
  )}
43
47
  </>
44
- )
48
+ );
45
49
  }
@@ -1,5 +1,5 @@
1
- import fs from 'fs'
2
- import path from 'path'
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
3
  import { parse as parseYaml } from 'yaml'
4
4
  import type { OpenAPIV2, OpenAPIV3 } from 'openapi-types'
5
5
  import type { ApiConfig, ApiServerConfig, ApiAuthConfig } from '@/types/config'
@@ -17,14 +17,14 @@ export interface ApiSpec {
17
17
  export type { SchemaField } from './schema'
18
18
  export { flattenSchema } from './schema'
19
19
 
20
- export function loadApiSpecs(apiConfigs: ApiConfig[]): ApiSpec[] {
21
- const contentDir = process.env.CHRONICLE_CONTENT_DIR ?? process.cwd()
22
- return apiConfigs.map((config) => loadApiSpec(config, contentDir))
20
+ export async function loadApiSpecs(apiConfigs: ApiConfig[]): Promise<ApiSpec[]> {
21
+ const projectRoot = typeof __CHRONICLE_PROJECT_ROOT__ !== 'undefined' ? __CHRONICLE_PROJECT_ROOT__ : process.cwd()
22
+ return Promise.all(apiConfigs.map((config) => loadApiSpec(config, projectRoot)))
23
23
  }
24
24
 
25
- export function loadApiSpec(config: ApiConfig, contentDir: string): ApiSpec {
26
- const specPath = path.resolve(contentDir, config.spec)
27
- const raw = fs.readFileSync(specPath, 'utf-8')
25
+ export async function loadApiSpec(config: ApiConfig, projectRoot: string): Promise<ApiSpec> {
26
+ const specPath = path.resolve(projectRoot, config.spec)
27
+ const raw = await fs.readFile(specPath, 'utf-8')
28
28
  const isYaml = specPath.endsWith('.yaml') || specPath.endsWith('.yml')
29
29
  const doc = (isYaml ? parseYaml(raw) : JSON.parse(raw)) as OpenAPIV2.Document | OpenAPIV3.Document
30
30
 
@@ -1,95 +1,124 @@
1
- import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
2
- import { useLocation } from 'react-router-dom'
3
- import type { ChronicleConfig, Frontmatter, PageTree } from '@/types'
4
- import type { ApiSpec } from '@/lib/openapi'
5
- import { getPage, loadPageComponent, buildPageTree } from '@/lib/source'
6
- import { mdxComponents } from '@/components/mdx'
7
- import React from 'react'
1
+ import React, {
2
+ createContext,
3
+ type ReactNode,
4
+ useContext,
5
+ useEffect,
6
+ useState
7
+ } from 'react';
8
+ import { useLocation } from 'react-router';
9
+ import { mdxComponents } from '@/components/mdx';
10
+ import type { ApiSpec } from '@/lib/openapi';
11
+ import type { ChronicleConfig, Frontmatter, Root } from '@/types';
8
12
 
9
13
  interface PageData {
10
- slug: string[]
11
- frontmatter: Frontmatter
12
- content: ReactNode
14
+ slug: string[];
15
+ frontmatter: Frontmatter;
16
+ content: ReactNode;
13
17
  }
14
18
 
15
19
  interface PageContextValue {
16
- config: ChronicleConfig
17
- tree: PageTree
18
- page: PageData | null
19
- apiSpecs: ApiSpec[]
20
+ config: ChronicleConfig;
21
+ tree: Root;
22
+ page: PageData | null;
23
+ apiSpecs: ApiSpec[];
20
24
  }
21
25
 
22
- const PageContext = createContext<PageContextValue | null>(null)
26
+ const PageContext = createContext<PageContextValue | null>(null);
23
27
 
24
28
  export function usePageContext(): PageContextValue {
25
- const ctx = useContext(PageContext)
29
+ const ctx = useContext(PageContext);
26
30
  if (!ctx) {
27
- console.error('usePageContext: no context found!')
31
+ console.error('usePageContext: no context found!');
28
32
  return {
29
33
  config: { title: 'Documentation' },
30
- tree: { name: 'root', children: [] },
34
+ tree: { name: 'root', children: [] } as Root,
31
35
  page: null,
32
- apiSpecs: [],
33
- }
36
+ apiSpecs: []
37
+ };
34
38
  }
35
- return ctx
39
+ return ctx;
36
40
  }
37
41
 
38
42
  interface PageProviderProps {
39
- initialConfig: ChronicleConfig
40
- initialTree: PageTree
41
- initialPage: PageData | null
42
- initialApiSpecs: ApiSpec[]
43
- children: ReactNode
43
+ initialConfig: ChronicleConfig;
44
+ initialTree: Root;
45
+ initialPage: PageData | null;
46
+ initialApiSpecs: ApiSpec[];
47
+ children: ReactNode;
48
+ }
49
+
50
+ const contentModules = import.meta.glob<{ default?: React.ComponentType<any> }>(
51
+ '../../.content/**/*.{mdx,md}'
52
+ );
53
+
54
+ async function loadMdxComponent(relativePath: string): Promise<ReactNode> {
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();
62
+ return mod.default
63
+ ? React.createElement(mod.default, { components: mdxComponents })
64
+ : null;
44
65
  }
45
66
 
46
- export function PageProvider({ initialConfig, initialTree, initialPage, initialApiSpecs, children }: PageProviderProps) {
47
- const { pathname } = useLocation()
48
- const [tree, setTree] = useState<PageTree>(initialTree)
49
- const [page, setPage] = useState<PageData | null>(initialPage)
50
- const [apiSpecs, setApiSpecs] = useState<ApiSpec[]>(initialApiSpecs)
51
- const [currentPath, setCurrentPath] = useState(pathname)
67
+ export function PageProvider({
68
+ initialConfig,
69
+ initialTree,
70
+ initialPage,
71
+ initialApiSpecs,
72
+ children
73
+ }: PageProviderProps) {
74
+ const { pathname } = useLocation();
75
+ const [tree] = useState<Root>(initialTree);
76
+ const [page, setPage] = useState<PageData | null>(initialPage);
77
+ const [apiSpecs, setApiSpecs] = useState<ApiSpec[]>(initialApiSpecs);
78
+ const [currentPath, setCurrentPath] = useState(pathname);
52
79
 
53
80
  useEffect(() => {
54
- if (pathname === currentPath) return
55
- setCurrentPath(pathname)
81
+ if (pathname === currentPath) return;
82
+ setCurrentPath(pathname);
56
83
 
57
- let cancelled = false
84
+ const cancelled = { current: false };
58
85
 
59
86
  if (pathname.startsWith('/apis')) {
60
- // Fetch API specs if not already loaded
61
87
  if (apiSpecs.length === 0) {
62
88
  fetch('/api/specs')
63
- .then((res) => res.json())
64
- .then((specs) => { if (!cancelled) setApiSpecs(specs) })
65
- .catch(() => {})
89
+ .then(res => res.json())
90
+ .then(specs => {
91
+ if (!cancelled.current) setApiSpecs(specs);
92
+ })
93
+ .catch(() => {});
66
94
  }
67
- return () => { cancelled = true }
95
+ return () => { cancelled.current = true; };
68
96
  }
69
97
 
70
- async function load() {
71
- const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean)
98
+ const slug = pathname === '/'
99
+ ? []
100
+ : pathname.slice(1).split('/').filter(Boolean);
72
101
 
73
- const [sourcePage, newTree] = await Promise.all([getPage(slug), buildPageTree()])
74
- if (cancelled || !sourcePage) return
102
+ const apiPath = slug.length === 0 ? '/api/page/' : `/api/page/${slug.join('/')}`;
75
103
 
76
- const component = await loadPageComponent(sourcePage)
77
- if (cancelled) return
78
-
79
- setTree(newTree)
80
- setPage({
81
- slug,
82
- frontmatter: sourcePage.frontmatter,
83
- content: component ? React.createElement(component, { components: mdxComponents }) : null,
104
+ fetch(apiPath)
105
+ .then(res => res.json())
106
+ .then(async (data: { frontmatter: Frontmatter; relativePath: string }) => {
107
+ if (cancelled.current) return;
108
+ const content = await loadMdxComponent(data.relativePath);
109
+ if (cancelled.current) return;
110
+ setPage({ slug, frontmatter: data.frontmatter, content });
84
111
  })
85
- }
86
- load()
87
- return () => { cancelled = true }
88
- }, [pathname])
112
+ .catch(() => {});
113
+
114
+ return () => { cancelled.current = true; };
115
+ }, [pathname]);
89
116
 
90
117
  return (
91
- <PageContext.Provider value={{ config: initialConfig, tree, page, apiSpecs }}>
118
+ <PageContext.Provider
119
+ value={{ config: initialConfig, tree, page, apiSpecs }}
120
+ >
92
121
  {children}
93
122
  </PageContext.Provider>
94
- )
123
+ );
95
124
  }
package/src/lib/source.ts CHANGED
@@ -1,138 +1,139 @@
1
- import type { MDXContent } from 'mdx/types'
2
- import type { Frontmatter, PageTree, PageTreeItem } from '@/types'
3
-
4
- const meta: Record<string, Frontmatter> = import.meta.glob(
5
- '@content/**/*.{mdx,md}',
6
- { eager: true, import: 'frontmatter' }
7
- )
8
-
9
- const loaders: Record<string, () => Promise<{ default: MDXContent }>> = import.meta.glob(
10
- '@content/**/*.{mdx,md}'
11
- )
12
-
13
- export interface SourcePage {
14
- url: string
15
- slugs: string[]
16
- filePath: string
17
- frontmatter: Frontmatter
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { loader } from 'fumadocs-core/source';
4
+ import type { Root, Node, Folder } from 'fumadocs-core/page-tree';
5
+ import matter from 'gray-matter';
6
+ import type { MDXContent } from 'mdx/types';
7
+
8
+ function getContentDir(): string {
9
+ return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
18
10
  }
19
11
 
20
- // Compute common directory prefix of all glob keys once
21
- function computePrefix(keys: string[]): string {
22
- if (keys.length === 0) return ''
23
- const dirs = keys.map((k) => k.split('/').slice(0, -1)) // drop filename
24
- const first = dirs[0]
25
- let depth = 0
26
- for (let i = 0; i < first.length; i++) {
27
- if (dirs.every((d) => d[i] === first[i])) {
28
- depth = i + 1
29
- } else {
30
- break
12
+ async function scanFiles(contentDir: string) {
13
+ const files: {
14
+ type: 'page' | 'meta';
15
+ path: string;
16
+ data: Record<string, unknown>;
17
+ }[] = [];
18
+
19
+ async function scan(dir: string, prefix: string[] = []) {
20
+ try {
21
+ const entries = await fs.readdir(dir, { withFileTypes: true });
22
+ for (const entry of entries) {
23
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
24
+ continue;
25
+ const fullPath = path.join(dir, entry.name);
26
+ const relativePath = [...prefix, entry.name].join('/');
27
+
28
+ if (entry.isDirectory()) {
29
+ await scan(fullPath, [...prefix, entry.name]);
30
+ continue;
31
+ }
32
+
33
+ if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {
34
+ const raw = await fs.readFile(fullPath, 'utf-8');
35
+ const { data } = matter(raw);
36
+ files.push({
37
+ type: 'page',
38
+ path: relativePath,
39
+ data: { ...data, _relativePath: relativePath }
40
+ });
41
+ } else if (entry.name === 'meta.json' || entry.name === 'meta.yaml') {
42
+ const raw = await fs.readFile(fullPath, 'utf-8');
43
+ const data = entry.name.endsWith('.json')
44
+ ? JSON.parse(raw)
45
+ : matter(raw).data;
46
+ files.push({ type: 'meta', path: relativePath, data });
47
+ }
48
+ }
49
+ } catch {
50
+ /* directory not readable */
31
51
  }
32
52
  }
33
- return first.slice(0, depth).join('/') + '/'
34
- }
35
-
36
- const prefix = computePrefix(Object.keys(meta))
37
53
 
38
- function filePathToSlugs(filePath: string): string[] {
39
- const relative = filePath.slice(prefix.length)
40
- const withoutExt = relative.replace(/\.(mdx|md)$/, '')
41
- const parts = withoutExt.split('/').filter(Boolean)
42
- if (parts[parts.length - 1] === 'index') parts.pop()
43
- return parts
54
+ await scan(contentDir);
55
+ return files;
44
56
  }
45
57
 
46
- function slugsToUrl(slugs: string[]): string {
47
- return slugs.length === 0 ? '/' : '/' + slugs.join('/')
58
+ let cachedSource: ReturnType<typeof loader> | null = null;
59
+
60
+ async function getSource() {
61
+ if (cachedSource) return cachedSource;
62
+ const contentDir = getContentDir();
63
+ const files = await scanFiles(contentDir);
64
+ cachedSource = loader({
65
+ source: { files },
66
+ baseUrl: '/'
67
+ });
68
+ return cachedSource;
48
69
  }
49
70
 
50
- let cachedPages: SourcePage[] | null = null
51
-
52
- export async function getPages(): Promise<SourcePage[]> {
53
- if (cachedPages) return cachedPages
54
-
55
- cachedPages = Object.entries(meta).map(([filePath, fm]) => {
56
- const slugs = filePathToSlugs(filePath)
57
- const baseName = slugs[slugs.length - 1] ?? 'index'
58
- return {
59
- url: slugsToUrl(slugs),
60
- slugs,
61
- filePath,
62
- frontmatter: {
63
- title: fm?.title ?? baseName,
64
- description: fm?.description,
65
- order: fm?.order,
66
- icon: fm?.icon,
67
- lastModified: fm?.lastModified,
68
- },
69
- }
70
- })
71
+ export { getSource as source };
71
72
 
72
- return cachedPages
73
+ export function invalidate() {
74
+ cachedSource = null;
73
75
  }
74
76
 
75
- export async function getPage(slug?: string[]): Promise<SourcePage | null> {
76
- const pages = await getPages()
77
- const targetUrl = !slug || slug.length === 0 ? '/' : '/' + slug.join('/')
78
- return pages.find((p) => p.url === targetUrl) ?? null
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;
79
81
  }
80
82
 
81
- export async function loadPageComponent(page: SourcePage): Promise<MDXContent | null> {
82
- const loader = loaders[page.filePath]
83
- if (!loader) return null
84
- const mod = await loader()
85
- return mod.default
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
+ );
86
95
  }
87
96
 
88
- export function invalidate() {
89
- cachedPages = null
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) };
90
106
  }
91
107
 
92
- export async function buildPageTree(): Promise<PageTree> {
93
- const pages = await getPages()
94
- const folders = new Map<string, PageTreeItem[]>()
95
- const rootPages: PageTreeItem[] = []
96
-
97
- pages.forEach((page) => {
98
- const isIndex = page.url === '/'
99
- const item: PageTreeItem = {
100
- type: 'page',
101
- name: page.frontmatter.title,
102
- url: page.url,
103
- order: page.frontmatter.order ?? (isIndex ? 0 : undefined),
104
- }
105
-
106
- if (page.slugs.length > 1) {
107
- const folder = page.slugs[0]
108
- if (!folders.has(folder)) {
109
- folders.set(folder, [])
110
- }
111
- folders.get(folder)?.push(item)
112
- } else {
113
- rootPages.push(item)
114
- }
115
- })
116
-
117
- const sortByOrder = (items: PageTreeItem[]) =>
118
- items.sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER))
119
-
120
- const children: PageTreeItem[] = sortByOrder(rootPages)
108
+ export async function getPageTree(): Promise<Root> {
109
+ const s = await getSource();
110
+ return sortTreeByOrder(s.pageTree as Root, s.getPages());
111
+ }
121
112
 
122
- const folderItems: PageTreeItem[] = []
123
- folders.forEach((items, folder) => {
124
- const sorted = sortByOrder(items)
125
- const indexPage = items.find(item => item.url === `/${folder}`)
126
- const folderOrder = indexPage?.order ?? sorted[0]?.order
127
- folderItems.push({
128
- type: 'folder',
129
- name: folder.charAt(0).toUpperCase() + folder.slice(1),
130
- order: folderOrder,
131
- children: sorted,
132
- })
133
- })
113
+ export async function getPages() {
114
+ const s = await getSource();
115
+ return s.getPages();
116
+ }
134
117
 
135
- children.push(...sortByOrder(folderItems))
118
+ export async function getPage(slugs?: string[]) {
119
+ const s = await getSource();
120
+ return s.getPage(slugs);
121
+ }
136
122
 
137
- return { name: 'root', children }
123
+ export async function loadPageComponent(
124
+ relativePath: string
125
+ ): Promise<MDXContent | null> {
126
+ if (!relativePath) return null;
127
+ const contentDir = getContentDir();
128
+ const fullPath = path.join(contentDir, relativePath);
129
+ try {
130
+ await fs.access(fullPath);
131
+ } catch {
132
+ return null;
133
+ }
134
+ const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
135
+ const mod = relativePath.endsWith('.md')
136
+ ? await import(`../../.content/${withoutExt}.md`)
137
+ : await import(`../../.content/${withoutExt}.mdx`);
138
+ return mod.default;
138
139
  }
@@ -1,29 +1,33 @@
1
- import type { ReactNode } from 'react'
2
- import { cx } from 'class-variance-authority'
3
- import { usePageContext } from '@/lib/page-context'
4
- import { buildApiPageTree } from '@/lib/api-routes'
5
- import { getTheme } from '@/themes/registry'
6
- import { Search } from '@/components/ui/search'
7
- import styles from './ApiLayout.module.css'
1
+ import { cx } from 'class-variance-authority';
2
+ import type { ReactNode } from 'react';
3
+ import { Search } from '@/components/ui/search';
4
+ import { buildApiPageTree } from '@/lib/api-routes';
5
+ import { usePageContext } from '@/lib/page-context';
6
+ import { getTheme } from '@/themes/registry';
7
+ import styles from './ApiLayout.module.css';
8
8
 
9
9
  interface ApiLayoutProps {
10
- children: ReactNode
10
+ children: ReactNode;
11
11
  }
12
12
 
13
13
  export function ApiLayout({ children }: ApiLayoutProps) {
14
- const { config, apiSpecs } = usePageContext()
15
- const { Layout, className } = getTheme(config.theme?.name)
16
- const tree = buildApiPageTree(apiSpecs)
14
+ const { config, apiSpecs } = usePageContext();
15
+ const { Layout, className } = getTheme(config.theme?.name);
16
+ const tree = buildApiPageTree(apiSpecs);
17
17
 
18
18
  return (
19
- <Layout config={config} tree={tree} classNames={{
20
- layout: cx(styles.layout, className),
21
- body: styles.body,
22
- sidebar: styles.sidebar,
23
- content: styles.content,
24
- }}>
19
+ <Layout
20
+ config={config}
21
+ tree={tree}
22
+ classNames={{
23
+ layout: cx(styles.layout, className),
24
+ body: styles.body,
25
+ sidebar: styles.sidebar,
26
+ content: styles.content
27
+ }}
28
+ >
25
29
  <Search className={styles.hiddenSearch} />
26
30
  {children}
27
31
  </Layout>
28
- )
32
+ );
29
33
  }