create-fumadocs-app 16.0.1 → 16.0.3

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 (64) hide show
  1. package/dist/chunk-3VWJJEHU.js +204 -0
  2. package/dist/chunk-HLHY7KAF.js +181 -0
  3. package/dist/create-app.d.ts +23 -8
  4. package/dist/create-app.js +4 -5
  5. package/dist/index.js +33 -31
  6. package/dist/orama-cloud-VQUOOEZ2.js +313 -0
  7. package/package.json +5 -3
  8. package/template/+next+fuma-docs-mdx/app/(home)/page.tsx +16 -0
  9. package/template/{+next+tailwindcss → +next+fuma-docs-mdx}/app/layout.tsx +1 -1
  10. package/template/{+next → +next+fuma-docs-mdx}/example.gitignore +0 -2
  11. package/template/+next+fuma-docs-mdx/lib/layout.shared.tsx +9 -0
  12. package/template/+next+fuma-docs-mdx/lib/source.ts +1 -1
  13. package/template/{+next → +next+fuma-docs-mdx}/mdx-components.tsx +0 -1
  14. package/template/+next+fuma-docs-mdx/package.json +29 -0
  15. package/template/+next+fuma-docs-mdx/source.config.ts +1 -0
  16. package/template/+next+fuma-docs-mdx/tsconfig.json +2 -2
  17. package/template/+orama-cloud/@app/components/search.tsx +59 -0
  18. package/template/+orama-cloud/@app/lib/export-static-indexes.ts +14 -0
  19. package/template/+orama-cloud/@root/.env.example +6 -0
  20. package/template/react-router/app/docs/page.tsx +4 -4
  21. package/template/react-router/example.gitignore +0 -1
  22. package/template/react-router/react-router.config.ts +3 -3
  23. package/template/react-router-spa/README.md +12 -0
  24. package/template/react-router-spa/app/app.css +3 -0
  25. package/template/react-router-spa/app/components/search.tsx +50 -0
  26. package/template/react-router-spa/app/docs/page.tsx +53 -0
  27. package/template/react-router-spa/app/docs/search.ts +10 -0
  28. package/template/react-router-spa/app/lib/layout.shared.tsx +9 -0
  29. package/template/react-router-spa/app/lib/source.ts +7 -0
  30. package/template/react-router-spa/app/root.tsx +76 -0
  31. package/template/react-router-spa/app/routes/home.tsx +30 -0
  32. package/template/react-router-spa/app/routes.ts +7 -0
  33. package/template/react-router-spa/content/docs/index.mdx +32 -0
  34. package/template/react-router-spa/content/docs/test.mdx +24 -0
  35. package/template/react-router-spa/example.gitignore +7 -0
  36. package/template/react-router-spa/package.json +36 -0
  37. package/template/react-router-spa/public/favicon.ico +0 -0
  38. package/template/react-router-spa/react-router.config.ts +23 -0
  39. package/template/react-router-spa/serve.json +3 -0
  40. package/template/react-router-spa/source.config.ts +7 -0
  41. package/template/react-router-spa/tsconfig.json +28 -0
  42. package/template/react-router-spa/vite.config.ts +10 -0
  43. package/template/tanstack-start/example.gitignore +9 -1
  44. package/template/tanstack-start/package.json +6 -6
  45. package/template/waku/example.gitignore +3 -1
  46. package/template/waku/package.json +3 -3
  47. package/dist/chunk-YVEWA65C.js +0 -370
  48. package/template/+next/README.md +0 -37
  49. package/template/+next/app/(home)/page.tsx +0 -42
  50. package/template/+next/app/layout.tsx +0 -23
  51. package/template/+next/lib/layout.shared.tsx +0 -30
  52. package/template/+next+content-collections/app/docs/[[...slug]]/page.tsx +0 -51
  53. package/template/+next+content-collections/content-collections.ts +0 -26
  54. package/template/+next+content-collections/lib/source.ts +0 -8
  55. package/template/+next+content-collections/next.config.mjs +0 -8
  56. package/template/+next+content-collections/tsconfig.json +0 -35
  57. package/template/+next+tailwindcss/app/(home)/page.tsx +0 -19
  58. package/template/{+next → +next+fuma-docs-mdx}/app/(home)/layout.tsx +0 -0
  59. package/template/{+next → +next+fuma-docs-mdx}/app/api/search/route.ts +0 -0
  60. package/template/{+next → +next+fuma-docs-mdx}/app/docs/layout.tsx +1 -1
  61. /package/template/{+next+tailwindcss → +next+fuma-docs-mdx}/app/global.css +0 -0
  62. /package/template/{+next → +next+fuma-docs-mdx}/content/docs/index.mdx +0 -0
  63. /package/template/{+next → +next+fuma-docs-mdx}/content/docs/test.mdx +0 -0
  64. /package/template/{+next+tailwindcss → +next+fuma-docs-mdx}/postcss.config.mjs +0 -0
@@ -0,0 +1,313 @@
1
+ import {
2
+ copy,
3
+ depVersions,
4
+ pick,
5
+ sourceDir,
6
+ writeFile
7
+ } from "./chunk-HLHY7KAF.js";
8
+
9
+ // src/plugins/orama-cloud.ts
10
+ import path from "path";
11
+ import fs2 from "fs/promises";
12
+
13
+ // src/transform/shared.ts
14
+ import { IndentationText, Project, QuoteKind } from "ts-morph";
15
+ import fs from "fs/promises";
16
+ var project = new Project({
17
+ manipulationSettings: {
18
+ indentationText: IndentationText.TwoSpaces,
19
+ quoteKind: QuoteKind.Single
20
+ }
21
+ });
22
+ async function createSourceFile(path2) {
23
+ return project.createSourceFile(path2, (await fs.readFile(path2)).toString(), {
24
+ overwrite: true
25
+ });
26
+ }
27
+ function getCodeValue(v) {
28
+ return new Function(`return ${v}`)();
29
+ }
30
+
31
+ // src/transform/tanstack-start.ts
32
+ import { SyntaxKind } from "ts-morph";
33
+ function addTanstackPrerender(sourceFile, paths) {
34
+ const optionsArg = getTanstackStartCall(sourceFile)?.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
35
+ if (!optionsArg) {
36
+ return;
37
+ }
38
+ const pagesProperty = optionsArg.getProperty("pages")?.asKind(SyntaxKind.PropertyAssignment);
39
+ if (pagesProperty) {
40
+ const initializer = pagesProperty.getInitializerIfKindOrThrow(
41
+ SyntaxKind.ArrayLiteralExpression
42
+ );
43
+ const existingPaths = /* @__PURE__ */ new Set();
44
+ for (const element of initializer.getElements()) {
45
+ const value = element.asKind(SyntaxKind.ObjectLiteralExpression)?.getProperty("path")?.asKind(SyntaxKind.PropertyAssignment)?.getInitializer()?.getText();
46
+ if (value) {
47
+ existingPaths.add(getCodeValue(value));
48
+ }
49
+ }
50
+ for (const path2 of paths) {
51
+ if (existingPaths.has(path2)) continue;
52
+ initializer.addElement(toItem(path2));
53
+ }
54
+ } else {
55
+ optionsArg.addProperty(
56
+ `pages: [
57
+ ${paths.map((path2) => ` ${toItem(path2)}`).join(",\n")}
58
+ ]`
59
+ );
60
+ }
61
+ }
62
+ function toItem(path2) {
63
+ return `{ path: '${path2}' }`;
64
+ }
65
+ function getTanstackStartCall(sourceFile) {
66
+ const pluginsProperty = sourceFile.getDefaultExportSymbol()?.getValueDeclaration()?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression)?.getProperty("plugins")?.getFirstChildByKind(SyntaxKind.ArrayLiteralExpression);
67
+ if (!pluginsProperty) return;
68
+ for (const element of pluginsProperty.getElements()) {
69
+ const expression = element.asKind(SyntaxKind.CallExpression);
70
+ if (expression && expression.getFirstChildByKind(SyntaxKind.Identifier)?.getText() === "tanstackStart") {
71
+ return expression;
72
+ }
73
+ }
74
+ }
75
+
76
+ // src/transform/react-router.ts
77
+ import {
78
+ ts
79
+ } from "ts-morph";
80
+ var SyntaxKind2 = ts.SyntaxKind;
81
+ function removeReactRouterPrerenderExclude(sourceFile, paths) {
82
+ const methodBody = getPrerenderMethod(sourceFile)?.getBody();
83
+ if (!methodBody) return;
84
+ const initializer = methodBody.getDescendantsOfKind(SyntaxKind2.VariableDeclaration).find((item) => item.getName() === "excluded")?.getInitializerIfKind(SyntaxKind2.ArrayLiteralExpression);
85
+ if (!initializer) return;
86
+ for (const element of initializer.getElements()) {
87
+ const value = getCodeValue(element.getText());
88
+ if (paths.includes(value)) {
89
+ initializer.removeElement(element);
90
+ }
91
+ }
92
+ }
93
+ function addReactRouterRoute(sourceFile, routes) {
94
+ modifyReactRouterRoutes(sourceFile, (arr) => {
95
+ for (const { path: path2, entry } of routes) {
96
+ arr.addElement(`route('${path2}', '${entry}')`);
97
+ }
98
+ });
99
+ }
100
+ function filterReactRouterRoute(sourceFile, filter) {
101
+ modifyReactRouterRoutes(sourceFile, (arr) => {
102
+ for (const element of arr.getElements()) {
103
+ if (!element.isKind(SyntaxKind2.CallExpression) || element.getFirstChildByKind(SyntaxKind2.Identifier)?.getText() !== "route" || filter({
104
+ path: getCodeValue(element.getArguments()[0].getText())
105
+ }))
106
+ continue;
107
+ arr.removeElement(element);
108
+ }
109
+ });
110
+ }
111
+ function modifyReactRouterRoutes(sourceFile, mod) {
112
+ const initializer = sourceFile.getDefaultExportSymbol()?.getValueDeclaration()?.getFirstDescendantByKind(SyntaxKind2.ArrayLiteralExpression);
113
+ if (initializer) mod(initializer);
114
+ }
115
+ function getPrerenderMethod(sourceFile) {
116
+ return sourceFile.getDefaultExportSymbol()?.getValueDeclaration()?.getFirstDescendantByKind(SyntaxKind2.ObjectLiteralExpression)?.getProperty("prerender")?.asKind(SyntaxKind2.MethodDeclaration) ?? null;
117
+ }
118
+
119
+ // src/transform/provider.ts
120
+ import { StructureKind, SyntaxKind as SyntaxKind3 } from "ts-morph";
121
+ function addSearchDialog(sourceFile) {
122
+ const elements = sourceFile.getDescendantsOfKind(SyntaxKind3.JsxElement);
123
+ for (const element of elements) {
124
+ const provider = element.getFirstChildByKind(SyntaxKind3.JsxOpeningElement);
125
+ if (provider?.getTagNameNode().getText() !== "RootProvider") continue;
126
+ if (provider.getAttributes().some(
127
+ (attr) => attr.isKind(SyntaxKind3.JsxAttribute) && attr.getNameNode().getText() === "search"
128
+ ))
129
+ continue;
130
+ provider.addAttribute({
131
+ kind: StructureKind.JsxAttribute,
132
+ name: "search",
133
+ initializer: "{{ SearchDialog }}"
134
+ });
135
+ }
136
+ sourceFile.addImportDeclaration({
137
+ moduleSpecifier: "@/components/search",
138
+ defaultImport: "SearchDialog"
139
+ });
140
+ }
141
+
142
+ // src/plugins/orama-cloud.ts
143
+ function oramaCloud() {
144
+ return {
145
+ packageJson(packageJson) {
146
+ return {
147
+ ...packageJson,
148
+ scripts: {
149
+ ...packageJson.scripts,
150
+ build: `${packageJson.scripts.build} && bun scripts/sync-content.ts`
151
+ },
152
+ dependencies: {
153
+ ...packageJson.dependencies,
154
+ ...pick(depVersions, ["@orama/core"])
155
+ }
156
+ };
157
+ },
158
+ readme(content) {
159
+ return `${content}
160
+
161
+ ## Orama Cloud
162
+
163
+ This project uses Orama Cloud for 3rd party search solution.
164
+
165
+ See https://fumadocs.dev/docs/headless/search/orama-cloud for integrating Orama Cloud to Fumadocs.`;
166
+ },
167
+ async afterWrite() {
168
+ const { dest, template, options } = this;
169
+ const appDir = path.join(dest, options.useSrcDir ? "src" : ".");
170
+ await copy(path.join(sourceDir, "template/+orama-cloud/@root"), dest);
171
+ await copy(path.join(sourceDir, "template/+orama-cloud/@app"), appDir);
172
+ if (template.value === "tanstack-start") {
173
+ await Promise.all([
174
+ fluent(
175
+ createSourceFile(path.join(dest, "vite.config.ts")),
176
+ (file) => addTanstackPrerender(file, ["/static.json"]),
177
+ (file) => file.save()
178
+ ),
179
+ fs2.unlink(path.join(dest, "src/routes/api/search.ts")).catch(() => null),
180
+ writeFile(
181
+ path.join(dest, "src/routes/static[.]json.ts"),
182
+ route.tanstack
183
+ ),
184
+ updateRootProvider(path.join(dest, "src/routes/__root.tsx"))
185
+ ]);
186
+ } else if (template.value.startsWith("react-router")) {
187
+ await Promise.all([
188
+ fluent(
189
+ createSourceFile(path.join(dest, "app/routes.ts")),
190
+ (file) => filterReactRouterRoute(file, ({ path: path2 }) => path2 !== "api/search"),
191
+ (file) => addReactRouterRoute(file, [
192
+ {
193
+ path: "static.json",
194
+ entry: "routes/static.ts"
195
+ }
196
+ ]),
197
+ (file) => file.save()
198
+ ),
199
+ fluent(
200
+ createSourceFile(path.join(dest, "react-router.config.ts")),
201
+ (file) => removeReactRouterPrerenderExclude(file, ["/api/search"]),
202
+ (file) => file.save()
203
+ ),
204
+ fs2.unlink(path.join(dest, "app/docs/search.ts")).catch(() => null),
205
+ writeFile(
206
+ path.join(dest, "app/routes/static.ts"),
207
+ route["react-router"]
208
+ ),
209
+ updateRootProvider(path.join(dest, "app/root.tsx"))
210
+ ]);
211
+ } else if (template.value.startsWith("+next")) {
212
+ await Promise.all([
213
+ fs2.unlink(path.join(appDir, "app/api/search/route.ts")).catch(() => null),
214
+ writeFile(path.join(appDir, "app/static.json/route.ts"), route.next),
215
+ updateRootProvider(path.join(appDir, "app/layout.tsx"))
216
+ ]);
217
+ } else {
218
+ await Promise.all([
219
+ fs2.unlink(path.join(dest, "src/pages/api/search.ts")).catch(() => null),
220
+ writeFile(
221
+ path.join(dest, "src/pages/api/static.json.ts"),
222
+ route.waku
223
+ ),
224
+ updateRootProvider(path.join(dest, "src/components/provider.tsx"))
225
+ ]);
226
+ }
227
+ const filePath = {
228
+ "+next+fuma-docs-mdx": ".next/server/app/static.json.body",
229
+ "tanstack-start": ".output/public/static.json",
230
+ "react-router": "build/client/static.json",
231
+ "react-router-spa": "build/client/static.json",
232
+ waku: "dist/public/static.json"
233
+ }[template.value];
234
+ await writeFile(
235
+ path.join(dest, "scripts/sync-content.ts"),
236
+ `import { type OramaDocument, sync } from 'fumadocs-core/search/orama-cloud';
237
+ import * as fs from 'node:fs/promises';
238
+ import { OramaCloud } from '@orama/core';
239
+
240
+ // the path of pre-rendered \`static.json\`
241
+ const filePath = '${filePath}';
242
+
243
+ async function main() {
244
+ const orama = new OramaCloud({
245
+ projectId: process.env.NEXT_PUBLIC_ORAMA_PROJECT_ID,
246
+ apiKey: process.env.ORAMA_PRIVATE_API_KEY,
247
+ });
248
+
249
+ const content = await fs.readFile(filePath);
250
+ const records = JSON.parse(content.toString()) as OramaDocument[];
251
+
252
+ await sync(orama, {
253
+ index: process.env.NEXT_PUBLIC_ORAMA_DATASOURCE_ID,
254
+ documents: records,
255
+ });
256
+
257
+ console.log(\`search updated: \${records.length} records\`);
258
+ }
259
+
260
+ void main();`
261
+ );
262
+ }
263
+ };
264
+ }
265
+ async function updateRootProvider(rootPath) {
266
+ await fluent(
267
+ createSourceFile(rootPath),
268
+ (file) => addSearchDialog(file),
269
+ (file) => file.save()
270
+ );
271
+ }
272
+ async function fluent(value, ...actions) {
273
+ for (const action of actions) {
274
+ await action(await value);
275
+ }
276
+ return value;
277
+ }
278
+ var route = {
279
+ next: `import { exportSearchIndexes } from '@/lib/export-search-indexes';
280
+
281
+ export const revalidate = false;
282
+
283
+ export async function GET() {
284
+ return Response.json(await exportSearchIndexes());
285
+ }`,
286
+ "react-router": `import { exportSearchIndexes } from '@/lib/export-search-indexes';
287
+
288
+ export async function loader() {
289
+ return Response.json(await exportSearchIndexes());
290
+ }`,
291
+ tanstack: `import { createFileRoute } from '@tanstack/react-router';
292
+ import { exportSearchIndexes } from '@/lib/export-search-indexes';
293
+
294
+ export const Route = createFileRoute('/static.json')({
295
+ server: {
296
+ handlers: {
297
+ GET: async () => Response.json(await exportSearchIndexes()),
298
+ },
299
+ },
300
+ });`,
301
+ waku: `import { exportSearchIndexes } from '@/lib/export-search-indexes';
302
+
303
+ export async function GET() {
304
+ return Response.json(await exportSearchIndexes());
305
+ }
306
+
307
+ export const getConfig = () => ({
308
+ render: 'static',
309
+ });`
310
+ };
311
+ export {
312
+ oramaCloud
313
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fumadocs-app",
3
- "version": "16.0.1",
3
+ "version": "16.0.3",
4
4
  "description": "Create a new documentation site with Fumadocs",
5
5
  "keywords": [
6
6
  "NextJs",
@@ -24,7 +24,8 @@
24
24
  "@clack/prompts": "^0.11.0",
25
25
  "commander": "^14.0.1",
26
26
  "picocolors": "^1.1.1",
27
- "tinyexec": "^1.0.1"
27
+ "tinyexec": "^1.0.1",
28
+ "ts-morph": "^27.0.2"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@types/cross-spawn": "^6.0.6",
@@ -45,6 +46,7 @@
45
46
  "clean": "rimraf dist",
46
47
  "dev": "tsup --watch",
47
48
  "lint": "eslint .",
48
- "types:check": "tsc --noEmit"
49
+ "types:check": "tsc --noEmit",
50
+ "sync": "bun ./scripts/sync.ts"
49
51
  }
50
52
  }
@@ -0,0 +1,16 @@
1
+ import Link from 'next/link';
2
+
3
+ export default function HomePage() {
4
+ return (
5
+ <div className="flex flex-col justify-center text-center flex-1">
6
+ <h1 className="text-2xl font-bold mb-4">Hello World</h1>
7
+ <p>
8
+ You can open{' '}
9
+ <Link href="/docs" className="font-medium underline">
10
+ /docs
11
+ </Link>{' '}
12
+ and see the documentation.
13
+ </p>
14
+ </div>
15
+ );
16
+ }
@@ -1,5 +1,5 @@
1
- import '@/app/global.css';
2
1
  import { RootProvider } from 'fumadocs-ui/provider/next';
2
+ import './global.css';
3
3
  import { Inter } from 'next/font/google';
4
4
 
5
5
  const inter = Inter({
@@ -2,8 +2,6 @@
2
2
  /node_modules
3
3
 
4
4
  # generated content
5
- .contentlayer
6
- .content-collections
7
5
  .source
8
6
 
9
7
  # test & build
@@ -0,0 +1,9 @@
1
+ import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
2
+
3
+ export function baseOptions(): BaseLayoutProps {
4
+ return {
5
+ nav: {
6
+ title: 'My App',
7
+ },
8
+ };
9
+ }
@@ -21,7 +21,7 @@ export function getPageImage(page: InferPageType<typeof source>) {
21
21
  export async function getLLMText(page: InferPageType<typeof source>) {
22
22
  const processed = await page.data.getText('processed');
23
23
 
24
- return `# ${page.data.title} (${page.url})
24
+ return `# ${page.data.title}
25
25
 
26
26
  ${processed}`;
27
27
  }
@@ -1,7 +1,6 @@
1
1
  import defaultMdxComponents from 'fumadocs-ui/mdx';
2
2
  import type { MDXComponents } from 'mdx/types';
3
3
 
4
- // use this function to get MDX components, you will need it for rendering MDX
5
4
  export function getMDXComponents(components?: MDXComponents): MDXComponents {
6
5
  return {
7
6
  ...defaultMdxComponents,
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "example-next-mdx",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "build": "next build",
7
+ "dev": "next dev",
8
+ "start": "next start"
9
+ },
10
+ "dependencies": {
11
+ "fumadocs-core": "workspace:*",
12
+ "fumadocs-mdx": "workspace:*",
13
+ "fumadocs-ui": "workspace:*",
14
+ "lucide-react": "^0.546.0",
15
+ "next": "16.0.0",
16
+ "react": "^19.2.0",
17
+ "react-dom": "^19.2.0"
18
+ },
19
+ "devDependencies": {
20
+ "@tailwindcss/postcss": "^4.1.15",
21
+ "@types/mdx": "^2.0.13",
22
+ "@types/node": "^24.9.1",
23
+ "@types/react": "^19.2.2",
24
+ "@types/react-dom": "^19.2.2",
25
+ "postcss": "^8.5.6",
26
+ "tailwindcss": "^4.1.15",
27
+ "typescript": "^5.9.3"
28
+ }
29
+ }
@@ -8,6 +8,7 @@ import {
8
8
  // You can customise Zod schemas for frontmatter and `meta.json` here
9
9
  // see https://fumadocs.dev/docs/mdx/collections
10
10
  export const docs = defineDocs({
11
+ dir: 'content/docs',
11
12
  docs: {
12
13
  schema: frontmatterSchema,
13
14
  postprocess: {
@@ -16,8 +16,8 @@
16
16
  "jsx": "preserve",
17
17
  "incremental": true,
18
18
  "paths": {
19
- "@/.source": [".source"],
20
- "@/*": ["./*"]
19
+ "@/*": ["./*"],
20
+ "@/.source": [".source"]
21
21
  },
22
22
  "plugins": [
23
23
  {
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import {
4
+ SearchDialog,
5
+ SearchDialogClose,
6
+ SearchDialogContent,
7
+ SearchDialogFooter,
8
+ SearchDialogHeader,
9
+ SearchDialogIcon,
10
+ SearchDialogInput,
11
+ SearchDialogList,
12
+ SearchDialogOverlay,
13
+ type SharedProps,
14
+ } from 'fumadocs-ui/components/dialog/search';
15
+ import { useDocsSearch } from 'fumadocs-core/search/client';
16
+ import { OramaCloud } from '@orama/core';
17
+ import { useI18n } from 'fumadocs-ui/contexts/i18n';
18
+
19
+ const client = new OramaCloud({
20
+ projectId: process.env.NEXT_PUBLIC_ORAMA_DATASOURCE_ID,
21
+ apiKey: process.env.NEXT_PUBLIC_ORAMA_API_KEY,
22
+ });
23
+
24
+ export default function CustomSearchDialog(props: SharedProps) {
25
+ const { locale } = useI18n(); // (optional) for i18n
26
+ const { search, setSearch, query } = useDocsSearch({
27
+ type: 'orama-cloud',
28
+ client,
29
+ locale,
30
+ });
31
+
32
+ return (
33
+ <SearchDialog
34
+ search={search}
35
+ onSearchChange={setSearch}
36
+ isLoading={query.isLoading}
37
+ {...props}
38
+ >
39
+ <SearchDialogOverlay />
40
+ <SearchDialogContent>
41
+ <SearchDialogHeader>
42
+ <SearchDialogIcon />
43
+ <SearchDialogInput />
44
+ <SearchDialogClose />
45
+ </SearchDialogHeader>
46
+ <SearchDialogList items={query.data !== 'empty' ? query.data : null} />
47
+ <SearchDialogFooter>
48
+ <a
49
+ href="https://orama.com"
50
+ rel="noreferrer noopener"
51
+ className="ms-auto text-xs text-fd-muted-foreground"
52
+ >
53
+ Search powered by Orama
54
+ </a>
55
+ </SearchDialogFooter>
56
+ </SearchDialogContent>
57
+ </SearchDialog>
58
+ );
59
+ }
@@ -0,0 +1,14 @@
1
+ import { source } from '@/lib/source';
2
+ import type { OramaDocument } from 'fumadocs-core/search/orama-cloud';
3
+
4
+ export async function exportSearchIndexes() {
5
+ return source.getPages().map((page) => {
6
+ return {
7
+ id: page.url,
8
+ structured: page.data.structuredData,
9
+ url: page.url,
10
+ title: page.data.title,
11
+ description: page.data.description,
12
+ } satisfies OramaDocument;
13
+ });
14
+ }
@@ -0,0 +1,6 @@
1
+ NEXT_PUBLIC_ORAMA_DATASOURCE_ID="Rest API data source ID"
2
+ NEXT_PUBLIC_ORAMA_PROJECT_ID="project ID"
3
+ NEXT_PUBLIC_ORAMA_API_KEY="public API key"
4
+
5
+ # keep it secretly!
6
+ ORAMA_PRIVATE_API_KEY="private API key"
@@ -7,11 +7,11 @@ import {
7
7
  DocsTitle,
8
8
  } from 'fumadocs-ui/page';
9
9
  import { source } from '@/lib/source';
10
- import { baseOptions } from '@/lib/layout.shared';
11
10
  import type * as PageTree from 'fumadocs-core/page-tree';
12
11
  import defaultMdxComponents from 'fumadocs-ui/mdx';
13
12
  import { docs } from '@/.source';
14
13
  import { toClientRenderer } from 'fumadocs-mdx/runtime/vite';
14
+ import { baseOptions } from '@/lib/layout.shared';
15
15
 
16
16
  export async function loader({ params }: Route.LoaderArgs) {
17
17
  const slugs = params['*'].split('/').filter((v) => v.length > 0);
@@ -20,7 +20,7 @@ export async function loader({ params }: Route.LoaderArgs) {
20
20
 
21
21
  return {
22
22
  path: page.path,
23
- tree: source.pageTree,
23
+ tree: source.getPageTree(),
24
24
  };
25
25
  }
26
26
 
@@ -41,8 +41,8 @@ const renderer = toClientRenderer(
41
41
  },
42
42
  );
43
43
 
44
- export default function Page(props: Route.ComponentProps) {
45
- const { tree, path } = props.loaderData;
44
+ export default function Page({ loaderData }: Route.ComponentProps) {
45
+ const { tree, path } = loaderData;
46
46
  const Content = renderer[path];
47
47
 
48
48
  return (
@@ -4,5 +4,4 @@
4
4
  # React Router
5
5
  /.react-router/
6
6
  /build/
7
-
8
7
  .source
@@ -8,10 +8,10 @@ export default {
8
8
  ssr: true,
9
9
  async prerender({ getStaticPaths }) {
10
10
  const paths: string[] = [];
11
+ const excluded: string[] = ['/api/search'];
12
+
11
13
  for (const path of getStaticPaths()) {
12
- // ignore dynamic document search
13
- if (path === '/api/search') continue;
14
- paths.push(path);
14
+ if (!excluded.includes(path)) paths.push(path);
15
15
  }
16
16
 
17
17
  for await (const entry of glob('**/*.mdx', { cwd: 'content/docs' })) {
@@ -0,0 +1,12 @@
1
+ This is a React Router application generated with
2
+ [Create Fumadocs](https://github.com/fuma-nama/fumadocs).
3
+
4
+ Run development server:
5
+
6
+ ```bash
7
+ npm run dev
8
+ # or
9
+ pnpm dev
10
+ # or
11
+ yarn dev
12
+ ```
@@ -0,0 +1,3 @@
1
+ @import 'tailwindcss';
2
+ @import 'fumadocs-ui/css/neutral.css';
3
+ @import 'fumadocs-ui/css/preset.css';
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+ import {
3
+ SearchDialog,
4
+ SearchDialogClose,
5
+ SearchDialogContent,
6
+ SearchDialogHeader,
7
+ SearchDialogIcon,
8
+ SearchDialogInput,
9
+ SearchDialogList,
10
+ SearchDialogOverlay,
11
+ type SharedProps,
12
+ } from 'fumadocs-ui/components/dialog/search';
13
+ import { useDocsSearch } from 'fumadocs-core/search/client';
14
+ import { create } from '@orama/orama';
15
+ import { useI18n } from 'fumadocs-ui/contexts/i18n';
16
+
17
+ function initOrama() {
18
+ return create({
19
+ schema: { _: 'string' },
20
+ language: 'english',
21
+ });
22
+ }
23
+
24
+ export default function DefaultSearchDialog(props: SharedProps) {
25
+ const { locale } = useI18n();
26
+ const { search, setSearch, query } = useDocsSearch({
27
+ type: 'static',
28
+ initOrama,
29
+ locale,
30
+ });
31
+
32
+ return (
33
+ <SearchDialog
34
+ search={search}
35
+ onSearchChange={setSearch}
36
+ isLoading={query.isLoading}
37
+ {...props}
38
+ >
39
+ <SearchDialogOverlay />
40
+ <SearchDialogContent>
41
+ <SearchDialogHeader>
42
+ <SearchDialogIcon />
43
+ <SearchDialogInput />
44
+ <SearchDialogClose />
45
+ </SearchDialogHeader>
46
+ <SearchDialogList items={query.data !== 'empty' ? query.data : null} />
47
+ </SearchDialogContent>
48
+ </SearchDialog>
49
+ );
50
+ }