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

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 (82) hide show
  1. package/dist/cli/index.js +150 -416
  2. package/package.json +13 -9
  3. package/src/cli/commands/build.ts +30 -48
  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/code.tsx +1 -10
  14. package/src/components/mdx/details.module.css +24 -1
  15. package/src/components/mdx/details.tsx +3 -2
  16. package/src/components/mdx/image.tsx +5 -20
  17. package/src/components/mdx/index.tsx +3 -3
  18. package/src/components/mdx/link.tsx +24 -20
  19. package/src/components/ui/footer.tsx +2 -3
  20. package/src/components/ui/search.tsx +116 -71
  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 +76 -57
  26. package/src/lib/source.ts +144 -96
  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 +18 -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 +52 -56
  40. package/src/server/entry-server.tsx +95 -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 +39 -0
  45. package/src/server/utils/safe-path.ts +17 -0
  46. package/src/server/vite-config.ts +50 -49
  47. package/src/themes/default/Layout.tsx +69 -41
  48. package/src/themes/default/Page.module.css +0 -60
  49. package/src/themes/default/Page.tsx +9 -11
  50. package/src/themes/default/Toc.tsx +30 -28
  51. package/src/themes/default/index.ts +7 -9
  52. package/src/themes/paper/ChapterNav.tsx +59 -39
  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 +11 -4
  56. package/src/themes/paper/Page.tsx +67 -47
  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/globals.d.ts +4 -0
  61. package/src/cli/__tests__/config.test.ts +0 -25
  62. package/src/cli/__tests__/scaffold.test.ts +0 -10
  63. package/src/pages/__tests__/head.test.tsx +0 -57
  64. package/src/server/__tests__/entry-server.test.tsx +0 -35
  65. package/src/server/__tests__/handlers.test.ts +0 -77
  66. package/src/server/__tests__/og.test.ts +0 -23
  67. package/src/server/__tests__/router.test.ts +0 -72
  68. package/src/server/__tests__/vite-config.test.ts +0 -25
  69. package/src/server/dev.ts +0 -156
  70. package/src/server/entry-prod.ts +0 -127
  71. package/src/server/handlers/apis-proxy.ts +0 -52
  72. package/src/server/handlers/health.ts +0 -3
  73. package/src/server/handlers/llms.ts +0 -58
  74. package/src/server/handlers/og.ts +0 -87
  75. package/src/server/handlers/robots.ts +0 -11
  76. package/src/server/handlers/search.ts +0 -140
  77. package/src/server/handlers/sitemap.ts +0 -39
  78. package/src/server/handlers/specs.ts +0 -9
  79. package/src/server/index.html +0 -12
  80. package/src/server/prod.ts +0 -18
  81. package/src/server/router.ts +0 -42
  82. package/src/themes/default/font.ts +0 -4
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,114 @@
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, PageTree } 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: PageTree;
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
34
  tree: { name: 'root', children: [] },
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: PageTree;
45
+ initialPage: PageData | null;
46
+ initialApiSpecs: ApiSpec[];
47
+ children: ReactNode;
48
+ }
49
+
50
+ async function loadMdxComponent(relativePath: string): Promise<ReactNode> {
51
+ const mod = await import(/* @vite-ignore */ `/.content/${relativePath}`);
52
+ return mod.default
53
+ ? React.createElement(mod.default, { components: mdxComponents })
54
+ : null;
44
55
  }
45
56
 
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)
57
+ export function PageProvider({
58
+ initialConfig,
59
+ initialTree,
60
+ initialPage,
61
+ initialApiSpecs,
62
+ children
63
+ }: PageProviderProps) {
64
+ const { pathname } = useLocation();
65
+ const [tree] = useState<PageTree>(initialTree);
66
+ const [page, setPage] = useState<PageData | null>(initialPage);
67
+ const [apiSpecs, setApiSpecs] = useState<ApiSpec[]>(initialApiSpecs);
68
+ const [currentPath, setCurrentPath] = useState(pathname);
52
69
 
53
70
  useEffect(() => {
54
- if (pathname === currentPath) return
55
- setCurrentPath(pathname)
71
+ if (pathname === currentPath) return;
72
+ setCurrentPath(pathname);
56
73
 
57
- let cancelled = false
74
+ const cancelled = { current: false };
58
75
 
59
76
  if (pathname.startsWith('/apis')) {
60
- // Fetch API specs if not already loaded
61
77
  if (apiSpecs.length === 0) {
62
78
  fetch('/api/specs')
63
- .then((res) => res.json())
64
- .then((specs) => { if (!cancelled) setApiSpecs(specs) })
65
- .catch(() => {})
79
+ .then(res => res.json())
80
+ .then(specs => {
81
+ if (!cancelled.current) setApiSpecs(specs);
82
+ })
83
+ .catch(() => {});
66
84
  }
67
- return () => { cancelled = true }
85
+ return () => { cancelled.current = true; };
68
86
  }
69
87
 
70
- async function load() {
71
- const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean)
88
+ const slug = pathname === '/'
89
+ ? []
90
+ : pathname.slice(1).split('/').filter(Boolean);
72
91
 
73
- const [sourcePage, newTree] = await Promise.all([getPage(slug), buildPageTree()])
74
- if (cancelled || !sourcePage) return
92
+ const apiPath = slug.length === 0 ? '/api/page/' : `/api/page/${slug.join('/')}`;
75
93
 
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,
94
+ fetch(apiPath)
95
+ .then(res => res.json())
96
+ .then(async (data: { frontmatter: Frontmatter; relativePath: string }) => {
97
+ if (cancelled.current) return;
98
+ const content = await loadMdxComponent(data.relativePath);
99
+ if (cancelled.current) return;
100
+ setPage({ slug, frontmatter: data.frontmatter, content });
84
101
  })
85
- }
86
- load()
87
- return () => { cancelled = true }
88
- }, [pathname])
102
+ .catch(() => {});
103
+
104
+ return () => { cancelled.current = true; };
105
+ }, [pathname]);
89
106
 
90
107
  return (
91
- <PageContext.Provider value={{ config: initialConfig, tree, page, apiSpecs }}>
108
+ <PageContext.Provider
109
+ value={{ config: initialConfig, tree, page, apiSpecs }}
110
+ >
92
111
  {children}
93
112
  </PageContext.Provider>
94
- )
113
+ );
95
114
  }
package/src/lib/source.ts CHANGED
@@ -1,138 +1,186 @@
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
- )
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { loader } from 'fumadocs-core/source';
4
+ import matter from 'gray-matter';
5
+ import type { MDXContent } from 'mdx/types';
6
+ import type { Frontmatter, PageTree, PageTreeItem } from '@/types';
12
7
 
13
8
  export interface SourcePage {
14
- url: string
15
- slugs: string[]
16
- filePath: string
17
- frontmatter: Frontmatter
9
+ url: string;
10
+ slugs: string[];
11
+ filePath: string;
12
+ frontmatter: Frontmatter;
18
13
  }
19
14
 
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
31
- }
32
- }
33
- return first.slice(0, depth).join('/') + '/'
15
+ function getContentDir(): string {
16
+ return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
34
17
  }
35
18
 
36
- const prefix = computePrefix(Object.keys(meta))
19
+ async function scanFiles(contentDir: string) {
20
+ const files: {
21
+ type: 'page' | 'meta';
22
+ path: string;
23
+ data: Record<string, unknown>;
24
+ }[] = [];
25
+
26
+ async function scan(dir: string, prefix: string[] = []) {
27
+ try {
28
+ const entries = await fs.readdir(dir, { withFileTypes: true });
29
+ for (const entry of entries) {
30
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
31
+ continue;
32
+ const fullPath = path.join(dir, entry.name);
33
+ const relativePath = [...prefix, entry.name].join('/');
34
+
35
+ if (entry.isDirectory()) {
36
+ await scan(fullPath, [...prefix, entry.name]);
37
+ continue;
38
+ }
39
+
40
+ if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {
41
+ const raw = await fs.readFile(fullPath, 'utf-8');
42
+ const { data } = matter(raw);
43
+ files.push({
44
+ type: 'page',
45
+ path: relativePath,
46
+ data: { ...data, _absolutePath: fullPath }
47
+ });
48
+ } else if (entry.name === 'meta.json' || entry.name === 'meta.yaml') {
49
+ const raw = await fs.readFile(fullPath, 'utf-8');
50
+ const data = entry.name.endsWith('.json')
51
+ ? JSON.parse(raw)
52
+ : matter(raw).data;
53
+ files.push({ type: 'meta', path: relativePath, data });
54
+ }
55
+ }
56
+ } catch {
57
+ /* directory not readable */
58
+ }
59
+ }
37
60
 
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
61
+ await scan(contentDir);
62
+ return files;
44
63
  }
45
64
 
46
- function slugsToUrl(slugs: string[]): string {
47
- return slugs.length === 0 ? '/' : '/' + slugs.join('/')
65
+ let cachedSource: ReturnType<typeof loader> | null = null;
66
+ let cachedPages: SourcePage[] | null = null;
67
+
68
+ async function getSource() {
69
+ if (cachedSource) return cachedSource;
70
+ const contentDir = getContentDir();
71
+ const files = await scanFiles(contentDir);
72
+ cachedSource = loader({
73
+ source: { files },
74
+ baseUrl: '/'
75
+ });
76
+ return cachedSource;
48
77
  }
49
78
 
50
- let cachedPages: SourcePage[] | null = null
79
+ export function invalidate() {
80
+ cachedSource = null;
81
+ cachedPages = null;
82
+ }
51
83
 
52
84
  export async function getPages(): Promise<SourcePage[]> {
53
- if (cachedPages) return cachedPages
85
+ if (cachedPages) return cachedPages;
54
86
 
55
- cachedPages = Object.entries(meta).map(([filePath, fm]) => {
56
- const slugs = filePathToSlugs(filePath)
57
- const baseName = slugs[slugs.length - 1] ?? 'index'
87
+ const s = await getSource();
88
+ cachedPages = s.getPages().map(page => {
89
+ const data = page.data as Record<string, unknown>;
58
90
  return {
59
- url: slugsToUrl(slugs),
60
- slugs,
61
- filePath,
91
+ url: page.url,
92
+ slugs: page.slugs,
93
+ filePath: (data._absolutePath as string) ?? '',
62
94
  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
- })
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
+ });
71
106
 
72
- return cachedPages
107
+ return cachedPages;
73
108
  }
74
109
 
75
110
  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
79
- }
80
-
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
111
+ const pages = await getPages();
112
+ const targetUrl = !slug || slug.length === 0 ? '/' : `/${slug.join('/')}`;
113
+ return pages.find(p => p.url === targetUrl) ?? null;
86
114
  }
87
115
 
88
- export function invalidate() {
89
- cachedPages = null
116
+ export async function loadPageComponent(
117
+ page: SourcePage
118
+ ): Promise<MDXContent | null> {
119
+ if (!page.filePath) return null;
120
+ try {
121
+ await fs.access(page.filePath);
122
+ } catch {
123
+ return null;
124
+ }
125
+ const contentDir = getContentDir();
126
+ const relativePath = path.relative(contentDir, page.filePath);
127
+ const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
128
+ const mod = relativePath.endsWith('.md')
129
+ ? await import(`@content/${withoutExt}.md`)
130
+ : await import(`@content/${withoutExt}.mdx`);
131
+ return mod.default;
90
132
  }
91
133
 
92
134
  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 === '/'
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 === '/';
99
143
  const item: PageTreeItem = {
100
144
  type: 'page',
101
- name: page.frontmatter.title,
145
+ name: (data.title as string) ?? page.slugs.join('/') ?? 'Untitled',
102
146
  url: page.url,
103
- order: page.frontmatter.order ?? (isIndex ? 0 : undefined),
104
- }
147
+ order: (data.order as number | undefined) ?? (isIndex ? 0 : undefined)
148
+ };
105
149
 
106
150
  if (page.slugs.length > 1) {
107
- const folder = page.slugs[0]
151
+ const folder = page.slugs[0];
108
152
  if (!folders.has(folder)) {
109
- folders.set(folder, [])
153
+ folders.set(folder, []);
110
154
  }
111
- folders.get(folder)?.push(item)
155
+ folders.get(folder)?.push(item);
112
156
  } else {
113
- rootPages.push(item)
157
+ rootPages.push(item);
114
158
  }
115
- })
159
+ }
116
160
 
117
161
  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)
121
-
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
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;
127
175
  folderItems.push({
128
176
  type: 'folder',
129
- name: folder.charAt(0).toUpperCase() + folder.slice(1),
177
+ name: `${folder.charAt(0).toUpperCase()}${folder.slice(1)}`,
130
178
  order: folderOrder,
131
- children: sorted,
132
- })
133
- })
179
+ children: sorted
180
+ });
181
+ }
134
182
 
135
- children.push(...sortByOrder(folderItems))
183
+ children.push(...sortByOrder(folderItems));
136
184
 
137
- return { name: 'root', children }
185
+ return { name: 'root', children };
138
186
  }
@@ -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
  }