create-specra 0.1.5 → 0.1.7
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/package.json +1 -1
- package/templates/minimal/app/api/mdx-watch/route.ts +3 -110
- package/templates/minimal/app/api/search/route.ts +75 -0
- package/templates/minimal/app/docs/[version]/[...slug]/page.tsx +51 -59
- package/templates/minimal/app/docs/[version]/[...slug]/page.tsx.bak +203 -0
- package/templates/minimal/app/globals.css +9 -1
- package/templates/minimal/app/layout.tsx +9 -38
- package/templates/minimal/app/not-found.tsx +10 -0
- package/templates/minimal/app/page.tsx +3 -4
- package/templates/minimal/docs/v2.0.0/about.mdx +57 -0
- package/templates/minimal/docs/v2.0.0/components/_category_.json +8 -0
- package/templates/minimal/docs/v2.0.0/components/callout.mdx +83 -0
- package/templates/minimal/docs/v2.0.0/components/code-block.mdx +103 -0
- package/templates/minimal/docs/v2.0.0/components/index.mdx +8 -0
- package/templates/minimal/docs/v2.0.0/components/tabs.mdx +92 -0
- package/templates/minimal/docs/v2.0.0/configuration.mdx +322 -0
- package/templates/minimal/docs/v2.0.0/features.mdx +197 -0
- package/templates/minimal/docs/v2.0.0/getting-started.mdx +183 -0
- package/templates/minimal/next.config.mjs +1 -18
- package/templates/minimal/package-lock.json +1666 -288
- package/templates/minimal/package.json +6 -5
- package/templates/minimal/scripts/index-search.ts +25 -2
- package/templates/minimal/specra.config.json +4 -4
- package/templates/minimal/tsconfig.json +1 -1
- package/templates/minimal/docs/v1.0.0/index.mdx +0 -29
- package/templates/minimal/next.config.default.mjs +0 -40
- package/templates/minimal/next.config.export.mjs +0 -64
- package/templates/minimal/yarn.lock +0 -3666
package/package.json
CHANGED
|
@@ -1,113 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import { watch, type FSWatcher } from 'fs'
|
|
3
|
-
import { join } from 'path'
|
|
4
|
-
|
|
5
|
-
// Mark route as incompatible with static export (since it's dev-only SSE endpoint)
|
|
1
|
+
// Next.js requires dynamic to be statically analyzable, so we can't re-export it
|
|
6
2
|
export const dynamic = 'error'
|
|
7
3
|
export const runtime = 'nodejs'
|
|
8
4
|
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// Clean up all watchers on process termination
|
|
13
|
-
if (process.env.NODE_ENV === 'development') {
|
|
14
|
-
const cleanup = () => {
|
|
15
|
-
if (activeWatchers.size > 0) {
|
|
16
|
-
console.log('[MDX Watch] Cleaning up watchers...')
|
|
17
|
-
activeWatchers.forEach((watcher) => {
|
|
18
|
-
try {
|
|
19
|
-
watcher.close()
|
|
20
|
-
} catch (error) {
|
|
21
|
-
// Ignore errors during cleanup
|
|
22
|
-
}
|
|
23
|
-
})
|
|
24
|
-
activeWatchers.clear()
|
|
25
|
-
console.log('[MDX Watch] Cleanup complete')
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Use regular event listeners instead of 'once' to ensure cleanup happens
|
|
30
|
-
process.on('SIGINT', cleanup)
|
|
31
|
-
process.on('SIGTERM', cleanup)
|
|
32
|
-
process.on('beforeExit', cleanup)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function GET(request: NextRequest) {
|
|
36
|
-
// Only allow in development mode
|
|
37
|
-
if (process.env.NODE_ENV !== 'development') {
|
|
38
|
-
return new Response('Not available in production', { status: 404 })
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const encoder = new TextEncoder()
|
|
42
|
-
|
|
43
|
-
const stream = new ReadableStream({
|
|
44
|
-
start(controller) {
|
|
45
|
-
// Send initial connection message
|
|
46
|
-
const connectMsg = `data: ${JSON.stringify({ type: 'connected' })}\n\n`
|
|
47
|
-
controller.enqueue(encoder.encode(connectMsg))
|
|
48
|
-
|
|
49
|
-
const docsPath = join(process.cwd(), 'docs')
|
|
50
|
-
|
|
51
|
-
// Watch the docs directory recursively
|
|
52
|
-
const watcher = watch(
|
|
53
|
-
docsPath,
|
|
54
|
-
{ recursive: true },
|
|
55
|
-
(eventType, filename) => {
|
|
56
|
-
if (!filename) return
|
|
57
|
-
|
|
58
|
-
// Only watch for .mdx and .json files (MDX files and category configs)
|
|
59
|
-
if (filename.endsWith('.mdx') || filename.endsWith('.json')) {
|
|
60
|
-
console.log(`[MDX Watch] ${eventType}: ${filename}`)
|
|
61
|
-
|
|
62
|
-
const message = `data: ${JSON.stringify({
|
|
63
|
-
type: 'change',
|
|
64
|
-
file: filename,
|
|
65
|
-
eventType
|
|
66
|
-
})}\n\n`
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
controller.enqueue(encoder.encode(message))
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.error('[MDX Watch] Error sending message:', error)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
// Add to active watchers for cleanup on process exit
|
|
78
|
-
activeWatchers.add(watcher)
|
|
79
|
-
|
|
80
|
-
// Handle cleanup
|
|
81
|
-
const cleanupWatcher = () => {
|
|
82
|
-
console.log('[MDX Watch] Closing watcher')
|
|
83
|
-
activeWatchers.delete(watcher)
|
|
84
|
-
watcher.close()
|
|
85
|
-
try {
|
|
86
|
-
controller.close()
|
|
87
|
-
} catch (error) {
|
|
88
|
-
// Controller might already be closed
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Handle client disconnect
|
|
93
|
-
request.signal.addEventListener('abort', () => {
|
|
94
|
-
console.log('[MDX Watch] Client disconnected')
|
|
95
|
-
cleanupWatcher()
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
// Handle errors
|
|
99
|
-
watcher.on('error', (error) => {
|
|
100
|
-
console.error('[MDX Watch] Watcher error:', error)
|
|
101
|
-
cleanupWatcher()
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
return new Response(stream, {
|
|
107
|
-
headers: {
|
|
108
|
-
'Content-Type': 'text/event-stream',
|
|
109
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
110
|
-
'Connection': 'keep-alive',
|
|
111
|
-
},
|
|
112
|
-
})
|
|
113
|
-
}
|
|
5
|
+
// Re-export the GET handler from SDK
|
|
6
|
+
export { GET } from "specra/app/api/mdx-watch"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server"
|
|
2
|
+
import { MeiliSearch } from "meilisearch"
|
|
3
|
+
import { SpecraConfig } from "specra"
|
|
4
|
+
import specraConfig from "../../../specra.config.json"
|
|
5
|
+
|
|
6
|
+
export async function POST(request: NextRequest) {
|
|
7
|
+
try {
|
|
8
|
+
const config: SpecraConfig = specraConfig as any
|
|
9
|
+
const searchConfig = config.search
|
|
10
|
+
|
|
11
|
+
// Check if search is enabled and Meilisearch is configured
|
|
12
|
+
if (!searchConfig?.enabled || searchConfig.provider !== "meilisearch") {
|
|
13
|
+
return NextResponse.json(
|
|
14
|
+
{ error: "Search is not enabled" },
|
|
15
|
+
{ status: 400 }
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const meilisearchConfig = searchConfig.meilisearch
|
|
20
|
+
if (!meilisearchConfig) {
|
|
21
|
+
return NextResponse.json(
|
|
22
|
+
{ error: "Meilisearch is not configured" },
|
|
23
|
+
{ status: 400 }
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { query } = await request.json()
|
|
28
|
+
|
|
29
|
+
if (!query || typeof query !== "string") {
|
|
30
|
+
return NextResponse.json(
|
|
31
|
+
{ error: "Invalid query" },
|
|
32
|
+
{ status: 400 }
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Initialize Meilisearch client with API key
|
|
37
|
+
const client = new MeiliSearch({
|
|
38
|
+
host: meilisearchConfig.host,
|
|
39
|
+
apiKey: meilisearchConfig.apiKey || "",
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Search the index
|
|
43
|
+
const index = client.index(meilisearchConfig.indexName)
|
|
44
|
+
const searchResults = await index.search(query, {
|
|
45
|
+
limit: 50, // Get more results before deduplication
|
|
46
|
+
attributesToHighlight: ["title", "content"],
|
|
47
|
+
attributesToCrop: ["content"],
|
|
48
|
+
cropLength: 100,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Deduplicate results by slug and version on the server side
|
|
52
|
+
const seenDocs = new Set<string>()
|
|
53
|
+
const uniqueHits = searchResults.hits.filter((hit: any) => {
|
|
54
|
+
const key = `${hit.version}-${hit.slug}`
|
|
55
|
+
if (seenDocs.has(key)) {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
seenDocs.add(key)
|
|
59
|
+
return true
|
|
60
|
+
}).slice(0, 20) // Return only top 20 unique results
|
|
61
|
+
|
|
62
|
+
return NextResponse.json({
|
|
63
|
+
hits: uniqueHits,
|
|
64
|
+
query: query,
|
|
65
|
+
processingTimeMs: searchResults.processingTimeMs,
|
|
66
|
+
estimatedTotalHits: uniqueHits.length,
|
|
67
|
+
})
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error("Search API error:", error)
|
|
70
|
+
return NextResponse.json(
|
|
71
|
+
{ error: "Search failed", details: error instanceof Error ? error.message : "Unknown error" },
|
|
72
|
+
{ status: 500 }
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -8,22 +8,21 @@ import {
|
|
|
8
8
|
getCachedAllDocs,
|
|
9
9
|
getCachedDocBySlug,
|
|
10
10
|
getConfig,
|
|
11
|
-
SpecraConfig,
|
|
12
11
|
} from "specra/lib"
|
|
13
12
|
import {
|
|
14
|
-
DocLayout,
|
|
15
13
|
TableOfContents,
|
|
16
14
|
Header,
|
|
17
15
|
DocLayoutWrapper,
|
|
18
16
|
HotReloadIndicator,
|
|
19
17
|
DevModeBadge,
|
|
20
18
|
MdxHotReload,
|
|
21
|
-
CategoryIndex,
|
|
22
19
|
NotFoundContent,
|
|
23
20
|
DocLoading,
|
|
21
|
+
SearchHighlight,
|
|
24
22
|
} from "specra/components"
|
|
25
|
-
|
|
26
|
-
import
|
|
23
|
+
import { CategoryIndex, DocLayout } from "specra/layouts"
|
|
24
|
+
import { mdxComponents } from "specra/mdx-components"
|
|
25
|
+
// import { mdxComponents } from "specra/mdx-components"
|
|
27
26
|
|
|
28
27
|
interface PageProps {
|
|
29
28
|
params: Promise<{
|
|
@@ -35,7 +34,6 @@ interface PageProps {
|
|
|
35
34
|
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
36
35
|
const { version, slug: slugArray } = await params
|
|
37
36
|
const slug = slugArray.join("/")
|
|
38
|
-
|
|
39
37
|
const doc = await getCachedDocBySlug(slug, version)
|
|
40
38
|
|
|
41
39
|
if (!doc) {
|
|
@@ -78,7 +76,6 @@ export async function generateStaticParams() {
|
|
|
78
76
|
for (const version of versions) {
|
|
79
77
|
const docs = await getCachedAllDocs(version)
|
|
80
78
|
for (const doc of docs) {
|
|
81
|
-
// Add the custom slug path
|
|
82
79
|
params.push({
|
|
83
80
|
version,
|
|
84
81
|
slug: doc.slug.split("/").filter(Boolean),
|
|
@@ -97,36 +94,33 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
97
94
|
const versions = getCachedVersions()
|
|
98
95
|
const config = getConfig()
|
|
99
96
|
const isCategory = isCategoryPage(slug, allDocs)
|
|
100
|
-
|
|
101
|
-
// Try to get the doc (might be index.mdx or regular .mdx)
|
|
102
97
|
const doc = await getCachedDocBySlug(slug, version)
|
|
103
98
|
|
|
104
|
-
// If no doc found and it's a category, show category index
|
|
105
99
|
if (!doc && isCategory) {
|
|
106
|
-
// Find a doc in this category to get the tab group
|
|
107
100
|
const categoryDoc = allDocs.find((d) => d.slug.startsWith(slug + "/"))
|
|
108
101
|
const categoryTabGroup = categoryDoc?.meta?.tab_group || categoryDoc?.categoryTabGroup
|
|
109
102
|
|
|
110
103
|
return (
|
|
111
104
|
<>
|
|
112
105
|
<DocLayoutWrapper
|
|
106
|
+
key={'doc-layout'}
|
|
113
107
|
header={<Header currentVersion={version} versions={versions} config={config} />}
|
|
114
108
|
docs={allDocs}
|
|
115
109
|
version={version}
|
|
116
|
-
content={
|
|
117
|
-
<CategoryIndex
|
|
118
|
-
categoryPath={slug}
|
|
119
|
-
version={version}
|
|
120
|
-
allDocs={allDocs}
|
|
121
|
-
title={slug.split("/").pop()?.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()) || "Category"}
|
|
122
|
-
description="Browse the documentation in this section."
|
|
123
|
-
config={config}
|
|
124
|
-
/>
|
|
125
|
-
}
|
|
126
110
|
toc={<div />}
|
|
127
111
|
config={config}
|
|
128
112
|
currentPageTabGroup={categoryTabGroup}
|
|
129
|
-
|
|
113
|
+
>
|
|
114
|
+
<CategoryIndex
|
|
115
|
+
categoryPath={slug}
|
|
116
|
+
version={version}
|
|
117
|
+
allDocs={allDocs}
|
|
118
|
+
title={slug.split("/").pop()?.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()) || "Category"}
|
|
119
|
+
description="Browse the documentation in this section."
|
|
120
|
+
config={config}
|
|
121
|
+
mdxComponents={mdxComponents}
|
|
122
|
+
/>
|
|
123
|
+
</DocLayoutWrapper>
|
|
130
124
|
<MdxHotReload />
|
|
131
125
|
<HotReloadIndicator />
|
|
132
126
|
<DevModeBadge />
|
|
@@ -134,7 +128,6 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
134
128
|
)
|
|
135
129
|
}
|
|
136
130
|
|
|
137
|
-
// If no doc found, render 404 content within the layout (keeps sidebar visible)
|
|
138
131
|
if (!doc) {
|
|
139
132
|
return (
|
|
140
133
|
<>
|
|
@@ -143,11 +136,12 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
143
136
|
header={<Header currentVersion={version} versions={versions} config={config} />}
|
|
144
137
|
docs={allDocs}
|
|
145
138
|
version={version}
|
|
146
|
-
content={<NotFoundContent version={version} />}
|
|
147
139
|
toc={<div />}
|
|
148
140
|
config={config}
|
|
149
141
|
currentPageTabGroup={undefined}
|
|
150
|
-
|
|
142
|
+
>
|
|
143
|
+
<NotFoundContent version={version} />
|
|
144
|
+
</DocLayoutWrapper>
|
|
151
145
|
<MdxHotReload />
|
|
152
146
|
<HotReloadIndicator />
|
|
153
147
|
<DevModeBadge />
|
|
@@ -158,15 +152,7 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
158
152
|
|
|
159
153
|
const toc = extractTableOfContents(doc.content)
|
|
160
154
|
const { previous, next } = getAdjacentDocs(slug, allDocs)
|
|
161
|
-
|
|
162
|
-
// console.log("[v0] Extracted ToC:", toc)
|
|
163
|
-
|
|
164
|
-
// If doc exists but is also a category, show both content and children
|
|
165
155
|
const showCategoryIndex = isCategory && doc
|
|
166
|
-
|
|
167
|
-
// console.log("showCategoryIndex: ", showCategoryIndex)
|
|
168
|
-
|
|
169
|
-
// Get current page's tab group from doc metadata or category
|
|
170
156
|
const currentPageTabGroup = doc.meta?.tab_group || doc.categoryTabGroup
|
|
171
157
|
|
|
172
158
|
return (
|
|
@@ -176,34 +162,39 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
176
162
|
header={<Header currentVersion={version} versions={versions} config={config} />}
|
|
177
163
|
docs={allDocs}
|
|
178
164
|
version={version}
|
|
179
|
-
|
|
180
|
-
showCategoryIndex ? (
|
|
181
|
-
<CategoryIndex
|
|
182
|
-
categoryPath={slug}
|
|
183
|
-
version={version}
|
|
184
|
-
allDocs={allDocs}
|
|
185
|
-
title={doc.meta.title}
|
|
186
|
-
description={doc.meta.description}
|
|
187
|
-
content={doc.content}
|
|
188
|
-
config={config}
|
|
189
|
-
/>
|
|
190
|
-
) : (
|
|
191
|
-
<DocLayout
|
|
192
|
-
key={`doc-layout`}
|
|
193
|
-
meta={doc.meta}
|
|
194
|
-
content={doc.content}
|
|
195
|
-
previousDoc={previous ? { title: previous.meta.title, slug: previous.slug } : undefined}
|
|
196
|
-
nextDoc={next ? { title: next.meta.title, slug: next.slug } : undefined}
|
|
197
|
-
version={version}
|
|
198
|
-
slug={slug}
|
|
199
|
-
config={config}
|
|
200
|
-
/>
|
|
201
|
-
)
|
|
202
|
-
}
|
|
203
|
-
toc={showCategoryIndex ? <div /> : <TableOfContents key={'table-of-contents'} items={toc} config={config} />}
|
|
165
|
+
toc={showCategoryIndex ? <div /> : <TableOfContents key={'toc'} items={toc} config={config} />}
|
|
204
166
|
config={config}
|
|
205
167
|
currentPageTabGroup={currentPageTabGroup}
|
|
206
|
-
|
|
168
|
+
>
|
|
169
|
+
{showCategoryIndex ? (
|
|
170
|
+
<CategoryIndex
|
|
171
|
+
key="category-index"
|
|
172
|
+
categoryPath={slug}
|
|
173
|
+
version={version}
|
|
174
|
+
allDocs={allDocs}
|
|
175
|
+
title={doc.meta.title}
|
|
176
|
+
description={doc.meta.description}
|
|
177
|
+
content={doc.content}
|
|
178
|
+
config={config}
|
|
179
|
+
mdxComponents={mdxComponents}
|
|
180
|
+
/>
|
|
181
|
+
) : (
|
|
182
|
+
<>
|
|
183
|
+
<SearchHighlight />
|
|
184
|
+
<DocLayout
|
|
185
|
+
key="doc-layout"
|
|
186
|
+
meta={doc.meta}
|
|
187
|
+
content={doc.content}
|
|
188
|
+
previousDoc={previous ? { title: previous.meta.title, slug: previous.slug } : undefined}
|
|
189
|
+
nextDoc={next ? { title: next.meta.title, slug: next.slug } : undefined}
|
|
190
|
+
version={version}
|
|
191
|
+
slug={slug}
|
|
192
|
+
config={config}
|
|
193
|
+
mdxComponents={mdxComponents}
|
|
194
|
+
/>
|
|
195
|
+
</>
|
|
196
|
+
)}
|
|
197
|
+
</DocLayoutWrapper>
|
|
207
198
|
<MdxHotReload />
|
|
208
199
|
<HotReloadIndicator />
|
|
209
200
|
<DevModeBadge />
|
|
@@ -211,3 +202,4 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
211
202
|
</>
|
|
212
203
|
)
|
|
213
204
|
}
|
|
205
|
+
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { Metadata } from "next"
|
|
2
|
+
import { Suspense } from "react"
|
|
3
|
+
import {
|
|
4
|
+
extractTableOfContents,
|
|
5
|
+
getAdjacentDocs,
|
|
6
|
+
isCategoryPage,
|
|
7
|
+
getCachedVersions,
|
|
8
|
+
getCachedAllDocs,
|
|
9
|
+
getCachedDocBySlug,
|
|
10
|
+
getConfig,
|
|
11
|
+
} from "specra/lib"
|
|
12
|
+
import {
|
|
13
|
+
TableOfContents,
|
|
14
|
+
Header,
|
|
15
|
+
DocLayoutWrapper,
|
|
16
|
+
HotReloadIndicator,
|
|
17
|
+
DevModeBadge,
|
|
18
|
+
MdxHotReload,
|
|
19
|
+
NotFoundContent,
|
|
20
|
+
DocLoading,
|
|
21
|
+
} from "specra/components"
|
|
22
|
+
import { CategoryIndex, DocLayout } from "specra/layouts"
|
|
23
|
+
import { mdxComponents } from "specra/mdx-components"
|
|
24
|
+
// import { mdxComponents } from "specra/mdx-components"
|
|
25
|
+
|
|
26
|
+
interface PageProps {
|
|
27
|
+
params: Promise<{
|
|
28
|
+
version: string
|
|
29
|
+
slug: string[]
|
|
30
|
+
}>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
34
|
+
const { version, slug: slugArray } = await params
|
|
35
|
+
const slug = slugArray.join("/")
|
|
36
|
+
const doc = await getCachedDocBySlug(slug, version)
|
|
37
|
+
|
|
38
|
+
if (!doc) {
|
|
39
|
+
return {
|
|
40
|
+
title: "Page Not Found",
|
|
41
|
+
description: "The requested documentation page could not be found.",
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const title = doc.meta.title || doc.title
|
|
46
|
+
const description = doc.meta.description || `Documentation for ${title}`
|
|
47
|
+
const url = `/docs/${version}/${slug}`
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
title: `${title}`,
|
|
51
|
+
description,
|
|
52
|
+
openGraph: {
|
|
53
|
+
title,
|
|
54
|
+
description,
|
|
55
|
+
url,
|
|
56
|
+
siteName: "Documentation Platform",
|
|
57
|
+
type: "article",
|
|
58
|
+
locale: "en_US",
|
|
59
|
+
},
|
|
60
|
+
twitter: {
|
|
61
|
+
card: "summary_large_image",
|
|
62
|
+
title,
|
|
63
|
+
description,
|
|
64
|
+
},
|
|
65
|
+
alternates: {
|
|
66
|
+
canonical: url,
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function generateStaticParams() {
|
|
72
|
+
const versions = getCachedVersions()
|
|
73
|
+
const params = []
|
|
74
|
+
|
|
75
|
+
for (const version of versions) {
|
|
76
|
+
const docs = await getCachedAllDocs(version)
|
|
77
|
+
for (const doc of docs) {
|
|
78
|
+
params.push({
|
|
79
|
+
version,
|
|
80
|
+
slug: doc.slug.split("/").filter(Boolean),
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return params
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default async function DocPage({ params }: PageProps) {
|
|
89
|
+
const { version, slug: slugArray } = await params
|
|
90
|
+
const slug = slugArray.join("/")
|
|
91
|
+
|
|
92
|
+
const allDocs = await getCachedAllDocs(version)
|
|
93
|
+
const versions = getCachedVersions()
|
|
94
|
+
const config = getConfig()
|
|
95
|
+
const isCategory = isCategoryPage(slug, allDocs)
|
|
96
|
+
const doc = await getCachedDocBySlug(slug, version)
|
|
97
|
+
|
|
98
|
+
if (!doc && isCategory) {
|
|
99
|
+
const categoryDoc = allDocs.find((d) => d.slug.startsWith(slug + "/"))
|
|
100
|
+
const categoryTabGroup = categoryDoc?.meta?.tab_group || categoryDoc?.categoryTabGroup
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<>
|
|
104
|
+
<DocLayoutWrapper
|
|
105
|
+
key={'doc-layout'}
|
|
106
|
+
header={<Header currentVersion={version} versions={versions} config={config} />}
|
|
107
|
+
docs={allDocs}
|
|
108
|
+
version={version}
|
|
109
|
+
toc={<div />}
|
|
110
|
+
config={config}
|
|
111
|
+
currentPageTabGroup={categoryTabGroup}
|
|
112
|
+
>
|
|
113
|
+
<CategoryIndex
|
|
114
|
+
categoryPath={slug}
|
|
115
|
+
version={version}
|
|
116
|
+
allDocs={allDocs}
|
|
117
|
+
title={slug.split("/").pop()?.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()) || "Category"}
|
|
118
|
+
description="Browse the documentation in this section."
|
|
119
|
+
config={config}
|
|
120
|
+
mdxComponents={mdxComponents}
|
|
121
|
+
/>
|
|
122
|
+
</DocLayoutWrapper>
|
|
123
|
+
<MdxHotReload />
|
|
124
|
+
<HotReloadIndicator />
|
|
125
|
+
<DevModeBadge />
|
|
126
|
+
</>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!doc) {
|
|
131
|
+
return (
|
|
132
|
+
<>
|
|
133
|
+
<Suspense fallback={<DocLoading />}>
|
|
134
|
+
<DocLayoutWrapper
|
|
135
|
+
header={<Header currentVersion={version} versions={versions} config={config} />}
|
|
136
|
+
docs={allDocs}
|
|
137
|
+
version={version}
|
|
138
|
+
toc={<div />}
|
|
139
|
+
config={config}
|
|
140
|
+
currentPageTabGroup={undefined}
|
|
141
|
+
>
|
|
142
|
+
<NotFoundContent version={version} />
|
|
143
|
+
</DocLayoutWrapper>
|
|
144
|
+
<MdxHotReload />
|
|
145
|
+
<HotReloadIndicator />
|
|
146
|
+
<DevModeBadge />
|
|
147
|
+
</Suspense>
|
|
148
|
+
</>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const toc = extractTableOfContents(doc.content)
|
|
153
|
+
const { previous, next } = getAdjacentDocs(slug, allDocs)
|
|
154
|
+
const showCategoryIndex = isCategory && doc
|
|
155
|
+
const currentPageTabGroup = doc.meta?.tab_group || doc.categoryTabGroup
|
|
156
|
+
|
|
157
|
+
console.log(mdxComponents)
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<>
|
|
161
|
+
<Suspense fallback={<DocLoading />}>
|
|
162
|
+
<DocLayoutWrapper
|
|
163
|
+
header={<Header currentVersion={version} versions={versions} config={config} />}
|
|
164
|
+
docs={allDocs}
|
|
165
|
+
version={version}
|
|
166
|
+
toc={showCategoryIndex ? <div /> : <TableOfContents key={'toc'} items={toc} config={config} />}
|
|
167
|
+
config={config}
|
|
168
|
+
currentPageTabGroup={currentPageTabGroup}
|
|
169
|
+
>
|
|
170
|
+
{showCategoryIndex ? (
|
|
171
|
+
<CategoryIndex
|
|
172
|
+
key="category-index"
|
|
173
|
+
categoryPath={slug}
|
|
174
|
+
version={version}
|
|
175
|
+
allDocs={allDocs}
|
|
176
|
+
title={doc.meta.title}
|
|
177
|
+
description={doc.meta.description}
|
|
178
|
+
content={doc.content}
|
|
179
|
+
config={config}
|
|
180
|
+
mdxComponents={mdxComponents}
|
|
181
|
+
/>
|
|
182
|
+
) : (
|
|
183
|
+
<DocLayout
|
|
184
|
+
key="doc-layout"
|
|
185
|
+
meta={doc.meta}
|
|
186
|
+
content={doc.content}
|
|
187
|
+
previousDoc={previous ? { title: previous.meta.title, slug: previous.slug } : undefined}
|
|
188
|
+
nextDoc={next ? { title: next.meta.title, slug: next.slug } : undefined}
|
|
189
|
+
version={version}
|
|
190
|
+
slug={slug}
|
|
191
|
+
config={config}
|
|
192
|
+
mdxComponents={mdxComponents}
|
|
193
|
+
/>
|
|
194
|
+
)}
|
|
195
|
+
</DocLayoutWrapper>
|
|
196
|
+
<MdxHotReload />
|
|
197
|
+
<HotReloadIndicator />
|
|
198
|
+
<DevModeBadge />
|
|
199
|
+
</Suspense>
|
|
200
|
+
</>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
@@ -1,33 +1,16 @@
|
|
|
1
|
-
import "server-only"
|
|
2
|
-
|
|
3
|
-
import type React from "react"
|
|
4
1
|
import type { Metadata } from "next"
|
|
5
2
|
import { Geist, Geist_Mono } from "next/font/google"
|
|
6
|
-
|
|
7
3
|
import { getConfig, getAssetPath, SpecraConfig, initConfig } from "specra/lib"
|
|
8
|
-
import {
|
|
9
|
-
|
|
4
|
+
import { ConfigProvider, TabProvider } from "specra/components"
|
|
10
5
|
import specraConfig from "../specra.config.json"
|
|
11
6
|
import "./globals.css"
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
------------------------------ */
|
|
16
|
-
const geist = Geist({ subsets: ["latin"] })
|
|
17
|
-
const geistMono = Geist_Mono({ subsets: ["latin"] })
|
|
18
|
-
|
|
19
|
-
// const sans = Geist({ subsets: ["latin"] })
|
|
20
|
-
// const geistMono = Geist_Mono({ subsets: ["latin"] })
|
|
8
|
+
const _geist = Geist({ subsets: ["latin"] })
|
|
9
|
+
const _geistMono = Geist_Mono({ subsets: ["latin"] })
|
|
21
10
|
|
|
22
|
-
|
|
23
|
-
Initialize Specra config ONCE
|
|
24
|
-
(module scope, server-only)
|
|
25
|
-
------------------------------ */
|
|
11
|
+
// Initialize Specra config
|
|
26
12
|
initConfig(specraConfig as unknown as Partial<SpecraConfig>)
|
|
27
13
|
|
|
28
|
-
/* -----------------------------
|
|
29
|
-
Runtime Metadata (REQUIRED)
|
|
30
|
-
------------------------------ */
|
|
31
14
|
export async function generateMetadata(): Promise<Metadata> {
|
|
32
15
|
const config = getConfig()
|
|
33
16
|
|
|
@@ -37,13 +20,9 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
37
20
|
template: `%s | ${config.site.title}`,
|
|
38
21
|
},
|
|
39
22
|
description: config.site.description || "Modern documentation platform",
|
|
40
|
-
metadataBase: config.site.url
|
|
41
|
-
? new URL(config.site.url)
|
|
42
|
-
: undefined,
|
|
23
|
+
metadataBase: config.site.url ? new URL(config.site.url) : undefined,
|
|
43
24
|
icons: {
|
|
44
|
-
icon: config.site.favicon
|
|
45
|
-
? [{ url: getAssetPath(config.site.favicon) }]
|
|
46
|
-
: [],
|
|
25
|
+
icon: config.site.favicon ? [{ url: getAssetPath(config.site.favicon) }] : [],
|
|
47
26
|
apple: getAssetPath("/apple-icon.png"),
|
|
48
27
|
},
|
|
49
28
|
openGraph: {
|
|
@@ -62,22 +41,14 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
62
41
|
}
|
|
63
42
|
}
|
|
64
43
|
|
|
65
|
-
|
|
66
|
-
Root Layout
|
|
67
|
-
------------------------------ */
|
|
68
|
-
export default function RootLayout({
|
|
69
|
-
children,
|
|
70
|
-
}: {
|
|
71
|
-
children: React.ReactNode
|
|
72
|
-
}) {
|
|
44
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
73
45
|
const config = getConfig()
|
|
74
|
-
const defaultTab =
|
|
75
|
-
config.navigation?.tabGroups?.[0]?.id ?? ""
|
|
46
|
+
const defaultTab = config.navigation?.tabGroups?.[0]?.id ?? ""
|
|
76
47
|
|
|
77
48
|
return (
|
|
78
49
|
<html lang={config.site.language || "en"}>
|
|
79
50
|
<body
|
|
80
|
-
// className={`${geist.className} ${
|
|
51
|
+
// className={`${geist.className} ${geist.style.fontFamily} font-sans antialiased`}
|
|
81
52
|
className={`font-sans antialiased`}
|
|
82
53
|
>
|
|
83
54
|
<ConfigProvider config={config}>
|