@raystack/chronicle 0.3.0 → 0.5.0

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 (84) hide show
  1. package/dist/cli/index.js +425 -9937
  2. package/package.json +19 -10
  3. package/src/cli/commands/build.ts +33 -31
  4. package/src/cli/commands/dev.ts +23 -31
  5. package/src/cli/commands/init.ts +38 -132
  6. package/src/cli/commands/serve.ts +41 -55
  7. package/src/cli/commands/start.ts +20 -31
  8. package/src/cli/index.ts +14 -14
  9. package/src/cli/utils/config.ts +58 -30
  10. package/src/cli/utils/index.ts +3 -3
  11. package/src/cli/utils/resolve.ts +7 -3
  12. package/src/cli/utils/scaffold.ts +11 -130
  13. package/src/components/mdx/code.tsx +10 -1
  14. package/src/components/mdx/details.module.css +1 -26
  15. package/src/components/mdx/details.tsx +2 -3
  16. package/src/components/mdx/image.tsx +5 -34
  17. package/src/components/mdx/index.tsx +15 -1
  18. package/src/components/mdx/link.tsx +18 -15
  19. package/src/components/ui/breadcrumbs.tsx +8 -42
  20. package/src/components/ui/search.tsx +63 -51
  21. package/src/lib/api-routes.ts +6 -8
  22. package/src/lib/config.ts +12 -36
  23. package/src/lib/head.tsx +49 -0
  24. package/src/lib/openapi.ts +8 -8
  25. package/src/lib/page-context.tsx +111 -0
  26. package/src/lib/remark-strip-md-extensions.ts +14 -0
  27. package/src/lib/source.ts +139 -63
  28. package/src/pages/ApiLayout.tsx +33 -0
  29. package/src/pages/ApiPage.tsx +73 -0
  30. package/src/pages/DocsLayout.tsx +18 -0
  31. package/src/pages/DocsPage.tsx +43 -0
  32. package/src/pages/NotFound.tsx +17 -0
  33. package/src/server/App.tsx +72 -0
  34. package/src/server/api/apis-proxy.ts +69 -0
  35. package/src/server/api/health.ts +5 -0
  36. package/src/server/api/page/[...slug].ts +18 -0
  37. package/src/server/api/search.ts +118 -0
  38. package/src/server/api/specs.ts +9 -0
  39. package/src/server/build-search-index.ts +117 -0
  40. package/src/server/entry-client.tsx +88 -0
  41. package/src/server/entry-server.tsx +102 -0
  42. package/src/server/routes/llms.txt.ts +21 -0
  43. package/src/server/routes/og.tsx +75 -0
  44. package/src/server/routes/robots.txt.ts +11 -0
  45. package/src/server/routes/sitemap.xml.ts +40 -0
  46. package/src/server/utils/safe-path.ts +17 -0
  47. package/src/server/vite-config.ts +133 -0
  48. package/src/themes/default/Layout.tsx +78 -48
  49. package/src/themes/default/Page.module.css +44 -0
  50. package/src/themes/default/Page.tsx +9 -11
  51. package/src/themes/default/Toc.tsx +25 -39
  52. package/src/themes/default/index.ts +7 -9
  53. package/src/themes/paper/ChapterNav.tsx +64 -45
  54. package/src/themes/paper/Layout.module.css +1 -1
  55. package/src/themes/paper/Layout.tsx +24 -12
  56. package/src/themes/paper/Page.module.css +16 -4
  57. package/src/themes/paper/Page.tsx +56 -63
  58. package/src/themes/paper/ReadingProgress.tsx +160 -139
  59. package/src/themes/paper/index.ts +5 -5
  60. package/src/themes/registry.ts +14 -7
  61. package/src/types/config.ts +86 -67
  62. package/src/types/content.ts +5 -21
  63. package/src/types/globals.d.ts +4 -0
  64. package/src/types/theme.ts +4 -3
  65. package/tsconfig.json +2 -3
  66. package/next.config.mjs +0 -10
  67. package/source.config.ts +0 -51
  68. package/src/app/[[...slug]]/layout.tsx +0 -15
  69. package/src/app/[[...slug]]/page.tsx +0 -106
  70. package/src/app/api/apis-proxy/route.ts +0 -59
  71. package/src/app/api/health/route.ts +0 -3
  72. package/src/app/api/search/route.ts +0 -90
  73. package/src/app/apis/[[...slug]]/layout.tsx +0 -26
  74. package/src/app/apis/[[...slug]]/page.tsx +0 -117
  75. package/src/app/layout.tsx +0 -57
  76. package/src/app/llms-full.txt/route.ts +0 -18
  77. package/src/app/llms.txt/route.ts +0 -15
  78. package/src/app/og/route.tsx +0 -62
  79. package/src/app/providers.tsx +0 -8
  80. package/src/app/robots.ts +0 -10
  81. package/src/app/sitemap.ts +0 -29
  82. package/src/cli/utils/process.ts +0 -7
  83. package/src/themes/default/font.ts +0 -6
  84. /package/src/{app/apis/[[...slug]]/layout.module.css → pages/ApiLayout.module.css} +0 -0
@@ -1,43 +1,71 @@
1
- import fs from 'fs'
2
- import path from 'path'
3
- import { parse } from 'yaml'
4
- import chalk from 'chalk'
5
- import type { ChronicleConfig } from '@/types'
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { parse } from 'yaml';
5
+ import { chronicleConfigSchema, type ChronicleConfig } from '@/types';
6
6
 
7
7
  export interface CLIConfig {
8
- config: ChronicleConfig
9
- configPath: string
10
- contentDir: string
8
+ config: ChronicleConfig;
9
+ configPath: string;
10
+ contentDir: string;
11
+ preset?: string;
11
12
  }
12
13
 
13
- export function resolveContentDir(contentFlag?: string): string {
14
- if (contentFlag) return path.resolve(contentFlag)
15
- if (process.env.CHRONICLE_CONTENT_DIR) return path.resolve(process.env.CHRONICLE_CONTENT_DIR)
16
- return path.resolve('content')
14
+ export function resolveConfigPath(configPath?: string): string | undefined {
15
+ if (configPath) return path.resolve(configPath);
16
+ return undefined;
17
17
  }
18
18
 
19
- function resolveConfigPath(contentDir: string): string | null {
20
- const cwdPath = path.join(process.cwd(), 'chronicle.yaml')
21
- if (fs.existsSync(cwdPath)) return cwdPath
22
- const contentPath = path.join(contentDir, 'chronicle.yaml')
23
- if (fs.existsSync(contentPath)) return contentPath
24
- return null
19
+ async function readConfig(configPath: string): Promise<string> {
20
+ return fs.readFile(configPath, 'utf-8').catch((error: NodeJS.ErrnoException) => {
21
+ if (error.code === 'ENOENT') {
22
+ console.log(chalk.red(`Error: chronicle.yaml not found at '${configPath}'`));
23
+ console.log(chalk.gray("Run 'chronicle init' to create one"));
24
+ } else {
25
+ console.log(chalk.red(`Error: Failed to read '${configPath}'`));
26
+ console.log(chalk.gray(error.message));
27
+ }
28
+ process.exit(1);
29
+ });
25
30
  }
26
31
 
27
- export function loadCLIConfig(contentDir: string): CLIConfig {
28
- const configPath = resolveConfigPath(contentDir)
32
+ function validateConfig(raw: string, configPath: string): ChronicleConfig {
33
+ const parsed = parse(raw);
34
+ const result = chronicleConfigSchema.safeParse(parsed);
29
35
 
30
- if (!configPath) {
31
- console.log(chalk.red(`Error: chronicle.yaml not found in '${process.cwd()}' or '${contentDir}'`))
32
- console.log(chalk.gray(`Run 'chronicle init' to create one`))
33
- process.exit(1)
36
+ if (!result.success) {
37
+ console.log(chalk.red(`Error: Invalid chronicle.yaml at '${configPath}'`));
38
+ for (const issue of result.error.issues) {
39
+ const path = issue.path.join('.');
40
+ console.log(chalk.gray(` ${path ? `${path}: ` : ''}${issue.message}`));
41
+ }
42
+ process.exit(1);
34
43
  }
35
44
 
36
- const config = parse(fs.readFileSync(configPath, 'utf-8')) as ChronicleConfig
45
+ return result.data;
46
+ }
37
47
 
38
- return {
39
- config,
40
- configPath,
41
- contentDir,
42
- }
48
+ export function resolveContentDir(config: ChronicleConfig, configPath: string, contentFlag?: string): string {
49
+ if (contentFlag) return path.resolve(contentFlag);
50
+ if (config.content) return path.resolve(path.dirname(configPath), config.content);
51
+ return path.resolve('content');
52
+ }
53
+
54
+ export function resolvePreset(config: ChronicleConfig, presetFlag?: string): string | undefined {
55
+ return presetFlag ?? config.preset;
56
+ }
57
+
58
+ export async function loadCLIConfig(
59
+ configPath?: string,
60
+ options?: { content?: string; preset?: string }
61
+ ): Promise<CLIConfig> {
62
+ const resolvedConfigPath = resolveConfigPath(configPath)
63
+ ?? path.join(process.cwd(), 'chronicle.yaml');
64
+
65
+ const raw = await readConfig(resolvedConfigPath);
66
+ const config = validateConfig(raw, resolvedConfigPath);
67
+ const contentDir = resolveContentDir(config, resolvedConfigPath, options?.content);
68
+ const preset = resolvePreset(config, options?.preset);
69
+
70
+ return { config, configPath: resolvedConfigPath, contentDir, preset };
43
71
  }
@@ -1,3 +1,3 @@
1
- export * from './config'
2
- export * from './process'
3
- export * from './scaffold'
1
+ export * from './config';
2
+ export * from './resolve';
3
+ export * from './scaffold';
@@ -1,6 +1,10 @@
1
- import path from 'path'
2
- import { fileURLToPath } from 'url'
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
3
 
4
4
  // After bundling: dist/cli/index.js → ../.. = package root
5
5
  // After install: node_modules/@raystack/chronicle/dist/cli/index.js → ../.. = package root
6
- export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
6
+ export const PACKAGE_ROOT = path.resolve(
7
+ path.dirname(fileURLToPath(import.meta.url)),
8
+ '..',
9
+ '..'
10
+ );
@@ -1,137 +1,18 @@
1
- import { execSync } from 'child_process'
2
- import { createRequire } from 'module'
3
- import fs from 'fs'
4
- import path from 'path'
5
- import chalk from 'chalk'
6
- import { PACKAGE_ROOT } from './resolve'
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { PACKAGE_ROOT } from './resolve';
7
4
 
8
- const COPY_FILES = ['src', 'source.config.ts', 'tsconfig.json']
5
+ export async function linkContent(contentDir: string): Promise<void> {
6
+ const linkPath = path.join(PACKAGE_ROOT, '.content');
7
+ const target = path.resolve(contentDir);
9
8
 
10
- function copyRecursive(src: string, dest: string) {
11
- const stat = fs.statSync(src)
12
- if (stat.isDirectory()) {
13
- fs.mkdirSync(dest, { recursive: true })
14
- for (const entry of fs.readdirSync(src)) {
15
- copyRecursive(path.join(src, entry), path.join(dest, entry))
16
- }
17
- } else {
18
- fs.copyFileSync(src, dest)
19
- }
20
- }
21
-
22
- function ensureRemoved(targetPath: string) {
23
9
  try {
24
- fs.lstatSync(targetPath)
25
- fs.rmSync(targetPath, { recursive: true, force: true })
10
+ const existing = await fs.readlink(linkPath);
11
+ if (existing === target) return;
12
+ await fs.unlink(linkPath);
26
13
  } catch {
27
- // nothing exists, proceed
28
- }
29
- }
30
-
31
- export function detectPackageManager(): string {
32
- if (process.env.npm_config_user_agent) {
33
- return process.env.npm_config_user_agent.split('/')[0]
34
- }
35
- const cwd = process.cwd()
36
- if (fs.existsSync(path.join(cwd, 'bun.lock')) || fs.existsSync(path.join(cwd, 'bun.lockb'))) return 'bun'
37
- if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
38
- if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn'
39
- return 'npm'
40
- }
41
-
42
- function generateNextConfig(scaffoldPath: string) {
43
- const config = `import { createMDX } from 'fumadocs-mdx/next'
44
-
45
- const withMDX = createMDX()
46
-
47
- /** @type {import('next').NextConfig} */
48
- const nextConfig = {
49
- reactStrictMode: true,
50
- }
51
-
52
- export default withMDX(nextConfig)
53
- `
54
- fs.writeFileSync(path.join(scaffoldPath, 'next.config.mjs'), config)
55
- }
56
-
57
- function createPackageJson(): Record<string, unknown> {
58
- return {
59
- name: 'chronicle-docs',
60
- private: true,
61
- dependencies: {
62
- '@raystack/chronicle': `^${getChronicleVersion()}`,
63
- },
64
- devDependencies: {
65
- '@raystack/tools-config': '0.56.0',
66
- 'openapi-types': '^12.1.3',
67
- typescript: '5.9.3',
68
- '@types/react': '^19.2.10',
69
- '@types/node': '^25.1.0',
70
- },
71
- }
72
- }
73
-
74
- function ensureDeps() {
75
- const cwd = process.cwd()
76
- const cwdPkgJson = path.join(cwd, 'package.json')
77
- const cwdNodeModules = path.join(cwd, 'node_modules')
78
-
79
- if (fs.existsSync(cwdPkgJson) && fs.existsSync(cwdNodeModules)) {
80
- // Case 1: existing project with deps installed
81
- return
14
+ // link doesn't exist
82
15
  }
83
16
 
84
- // Case 2: no package.json — create in cwd and install
85
- if (!fs.existsSync(cwdPkgJson)) {
86
- fs.writeFileSync(cwdPkgJson, JSON.stringify(createPackageJson(), null, 2) + '\n')
87
- }
88
-
89
- if (!fs.existsSync(cwdNodeModules)) {
90
- const pm = detectPackageManager()
91
- console.log(chalk.cyan(`Installing dependencies with ${pm}...`))
92
- execSync(`${pm} install`, { cwd, stdio: 'inherit' })
93
- }
94
- }
95
-
96
- export function getChronicleVersion(): string {
97
- const pkgPath = path.join(PACKAGE_ROOT, 'package.json')
98
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
99
- return pkg.version
100
- }
101
-
102
- export function resolveNextCli(): string {
103
- const chronicleRequire = createRequire(path.join(PACKAGE_ROOT, 'package.json'))
104
- return chronicleRequire.resolve('next/dist/bin/next')
105
- }
106
-
107
- export function scaffoldDir(contentDir: string): string {
108
- const scaffoldPath = path.join(process.cwd(), '.chronicle')
109
-
110
- // Create .chronicle/ if not exists
111
- if (!fs.existsSync(scaffoldPath)) {
112
- fs.mkdirSync(scaffoldPath, { recursive: true })
113
- }
114
-
115
- // Copy package files
116
- for (const name of COPY_FILES) {
117
- const src = path.join(PACKAGE_ROOT, name)
118
- const dest = path.join(scaffoldPath, name)
119
- ensureRemoved(dest)
120
- copyRecursive(src, dest)
121
- }
122
-
123
- // Generate next.config.mjs
124
- generateNextConfig(scaffoldPath)
125
-
126
- // Symlink content dir
127
- const contentLink = path.join(scaffoldPath, 'content')
128
- ensureRemoved(contentLink)
129
- fs.symlinkSync(path.resolve(contentDir), contentLink)
130
-
131
- // Ensure dependencies are available
132
- ensureDeps()
133
-
134
- console.log(chalk.gray(`Scaffold: ${scaffoldPath}`))
135
-
136
- return scaffoldPath
17
+ await fs.symlink(target, linkPath);
137
18
  }
@@ -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,38 +1,9 @@
1
- 'use client'
1
+ import type { ComponentProps } from 'react';
2
2
 
3
- import NextImage from 'next/image'
4
- import type { ComponentProps } from 'react'
3
+ type ImageProps = ComponentProps<'img'>;
5
4
 
6
- type ImageProps = Omit<ComponentProps<'img'>, 'src'> & {
7
- src?: string
8
- width?: number | string
9
- height?: number | string
10
- }
11
-
12
- export function Image({ src, alt, width, height, ...props }: ImageProps) {
13
- if (!src || typeof src !== 'string') return null
14
-
15
- const isExternal = src.startsWith('http://') || src.startsWith('https://')
16
-
17
- if (isExternal) {
18
- return (
19
- // eslint-disable-next-line @next/next/no-img-element
20
- <img
21
- src={src}
22
- alt={alt ?? ''}
23
- width={width}
24
- height={height}
25
- {...props}
26
- />
27
- )
28
- }
5
+ export function Image({ src, alt, ...props }: ImageProps) {
6
+ if (!src) return null;
29
7
 
30
- return (
31
- <NextImage
32
- src={src}
33
- alt={alt ?? ''}
34
- width={typeof width === 'string' ? parseInt(width, 10) : (width ?? 800)}
35
- height={typeof height === 'string' ? parseInt(height, 10) : (height ?? 400)}
36
- />
37
- )
8
+ return <img src={src} alt={alt ?? ''} loading='lazy' {...props} />;
38
9
  }
@@ -8,6 +8,20 @@ import { Mermaid } from './mermaid'
8
8
  import { MdxParagraph } from './paragraph'
9
9
  import { CalloutContainer, CalloutTitle, CalloutDescription, MdxBlockquote } from '@/components/common/callout'
10
10
  import { Tabs } from '@raystack/apsara'
11
+ import { type ComponentProps, useEffect, useState } from 'react'
12
+
13
+ function ClientOnly({ children }: { children: React.ReactNode }) {
14
+ const [mounted, setMounted] = useState(false)
15
+ useEffect(() => setMounted(true), [])
16
+ return mounted ? <>{children}</> : null
17
+ }
18
+
19
+ function MdxTabs(props: ComponentProps<typeof Tabs>) {
20
+ return <ClientOnly><Tabs {...props} /></ClientOnly>
21
+ }
22
+ MdxTabs.List = Tabs.List
23
+ MdxTabs.Trigger = Tabs.Trigger
24
+ MdxTabs.Content = Tabs.Content
11
25
 
12
26
  export const mdxComponents: MDXComponents = {
13
27
  p: MdxParagraph,
@@ -27,7 +41,7 @@ export const mdxComponents: MDXComponents = {
27
41
  Callout: CalloutContainer,
28
42
  CalloutTitle,
29
43
  CalloutDescription,
30
- Tabs,
44
+ Tabs: MdxTabs,
31
45
  Mermaid,
32
46
  }
33
47
 
@@ -1,38 +1,41 @@
1
- 'use client'
1
+ import { Link as ApsaraLink } from '@raystack/apsara';
2
+ import type { ComponentProps } from 'react';
3
+ import { Link as RouterLink } from 'react-router';
2
4
 
3
- import NextLink from 'next/link'
4
- import { Link as ApsaraLink } from '@raystack/apsara'
5
- import type { ComponentProps } from 'react'
6
-
7
- type LinkProps = ComponentProps<'a'>
5
+ type LinkProps = ComponentProps<'a'>;
8
6
 
9
7
  export function Link({ href, children, ...props }: LinkProps) {
10
8
  if (!href) {
11
- return <span {...props}>{children}</span>
9
+ return <span {...props}>{children}</span>;
12
10
  }
13
11
 
14
- const isExternal = href.startsWith('http://') || href.startsWith('https://')
15
- const isAnchor = href.startsWith('#')
12
+ const isExternal = href.startsWith('http://') || href.startsWith('https://');
13
+ const isAnchor = href.startsWith('#');
16
14
 
17
15
  if (isAnchor) {
18
16
  return (
19
17
  <ApsaraLink href={href} {...props}>
20
18
  {children}
21
19
  </ApsaraLink>
22
- )
20
+ );
23
21
  }
24
22
 
25
23
  if (isExternal) {
26
24
  return (
27
- <ApsaraLink href={href} target="_blank" rel="noopener noreferrer" {...props}>
25
+ <ApsaraLink
26
+ href={href}
27
+ target='_blank'
28
+ rel='noopener noreferrer'
29
+ {...props}
30
+ >
28
31
  {children}
29
32
  </ApsaraLink>
30
- )
33
+ );
31
34
  }
32
35
 
33
36
  return (
34
- <NextLink href={href} className={props.className}>
37
+ <RouterLink to={href} className={props.className}>
35
38
  {children}
36
- </NextLink>
37
- )
39
+ </RouterLink>
40
+ );
38
41
  }
@@ -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]