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.
Files changed (28) hide show
  1. package/package.json +1 -1
  2. package/templates/minimal/app/api/mdx-watch/route.ts +3 -110
  3. package/templates/minimal/app/api/search/route.ts +75 -0
  4. package/templates/minimal/app/docs/[version]/[...slug]/page.tsx +51 -59
  5. package/templates/minimal/app/docs/[version]/[...slug]/page.tsx.bak +203 -0
  6. package/templates/minimal/app/globals.css +9 -1
  7. package/templates/minimal/app/layout.tsx +9 -38
  8. package/templates/minimal/app/not-found.tsx +10 -0
  9. package/templates/minimal/app/page.tsx +3 -4
  10. package/templates/minimal/docs/v2.0.0/about.mdx +57 -0
  11. package/templates/minimal/docs/v2.0.0/components/_category_.json +8 -0
  12. package/templates/minimal/docs/v2.0.0/components/callout.mdx +83 -0
  13. package/templates/minimal/docs/v2.0.0/components/code-block.mdx +103 -0
  14. package/templates/minimal/docs/v2.0.0/components/index.mdx +8 -0
  15. package/templates/minimal/docs/v2.0.0/components/tabs.mdx +92 -0
  16. package/templates/minimal/docs/v2.0.0/configuration.mdx +322 -0
  17. package/templates/minimal/docs/v2.0.0/features.mdx +197 -0
  18. package/templates/minimal/docs/v2.0.0/getting-started.mdx +183 -0
  19. package/templates/minimal/next.config.mjs +1 -18
  20. package/templates/minimal/package-lock.json +1666 -288
  21. package/templates/minimal/package.json +6 -5
  22. package/templates/minimal/scripts/index-search.ts +25 -2
  23. package/templates/minimal/specra.config.json +4 -4
  24. package/templates/minimal/tsconfig.json +1 -1
  25. package/templates/minimal/docs/v1.0.0/index.mdx +0 -29
  26. package/templates/minimal/next.config.default.mjs +0 -40
  27. package/templates/minimal/next.config.export.mjs +0 -64
  28. package/templates/minimal/yarn.lock +0 -3666
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-specra",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "CLI tool to scaffold a new Specra documentation site with Next.js",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,113 +1,6 @@
1
- import { NextRequest } from 'next/server'
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
- // Store active watchers globally to clean up on process exit
10
- const activeWatchers = new Set<FSWatcher>()
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 specraConfig from "./../../../../specra.config.json"
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
- content={
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 +1,9 @@
1
- @import "specra/styles";
1
+ @import "specra/styles";
2
+
3
+ /* Scan app files for Tailwind utilities */
4
+ @source "./**/*.{js,ts,jsx,tsx,mdx}";
5
+
6
+ @theme inline {
7
+ --font-sans: "Geist", "Geist Fallback";
8
+ --font-mono: "Geist Mono", "Geist Mono Fallback";
9
+ }
@@ -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 { TabProvider, ConfigProvider } from "specra/components"
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
- Fonts
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} ${geistMono.className} antialiased`}
51
+ // className={`${geist.className} ${geist.style.fontFamily} font-sans antialiased`}
81
52
  className={`font-sans antialiased`}
82
53
  >
83
54
  <ConfigProvider config={config}>