@raystack/chronicle 0.1.0-canary.5a730d4 → 0.1.0-canary.6511afe

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 +165 -414
  2. package/package.json +13 -10
  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 -72
  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 +64 -46
  47. package/src/themes/default/Layout.tsx +69 -41
  48. package/src/themes/default/Page.module.css +0 -56
  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
@@ -1,20 +1,18 @@
1
- import fs from 'fs'
2
- import path from 'path'
3
- import { PACKAGE_ROOT } from './resolve'
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { PACKAGE_ROOT } from './resolve';
4
4
 
5
- export function detectPackageManager(): string {
6
- if (process.env.npm_config_user_agent) {
7
- return process.env.npm_config_user_agent.split('/')[0]
5
+ export async function linkContent(contentDir: string): Promise<void> {
6
+ const linkPath = path.join(PACKAGE_ROOT, '.content');
7
+ const target = path.resolve(contentDir);
8
+
9
+ try {
10
+ const existing = await fs.readlink(linkPath);
11
+ if (existing === target) return;
12
+ await fs.unlink(linkPath);
13
+ } catch {
14
+ // link doesn't exist
8
15
  }
9
- const cwd = process.cwd()
10
- if (fs.existsSync(path.join(cwd, 'bun.lock')) || fs.existsSync(path.join(cwd, 'bun.lockb'))) return 'bun'
11
- if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
12
- if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn'
13
- return 'npm'
14
- }
15
16
 
16
- export function getChronicleVersion(): string {
17
- const pkgPath = path.join(PACKAGE_ROOT, 'package.json')
18
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
19
- return pkg.version
17
+ await fs.symlink(target, linkPath);
20
18
  }
@@ -1,7 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { type ComponentProps, isValidElement, Children } from 'react'
4
- import { Mermaid } from './mermaid'
3
+ import type { ComponentProps } from 'react'
5
4
  import styles from './code.module.css'
6
5
 
7
6
  type PreProps = ComponentProps<'pre'> & {
@@ -17,14 +16,6 @@ export function MdxCode({ children, className, ...props }: ComponentProps<'code'
17
16
  }
18
17
 
19
18
  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
-
28
19
  return (
29
20
  <div className={styles.codeBlock}>
30
21
  {title && <div className={styles.codeHeader}>{title}</div>}
@@ -4,9 +4,32 @@
4
4
  margin: var(--rs-space-5) 0;
5
5
  }
6
6
 
7
- .trigger {
7
+ .summary {
8
+ padding: var(--rs-space-4) var(--rs-space-5);
9
+ cursor: pointer;
8
10
  font-weight: 500;
9
11
  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);
10
33
  }
11
34
 
12
35
  .content {
@@ -1,8 +1,9 @@
1
1
  import type { ComponentProps } from 'react'
2
+ import styles from './details.module.css'
2
3
 
3
4
  export function MdxDetails({ children, className, ...props }: ComponentProps<'details'>) {
4
5
  return (
5
- <details className={className} {...props}>
6
+ <details className={`${styles.details} ${className ?? ''}`} {...props}>
6
7
  {children}
7
8
  </details>
8
9
  )
@@ -10,7 +11,7 @@ export function MdxDetails({ children, className, ...props }: ComponentProps<'de
10
11
 
11
12
  export function MdxSummary({ children, className, ...props }: ComponentProps<'summary'>) {
12
13
  return (
13
- <summary className={className} {...props}>
14
+ <summary className={`${styles.summary} ${className ?? ''}`} {...props}>
14
15
  {children}
15
16
  </summary>
16
17
  )
@@ -1,24 +1,9 @@
1
- 'use client'
1
+ import type { ComponentProps } from 'react';
2
2
 
3
- import type { ComponentProps } from 'react'
3
+ type ImageProps = ComponentProps<'img'>;
4
4
 
5
- type ImageProps = Omit<ComponentProps<'img'>, 'src'> & {
6
- src?: string
7
- width?: number | string
8
- height?: number | string
9
- }
10
-
11
- export function Image({ src, alt, width, height, ...props }: ImageProps) {
12
- if (!src || typeof src !== 'string') return null
5
+ export function Image({ src, alt, ...props }: ImageProps) {
6
+ if (!src) return null;
13
7
 
14
- return (
15
- <img
16
- src={src}
17
- alt={alt ?? ''}
18
- width={width}
19
- height={height}
20
- loading="lazy"
21
- {...props}
22
- />
23
- )
8
+ return <img src={src} alt={alt ?? ''} loading='lazy' {...props} />;
24
9
  }
@@ -1,6 +1,6 @@
1
1
  import type { MDXComponents } from 'mdx/types'
2
2
  import { Image } from './image'
3
- import { MdxLink } from './link'
3
+ import { Link } from './link'
4
4
  import { MdxTable, MdxThead, MdxTbody, MdxTr, MdxTh, MdxTd } from './table'
5
5
  import { MdxPre, MdxCode } from './code'
6
6
  import { MdxDetails, MdxSummary } from './details'
@@ -12,7 +12,7 @@ import { Tabs } from '@raystack/apsara'
12
12
  export const mdxComponents: MDXComponents = {
13
13
  p: MdxParagraph,
14
14
  img: Image,
15
- a: MdxLink,
15
+ a: Link,
16
16
  table: MdxTable,
17
17
  thead: MdxThead,
18
18
  tbody: MdxTbody,
@@ -32,4 +32,4 @@ export const mdxComponents: MDXComponents = {
32
32
  }
33
33
 
34
34
  export { Image } from './image'
35
- export { MdxLink } from './link'
35
+ export { Link } from './link'
@@ -1,37 +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 { Link } from 'react-router-dom'
4
- import type { ComponentProps } from 'react'
5
+ type LinkProps = ComponentProps<'a'>;
5
6
 
6
- type LinkProps = ComponentProps<'a'>
7
-
8
- export function MdxLink({ href, children, ...props }: LinkProps) {
7
+ export function Link({ href, children, ...props }: LinkProps) {
9
8
  if (!href) {
10
- return <span {...props}>{children}</span>
9
+ return <span {...props}>{children}</span>;
11
10
  }
12
11
 
13
- const isExternal = href.startsWith('http://') || href.startsWith('https://')
14
- const isAnchor = href.startsWith('#')
12
+ const isExternal = href.startsWith('http://') || href.startsWith('https://');
13
+ const isAnchor = href.startsWith('#');
15
14
 
16
- if (isExternal) {
15
+ if (isAnchor) {
17
16
  return (
18
- <a href={href} target="_blank" rel="noopener noreferrer" {...props}>
17
+ <ApsaraLink href={href} {...props}>
19
18
  {children}
20
- </a>
21
- )
19
+ </ApsaraLink>
20
+ );
22
21
  }
23
22
 
24
- if (isAnchor) {
23
+ if (isExternal) {
25
24
  return (
26
- <a href={href} {...props}>
25
+ <ApsaraLink
26
+ href={href}
27
+ target='_blank'
28
+ rel='noopener noreferrer'
29
+ {...props}
30
+ >
27
31
  {children}
28
- </a>
29
- )
32
+ </ApsaraLink>
33
+ );
30
34
  }
31
35
 
32
36
  return (
33
- <Link to={href} className={props.className}>
37
+ <RouterLink to={href} className={props.className}>
34
38
  {children}
35
- </Link>
36
- )
39
+ </RouterLink>
40
+ );
37
41
  }
@@ -1,5 +1,4 @@
1
- import { Link } from "react-router-dom";
2
- import { Flex, Text } from "@raystack/apsara";
1
+ import { Flex, Link, Text } from "@raystack/apsara";
3
2
  import type { FooterConfig } from "@/types";
4
3
  import styles from "./footer.module.css";
5
4
 
@@ -19,7 +18,7 @@ export function Footer({ config }: FooterProps) {
19
18
  {config?.links && config.links.length > 0 && (
20
19
  <Flex gap="medium" className={styles.links}>
21
20
  {config.links.map((link) => (
22
- <Link key={link.href} to={link.href} className={styles.link}>
21
+ <Link key={link.href} href={link.href} className={styles.link}>
23
22
  {link.label}
24
23
  </Link>
25
24
  ))}
@@ -1,51 +1,19 @@
1
- "use client";
2
-
3
- import { useState, useEffect, useCallback, useRef } from "react";
4
- import { useNavigate } from "react-router-dom";
5
- import { Button, Command, Dialog, Text } from "@raystack/apsara";
6
- import { cx } from "class-variance-authority";
7
- import { DocumentIcon, HashtagIcon } from "@heroicons/react/24/outline";
8
- import { isMacOs } from "react-device-detect";
9
- import { MethodBadge } from "@/components/api/method-badge";
10
- import styles from "./search.module.css";
11
-
12
- interface SearchResult {
13
- id: string;
14
- url: string;
15
- type: "page" | "api";
16
- content: string;
17
- }
18
-
19
- function useSearch(query: string) {
20
- const [results, setResults] = useState<SearchResult[]>([]);
21
- const [isLoading, setIsLoading] = useState(false);
22
- const timerRef = useRef<ReturnType<typeof setTimeout>>();
23
-
24
- useEffect(() => {
25
- clearTimeout(timerRef.current);
26
- timerRef.current = setTimeout(async () => {
27
- setIsLoading(true);
28
- try {
29
- const params = new URLSearchParams();
30
- if (query) params.set("query", query);
31
- const res = await fetch(`/api/search?${params}`);
32
- setResults(await res.json());
33
- } catch {
34
- setResults([]);
35
- }
36
- setIsLoading(false);
37
- }, 100);
38
- return () => clearTimeout(timerRef.current);
39
- }, [query]);
40
-
41
- return { results, isLoading };
42
- }
1
+ import { DocumentIcon, HashtagIcon } from '@heroicons/react/24/outline';
2
+ import { Button, Command, Dialog, Text } from '@raystack/apsara';
3
+ import { cx } from 'class-variance-authority';
4
+ import type { SortedResult } from 'fumadocs-core/search';
5
+ import { useDocsSearch } from 'fumadocs-core/search/client';
6
+ import { useCallback, useEffect, useState } from 'react';
7
+ import { useNavigate } from 'react-router';
8
+ import { MethodBadge } from '@/components/api/method-badge';
9
+ import styles from './search.module.css';
43
10
 
44
11
  function SearchShortcutKey({ className }: { className?: string }) {
45
- const [key, setKey] = useState("\u2318");
12
+ const [key, setKey] = useState('⌘');
46
13
 
47
14
  useEffect(() => {
48
- setKey(isMacOs ? "\u2318" : "Ctrl");
15
+ const isMac = navigator.platform?.toUpperCase().includes('MAC');
16
+ setKey(isMac ? '⌘' : 'Ctrl');
49
17
  }, []);
50
18
 
51
19
  return (
@@ -62,35 +30,44 @@ interface SearchProps {
62
30
  export function Search({ className }: SearchProps) {
63
31
  const [open, setOpen] = useState(false);
64
32
  const navigate = useNavigate();
65
- const [search, setSearch] = useState("");
66
- const { results, isLoading } = useSearch(search);
33
+
34
+ const { search, setSearch, query } = useDocsSearch({
35
+ type: 'fetch',
36
+ api: '/api/search',
37
+ delayMs: 100,
38
+ allowEmpty: true
39
+ });
67
40
 
68
41
  const onSelect = useCallback(
69
42
  (url: string) => {
70
43
  setOpen(false);
71
44
  navigate(url);
72
45
  },
73
- [navigate],
46
+ [navigate]
74
47
  );
75
48
 
76
49
  useEffect(() => {
77
50
  const down = (e: KeyboardEvent) => {
78
- if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
51
+ if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
79
52
  e.preventDefault();
80
- setOpen((open) => !open);
53
+ setOpen(open => !open);
81
54
  }
82
55
  };
83
56
 
84
- document.addEventListener("keydown", down);
85
- return () => document.removeEventListener("keydown", down);
57
+ document.addEventListener('keydown', down);
58
+ return () => document.removeEventListener('keydown', down);
86
59
  }, []);
87
60
 
61
+ const results = deduplicateByUrl(
62
+ query.data === 'empty' ? [] : (query.data ?? [])
63
+ );
64
+
88
65
  return (
89
66
  <>
90
67
  <Button
91
- variant="outline"
92
- color="neutral"
93
- size="small"
68
+ variant='outline'
69
+ color='neutral'
70
+ size='small'
94
71
  onClick={() => setOpen(true)}
95
72
  className={cx(styles.trigger, className)}
96
73
  trailingIcon={<SearchShortcutKey className={styles.kbd} />}
@@ -105,24 +82,24 @@ export function Search({ className }: SearchProps) {
105
82
  </Dialog.Title>
106
83
  <Command loop>
107
84
  <Command.Input
108
- placeholder="Search"
85
+ placeholder='Search'
109
86
  value={search}
110
87
  onValueChange={setSearch}
111
88
  className={styles.input}
112
89
  />
113
90
 
114
91
  <Command.List className={styles.list}>
115
- {isLoading && <Command.Empty>Loading...</Command.Empty>}
116
- {!isLoading &&
92
+ {query.isLoading && <Command.Empty>Loading...</Command.Empty>}
93
+ {!query.isLoading &&
117
94
  search.length > 0 &&
118
95
  results.length === 0 && (
119
96
  <Command.Empty>No results found.</Command.Empty>
120
97
  )}
121
- {!isLoading &&
98
+ {!query.isLoading &&
122
99
  search.length === 0 &&
123
100
  results.length > 0 && (
124
- <Command.Group heading="Suggestions">
125
- {results.slice(0, 8).map((result) => (
101
+ <Command.Group heading='Suggestions'>
102
+ {results.slice(0, 8).map((result: SortedResult) => (
126
103
  <Command.Item
127
104
  key={result.id}
128
105
  value={result.id}
@@ -132,7 +109,9 @@ export function Search({ className }: SearchProps) {
132
109
  <div className={styles.itemContent}>
133
110
  {getResultIcon(result)}
134
111
  <Text className={styles.pageText}>
135
- {result.content}
112
+ <HighlightedText
113
+ html={stripMethod(result.content)}
114
+ />
136
115
  </Text>
137
116
  </div>
138
117
  </Command.Item>
@@ -140,7 +119,7 @@ export function Search({ className }: SearchProps) {
140
119
  </Command.Group>
141
120
  )}
142
121
  {search.length > 0 &&
143
- results.map((result) => (
122
+ results.map((result: SortedResult) => (
144
123
  <Command.Item
145
124
  key={result.id}
146
125
  value={result.id}
@@ -149,9 +128,27 @@ export function Search({ className }: SearchProps) {
149
128
  >
150
129
  <div className={styles.itemContent}>
151
130
  {getResultIcon(result)}
152
- <Text className={styles.pageText}>
153
- {result.content}
154
- </Text>
131
+ <div className={styles.resultText}>
132
+ {result.type === 'heading' ? (
133
+ <>
134
+ <Text className={styles.headingText}>
135
+ <HighlightedText
136
+ html={stripMethod(result.content)}
137
+ />
138
+ </Text>
139
+ <Text className={styles.separator}>-</Text>
140
+ <Text className={styles.pageText}>
141
+ {getPageTitle(result.url)}
142
+ </Text>
143
+ </>
144
+ ) : (
145
+ <Text className={styles.pageText}>
146
+ <HighlightedText
147
+ html={stripMethod(result.content)}
148
+ />
149
+ </Text>
150
+ )}
151
+ </div>
155
152
  </div>
156
153
  </Command.Item>
157
154
  ))}
@@ -163,12 +160,59 @@ export function Search({ className }: SearchProps) {
163
160
  );
164
161
  }
165
162
 
166
- function getResultIcon(result: SearchResult): React.ReactNode {
167
- if (result.type === "api") {
168
- const method = result.content.split(" ")[0];
169
- return ["GET", "POST", "PUT", "DELETE", "PATCH"].includes(method)
170
- ? <MethodBadge method={method} size="micro" />
171
- : null;
163
+ function deduplicateByUrl(results: SortedResult[]): SortedResult[] {
164
+ const seen = new Set<string>();
165
+ return results.filter(r => {
166
+ const base = r.url.split('#')[0];
167
+ if (seen.has(base)) return false;
168
+ seen.add(base);
169
+ return true;
170
+ });
171
+ }
172
+
173
+ const API_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']);
174
+
175
+ function extractMethod(content: string): string | null {
176
+ const first = content.split(' ')[0];
177
+ return API_METHODS.has(first) ? first : null;
178
+ }
179
+
180
+ function stripMethod(content: string): string {
181
+ const first = content.split(' ')[0];
182
+ return API_METHODS.has(first) ? content.slice(first.length + 1) : content;
183
+ }
184
+
185
+ function HighlightedText({
186
+ html,
187
+ className
188
+ }: {
189
+ html: string;
190
+ className?: string;
191
+ }) {
192
+ return (
193
+ <span className={className} dangerouslySetInnerHTML={{ __html: html }} />
194
+ );
195
+ }
196
+
197
+ function getResultIcon(result: SortedResult): React.ReactNode {
198
+ if (!result.url.startsWith('/apis/')) {
199
+ return result.type === 'page' ? (
200
+ <DocumentIcon className={styles.icon} />
201
+ ) : (
202
+ <HashtagIcon className={styles.icon} />
203
+ );
172
204
  }
173
- return <DocumentIcon className={styles.icon} />;
205
+ const method = extractMethod(result.content);
206
+ return method ? <MethodBadge method={method} size='micro' /> : null;
207
+ }
208
+
209
+ function getPageTitle(url: string): string {
210
+ const path = url.split('#')[0];
211
+ const segments = path.split('/').filter(Boolean);
212
+ const lastSegment = segments[segments.length - 1];
213
+ if (!lastSegment) return 'Home';
214
+ return lastSegment
215
+ .split('-')
216
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
217
+ .join(' ');
174
218
  }
package/src/lib/config.ts CHANGED
@@ -1,56 +1,58 @@
1
- import fs from 'fs'
2
- import path from 'path'
3
- import { parse } from 'yaml'
4
- import type { ChronicleConfig } from '@/types'
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parse } from 'yaml';
4
+ import type { ChronicleConfig } from '@/types';
5
5
 
6
- const CONFIG_FILE = 'chronicle.yaml'
6
+ const CONFIG_FILE = 'chronicle.yaml';
7
7
 
8
8
  const defaultConfig: ChronicleConfig = {
9
9
  title: 'Documentation',
10
10
  theme: { name: 'default' },
11
- search: { enabled: true, placeholder: 'Search...' },
12
- }
11
+ search: { enabled: true, placeholder: 'Search...' }
12
+ };
13
13
 
14
14
  function resolveConfigPath(): string | null {
15
- // Check project root via env var
16
- const projectRoot = process.env.CHRONICLE_PROJECT_ROOT
17
- if (projectRoot) {
18
- const rootPath = path.join(projectRoot, CONFIG_FILE)
19
- if (fs.existsSync(rootPath)) return rootPath
20
- }
21
- // Check cwd
22
- const cwdPath = path.join(process.cwd(), CONFIG_FILE)
23
- if (fs.existsSync(cwdPath)) return cwdPath
24
- // Check content dir
25
- const contentDir = process.env.CHRONICLE_CONTENT_DIR
15
+ const projectRoot =
16
+ typeof __CHRONICLE_PROJECT_ROOT__ !== 'undefined'
17
+ ? __CHRONICLE_PROJECT_ROOT__
18
+ : process.cwd();
19
+
20
+ const rootPath = path.join(projectRoot, CONFIG_FILE);
21
+ if (fs.existsSync(rootPath)) return rootPath;
22
+
23
+ const contentDir =
24
+ typeof __CHRONICLE_CONTENT_DIR__ !== 'undefined'
25
+ ? __CHRONICLE_CONTENT_DIR__
26
+ : undefined;
26
27
  if (contentDir) {
27
- const contentPath = path.join(contentDir, CONFIG_FILE)
28
- if (fs.existsSync(contentPath)) return contentPath
28
+ const contentPath = path.join(contentDir, CONFIG_FILE);
29
+ if (fs.existsSync(contentPath)) return contentPath;
29
30
  }
30
- return null
31
+
32
+ return null;
31
33
  }
32
34
 
33
35
  export function loadConfig(): ChronicleConfig {
34
- const configPath = resolveConfigPath()
36
+ const configPath = resolveConfigPath();
35
37
 
36
38
  if (!configPath) {
37
- return defaultConfig
39
+ return defaultConfig;
38
40
  }
39
41
 
40
- const raw = fs.readFileSync(configPath, 'utf-8')
41
- const userConfig = parse(raw) as Partial<ChronicleConfig>
42
+ const raw = fs.readFileSync(configPath, 'utf-8');
43
+ const userConfig = parse(raw) as Partial<ChronicleConfig>;
42
44
 
43
45
  return {
44
46
  ...defaultConfig,
45
47
  ...userConfig,
46
48
  theme: {
47
49
  name: userConfig.theme?.name ?? defaultConfig.theme!.name,
48
- colors: { ...defaultConfig.theme?.colors, ...userConfig.theme?.colors },
50
+ colors: { ...defaultConfig.theme?.colors, ...userConfig.theme?.colors }
49
51
  },
50
52
  search: { ...defaultConfig.search, ...userConfig.search },
51
53
  footer: userConfig.footer,
52
54
  api: userConfig.api,
53
55
  llms: { enabled: false, ...userConfig.llms },
54
- analytics: { enabled: false, ...userConfig.analytics },
55
- }
56
- }
56
+ analytics: { enabled: false, ...userConfig.analytics }
57
+ };
58
+ }
@@ -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
+ }