@raystack/chronicle 0.1.0-canary.e11f924 → 0.1.0-canary.f0d9bde
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.
- package/dist/cli/index.js +878 -9684
- package/package.json +17 -12
- package/src/cli/__tests__/config.test.ts +25 -0
- package/src/cli/__tests__/scaffold.test.ts +10 -0
- package/src/cli/commands/build.ts +66 -19
- package/src/cli/commands/dev.ts +9 -22
- package/src/cli/commands/init.ts +107 -11
- package/src/cli/commands/serve.ts +36 -35
- package/src/cli/commands/start.ts +11 -21
- package/src/cli/utils/config.ts +2 -2
- package/src/cli/utils/index.ts +1 -1
- package/src/cli/utils/resolve.ts +6 -0
- package/src/cli/utils/scaffold.ts +20 -0
- package/src/components/mdx/code.tsx +10 -1
- package/src/components/mdx/details.module.css +1 -24
- package/src/components/mdx/details.tsx +2 -3
- package/src/components/mdx/image.tsx +5 -19
- package/src/components/mdx/index.tsx +3 -3
- package/src/components/mdx/link.tsx +10 -11
- package/src/components/ui/footer.tsx +3 -2
- package/src/components/ui/search.module.css +7 -0
- package/src/components/ui/search.tsx +62 -87
- package/src/lib/config.ts +9 -0
- package/src/lib/head.tsx +45 -0
- package/src/lib/page-context.tsx +95 -0
- package/src/lib/source.ts +92 -21
- package/src/{app/apis/[[...slug]]/layout.tsx → pages/ApiLayout.tsx} +10 -7
- package/src/pages/ApiPage.tsx +68 -0
- package/src/pages/DocsLayout.tsx +18 -0
- package/src/pages/DocsPage.tsx +43 -0
- package/src/pages/NotFound.tsx +10 -0
- package/src/pages/__tests__/head.test.tsx +57 -0
- package/src/server/App.tsx +59 -0
- package/src/server/__tests__/entry-server.test.tsx +35 -0
- package/src/server/__tests__/handlers.test.ts +77 -0
- package/src/server/__tests__/og.test.ts +23 -0
- package/src/server/__tests__/router.test.ts +72 -0
- package/src/server/__tests__/vite-config.test.ts +25 -0
- package/src/server/adapters/vercel.ts +133 -0
- package/src/server/build-search-index.ts +107 -0
- package/src/server/dev.ts +158 -0
- package/src/server/entry-client.tsx +74 -0
- package/src/server/entry-prod.ts +98 -0
- package/src/server/entry-server.tsx +35 -0
- package/src/server/entry-vercel.ts +28 -0
- package/src/server/handlers/apis-proxy.ts +57 -0
- package/src/{app/api/health/route.ts → server/handlers/health.ts} +1 -1
- package/src/server/handlers/llms.ts +58 -0
- package/src/server/handlers/og.ts +87 -0
- package/src/server/handlers/robots.ts +11 -0
- package/src/server/handlers/search.ts +172 -0
- package/src/server/handlers/sitemap.ts +39 -0
- package/src/server/handlers/specs.ts +9 -0
- package/src/server/index.html +12 -0
- package/src/server/prod.ts +18 -0
- package/src/server/request-handler.ts +64 -0
- package/src/server/router.ts +42 -0
- package/src/server/utils/safe-path.ts +14 -0
- package/src/server/vite-config.ts +71 -0
- package/src/themes/default/Layout.tsx +9 -10
- package/src/themes/default/Page.module.css +60 -0
- package/src/themes/default/font.ts +4 -6
- package/src/themes/paper/ChapterNav.tsx +5 -6
- package/src/themes/paper/Page.tsx +8 -9
- package/src/types/config.ts +11 -0
- package/src/types/content.ts +1 -0
- package/tsconfig.json +29 -0
- package/next.config.mjs +0 -10
- package/source.config.ts +0 -50
- package/src/app/[[...slug]]/layout.tsx +0 -15
- package/src/app/[[...slug]]/page.tsx +0 -57
- package/src/app/api/apis-proxy/route.ts +0 -59
- package/src/app/api/search/route.ts +0 -90
- package/src/app/apis/[[...slug]]/page.tsx +0 -57
- package/src/app/layout.tsx +0 -26
- package/src/app/llms-full.txt/route.ts +0 -18
- package/src/app/llms.txt/route.ts +0 -15
- package/src/app/providers.tsx +0 -8
- package/src/cli/utils/process.ts +0 -7
- package/src/lib/get-llm-text.ts +0 -10
- /package/src/{app/apis/[[...slug]]/layout.module.css → pages/ApiLayout.module.css} +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import satori from 'satori'
|
|
2
|
+
import { loadConfig } from '@/lib/config'
|
|
3
|
+
|
|
4
|
+
let fontData: ArrayBuffer | null = null
|
|
5
|
+
|
|
6
|
+
async function loadFont(): Promise<ArrayBuffer> {
|
|
7
|
+
if (fontData) return fontData
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const response = await fetch('https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hiA.woff2')
|
|
11
|
+
fontData = await response.arrayBuffer()
|
|
12
|
+
} catch {
|
|
13
|
+
// Fallback: create minimal valid font buffer
|
|
14
|
+
fontData = new ArrayBuffer(0)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return fontData
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function handleOg(req: Request): Promise<Response> {
|
|
21
|
+
const url = new URL(req.url)
|
|
22
|
+
const title = url.searchParams.get('title') ?? loadConfig().title
|
|
23
|
+
const description = url.searchParams.get('description') ?? ''
|
|
24
|
+
const siteName = loadConfig().title
|
|
25
|
+
|
|
26
|
+
const font = await loadFont()
|
|
27
|
+
|
|
28
|
+
const svg = await satori(
|
|
29
|
+
{
|
|
30
|
+
type: 'div',
|
|
31
|
+
props: {
|
|
32
|
+
style: {
|
|
33
|
+
height: '100%',
|
|
34
|
+
width: '100%',
|
|
35
|
+
display: 'flex',
|
|
36
|
+
flexDirection: 'column',
|
|
37
|
+
justifyContent: 'center',
|
|
38
|
+
padding: '60px 80px',
|
|
39
|
+
backgroundColor: '#0a0a0a',
|
|
40
|
+
color: '#fafafa',
|
|
41
|
+
},
|
|
42
|
+
children: [
|
|
43
|
+
{
|
|
44
|
+
type: 'div',
|
|
45
|
+
props: {
|
|
46
|
+
style: { fontSize: 24, color: '#888', marginBottom: 16 },
|
|
47
|
+
children: siteName,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: 'div',
|
|
52
|
+
props: {
|
|
53
|
+
style: { fontSize: 56, fontWeight: 700, lineHeight: 1.2, marginBottom: 24 },
|
|
54
|
+
children: title,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
...(description ? [{
|
|
58
|
+
type: 'div',
|
|
59
|
+
props: {
|
|
60
|
+
style: { fontSize: 24, color: '#999', lineHeight: 1.4 },
|
|
61
|
+
children: description,
|
|
62
|
+
},
|
|
63
|
+
}] : []),
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
width: 1200,
|
|
69
|
+
height: 630,
|
|
70
|
+
fonts: [
|
|
71
|
+
{
|
|
72
|
+
name: 'Inter',
|
|
73
|
+
data: font,
|
|
74
|
+
weight: 400,
|
|
75
|
+
style: 'normal' as const,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return new Response(svg, {
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'image/svg+xml',
|
|
84
|
+
'Cache-Control': 'public, max-age=86400',
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { loadConfig } from '@/lib/config'
|
|
2
|
+
|
|
3
|
+
export function handleRobots(): Response {
|
|
4
|
+
const config = loadConfig()
|
|
5
|
+
const sitemap = config.url ? `\nSitemap: ${config.url}/sitemap.xml` : ''
|
|
6
|
+
const body = `User-agent: *\nAllow: /${sitemap}`
|
|
7
|
+
|
|
8
|
+
return new Response(body, {
|
|
9
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
10
|
+
})
|
|
11
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import matter from 'gray-matter'
|
|
4
|
+
import MiniSearch from 'minisearch'
|
|
5
|
+
import type { OpenAPIV3 } from 'openapi-types'
|
|
6
|
+
import { loadConfig } from '@/lib/config'
|
|
7
|
+
import { loadApiSpecs, type ApiSpec } from '@/lib/openapi'
|
|
8
|
+
import { getSpecSlug } from '@/lib/api-routes'
|
|
9
|
+
|
|
10
|
+
interface SearchDocument {
|
|
11
|
+
id: string
|
|
12
|
+
url: string
|
|
13
|
+
title: string
|
|
14
|
+
content: string
|
|
15
|
+
type: 'page' | 'api'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let searchIndex: MiniSearch<SearchDocument> | null = null
|
|
19
|
+
let cachedDocs: SearchDocument[] | null = null
|
|
20
|
+
|
|
21
|
+
function createIndex(docs: SearchDocument[]): MiniSearch<SearchDocument> {
|
|
22
|
+
const index = new MiniSearch<SearchDocument>({
|
|
23
|
+
fields: ['title', 'content'],
|
|
24
|
+
storeFields: ['url', 'title', 'type'],
|
|
25
|
+
searchOptions: {
|
|
26
|
+
boost: { title: 2 },
|
|
27
|
+
fuzzy: 0.2,
|
|
28
|
+
prefix: true,
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
index.addAll(docs)
|
|
32
|
+
return index
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Try loading pre-built search index (generated at build time)
|
|
36
|
+
async function loadPrebuiltIndex(): Promise<SearchDocument[] | null> {
|
|
37
|
+
try {
|
|
38
|
+
// In bundled server, search-index.json is next to the entry file
|
|
39
|
+
const indexPath = path.resolve(__dirname, 'search-index.json')
|
|
40
|
+
const raw = await fs.readFile(indexPath, 'utf-8')
|
|
41
|
+
return JSON.parse(raw)
|
|
42
|
+
} catch {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Fallback: scan filesystem at runtime (dev mode)
|
|
48
|
+
function getContentDir(): string {
|
|
49
|
+
return process.env.CHRONICLE_CONTENT_DIR || path.join(process.cwd(), 'content')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function scanContent(): Promise<SearchDocument[]> {
|
|
53
|
+
const contentDir = getContentDir()
|
|
54
|
+
const docs: SearchDocument[] = []
|
|
55
|
+
|
|
56
|
+
async function scan(dir: string, prefix: string[] = []) {
|
|
57
|
+
let entries
|
|
58
|
+
try { entries = await fs.readdir(dir, { withFileTypes: true }) }
|
|
59
|
+
catch { return }
|
|
60
|
+
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
|
|
63
|
+
const fullPath = path.join(dir, entry.name)
|
|
64
|
+
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
await scan(fullPath, [...prefix, entry.name])
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!entry.name.endsWith('.mdx') && !entry.name.endsWith('.md')) continue
|
|
71
|
+
|
|
72
|
+
const raw = await fs.readFile(fullPath, 'utf-8')
|
|
73
|
+
const { data: fm, content } = matter(raw)
|
|
74
|
+
const baseName = entry.name.replace(/\.(mdx|md)$/, '')
|
|
75
|
+
const slugs = baseName === 'index' ? prefix : [...prefix, baseName]
|
|
76
|
+
const url = slugs.length === 0 ? '/' : '/' + slugs.join('/')
|
|
77
|
+
|
|
78
|
+
docs.push({
|
|
79
|
+
id: url,
|
|
80
|
+
url,
|
|
81
|
+
title: fm.title ?? baseName,
|
|
82
|
+
content: content.slice(0, 5000),
|
|
83
|
+
type: 'page',
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await scan(contentDir)
|
|
89
|
+
return docs
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildApiDocs(): SearchDocument[] {
|
|
93
|
+
const config = loadConfig()
|
|
94
|
+
if (!config.api?.length) return []
|
|
95
|
+
|
|
96
|
+
const docs: SearchDocument[] = []
|
|
97
|
+
const specs = loadApiSpecs(config.api)
|
|
98
|
+
|
|
99
|
+
for (const spec of specs) {
|
|
100
|
+
const specSlug = getSpecSlug(spec)
|
|
101
|
+
const paths = spec.document.paths ?? {}
|
|
102
|
+
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
|
103
|
+
if (!pathItem) continue
|
|
104
|
+
for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
|
|
105
|
+
const op = pathItem[method] as OpenAPIV3.OperationObject | undefined
|
|
106
|
+
if (!op?.operationId) continue
|
|
107
|
+
const url = `/apis/${specSlug}/${encodeURIComponent(op.operationId)}`
|
|
108
|
+
docs.push({
|
|
109
|
+
id: url,
|
|
110
|
+
url,
|
|
111
|
+
title: `${method.toUpperCase()} ${op.summary ?? op.operationId}`,
|
|
112
|
+
content: op.description ?? '',
|
|
113
|
+
type: 'api',
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return docs
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function loadDocuments(): Promise<SearchDocument[]> {
|
|
123
|
+
// Try pre-built index first
|
|
124
|
+
const prebuilt = await loadPrebuiltIndex()
|
|
125
|
+
if (prebuilt) return prebuilt
|
|
126
|
+
|
|
127
|
+
// Fallback to filesystem scanning (dev mode)
|
|
128
|
+
const [contentDocs, apiDocs] = await Promise.all([
|
|
129
|
+
scanContent(),
|
|
130
|
+
Promise.resolve(buildApiDocs()),
|
|
131
|
+
])
|
|
132
|
+
return [...contentDocs, ...apiDocs]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function getDocs(): Promise<SearchDocument[]> {
|
|
136
|
+
if (cachedDocs) return cachedDocs
|
|
137
|
+
cachedDocs = await loadDocuments()
|
|
138
|
+
return cachedDocs
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function getIndex(): Promise<MiniSearch<SearchDocument>> {
|
|
142
|
+
if (searchIndex) return searchIndex
|
|
143
|
+
const docs = await getDocs()
|
|
144
|
+
searchIndex = createIndex(docs)
|
|
145
|
+
return searchIndex
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function handleSearch(req: Request): Promise<Response> {
|
|
149
|
+
const url = new URL(req.url)
|
|
150
|
+
const query = url.searchParams.get('query') ?? ''
|
|
151
|
+
const index = await getIndex()
|
|
152
|
+
|
|
153
|
+
if (!query) {
|
|
154
|
+
const docs = await getDocs()
|
|
155
|
+
const suggestions = docs.filter(d => d.type === 'page').slice(0, 8).map((d) => ({
|
|
156
|
+
id: d.id,
|
|
157
|
+
url: d.url,
|
|
158
|
+
type: d.type,
|
|
159
|
+
content: d.title,
|
|
160
|
+
}))
|
|
161
|
+
return Response.json(suggestions)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const results = index.search(query).map((r) => ({
|
|
165
|
+
id: r.id,
|
|
166
|
+
url: r.url,
|
|
167
|
+
type: r.type,
|
|
168
|
+
content: r.title,
|
|
169
|
+
}))
|
|
170
|
+
|
|
171
|
+
return Response.json(results)
|
|
172
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { loadConfig } from '@/lib/config'
|
|
2
|
+
import { getPages } from '@/lib/source'
|
|
3
|
+
import { loadApiSpecs } from '@/lib/openapi'
|
|
4
|
+
import { buildApiRoutes } from '@/lib/api-routes'
|
|
5
|
+
|
|
6
|
+
export async function handleSitemap(): Promise<Response> {
|
|
7
|
+
const config = loadConfig()
|
|
8
|
+
if (!config.url) {
|
|
9
|
+
return new Response('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>', {
|
|
10
|
+
headers: { 'Content-Type': 'application/xml' },
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const baseUrl = config.url.replace(/\/$/, '')
|
|
15
|
+
|
|
16
|
+
const pages = await getPages()
|
|
17
|
+
const docPages = pages.map((page) => {
|
|
18
|
+
const lastmod = page.frontmatter.lastModified
|
|
19
|
+
? `<lastmod>${new Date(page.frontmatter.lastModified).toISOString()}</lastmod>`
|
|
20
|
+
: ''
|
|
21
|
+
return `<url><loc>${baseUrl}/${page.slugs.join('/')}</loc>${lastmod}</url>`
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const apiPages = config.api?.length
|
|
25
|
+
? buildApiRoutes(loadApiSpecs(config.api)).map((route) =>
|
|
26
|
+
`<url><loc>${baseUrl}/apis/${route.slug.join('/')}</loc></url>`
|
|
27
|
+
)
|
|
28
|
+
: []
|
|
29
|
+
|
|
30
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
31
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
32
|
+
<url><loc>${baseUrl}</loc></url>
|
|
33
|
+
${[...docPages, ...apiPages].join('\n')}
|
|
34
|
+
</urlset>`
|
|
35
|
+
|
|
36
|
+
return new Response(xml, {
|
|
37
|
+
headers: { 'Content-Type': 'application/xml' },
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { loadConfig } from '@/lib/config'
|
|
2
|
+
import { loadApiSpecs } from '@/lib/openapi'
|
|
3
|
+
|
|
4
|
+
export function handleSpecs(): Response {
|
|
5
|
+
const config = loadConfig()
|
|
6
|
+
const specs = config.api?.length ? loadApiSpecs(config.api) : []
|
|
7
|
+
|
|
8
|
+
return Response.json(specs)
|
|
9
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<!--head-outlet-->
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"><!--ssr-outlet--></div>
|
|
10
|
+
<script type="module" src="/src/server/entry-client.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
|
|
4
|
+
export interface ProdServerOptions {
|
|
5
|
+
port: number
|
|
6
|
+
root: string
|
|
7
|
+
distDir: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function startProdServer(options: ProdServerOptions) {
|
|
11
|
+
const { port, distDir } = options
|
|
12
|
+
|
|
13
|
+
const serverEntry = path.resolve(distDir, 'server/entry-prod.js')
|
|
14
|
+
const { startServer } = await import(serverEntry)
|
|
15
|
+
|
|
16
|
+
console.log(chalk.cyan('Starting production server...'))
|
|
17
|
+
return startServer({ port, distDir })
|
|
18
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Shared request handler for API routes + SSR rendering
|
|
2
|
+
// Used by entry-prod.ts (Node) and entry-vercel.ts (Vercel)
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { render } from './entry-server'
|
|
5
|
+
import { matchRoute } from './router'
|
|
6
|
+
import { loadConfig } from '@/lib/config'
|
|
7
|
+
import { loadApiSpecs } from '@/lib/openapi'
|
|
8
|
+
import { getPage, loadPageComponent, buildPageTree } from '@/lib/source'
|
|
9
|
+
import { mdxComponents } from '@/components/mdx'
|
|
10
|
+
|
|
11
|
+
export interface RequestHandlerOptions {
|
|
12
|
+
template: string
|
|
13
|
+
baseUrl: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function handleRequest(url: string, options: RequestHandlerOptions): Promise<Response> {
|
|
17
|
+
const { template, baseUrl } = options
|
|
18
|
+
const fullUrl = new URL(url, baseUrl).href
|
|
19
|
+
|
|
20
|
+
// API routes
|
|
21
|
+
const routeHandler = matchRoute(fullUrl)
|
|
22
|
+
if (routeHandler) {
|
|
23
|
+
return routeHandler(new Request(fullUrl))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// SSR render
|
|
27
|
+
const pathname = new URL(url, baseUrl).pathname
|
|
28
|
+
const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean)
|
|
29
|
+
|
|
30
|
+
const config = loadConfig()
|
|
31
|
+
const apiSpecs = config.api?.length ? loadApiSpecs(config.api) : []
|
|
32
|
+
|
|
33
|
+
const [tree, sourcePage] = await Promise.all([
|
|
34
|
+
buildPageTree(),
|
|
35
|
+
getPage(slug),
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
let pageData = null
|
|
39
|
+
let embeddedData: any = { config, tree, slug, frontmatter: null, filePath: null }
|
|
40
|
+
|
|
41
|
+
if (sourcePage) {
|
|
42
|
+
const component = await loadPageComponent(sourcePage)
|
|
43
|
+
pageData = {
|
|
44
|
+
slug,
|
|
45
|
+
frontmatter: sourcePage.frontmatter,
|
|
46
|
+
content: component ? React.createElement(component, { components: mdxComponents }) : null,
|
|
47
|
+
}
|
|
48
|
+
embeddedData.frontmatter = sourcePage.frontmatter
|
|
49
|
+
embeddedData.filePath = sourcePage.filePath
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const html = render(url, { config, tree, page: pageData, apiSpecs })
|
|
53
|
+
|
|
54
|
+
const safeJson = JSON.stringify(embeddedData).replace(/</g, '\\u003c')
|
|
55
|
+
const dataScript = `<script>window.__PAGE_DATA__ = ${safeJson}</script>`
|
|
56
|
+
const finalHtml = template
|
|
57
|
+
.replace('<!--head-outlet-->', `<!--head-outlet-->${dataScript}`)
|
|
58
|
+
.replace('<!--ssr-outlet-->', html)
|
|
59
|
+
|
|
60
|
+
return new Response(finalHtml, {
|
|
61
|
+
status: 200,
|
|
62
|
+
headers: { 'Content-Type': 'text/html' },
|
|
63
|
+
})
|
|
64
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { handleHealth } from './handlers/health'
|
|
2
|
+
import { handleSearch } from './handlers/search'
|
|
3
|
+
import { handleApisProxy } from './handlers/apis-proxy'
|
|
4
|
+
import { handleOg } from './handlers/og'
|
|
5
|
+
import { handleLlms } from './handlers/llms'
|
|
6
|
+
import { handleSitemap } from './handlers/sitemap'
|
|
7
|
+
import { handleRobots } from './handlers/robots'
|
|
8
|
+
import { handleSpecs } from './handlers/specs'
|
|
9
|
+
|
|
10
|
+
export type RouteHandler = (req: Request) => Response | Promise<Response>
|
|
11
|
+
|
|
12
|
+
interface Route {
|
|
13
|
+
pattern: URLPattern
|
|
14
|
+
handler: RouteHandler
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const routes: Route[] = []
|
|
18
|
+
|
|
19
|
+
function addRoute(path: string, handler: RouteHandler) {
|
|
20
|
+
routes.push({
|
|
21
|
+
pattern: new URLPattern({ pathname: path }),
|
|
22
|
+
handler,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
addRoute('/api/health', handleHealth)
|
|
27
|
+
addRoute('/api/search', handleSearch)
|
|
28
|
+
addRoute('/api/apis-proxy', handleApisProxy)
|
|
29
|
+
addRoute('/api/specs', handleSpecs)
|
|
30
|
+
addRoute('/og', handleOg)
|
|
31
|
+
addRoute('/llms.txt', handleLlms)
|
|
32
|
+
addRoute('/sitemap.xml', handleSitemap)
|
|
33
|
+
addRoute('/robots.txt', handleRobots)
|
|
34
|
+
|
|
35
|
+
export function matchRoute(url: string): RouteHandler | null {
|
|
36
|
+
for (const route of routes) {
|
|
37
|
+
if (route.pattern.test(url)) {
|
|
38
|
+
return route.handler
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a URL path within a base directory, preventing path traversal.
|
|
5
|
+
* Returns null if the resolved path escapes the base directory.
|
|
6
|
+
*/
|
|
7
|
+
export function safePath(baseDir: string, urlPath: string): string | null {
|
|
8
|
+
const decoded = decodeURIComponent(urlPath.split('?')[0])
|
|
9
|
+
const resolved = path.resolve(baseDir, '.' + decoded)
|
|
10
|
+
if (!resolved.startsWith(path.resolve(baseDir) + path.sep) && resolved !== path.resolve(baseDir)) {
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
return resolved
|
|
14
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { type InlineConfig } from 'vite'
|
|
3
|
+
import react from '@vitejs/plugin-react'
|
|
4
|
+
import mdx from '@mdx-js/rollup'
|
|
5
|
+
import remarkDirective from 'remark-directive'
|
|
6
|
+
import remarkGfm from 'remark-gfm'
|
|
7
|
+
import remarkFrontmatter from 'remark-frontmatter'
|
|
8
|
+
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'
|
|
9
|
+
import rehypeShiki from '@shikijs/rehype'
|
|
10
|
+
|
|
11
|
+
export interface ViteConfigOptions {
|
|
12
|
+
root: string
|
|
13
|
+
contentDir: string
|
|
14
|
+
isDev?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function createViteConfig(options: ViteConfigOptions): Promise<InlineConfig> {
|
|
18
|
+
const { root, contentDir, isDev = false } = options
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
root,
|
|
22
|
+
configFile: false,
|
|
23
|
+
resolve: {
|
|
24
|
+
alias: {
|
|
25
|
+
'@': path.resolve(root, 'src'),
|
|
26
|
+
'@content': contentDir,
|
|
27
|
+
},
|
|
28
|
+
dedupe: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
|
|
29
|
+
},
|
|
30
|
+
server: {
|
|
31
|
+
fs: {
|
|
32
|
+
allow: [root, contentDir],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
plugins: [
|
|
36
|
+
mdx({
|
|
37
|
+
remarkPlugins: [
|
|
38
|
+
remarkFrontmatter,
|
|
39
|
+
remarkMdxFrontmatter,
|
|
40
|
+
remarkGfm,
|
|
41
|
+
remarkDirective,
|
|
42
|
+
],
|
|
43
|
+
rehypePlugins: [
|
|
44
|
+
[rehypeShiki, { themes: { light: 'github-light', dark: 'github-dark' }, defaultColor: false }],
|
|
45
|
+
],
|
|
46
|
+
mdExtensions: ['.md'],
|
|
47
|
+
mdxExtensions: ['.mdx'],
|
|
48
|
+
}),
|
|
49
|
+
react(),
|
|
50
|
+
],
|
|
51
|
+
define: {
|
|
52
|
+
'process.env.CHRONICLE_CONTENT_DIR': JSON.stringify(contentDir),
|
|
53
|
+
'process.env.CHRONICLE_PROJECT_ROOT': JSON.stringify(root),
|
|
54
|
+
},
|
|
55
|
+
css: {
|
|
56
|
+
modules: {
|
|
57
|
+
localsConvention: 'camelCase',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
ssr: {
|
|
61
|
+
noExternal: ['@raystack/apsara'],
|
|
62
|
+
},
|
|
63
|
+
build: {
|
|
64
|
+
rolldownOptions: {
|
|
65
|
+
input: isDev ? undefined : {
|
|
66
|
+
client: path.resolve(root, 'src/server/index.html'),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useMemo, useEffect, useRef } from "react";
|
|
4
|
-
import {
|
|
5
|
-
import NextLink from "next/link";
|
|
4
|
+
import { useLocation, Link } from "react-router-dom";
|
|
6
5
|
import { cx } from "class-variance-authority";
|
|
7
|
-
import { Flex, Navbar, Headline,
|
|
6
|
+
import { Flex, Navbar, Headline, Sidebar, Button } from "@raystack/apsara";
|
|
8
7
|
import { RectangleStackIcon } from "@heroicons/react/24/outline";
|
|
9
8
|
import { ClientThemeSwitcher } from "@/components/ui/client-theme-switcher";
|
|
10
9
|
import { Search } from "@/components/ui/search";
|
|
@@ -25,7 +24,7 @@ const iconMap: Record<string, React.ReactNode> = {
|
|
|
25
24
|
let savedScrollTop = 0;
|
|
26
25
|
|
|
27
26
|
export function Layout({ children, config, tree, classNames }: ThemeLayoutProps) {
|
|
28
|
-
const pathname =
|
|
27
|
+
const { pathname } = useLocation();
|
|
29
28
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
30
29
|
|
|
31
30
|
useEffect(() => {
|
|
@@ -45,21 +44,21 @@ export function Layout({ children, config, tree, classNames }: ThemeLayoutProps)
|
|
|
45
44
|
<Flex direction="column" className={cx(styles.layout, classNames?.layout)}>
|
|
46
45
|
<Navbar className={styles.header}>
|
|
47
46
|
<Navbar.Start>
|
|
48
|
-
<
|
|
47
|
+
<Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
|
|
49
48
|
<Headline size="small" weight="medium" as="h1">
|
|
50
49
|
{config.title}
|
|
51
50
|
</Headline>
|
|
52
|
-
</
|
|
51
|
+
</Link>
|
|
53
52
|
</Navbar.Start>
|
|
54
53
|
<Navbar.End>
|
|
55
54
|
<Flex gap="medium" align="center" className={styles.navActions}>
|
|
56
55
|
{config.api?.map((api) => (
|
|
57
|
-
<
|
|
56
|
+
<Link key={api.name} to={api.basePath} className={styles.navButton}>
|
|
58
57
|
{api.name} API
|
|
59
|
-
</
|
|
58
|
+
</Link>
|
|
60
59
|
))}
|
|
61
60
|
{config.navigation?.links?.map((link) => (
|
|
62
|
-
<Link key={link.href}
|
|
61
|
+
<Link key={link.href} to={link.href}>
|
|
63
62
|
{link.label}
|
|
64
63
|
</Link>
|
|
65
64
|
))}
|
|
@@ -118,7 +117,7 @@ function SidebarNode({
|
|
|
118
117
|
|
|
119
118
|
const isActive = pathname === item.url;
|
|
120
119
|
const href = item.url ?? "#";
|
|
121
|
-
const link = useMemo(() => <
|
|
120
|
+
const link = useMemo(() => <Link to={href} />, [href]);
|
|
122
121
|
|
|
123
122
|
return (
|
|
124
123
|
<Sidebar.Item
|
|
@@ -38,9 +38,69 @@
|
|
|
38
38
|
margin-bottom: var(--rs-space-3);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
.content :global(pre) {
|
|
42
|
+
background-color: var(--shiki-light-bg, #fff);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.content :global(pre code span) {
|
|
46
|
+
color: var(--shiki-light);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
:global([data-theme="dark"]) .content :global(pre) {
|
|
50
|
+
background-color: var(--shiki-dark-bg, #24292e);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
:global([data-theme="dark"]) .content :global(pre code span) {
|
|
54
|
+
color: var(--shiki-dark);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.content img {
|
|
58
|
+
max-width: 100%;
|
|
59
|
+
height: auto;
|
|
60
|
+
}
|
|
61
|
+
|
|
41
62
|
.content table {
|
|
42
63
|
display: block;
|
|
43
64
|
max-width: 100%;
|
|
44
65
|
overflow-x: auto;
|
|
45
66
|
margin-bottom: var(--rs-space-5);
|
|
46
67
|
}
|
|
68
|
+
|
|
69
|
+
.content details {
|
|
70
|
+
border: 1px solid var(--rs-color-border-base-primary);
|
|
71
|
+
border-radius: var(--rs-radius-2);
|
|
72
|
+
margin: var(--rs-space-5) 0;
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.content details summary {
|
|
77
|
+
padding: var(--rs-space-4) var(--rs-space-5);
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
font-weight: 500;
|
|
80
|
+
font-size: var(--rs-font-size-small);
|
|
81
|
+
color: var(--rs-color-text-base-primary);
|
|
82
|
+
background: var(--rs-color-background-base-secondary);
|
|
83
|
+
list-style: none;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: var(--rs-space-3);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.content details summary::-webkit-details-marker {
|
|
90
|
+
display: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.content details summary::before {
|
|
94
|
+
content: '▶';
|
|
95
|
+
font-size: 10px;
|
|
96
|
+
transition: transform 0.2s ease;
|
|
97
|
+
color: var(--rs-color-text-base-secondary);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.content details[open] > summary::before {
|
|
101
|
+
transform: rotate(90deg);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.content details > :not(summary) {
|
|
105
|
+
padding: var(--rs-space-4) var(--rs-space-5);
|
|
106
|
+
}
|