@raystack/chronicle 0.1.0-canary.111b55a → 0.1.0-canary.1e5fdae

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 +212 -833
  2. package/package.json +13 -9
  3. package/src/cli/commands/build.ts +30 -70
  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 +18 -4
  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 +74 -58
  26. package/src/lib/source.ts +136 -114
  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 +17 -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 +78 -68
  39. package/src/server/entry-client.tsx +67 -55
  40. package/src/server/entry-server.tsx +100 -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 +87 -47
  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 -28
  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 -172
  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
@@ -1,107 +1,117 @@
1
- import fs from 'fs/promises'
2
- import path from 'path'
3
- import matter from 'gray-matter'
4
- import { loadConfig } from '@/lib/config'
5
- import { loadApiSpecs } from '@/lib/openapi'
6
- import { getSpecSlug } from '@/lib/api-routes'
7
- import type { OpenAPIV3 } from 'openapi-types'
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import matter from 'gray-matter';
4
+ import type { OpenAPIV3 } from 'openapi-types';
5
+ import remarkParse from 'remark-parse';
6
+ import { unified } from 'unified';
7
+ import { visit } from 'unist-util-visit';
8
+ import type { Heading, Text } from 'mdast';
9
+ import { getSpecSlug } from '@/lib/api-routes';
10
+ import { loadConfig } from '@/lib/config';
11
+ import { loadApiSpecs } from '@/lib/openapi';
8
12
 
9
13
  interface SearchDocument {
10
- id: string
11
- url: string
12
- title: string
13
- content: string
14
- type: 'page' | 'api'
14
+ id: string;
15
+ url: string;
16
+ title: string;
17
+ content: string;
18
+ type: 'page' | 'api';
15
19
  }
16
20
 
17
21
  function extractHeadings(markdown: string): string {
18
- const headingRegex = /^#{1,6}\s+(.+)$/gm
19
- const headings: string[] = []
20
- let match
21
- while ((match = headingRegex.exec(markdown)) !== null) {
22
- headings.push(match[1].trim())
23
- }
24
- return headings.join(' ')
22
+ const tree = unified().use(remarkParse).parse(markdown);
23
+ const headings: string[] = [];
24
+ visit(tree, 'heading', (node: Heading) => {
25
+ const text = node.children
26
+ .filter((child): child is Text => child.type === 'text')
27
+ .map(child => child.value)
28
+ .join('');
29
+ if (text) headings.push(text);
30
+ });
31
+ return headings.join(' ');
25
32
  }
26
33
 
27
34
  async function scanContent(contentDir: string): Promise<SearchDocument[]> {
28
- const docs: SearchDocument[] = []
35
+ const docs: SearchDocument[] = [];
29
36
 
30
37
  async function scan(dir: string, prefix: string[] = []) {
31
- let entries
32
- try { entries = await fs.readdir(dir, { withFileTypes: true }) }
33
- catch { return }
34
-
35
- for (const entry of entries) {
36
- if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
37
- const fullPath = path.join(dir, entry.name)
38
+ try {
39
+ const entries = await fs.readdir(dir, { withFileTypes: true });
40
+ for (const entry of entries) {
41
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
42
+ continue;
43
+ const fullPath = path.join(dir, entry.name);
44
+
45
+ if (entry.isDirectory()) {
46
+ await scan(fullPath, [...prefix, entry.name]);
47
+ continue;
48
+ }
49
+
50
+ if (!entry.name.endsWith('.mdx') && !entry.name.endsWith('.md'))
51
+ continue;
52
+
53
+ const raw = await fs.readFile(fullPath, 'utf-8');
54
+ const { data: fm, content } = matter(raw);
55
+ const baseName = entry.name.replace(/\.(mdx|md)$/, '');
56
+ const slugs = baseName === 'index' ? prefix : [...prefix, baseName];
57
+ const url = slugs.length === 0 ? '/' : `/${slugs.join('/')}`;
38
58
 
39
- if (entry.isDirectory()) {
40
- await scan(fullPath, [...prefix, entry.name])
41
- continue
59
+ docs.push({
60
+ id: url,
61
+ url,
62
+ title: fm.title ?? baseName,
63
+ content: extractHeadings(content),
64
+ type: 'page'
65
+ });
42
66
  }
43
-
44
- if (!entry.name.endsWith('.mdx') && !entry.name.endsWith('.md')) continue
45
-
46
- const raw = await fs.readFile(fullPath, 'utf-8')
47
- const { data: fm, content } = matter(raw)
48
- const baseName = entry.name.replace(/\.(mdx|md)$/, '')
49
- const slugs = baseName === 'index' ? prefix : [...prefix, baseName]
50
- const url = slugs.length === 0 ? '/' : '/' + slugs.join('/')
51
-
52
- docs.push({
53
- id: url,
54
- url,
55
- title: fm.title ?? baseName,
56
- content: extractHeadings(content),
57
- type: 'page',
58
- })
67
+ } catch {
68
+ /* directory not readable */
59
69
  }
60
70
  }
61
71
 
62
- await scan(contentDir)
63
- return docs
72
+ await scan(contentDir);
73
+ return docs;
64
74
  }
65
75
 
66
- function buildApiDocs(): SearchDocument[] {
67
- const config = loadConfig()
68
- if (!config.api?.length) return []
76
+ async function buildApiDocs(): Promise<SearchDocument[]> {
77
+ const config = loadConfig();
78
+ if (!config.api?.length) return [];
69
79
 
70
- const docs: SearchDocument[] = []
71
- const specs = loadApiSpecs(config.api)
80
+ const docs: SearchDocument[] = [];
81
+ const specs = await loadApiSpecs(config.api);
72
82
 
73
83
  for (const spec of specs) {
74
- const specSlug = getSpecSlug(spec)
75
- const paths = spec.document.paths ?? {}
84
+ const specSlug = getSpecSlug(spec);
85
+ const paths = spec.document.paths ?? {};
76
86
  for (const [, pathItem] of Object.entries(paths)) {
77
- if (!pathItem) continue
87
+ if (!pathItem) continue;
78
88
  for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
79
- const op = pathItem[method] as OpenAPIV3.OperationObject | undefined
80
- if (!op?.operationId) continue
81
- const url = `/apis/${specSlug}/${encodeURIComponent(op.operationId)}`
89
+ const op = pathItem[method] as OpenAPIV3.OperationObject | undefined;
90
+ if (!op?.operationId) continue;
91
+ const url = `/apis/${specSlug}/${encodeURIComponent(op.operationId)}`;
82
92
  docs.push({
83
93
  id: url,
84
94
  url,
85
95
  title: `${method.toUpperCase()} ${op.summary ?? op.operationId}`,
86
96
  content: op.description ?? '',
87
- type: 'api',
88
- })
97
+ type: 'api'
98
+ });
89
99
  }
90
100
  }
91
101
  }
92
102
 
93
- return docs
103
+ return docs;
94
104
  }
95
105
 
96
106
  export async function generateSearchIndex(contentDir: string, outDir: string) {
97
107
  const [contentDocs, apiDocs] = await Promise.all([
98
108
  scanContent(contentDir),
99
- Promise.resolve(buildApiDocs()),
100
- ])
109
+ buildApiDocs()
110
+ ]);
101
111
 
102
- const documents = [...contentDocs, ...apiDocs]
103
- const outPath = path.join(outDir, 'search-index.json')
104
- await fs.writeFile(outPath, JSON.stringify(documents))
112
+ const documents = [...contentDocs, ...apiDocs];
113
+ const outPath = path.join(outDir, 'search-index.json');
114
+ await fs.writeFile(outPath, JSON.stringify(documents));
105
115
 
106
- return documents.length
116
+ return documents.length;
107
117
  }
@@ -1,74 +1,86 @@
1
- import { hydrateRoot } from 'react-dom/client'
2
- import { BrowserRouter } from 'react-router-dom'
3
- import { PageProvider } from '@/lib/page-context'
4
- import { App } from './App'
5
- import { getPage, loadPageComponent, buildPageTree } from '@/lib/source'
6
- import { mdxComponents } from '@/components/mdx'
7
- import type { ChronicleConfig, PageTree } from '@/types'
8
- import type { ApiSpec } from '@/lib/openapi'
9
- import type { ReactNode } from 'react'
10
- import React from 'react'
1
+ import '@vitejs/plugin-react/preamble';
2
+ import React from 'react';
3
+ import { hydrateRoot } from 'react-dom/client';
4
+ import { BrowserRouter } from 'react-router';
5
+ import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
6
+ import { mdxComponents } from '@/components/mdx';
7
+ import { PageProvider } from '@/lib/page-context';
8
+ import type { ChronicleConfig, Frontmatter, Root, TableOfContents } from '@/types';
9
+ import type { ApiSpec } from '@/lib/openapi';
10
+ import type { ReactNode } from 'react';
11
+ import { App } from './App';
11
12
 
12
13
  interface EmbeddedData {
13
- config: ChronicleConfig
14
- tree: PageTree
15
- slug: string[]
16
- frontmatter: { title: string; description?: string; order?: number }
17
- filePath: string
14
+ config: ChronicleConfig;
15
+ tree: Root;
16
+ slug: string[];
17
+ frontmatter: Frontmatter;
18
+ relativePath: string;
18
19
  }
19
20
 
20
- async function hydrate() {
21
- try {
22
- const embedded: EmbeddedData | undefined = (window as any).__PAGE_DATA__
21
+ const contentModules = import.meta.glob<{ default?: React.ComponentType<any>; toc?: TableOfContents }>(
22
+ '../../.content/**/*.{mdx,md}'
23
+ );
23
24
 
24
- let config: ChronicleConfig = { title: 'Documentation' }
25
- let tree: PageTree = { name: 'root', children: [] }
26
- let page: { slug: string[]; frontmatter: any; content: ReactNode } | null = null
27
- let apiSpecs: ApiSpec[] = []
25
+ async function loadMdxModule(relativePath: string): Promise<{ content: ReactNode; toc: TableOfContents }> {
26
+ const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
27
+ const key = relativePath.endsWith('.md')
28
+ ? `../../.content/${withoutExt}.md`
29
+ : `../../.content/${withoutExt}.mdx`;
30
+ const loader = contentModules[key];
31
+ if (!loader) return { content: null, toc: [] };
32
+ const mod = await loader();
33
+ const content = mod.default
34
+ ? React.createElement(mod.default, { components: mdxComponents })
35
+ : null;
36
+ return { content, toc: mod.toc ?? [] };
37
+ }
28
38
 
29
- if (embedded) {
30
- config = embedded.config
31
- tree = embedded.tree
39
+ async function hydrate() {
40
+ try {
41
+ const embedded = (
42
+ window as unknown as { __PAGE_DATA__?: EmbeddedData }
43
+ ).__PAGE_DATA__;
32
44
 
33
- // Fetch API specs if on /apis route
34
- const isApiRoute = window.location.pathname.startsWith('/apis')
35
- if (isApiRoute && config.api?.length) {
36
- try {
37
- const res = await fetch('/api/specs')
38
- apiSpecs = await res.json()
39
- } catch { /* will load on demand */ }
40
- }
45
+ const config: ChronicleConfig = embedded?.config ?? {
46
+ title: 'Documentation'
47
+ };
48
+ const tree: Root = embedded?.tree ?? { name: 'root', children: [] };
49
+ const isApiPage =
50
+ window.location.pathname.startsWith('/apis') && !!config.api?.length;
51
+ const apiSpecs: ApiSpec[] = isApiPage
52
+ ? await fetch('/api/specs')
53
+ .then(r => r.json())
54
+ .catch(() => [])
55
+ : [];
41
56
 
42
- const sourcePage = await getPage(embedded.slug)
43
- if (sourcePage) {
44
- const component = await loadPageComponent(sourcePage)
45
- page = {
46
- slug: embedded.slug,
47
- frontmatter: embedded.frontmatter,
48
- content: component ? React.createElement(component, { components: mdxComponents }) : null,
49
- }
50
- } else {
51
- page = {
57
+ const page = embedded?.relativePath
58
+ ? {
52
59
  slug: embedded.slug,
53
60
  frontmatter: embedded.frontmatter,
54
- content: null,
61
+ ...(await loadMdxModule(embedded.relativePath)),
55
62
  }
56
- }
57
- } else {
58
- tree = await buildPageTree()
59
- }
63
+ : null;
60
64
 
61
65
  hydrateRoot(
62
66
  document.getElementById('root') as HTMLElement,
63
67
  <BrowserRouter>
64
- <PageProvider initialConfig={config} initialTree={tree} initialPage={page} initialApiSpecs={apiSpecs}>
65
- <App />
66
- </PageProvider>
67
- </BrowserRouter>,
68
- )
68
+ <ReactRouterProvider>
69
+ <PageProvider
70
+ initialConfig={config}
71
+ initialTree={tree}
72
+ initialPage={page}
73
+ initialApiSpecs={apiSpecs}
74
+ loadMdx={loadMdxModule}
75
+ >
76
+ <App />
77
+ </PageProvider>
78
+ </ReactRouterProvider>
79
+ </BrowserRouter>
80
+ );
69
81
  } catch (err) {
70
- console.error('Hydration failed:', err)
82
+ console.error('Hydration failed:', err);
71
83
  }
72
84
  }
73
85
 
74
- hydrate()
86
+ hydrate();
@@ -1,35 +1,100 @@
1
- import { renderToString } from 'react-dom/server'
2
- import { StaticRouter } from 'react-router-dom'
3
- import { PageProvider } from '@/lib/page-context'
4
- import { App } from './App'
5
- import type { ReactNode } from 'react'
6
- import type { ChronicleConfig, Frontmatter, PageTree } from '@/types'
7
- import type { ApiSpec } from '@/lib/openapi'
8
-
9
- export interface SSRData {
10
- config: ChronicleConfig
11
- tree: PageTree
12
- page: {
13
- slug: string[]
14
- frontmatter: Frontmatter
15
- content: ReactNode
16
- } | null
17
- apiSpecs: ApiSpec[]
18
- }
19
-
20
- export function render(url: string, data: SSRData): string {
21
- const pathname = new URL(url, 'http://localhost').pathname
22
-
23
- return renderToString(
24
- <StaticRouter location={pathname}>
25
- <PageProvider
26
- initialConfig={data.config}
27
- initialTree={data.tree}
28
- initialPage={data.page}
29
- initialApiSpecs={data.apiSpecs}
30
- >
31
- <App />
32
- </PageProvider>
33
- </StaticRouter>,
34
- )
35
- }
1
+ import '@raystack/apsara/normalize.css';
2
+ import '@raystack/apsara/style.css';
3
+ import React from 'react';
4
+ import { renderToReadableStream } from 'react-dom/server.edge';
5
+ import { StaticRouter } from 'react-router';
6
+ import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
7
+ import { mdxComponents } from '@/components/mdx';
8
+ import { loadConfig } from '@/lib/config';
9
+ import { loadApiSpecs } from '@/lib/openapi';
10
+ import { PageProvider } from '@/lib/page-context';
11
+ import { getPageTree, getPage, loadPageModule, extractFrontmatter, getRelativePath } from '@/lib/source';
12
+ import { App } from './App';
13
+
14
+ import clientAssets from './entry-client?assets=client';
15
+ import serverAssets from './entry-server?assets=ssr';
16
+
17
+ export default {
18
+ async fetch(req: Request) {
19
+ const url = new URL(req.url);
20
+ const pathname = url.pathname;
21
+ const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean);
22
+
23
+ const config = loadConfig();
24
+ const apiSpecs = config.api?.length
25
+ ? await loadApiSpecs(config.api).catch(() => [])
26
+ : [];
27
+
28
+ const [tree, page] = await Promise.all([
29
+ getPageTree(),
30
+ getPage(slug),
31
+ ]);
32
+
33
+ const relativePath = page ? getRelativePath(page) : null;
34
+ const mdxModule = relativePath ? await loadPageModule(relativePath) : null;
35
+
36
+ const pageData = page
37
+ ? {
38
+ slug,
39
+ frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
40
+ content: mdxModule?.default
41
+ ? React.createElement(mdxModule.default, { components: mdxComponents })
42
+ : null,
43
+ toc: mdxModule?.toc ?? [],
44
+ }
45
+ : null;
46
+
47
+ const embeddedData = {
48
+ config,
49
+ tree,
50
+ slug,
51
+ frontmatter: pageData?.frontmatter ?? null,
52
+ relativePath,
53
+ };
54
+ const safeJson = JSON.stringify(embeddedData).replace(/</g, '\\u003c');
55
+
56
+ const assets = clientAssets.merge(serverAssets);
57
+
58
+ const stream = await renderToReadableStream(
59
+ <html lang="en">
60
+ <head>
61
+ <meta charSet="UTF-8" />
62
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
63
+ {assets.css.map((attr: { href: string }) => (
64
+ <link key={attr.href} rel="stylesheet" {...attr} />
65
+ ))}
66
+ {assets.js.map((attr: { href: string }) => (
67
+ <link key={attr.href} rel="modulepreload" {...attr} />
68
+ ))}
69
+ <script type="module" src={assets.entry} />
70
+ <script dangerouslySetInnerHTML={{ __html: `window.__PAGE_DATA__ = ${safeJson}` }} />
71
+ </head>
72
+ <body>
73
+ <div id="root">
74
+ <StaticRouter location={pathname}>
75
+ <ReactRouterProvider>
76
+ <PageProvider
77
+ initialConfig={config}
78
+ initialTree={tree}
79
+ initialPage={pageData}
80
+ initialApiSpecs={apiSpecs}
81
+ loadMdx={async () => ({ content: null, toc: [] })}
82
+ >
83
+ <App />
84
+ </PageProvider>
85
+ </ReactRouterProvider>
86
+ </StaticRouter>
87
+ </div>
88
+ </body>
89
+ </html>,
90
+ );
91
+
92
+ const isApiRoute = pathname.startsWith('/apis');
93
+ const status = !page && !isApiRoute && slug.length > 0 ? 404 : 200;
94
+
95
+ return new Response(stream, {
96
+ status,
97
+ headers: { 'Content-Type': 'text/html;charset=utf-8' },
98
+ });
99
+ },
100
+ };
@@ -0,0 +1,61 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import matter from 'gray-matter';
4
+ import { defineHandler, HTTPError } from 'nitro';
5
+ import { loadConfig } from '@/lib/config';
6
+
7
+ function getContentDir(): string {
8
+ return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
9
+ }
10
+
11
+ async function scanPages(): Promise<{ title: string; url: string }[]> {
12
+ const contentDir = getContentDir();
13
+ const pages: { title: string; url: string }[] = [];
14
+
15
+ async function scan(dir: string, prefix: string[] = []) {
16
+ try {
17
+ const entries = await fs.readdir(dir, { withFileTypes: true });
18
+ for (const entry of entries) {
19
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
20
+ continue;
21
+ const fullPath = path.join(dir, entry.name);
22
+
23
+ if (entry.isDirectory()) {
24
+ await scan(fullPath, [...prefix, entry.name]);
25
+ continue;
26
+ }
27
+
28
+ if (!entry.name.endsWith('.mdx') && !entry.name.endsWith('.md'))
29
+ continue;
30
+
31
+ const raw = await fs.readFile(fullPath, 'utf-8');
32
+ const { data: fm } = matter(raw);
33
+ const baseName = entry.name.replace(/\.(mdx|md)$/, '');
34
+ const slugs = baseName === 'index' ? prefix : [...prefix, baseName];
35
+ const url = slugs.length === 0 ? '/' : `/${slugs.join('/')}`;
36
+
37
+ pages.push({ title: fm.title ?? baseName, url });
38
+ }
39
+ } catch {
40
+ /* directory not readable */
41
+ }
42
+ }
43
+
44
+ await scan(contentDir);
45
+ return pages;
46
+ }
47
+
48
+ export default defineHandler(async event => {
49
+ const config = loadConfig();
50
+
51
+ if (!config.llms?.enabled) {
52
+ throw new HTTPError({ status: 404, message: 'Not Found' });
53
+ }
54
+
55
+ const pages = await scanPages();
56
+ const index = pages.map(p => `- [${p.title}](${p.url})`).join('\n');
57
+ const body = `# ${config.title}\n\n${config.description ?? ''}\n\n${index}`;
58
+
59
+ event.res.headers.set('Content-Type', 'text/plain');
60
+ return body;
61
+ });
@@ -0,0 +1,75 @@
1
+ import { defineHandler } from 'nitro';
2
+ import React from 'react';
3
+ import satori from 'satori';
4
+ import { loadConfig } from '@/lib/config';
5
+
6
+ let fontData: ArrayBuffer | null = null;
7
+
8
+ async function loadFont(): Promise<ArrayBuffer> {
9
+ if (fontData) return fontData;
10
+
11
+ try {
12
+ const response = await fetch(
13
+ 'https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hiA.woff2'
14
+ );
15
+ fontData = await response.arrayBuffer();
16
+ } catch {
17
+ fontData = new ArrayBuffer(0);
18
+ }
19
+
20
+ return fontData;
21
+ }
22
+
23
+ export default defineHandler(async event => {
24
+ const config = loadConfig();
25
+ const title = event.url.searchParams.get('title') ?? config.title;
26
+ const description = event.url.searchParams.get('description') ?? '';
27
+ const siteName = config.title;
28
+
29
+ const font = await loadFont();
30
+
31
+ const svg = await satori(
32
+ <div
33
+ style={{
34
+ height: '100%',
35
+ width: '100%',
36
+ display: 'flex',
37
+ flexDirection: 'column',
38
+ justifyContent: 'center',
39
+ padding: '60px 80px',
40
+ backgroundColor: '#0a0a0a',
41
+ color: '#fafafa',
42
+ }}
43
+ >
44
+ <div style={{ fontSize: 24, color: '#888', marginBottom: 16 }}>
45
+ {siteName}
46
+ </div>
47
+ <div
48
+ style={{
49
+ fontSize: 56,
50
+ fontWeight: 700,
51
+ lineHeight: 1.2,
52
+ marginBottom: 24,
53
+ }}
54
+ >
55
+ {title}
56
+ </div>
57
+ {description && (
58
+ <div style={{ fontSize: 24, color: '#999', lineHeight: 1.4 }}>
59
+ {description}
60
+ </div>
61
+ )}
62
+ </div>,
63
+ {
64
+ width: 1200,
65
+ height: 630,
66
+ fonts: [
67
+ { name: 'Inter', data: font, weight: 400, style: 'normal' as const },
68
+ ],
69
+ },
70
+ );
71
+
72
+ event.res.headers.set('Content-Type', 'image/svg+xml');
73
+ event.res.headers.set('Cache-Control', 'public, max-age=86400');
74
+ return svg;
75
+ });
@@ -0,0 +1,11 @@
1
+ import { defineHandler } from 'nitro';
2
+ import { loadConfig } from '@/lib/config';
3
+
4
+ export default defineHandler(event => {
5
+ const config = loadConfig();
6
+ const sitemap = config.url ? `\nSitemap: ${config.url}/sitemap.xml` : '';
7
+ const body = `User-agent: *\nAllow: /${sitemap}`;
8
+
9
+ event.res.headers.set('Content-Type', 'text/plain');
10
+ return body;
11
+ });
@@ -0,0 +1,40 @@
1
+ import { defineHandler } from 'nitro';
2
+ import { buildApiRoutes } from '@/lib/api-routes';
3
+ import { loadConfig } from '@/lib/config';
4
+ import { loadApiSpecs } from '@/lib/openapi';
5
+ import { getPages } from '@/lib/source';
6
+
7
+ export default defineHandler(async event => {
8
+ const config = loadConfig();
9
+
10
+ if (!config.url) {
11
+ event.res.headers.set('Content-Type', 'application/xml');
12
+ return '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>';
13
+ }
14
+
15
+ const baseUrl = config.url.replace(/\/$/, '');
16
+
17
+ const pages = await getPages();
18
+ const docPages = pages.map(page => {
19
+ const data = page.data as Record<string, unknown>;
20
+ const lastmod = data.lastModified
21
+ ? `<lastmod>${new Date(data.lastModified as string).toISOString()}</lastmod>`
22
+ : '';
23
+ return `<url><loc>${baseUrl}/${page.slugs.join('/')}</loc>${lastmod}</url>`;
24
+ });
25
+
26
+ const apiPages = config.api?.length
27
+ ? buildApiRoutes(await loadApiSpecs(config.api)).map(
28
+ route => `<url><loc>${baseUrl}/apis/${route.slug.join('/')}</loc></url>`
29
+ )
30
+ : [];
31
+
32
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
33
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
34
+ <url><loc>${baseUrl}</loc></url>
35
+ ${[...docPages, ...apiPages].join('\n')}
36
+ </urlset>`;
37
+
38
+ event.res.headers.set('Content-Type', 'application/xml');
39
+ return xml;
40
+ });