create-fumadocs-app 16.0.0 → 16.0.2

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 (72) hide show
  1. package/dist/{chunk-KVKS2K3W.js → chunk-6C2B326N.js} +198 -196
  2. package/dist/create-app.d.ts +35 -8
  3. package/dist/create-app.js +3 -5
  4. package/dist/index.js +110 -32
  5. package/package.json +3 -2
  6. package/template/+next+fuma-docs-mdx/app/(home)/page.tsx +16 -0
  7. package/template/{+next+tailwindcss → +next+fuma-docs-mdx}/app/layout.tsx +1 -1
  8. package/template/{+next → +next+fuma-docs-mdx}/example.gitignore +0 -2
  9. package/template/+next+fuma-docs-mdx/lib/layout.shared.tsx +9 -0
  10. package/template/+next+fuma-docs-mdx/lib/source.ts +1 -1
  11. package/template/{+next → +next+fuma-docs-mdx}/mdx-components.tsx +0 -1
  12. package/template/+next+fuma-docs-mdx/package.json +29 -0
  13. package/template/+next+fuma-docs-mdx/source.config.ts +1 -0
  14. package/template/+next+fuma-docs-mdx/tsconfig.json +2 -2
  15. package/template/{+next → +orama-cloud/+next+fuma-docs-mdx}/app/layout.tsx +4 -9
  16. package/template/+orama-cloud/+next+fuma-docs-mdx/app/static.json/route.ts +7 -0
  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/+orama-cloud/react-router/app/root.tsx +76 -0
  21. package/template/+orama-cloud/react-router/app/routes/static.ts +5 -0
  22. package/template/+orama-cloud/react-router/app/routes.ts +8 -0
  23. package/template/+orama-cloud/react-router-spa/app/root.tsx +76 -0
  24. package/template/+orama-cloud/react-router-spa/app/routes/static.ts +5 -0
  25. package/template/+orama-cloud/react-router-spa/app/routes.ts +8 -0
  26. package/template/+orama-cloud/tanstack-start/src/routes/__root.tsx +51 -0
  27. package/template/+orama-cloud/tanstack-start/src/routes/static[.]json.ts +10 -0
  28. package/template/+orama-cloud/tanstack-start/vite.config.ts +27 -0
  29. package/template/+orama-cloud/waku/src/components/provider.tsx +8 -0
  30. package/template/+orama-cloud/waku/src/pages/api/static.json.ts +9 -0
  31. package/template/react-router/app/docs/page.tsx +4 -4
  32. package/template/react-router/example.gitignore +0 -1
  33. package/template/react-router-spa/README.md +12 -0
  34. package/template/react-router-spa/app/app.css +3 -0
  35. package/template/react-router-spa/app/components/search.tsx +50 -0
  36. package/template/react-router-spa/app/docs/page.tsx +53 -0
  37. package/template/react-router-spa/app/docs/search.ts +10 -0
  38. package/template/react-router-spa/app/lib/layout.shared.tsx +9 -0
  39. package/template/react-router-spa/app/lib/source.ts +7 -0
  40. package/template/react-router-spa/app/root.tsx +76 -0
  41. package/template/react-router-spa/app/routes/home.tsx +30 -0
  42. package/template/react-router-spa/app/routes.ts +7 -0
  43. package/template/react-router-spa/content/docs/index.mdx +32 -0
  44. package/template/react-router-spa/content/docs/test.mdx +24 -0
  45. package/template/react-router-spa/example.gitignore +7 -0
  46. package/template/react-router-spa/package.json +36 -0
  47. package/template/react-router-spa/public/favicon.ico +0 -0
  48. package/template/react-router-spa/react-router.config.ts +18 -0
  49. package/template/react-router-spa/serve.json +3 -0
  50. package/template/react-router-spa/source.config.ts +7 -0
  51. package/template/react-router-spa/tsconfig.json +28 -0
  52. package/template/react-router-spa/vite.config.ts +10 -0
  53. package/template/tanstack-start/example.gitignore +9 -1
  54. package/template/tanstack-start/package.json +6 -6
  55. package/template/waku/example.gitignore +3 -1
  56. package/template/waku/package.json +3 -3
  57. package/template/+next/README.md +0 -37
  58. package/template/+next/app/(home)/page.tsx +0 -42
  59. package/template/+next/lib/layout.shared.tsx +0 -30
  60. package/template/+next+content-collections/app/docs/[[...slug]]/page.tsx +0 -51
  61. package/template/+next+content-collections/content-collections.ts +0 -26
  62. package/template/+next+content-collections/lib/source.ts +0 -8
  63. package/template/+next+content-collections/next.config.mjs +0 -8
  64. package/template/+next+content-collections/tsconfig.json +0 -35
  65. package/template/+next+tailwindcss/app/(home)/page.tsx +0 -19
  66. package/template/{+next → +next+fuma-docs-mdx}/app/(home)/layout.tsx +0 -0
  67. package/template/{+next → +next+fuma-docs-mdx}/app/api/search/route.ts +0 -0
  68. package/template/{+next → +next+fuma-docs-mdx}/app/docs/layout.tsx +1 -1
  69. /package/template/{+next+tailwindcss → +next+fuma-docs-mdx}/app/global.css +0 -0
  70. /package/template/{+next → +next+fuma-docs-mdx}/content/docs/index.mdx +0 -0
  71. /package/template/{+next → +next+fuma-docs-mdx}/content/docs/test.mdx +0 -0
  72. /package/template/{+next+tailwindcss → +next+fuma-docs-mdx}/postcss.config.mjs +0 -0
package/dist/index.js CHANGED
@@ -1,15 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ copy,
3
4
  create,
4
5
  cwd,
6
+ depVersions,
5
7
  getPackageManager,
6
8
  managers,
9
+ pick,
10
+ sourceDir,
7
11
  templates
8
- } from "./chunk-KVKS2K3W.js";
12
+ } from "./chunk-6C2B326N.js";
9
13
 
10
14
  // src/index.ts
11
- import fs from "fs/promises";
12
- import path from "path";
15
+ import fs2 from "fs/promises";
16
+ import path2 from "path";
13
17
  import {
14
18
  cancel,
15
19
  confirm,
@@ -23,6 +27,85 @@ import {
23
27
  } from "@clack/prompts";
24
28
  import pc from "picocolors";
25
29
  import { program } from "commander";
30
+
31
+ // src/plugins/orama-cloud.ts
32
+ import path from "path";
33
+ import fs from "fs/promises";
34
+ var oramaCloud = {
35
+ packageJson(packageJson) {
36
+ return {
37
+ ...packageJson,
38
+ scripts: {
39
+ ...packageJson.scripts,
40
+ build: `${packageJson.build} && bun scripts/sync-content.ts`
41
+ },
42
+ dependencies: {
43
+ ...packageJson.dependencies,
44
+ ...pick(depVersions, ["@orama/core"])
45
+ }
46
+ };
47
+ },
48
+ readme(content) {
49
+ return `${content}
50
+
51
+ ## Orama Cloud
52
+
53
+ This project uses Orama Cloud for 3rd party search solution.
54
+
55
+ See https://fumadocs.dev/docs/headless/search/orama-cloud for integrating Orama Cloud to Fumadocs.`;
56
+ },
57
+ async afterWrite() {
58
+ const { dest, template, options } = this;
59
+ const appDir = path.join(dest, options.useSrcDir ? "src" : ".");
60
+ await copy(path.join(sourceDir, "template/+orama-cloud/@root"), dest);
61
+ await copy(path.join(sourceDir, "template/+orama-cloud/@app"), appDir);
62
+ const filePath = {
63
+ "+next+fuma-docs-mdx": ".next/server/app/static.json.body",
64
+ "tanstack-start": ".output/public/static.json",
65
+ "react-router": "build/client/static.json",
66
+ "react-router-spa": "build/client/static.json",
67
+ waku: "dist/public/static.json"
68
+ }[template.value];
69
+ const SyncContentScript = `import { type OramaDocument, sync } from 'fumadocs-core/search/orama-cloud';
70
+ import * as fs from 'node:fs/promises';
71
+ import { OramaCloud } from '@orama/core';
72
+
73
+ // the path of pre-rendered \`static.json\`
74
+ const filePath = ${JSON.stringify(filePath)};
75
+
76
+ async function main() {
77
+ const orama = new OramaCloud({
78
+ projectId: process.env.NEXT_PUBLIC_ORAMA_PROJECT_ID,
79
+ apiKey: process.env.ORAMA_PRIVATE_API_KEY,
80
+ });
81
+
82
+ const content = await fs.readFile(filePath);
83
+ const records = JSON.parse(content.toString()) as OramaDocument[];
84
+
85
+ await sync(orama, {
86
+ index: process.env.NEXT_PUBLIC_ORAMA_DATASOURCE_ID,
87
+ documents: records,
88
+ });
89
+
90
+ console.log(\`search updated: \${records.length} records\`);
91
+ }
92
+
93
+ void main();
94
+ `;
95
+ await fs.mkdir(path.join(dest, "scripts"), { recursive: true });
96
+ await fs.writeFile(
97
+ path.join(dest, "scripts/sync-content.ts"),
98
+ SyncContentScript
99
+ );
100
+ await copy(
101
+ path.join(sourceDir, `template/+orama-cloud/${template.value}`),
102
+ appDir
103
+ );
104
+ }
105
+ };
106
+ var orama_cloud_default = oramaCloud;
107
+
108
+ // src/index.ts
26
109
  program.argument("[name]", "the project name");
27
110
  program.option("--src", "(Next.js only) enable `src/` directory");
28
111
  program.option("--no-src", "(Next.js only) disable `src/` directory");
@@ -35,9 +118,9 @@ program.option("--no-install", "Disable installing packages automatically");
35
118
  program.option("--no-git", "Disable auto Git repository initialization");
36
119
  program.option(
37
120
  "--template <name>",
38
- `template to choose: ${templates.join(", ")}`,
121
+ `template to choose: ${templates.map((v) => v.value).join(", ")}`,
39
122
  (value) => {
40
- if (!templates.includes(value)) {
123
+ if (!templates.some((item) => item.value === value)) {
41
124
  throw new Error(`Invalid template: ${value}.`);
42
125
  }
43
126
  return value;
@@ -71,29 +154,7 @@ async function main(config) {
71
154
  return select({
72
155
  message: "Choose a template",
73
156
  initialValue: "+next+fuma-docs-mdx",
74
- options: [
75
- {
76
- value: "+next+fuma-docs-mdx",
77
- label: "Next.js: Fumadocs MDX",
78
- hint: "recommended"
79
- },
80
- {
81
- value: "+next+content-collections",
82
- label: "Next.js: Content Collections"
83
- },
84
- {
85
- value: "waku",
86
- label: "Waku: Fumadocs MDX"
87
- },
88
- {
89
- value: "react-router",
90
- label: "React Router: Fumadocs MDX (not RSC)"
91
- },
92
- {
93
- value: "tanstack-start",
94
- label: "Tanstack Start: Fumadocs MDX (not RSC)"
95
- }
96
- ]
157
+ options: templates
97
158
  });
98
159
  },
99
160
  src: async (v) => {
@@ -131,6 +192,23 @@ async function main(config) {
131
192
  ]
132
193
  });
133
194
  },
195
+ search: () => {
196
+ return select({
197
+ message: "Choose a search solution?",
198
+ options: [
199
+ {
200
+ value: "orama",
201
+ label: "Default",
202
+ hint: "local search powered by Orama, recommended"
203
+ },
204
+ {
205
+ value: "orama-cloud",
206
+ label: "Orama Cloud",
207
+ hint: "3rd party search solution, signup needed"
208
+ }
209
+ ]
210
+ });
211
+ },
134
212
  installDeps: () => {
135
213
  if (config.install !== void 0)
136
214
  return Promise.resolve(config.install);
@@ -147,8 +225,8 @@ async function main(config) {
147
225
  }
148
226
  );
149
227
  const projectName = options.name.toLowerCase().replace(/\s/, "-");
150
- const dest = path.resolve(cwd, projectName);
151
- const destDir = await fs.readdir(dest).catch(() => null);
228
+ const dest = path2.resolve(cwd, projectName);
229
+ const destDir = await fs2.readdir(dest).catch(() => null);
152
230
  if (destDir && destDir.length > 0) {
153
231
  const del = await confirm({
154
232
  message: `directory ${projectName} already exists, do you want to delete its files?`
@@ -162,7 +240,7 @@ async function main(config) {
162
240
  info2.start(`Deleting files in ${projectName}`);
163
241
  await Promise.all(
164
242
  destDir.map((item) => {
165
- return fs.rm(path.join(dest, item), {
243
+ return fs2.rm(path2.join(dest, item), {
166
244
  recursive: true,
167
245
  force: true
168
246
  });
@@ -175,13 +253,13 @@ async function main(config) {
175
253
  info.start(`Generating Project`);
176
254
  await create({
177
255
  packageManager: manager,
178
- tailwindcss: true,
179
256
  template: options.template,
180
257
  outputDir: dest,
181
258
  installDeps: options.installDeps,
182
259
  lint: options.lint,
183
260
  useSrcDir: options.src,
184
261
  initializeGit: config.git ?? true,
262
+ plugins: options.search === "orama-cloud" ? [orama_cloud_default] : [],
185
263
  log: (message) => {
186
264
  info.message(message);
187
265
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fumadocs-app",
3
- "version": "16.0.0",
3
+ "version": "16.0.2",
4
4
  "description": "Create a new documentation site with Fumadocs",
5
5
  "keywords": [
6
6
  "NextJs",
@@ -45,6 +45,7 @@
45
45
  "clean": "rimraf dist",
46
46
  "dev": "tsup --watch",
47
47
  "lint": "eslint .",
48
- "types:check": "tsc --noEmit"
48
+ "types:check": "tsc --noEmit",
49
+ "sync": "bun ./scripts/sync.ts"
49
50
  }
50
51
  }
@@ -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
  {
@@ -1,6 +1,7 @@
1
1
  import { RootProvider } from 'fumadocs-ui/provider/next';
2
- import 'fumadocs-ui/style.css';
2
+ import './global.css';
3
3
  import { Inter } from 'next/font/google';
4
+ import SearchDialog from '@/components/search';
4
5
 
5
6
  const inter = Inter({
6
7
  subsets: ['latin'],
@@ -9,14 +10,8 @@ const inter = Inter({
9
10
  export default function Layout({ children }: LayoutProps<'/'>) {
10
11
  return (
11
12
  <html lang="en" className={inter.className} suppressHydrationWarning>
12
- <body
13
- style={{
14
- display: 'flex',
15
- flexDirection: 'column',
16
- minHeight: '100vh',
17
- }}
18
- >
19
- <RootProvider>{children}</RootProvider>
13
+ <body className="flex flex-col min-h-screen">
14
+ <RootProvider search={{ SearchDialog }}>{children}</RootProvider>
20
15
  </body>
21
16
  </html>
22
17
  );
@@ -0,0 +1,7 @@
1
+ import { exportSearchIndexes } from '@/lib/export-search-indexes';
2
+
3
+ export const revalidate = false;
4
+
5
+ export async function GET() {
6
+ return Response.json(await exportSearchIndexes());
7
+ }
@@ -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"
@@ -0,0 +1,76 @@
1
+ import {
2
+ isRouteErrorResponse,
3
+ Links,
4
+ Meta,
5
+ Outlet,
6
+ Scripts,
7
+ ScrollRestoration,
8
+ } from 'react-router';
9
+ import { RootProvider } from 'fumadocs-ui/provider/react-router';
10
+ import type { Route } from './+types/root';
11
+ import './app.css';
12
+ import SearchDialog from '@/components/search';
13
+
14
+ export const links: Route.LinksFunction = () => [
15
+ { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
16
+ {
17
+ rel: 'preconnect',
18
+ href: 'https://fonts.gstatic.com',
19
+ crossOrigin: 'anonymous',
20
+ },
21
+ {
22
+ rel: 'stylesheet',
23
+ href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
24
+ },
25
+ ];
26
+
27
+ export function Layout({ children }: { children: React.ReactNode }) {
28
+ return (
29
+ <html lang="en" suppressHydrationWarning>
30
+ <head>
31
+ <meta charSet="utf-8" />
32
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
33
+ <Meta />
34
+ <Links />
35
+ </head>
36
+ <body className="flex flex-col min-h-screen">
37
+ <RootProvider search={{ SearchDialog }}>{children}</RootProvider>
38
+ <ScrollRestoration />
39
+ <Scripts />
40
+ </body>
41
+ </html>
42
+ );
43
+ }
44
+
45
+ export default function App() {
46
+ return <Outlet />;
47
+ }
48
+
49
+ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
50
+ let message = 'Oops!';
51
+ let details = 'An unexpected error occurred.';
52
+ let stack: string | undefined;
53
+
54
+ if (isRouteErrorResponse(error)) {
55
+ message = error.status === 404 ? '404' : 'Error';
56
+ details =
57
+ error.status === 404
58
+ ? 'The requested page could not be found.'
59
+ : error.statusText || details;
60
+ } else if (import.meta.env.DEV && error && error instanceof Error) {
61
+ details = error.message;
62
+ stack = error.stack;
63
+ }
64
+
65
+ return (
66
+ <main className="pt-16 p-4 container mx-auto">
67
+ <h1>{message}</h1>
68
+ <p>{details}</p>
69
+ {stack && (
70
+ <pre className="w-full p-4 overflow-x-auto">
71
+ <code>{stack}</code>
72
+ </pre>
73
+ )}
74
+ </main>
75
+ );
76
+ }
@@ -0,0 +1,5 @@
1
+ import { exportSearchIndexes } from '@/lib/export-search-indexes';
2
+
3
+ export async function loader() {
4
+ return Response.json(await exportSearchIndexes());
5
+ }
@@ -0,0 +1,8 @@
1
+ import { index, route, type RouteConfig } from '@react-router/dev/routes';
2
+
3
+ export default [
4
+ index('routes/home.tsx'),
5
+ route('docs/*', 'docs/page.tsx'),
6
+ route('api/search', 'docs/search.ts'),
7
+ route('static.json', 'routes/static.ts'),
8
+ ] satisfies RouteConfig;
@@ -0,0 +1,76 @@
1
+ import {
2
+ isRouteErrorResponse,
3
+ Links,
4
+ Meta,
5
+ Outlet,
6
+ Scripts,
7
+ ScrollRestoration,
8
+ } from 'react-router';
9
+ import { RootProvider } from 'fumadocs-ui/provider/react-router';
10
+ import type { Route } from './+types/root';
11
+ import './app.css';
12
+ import SearchDialog from '@/components/search';
13
+
14
+ export const links: Route.LinksFunction = () => [
15
+ { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
16
+ {
17
+ rel: 'preconnect',
18
+ href: 'https://fonts.gstatic.com',
19
+ crossOrigin: 'anonymous',
20
+ },
21
+ {
22
+ rel: 'stylesheet',
23
+ href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
24
+ },
25
+ ];
26
+
27
+ export function Layout({ children }: { children: React.ReactNode }) {
28
+ return (
29
+ <html lang="en" suppressHydrationWarning>
30
+ <head>
31
+ <meta charSet="utf-8" />
32
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
33
+ <Meta />
34
+ <Links />
35
+ </head>
36
+ <body className="flex flex-col min-h-screen">
37
+ <RootProvider search={{ SearchDialog }}>{children}</RootProvider>
38
+ <ScrollRestoration />
39
+ <Scripts />
40
+ </body>
41
+ </html>
42
+ );
43
+ }
44
+
45
+ export default function App() {
46
+ return <Outlet />;
47
+ }
48
+
49
+ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
50
+ let message = 'Oops!';
51
+ let details = 'An unexpected error occurred.';
52
+ let stack: string | undefined;
53
+
54
+ if (isRouteErrorResponse(error)) {
55
+ message = error.status === 404 ? '404' : 'Error';
56
+ details =
57
+ error.status === 404
58
+ ? 'The requested page could not be found.'
59
+ : error.statusText || details;
60
+ } else if (import.meta.env.DEV && error && error instanceof Error) {
61
+ details = error.message;
62
+ stack = error.stack;
63
+ }
64
+
65
+ return (
66
+ <main className="pt-16 p-4 container mx-auto">
67
+ <h1>{message}</h1>
68
+ <p>{details}</p>
69
+ {stack && (
70
+ <pre className="w-full p-4 overflow-x-auto">
71
+ <code>{stack}</code>
72
+ </pre>
73
+ )}
74
+ </main>
75
+ );
76
+ }
@@ -0,0 +1,5 @@
1
+ import { exportSearchIndexes } from '@/lib/export-search-indexes';
2
+
3
+ export async function loader() {
4
+ return Response.json(await exportSearchIndexes());
5
+ }
@@ -0,0 +1,8 @@
1
+ import { index, route, type RouteConfig } from '@react-router/dev/routes';
2
+
3
+ export default [
4
+ index('routes/home.tsx'),
5
+ route('docs/*', 'docs/page.tsx'),
6
+ route('api/search', 'docs/search.ts'),
7
+ route('static.json', 'routes/static.ts'),
8
+ ] satisfies RouteConfig;