@raystack/chronicle 0.1.0-canary.5a730d4 → 0.1.0-canary.67113f8

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 (85) hide show
  1. package/dist/cli/index.js +217 -412
  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/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 -72
  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/mdx-loader.ts +21 -0
  25. package/src/lib/openapi.ts +8 -8
  26. package/src/lib/page-context.tsx +74 -58
  27. package/src/lib/source.ts +136 -114
  28. package/src/pages/ApiLayout.tsx +22 -18
  29. package/src/pages/ApiPage.tsx +32 -27
  30. package/src/pages/DocsLayout.tsx +7 -7
  31. package/src/pages/DocsPage.tsx +11 -11
  32. package/src/pages/NotFound.tsx +11 -4
  33. package/src/server/App.tsx +35 -27
  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 +17 -0
  37. package/src/server/api/search.ts +170 -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 +47 -55
  41. package/src/server/entry-server.tsx +100 -35
  42. package/src/server/routes/llms.txt.ts +61 -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 +90 -44
  48. package/src/themes/default/Layout.tsx +78 -47
  49. package/src/themes/default/Page.module.css +0 -12
  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 +63 -43
  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 -62
  58. package/src/themes/paper/ReadingProgress.tsx +160 -139
  59. package/src/themes/paper/index.ts +5 -5
  60. package/src/themes/registry.ts +7 -7
  61. package/src/types/content.ts +5 -21
  62. package/src/types/globals.d.ts +3 -0
  63. package/src/types/theme.ts +4 -3
  64. package/src/cli/__tests__/config.test.ts +0 -25
  65. package/src/cli/__tests__/scaffold.test.ts +0 -10
  66. package/src/pages/__tests__/head.test.tsx +0 -57
  67. package/src/server/__tests__/entry-server.test.tsx +0 -35
  68. package/src/server/__tests__/handlers.test.ts +0 -77
  69. package/src/server/__tests__/og.test.ts +0 -23
  70. package/src/server/__tests__/router.test.ts +0 -72
  71. package/src/server/__tests__/vite-config.test.ts +0 -25
  72. package/src/server/dev.ts +0 -156
  73. package/src/server/entry-prod.ts +0 -127
  74. package/src/server/handlers/apis-proxy.ts +0 -52
  75. package/src/server/handlers/health.ts +0 -3
  76. package/src/server/handlers/llms.ts +0 -58
  77. package/src/server/handlers/og.ts +0 -87
  78. package/src/server/handlers/robots.ts +0 -11
  79. package/src/server/handlers/search.ts +0 -140
  80. package/src/server/handlers/sitemap.ts +0 -39
  81. package/src/server/handlers/specs.ts +0 -9
  82. package/src/server/index.html +0 -12
  83. package/src/server/prod.ts +0 -18
  84. package/src/server/router.ts +0 -42
  85. package/src/themes/default/font.ts +0 -4
@@ -0,0 +1,69 @@
1
+ import { defineHandler, HTTPError } from 'nitro';
2
+ import { loadConfig } from '@/lib/config';
3
+ import { loadApiSpecs } from '@/lib/openapi';
4
+
5
+ interface ProxyRequest {
6
+ specName: string;
7
+ method: string;
8
+ path: string;
9
+ headers?: Record<string, string>;
10
+ body?: unknown;
11
+ }
12
+
13
+ export default defineHandler(async event => {
14
+ if (event.req.method !== 'POST') {
15
+ throw new HTTPError({ status: 405, message: 'Method not allowed' });
16
+ }
17
+
18
+ const { specName, method, path, headers, body } =
19
+ (await event.req.json()) as ProxyRequest;
20
+
21
+ if (!specName || !method || !path) {
22
+ throw new HTTPError({
23
+ status: 400,
24
+ message: 'Missing specName, method, or path'
25
+ });
26
+ }
27
+
28
+ const config = loadConfig();
29
+ const specs = await loadApiSpecs(config.api ?? []);
30
+ const spec = specs.find(s => s.name === specName);
31
+
32
+ if (!spec) {
33
+ throw new HTTPError({ status: 404, message: `Unknown spec: ${specName}` });
34
+ }
35
+
36
+ if (/^[a-z]+:\/\//i.test(path) || path.includes('..')) {
37
+ throw new HTTPError({ status: 400, message: 'Invalid path' });
38
+ }
39
+
40
+ const url = spec.server.url + path;
41
+
42
+ try {
43
+ const response = await fetch(url, {
44
+ method,
45
+ headers,
46
+ body: body ? JSON.stringify(body) : undefined
47
+ });
48
+
49
+ const contentType = response.headers.get('content-type') ?? '';
50
+ const responseBody = contentType.includes('application/json')
51
+ ? await response.json()
52
+ : await response.text();
53
+
54
+ return {
55
+ status: response.status,
56
+ statusText: response.statusText,
57
+ body: responseBody
58
+ };
59
+ } catch (error) {
60
+ const message =
61
+ error instanceof Error
62
+ ? `${error.message}${error.cause ? `: ${(error.cause as Error).message}` : ''}`
63
+ : 'Request failed';
64
+ throw new HTTPError({
65
+ status: 502,
66
+ message: `Could not reach ${url}\n${message}`
67
+ });
68
+ }
69
+ });
@@ -0,0 +1,5 @@
1
+ import { defineHandler } from 'nitro';
2
+
3
+ export default defineHandler(() => {
4
+ return { status: 'ok' };
5
+ });
@@ -0,0 +1,17 @@
1
+ import { defineHandler, HTTPError } from 'nitro';
2
+ import { getPage, extractFrontmatter, getRelativePath } from '@/lib/source';
3
+
4
+ export default defineHandler(async event => {
5
+ const slugParam = event.context.params?.slug ?? '';
6
+ const slug = slugParam ? slugParam.split('/') : [];
7
+ const page = await getPage(slug);
8
+
9
+ if (!page) {
10
+ throw new HTTPError({ status: 404, message: 'Page not found' });
11
+ }
12
+
13
+ return {
14
+ frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
15
+ relativePath: getRelativePath(page),
16
+ };
17
+ });
@@ -0,0 +1,170 @@
1
+ import fs from 'node:fs/promises';
2
+ import matter from 'gray-matter';
3
+ import MiniSearch from 'minisearch';
4
+ import { defineHandler } from 'nitro';
5
+ import type { OpenAPIV3 } from 'openapi-types';
6
+ import path from 'node:path';
7
+ import { getSpecSlug } from '@/lib/api-routes';
8
+ import { loadConfig } from '@/lib/config';
9
+ import { loadApiSpecs } from '@/lib/openapi';
10
+
11
+ interface SearchDocument {
12
+ id: string;
13
+ url: string;
14
+ title: string;
15
+ content: string;
16
+ type: 'page' | 'api';
17
+ }
18
+
19
+ let searchIndex: MiniSearch<SearchDocument> | null = null;
20
+ let cachedDocs: SearchDocument[] | null = null;
21
+
22
+ function createIndex(docs: SearchDocument[]): MiniSearch<SearchDocument> {
23
+ const index = new MiniSearch<SearchDocument>({
24
+ fields: ['title', 'content'],
25
+ storeFields: ['url', 'title', 'type'],
26
+ searchOptions: {
27
+ boost: { title: 2 },
28
+ fuzzy: 0.2,
29
+ prefix: true
30
+ }
31
+ });
32
+ index.addAll(docs);
33
+ return index;
34
+ }
35
+
36
+ async function loadPrebuiltIndex(): Promise<SearchDocument[] | null> {
37
+ try {
38
+ const indexPath = path.resolve(__dirname, 'search-index.json');
39
+ const raw = await fs.readFile(indexPath, 'utf-8');
40
+ return JSON.parse(raw);
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function getContentDir(): string {
47
+ return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
48
+ }
49
+
50
+ async function scanContent(): Promise<SearchDocument[]> {
51
+ const contentDir = getContentDir();
52
+ const docs: SearchDocument[] = [];
53
+
54
+ async function scan(dir: string, prefix: string[] = []) {
55
+ try {
56
+ const entries = await fs.readdir(dir, { withFileTypes: true });
57
+ for (const entry of entries) {
58
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
59
+ continue;
60
+ const fullPath = path.join(dir, entry.name);
61
+
62
+ if (entry.isDirectory()) {
63
+ await scan(fullPath, [...prefix, entry.name]);
64
+ continue;
65
+ }
66
+
67
+ if (!entry.name.endsWith('.mdx') && !entry.name.endsWith('.md'))
68
+ continue;
69
+
70
+ const raw = await fs.readFile(fullPath, 'utf-8');
71
+ const { data: fm, content } = matter(raw);
72
+ const baseName = entry.name.replace(/\.(mdx|md)$/, '');
73
+ const slugs = baseName === 'index' ? prefix : [...prefix, baseName];
74
+ const url = slugs.length === 0 ? '/' : `/${slugs.join('/')}`;
75
+
76
+ docs.push({
77
+ id: url,
78
+ url,
79
+ title: fm.title ?? baseName,
80
+ content: content.slice(0, 5000),
81
+ type: 'page'
82
+ });
83
+ }
84
+ } catch {
85
+ /* directory not readable */
86
+ }
87
+ }
88
+
89
+ await scan(contentDir);
90
+ return docs;
91
+ }
92
+
93
+ async function buildApiDocs(): Promise<SearchDocument[]> {
94
+ const config = loadConfig();
95
+ if (!config.api?.length) return [];
96
+
97
+ const docs: SearchDocument[] = [];
98
+ const specs = await loadApiSpecs(config.api);
99
+
100
+ for (const spec of specs) {
101
+ const specSlug = getSpecSlug(spec);
102
+ const paths = spec.document.paths ?? {};
103
+ for (const [, pathItem] of Object.entries(paths)) {
104
+ if (!pathItem) continue;
105
+ for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
106
+ const op = pathItem[method] as OpenAPIV3.OperationObject | undefined;
107
+ if (!op?.operationId) continue;
108
+ const url = `/apis/${specSlug}/${encodeURIComponent(op.operationId)}`;
109
+ docs.push({
110
+ id: url,
111
+ url,
112
+ title: `${method.toUpperCase()} ${op.summary ?? op.operationId}`,
113
+ content: op.description ?? '',
114
+ type: 'api'
115
+ });
116
+ }
117
+ }
118
+ }
119
+
120
+ return docs;
121
+ }
122
+
123
+ async function loadDocuments(): Promise<SearchDocument[]> {
124
+ const prebuilt = await loadPrebuiltIndex();
125
+ if (prebuilt) return prebuilt;
126
+
127
+ const [contentDocs, apiDocs] = await Promise.all([
128
+ scanContent(),
129
+ buildApiDocs()
130
+ ]);
131
+ return [...contentDocs, ...apiDocs];
132
+ }
133
+
134
+ async function getDocs(): Promise<SearchDocument[]> {
135
+ if (cachedDocs) return cachedDocs;
136
+ cachedDocs = await loadDocuments();
137
+ return cachedDocs;
138
+ }
139
+
140
+ async function getIndex(): Promise<MiniSearch<SearchDocument>> {
141
+ if (searchIndex) return searchIndex;
142
+ const docs = await getDocs();
143
+ searchIndex = createIndex(docs);
144
+ return searchIndex;
145
+ }
146
+
147
+ export default defineHandler(async event => {
148
+ const query = event.url.searchParams.get('query') ?? '';
149
+ const index = await getIndex();
150
+
151
+ if (!query) {
152
+ const docs = await getDocs();
153
+ return docs
154
+ .filter(d => d.type === 'page')
155
+ .slice(0, 8)
156
+ .map(d => ({
157
+ id: d.id,
158
+ url: d.url,
159
+ type: d.type,
160
+ content: d.title
161
+ }));
162
+ }
163
+
164
+ return index.search(query).map(r => ({
165
+ id: r.id,
166
+ url: r.url,
167
+ type: r.type,
168
+ content: r.title
169
+ }));
170
+ });
@@ -0,0 +1,9 @@
1
+ import { defineHandler } from 'nitro';
2
+ import { loadConfig } from '@/lib/config';
3
+ import { loadApiSpecs } from '@/lib/openapi';
4
+
5
+ export default defineHandler(async () => {
6
+ const config = loadConfig();
7
+ const specs = config.api?.length ? await loadApiSpecs(config.api) : [];
8
+ return specs;
9
+ });
@@ -0,0 +1,117 @@
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';
12
+
13
+ interface SearchDocument {
14
+ id: string;
15
+ url: string;
16
+ title: string;
17
+ content: string;
18
+ type: 'page' | 'api';
19
+ }
20
+
21
+ function extractHeadings(markdown: string): string {
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(' ');
32
+ }
33
+
34
+ async function scanContent(contentDir: string): Promise<SearchDocument[]> {
35
+ const docs: SearchDocument[] = [];
36
+
37
+ async function scan(dir: string, prefix: string[] = []) {
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('/')}`;
58
+
59
+ docs.push({
60
+ id: url,
61
+ url,
62
+ title: fm.title ?? baseName,
63
+ content: extractHeadings(content),
64
+ type: 'page'
65
+ });
66
+ }
67
+ } catch {
68
+ /* directory not readable */
69
+ }
70
+ }
71
+
72
+ await scan(contentDir);
73
+ return docs;
74
+ }
75
+
76
+ async function buildApiDocs(): Promise<SearchDocument[]> {
77
+ const config = loadConfig();
78
+ if (!config.api?.length) return [];
79
+
80
+ const docs: SearchDocument[] = [];
81
+ const specs = await loadApiSpecs(config.api);
82
+
83
+ for (const spec of specs) {
84
+ const specSlug = getSpecSlug(spec);
85
+ const paths = spec.document.paths ?? {};
86
+ for (const [, pathItem] of Object.entries(paths)) {
87
+ if (!pathItem) continue;
88
+ for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
89
+ const op = pathItem[method] as OpenAPIV3.OperationObject | undefined;
90
+ if (!op?.operationId) continue;
91
+ const url = `/apis/${specSlug}/${encodeURIComponent(op.operationId)}`;
92
+ docs.push({
93
+ id: url,
94
+ url,
95
+ title: `${method.toUpperCase()} ${op.summary ?? op.operationId}`,
96
+ content: op.description ?? '',
97
+ type: 'api'
98
+ });
99
+ }
100
+ }
101
+ }
102
+
103
+ return docs;
104
+ }
105
+
106
+ export async function generateSearchIndex(contentDir: string, outDir: string) {
107
+ const [contentDocs, apiDocs] = await Promise.all([
108
+ scanContent(contentDir),
109
+ buildApiDocs()
110
+ ]);
111
+
112
+ const documents = [...contentDocs, ...apiDocs];
113
+ const outPath = path.join(outDir, 'search-index.json');
114
+ await fs.writeFile(outPath, JSON.stringify(documents));
115
+
116
+ return documents.length;
117
+ }
@@ -1,74 +1,66 @@
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 { hydrateRoot } from 'react-dom/client';
3
+ import { BrowserRouter } from 'react-router';
4
+ import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
5
+ import { loadMdxModule } from '@/lib/mdx-loader';
6
+ import { PageProvider } from '@/lib/page-context';
7
+ import type { ChronicleConfig, Frontmatter, Root } from '@/types';
8
+ import type { ApiSpec } from '@/lib/openapi';
9
+ import { App } from './App';
11
10
 
12
11
  interface EmbeddedData {
13
- config: ChronicleConfig
14
- tree: PageTree
15
- slug: string[]
16
- frontmatter: { title: string; description?: string; order?: number }
17
- filePath: string
12
+ config: ChronicleConfig;
13
+ tree: Root;
14
+ slug: string[];
15
+ frontmatter: Frontmatter;
16
+ relativePath: string;
18
17
  }
19
18
 
20
19
  async function hydrate() {
21
20
  try {
22
- const embedded: EmbeddedData | undefined = (window as any).__PAGE_DATA__
21
+ const embedded = (
22
+ window as unknown as { __PAGE_DATA__?: EmbeddedData }
23
+ ).__PAGE_DATA__;
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
+ const config: ChronicleConfig = embedded?.config ?? {
26
+ title: 'Documentation'
27
+ };
28
+ const tree: Root = embedded?.tree ?? { name: 'root', children: [] };
29
+ const isApiPage =
30
+ window.location.pathname.startsWith('/apis') && !!config.api?.length;
31
+ const apiSpecs: ApiSpec[] = isApiPage
32
+ ? await fetch('/api/specs')
33
+ .then(r => r.json())
34
+ .catch(() => [])
35
+ : [];
28
36
 
29
- if (embedded) {
30
- config = embedded.config
31
- tree = embedded.tree
32
-
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
- }
41
-
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 = {
37
+ const page = embedded?.relativePath
38
+ ? {
52
39
  slug: embedded.slug,
53
40
  frontmatter: embedded.frontmatter,
54
- content: null,
41
+ ...(await loadMdxModule(embedded.relativePath)),
55
42
  }
56
- }
57
- } else {
58
- tree = await buildPageTree()
59
- }
43
+ : null;
60
44
 
61
45
  hydrateRoot(
62
46
  document.getElementById('root') as HTMLElement,
63
47
  <BrowserRouter>
64
- <PageProvider initialConfig={config} initialTree={tree} initialPage={page} initialApiSpecs={apiSpecs}>
65
- <App />
66
- </PageProvider>
67
- </BrowserRouter>,
68
- )
48
+ <ReactRouterProvider>
49
+ <PageProvider
50
+ initialConfig={config}
51
+ initialTree={tree}
52
+ initialPage={page}
53
+ initialApiSpecs={apiSpecs}
54
+ loadMdx={loadMdxModule}
55
+ >
56
+ <App />
57
+ </PageProvider>
58
+ </ReactRouterProvider>
59
+ </BrowserRouter>
60
+ );
69
61
  } catch (err) {
70
- console.error('Hydration failed:', err)
62
+ console.error('Hydration failed:', err);
71
63
  }
72
64
  }
73
65
 
74
- hydrate()
66
+ 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
+ };