mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 10:55:57 +00:00
CLI: Add React Router example
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'create-fumadocs-app': patch
|
||||
---
|
||||
|
||||
Add React Router example
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'create-fumadocs-app': patch
|
||||
---
|
||||
|
||||
Remove Tailwind CSS option, enable by default
|
||||
@@ -73,7 +73,7 @@ bun create fumadocs-app
|
||||
|
||||
</Tabs>
|
||||
|
||||
It will initialize a new fumadocs app. Now you can start hacking!
|
||||
It will ask you the framework and content source to use, a new fumadocs app should be initialized. Now you can start hacking!
|
||||
|
||||
<Callout title='From Existing Codebase?'>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
DocsPage,
|
||||
DocsTitle,
|
||||
} from 'fumadocs-ui/page';
|
||||
import { source } from '~/source';
|
||||
import { source } from '@/source';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import { executeMdxSync } from '@fumadocs/mdx-remote/client';
|
||||
import type { PageTree } from 'fumadocs-core/server';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Route } from './+types/search';
|
||||
import { createSearchAPI } from 'fumadocs-core/search/server';
|
||||
import { source } from '~/source';
|
||||
import { source } from '@/source';
|
||||
import { structure } from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
const server = createSearchAPI('advanced', {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"rootDirs": [".", "./.react-router/types"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
"@/*": ["./app/*"]
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
|
||||
@@ -17,7 +17,18 @@
|
||||
"postcss": "^8.5.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"typescript": "^5.8.2",
|
||||
"@react-router/dev": "^7.4.0",
|
||||
"@tailwindcss/vite": "^4.0.17",
|
||||
"react-router-devtools": "^1.1.7",
|
||||
"tailwindcss": "^4.0.17",
|
||||
"typescript": "^5.8.2"
|
||||
"vite": "^6.2.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"@react-router/node": "^7.4.0",
|
||||
"@react-router/serve": "^7.4.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"isbot": "^5.1.17",
|
||||
"react-router": "^7.4.0",
|
||||
"shiki": "^3.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { PackageManager } from './auto-install';
|
||||
import { autoInstall } from './auto-install';
|
||||
import { sourceDir, cwd } from './constants';
|
||||
|
||||
export type Template = 'content-collections' | 'fuma-docs-mdx';
|
||||
export type Template = 'content-collections' | 'fuma-docs-mdx' | 'react-router';
|
||||
|
||||
export interface Options {
|
||||
outputDir: string;
|
||||
@@ -30,6 +30,9 @@ export async function create(options: Options): Promise<void> {
|
||||
} = options;
|
||||
const projectName = path.basename(options.outputDir);
|
||||
const dest = path.resolve(cwd, options.outputDir);
|
||||
const isNext =
|
||||
options.template === 'content-collections' ||
|
||||
options.template === 'fuma-docs-mdx';
|
||||
|
||||
function defaultRename(file: string): string {
|
||||
file = file.replace('example.gitignore', '.gitignore');
|
||||
@@ -38,18 +41,28 @@ export async function create(options: Options): Promise<void> {
|
||||
return file;
|
||||
}
|
||||
|
||||
for (const dir of ['app', 'lib']) {
|
||||
const relative = path.relative(path.join(dest, dir), file);
|
||||
if (isNext) {
|
||||
for (const dir of ['app', 'lib']) {
|
||||
const relative = path.relative(path.join(dest, dir), file);
|
||||
|
||||
if (!relative.startsWith(`..${path.sep}`)) {
|
||||
return path.join(dest, 'src', dir, relative);
|
||||
}
|
||||
}
|
||||
} else if (options.template === 'react-router') {
|
||||
const relative = path.relative(path.join(dest, 'app'), file);
|
||||
|
||||
if (!relative.startsWith(`..${path.sep}`)) {
|
||||
return path.join(dest, 'src', dir, relative);
|
||||
return path.join(dest, 'src', relative);
|
||||
}
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
await copy(path.join(sourceDir, `template/+shared`), dest, defaultRename);
|
||||
if (isNext) {
|
||||
await copy(path.join(sourceDir, `template/+next`), dest, defaultRename);
|
||||
}
|
||||
|
||||
await copy(
|
||||
path.join(sourceDir, `template/${options.template}`),
|
||||
@@ -57,20 +70,34 @@ export async function create(options: Options): Promise<void> {
|
||||
defaultRename,
|
||||
);
|
||||
|
||||
if (options.tailwindcss) {
|
||||
if (isNext && options.tailwindcss) {
|
||||
await copy(
|
||||
path.join(sourceDir, `template/+tailwindcss`),
|
||||
path.join(sourceDir, `template/+next+tailwindcss`),
|
||||
dest,
|
||||
defaultRename,
|
||||
);
|
||||
|
||||
if (options.useSrcDir) {
|
||||
const cssPath = path.join(dest, 'src/app/global.css');
|
||||
|
||||
await fs.writeFile(
|
||||
cssPath,
|
||||
(await fs.readFile(cssPath)).toString().replace('../', '../../'),
|
||||
);
|
||||
}
|
||||
log('Configured Tailwind CSS');
|
||||
}
|
||||
|
||||
if (options.eslint) {
|
||||
await copy(path.join(sourceDir, `template/+eslint`), dest, defaultRename);
|
||||
if (isNext && options.eslint) {
|
||||
await copy(
|
||||
path.join(sourceDir, `template/+next+eslint`),
|
||||
dest,
|
||||
defaultRename,
|
||||
);
|
||||
log('Configured ESLint');
|
||||
}
|
||||
|
||||
// update tsconfig.json for src dir
|
||||
if (options.useSrcDir) {
|
||||
const tsconfigPath = path.join(dest, 'tsconfig.json');
|
||||
const content = (await fs.readFile(tsconfigPath)).toString();
|
||||
@@ -84,19 +111,13 @@ export async function create(options: Options): Promise<void> {
|
||||
}
|
||||
|
||||
await fs.writeFile(tsconfigPath, JSON.stringify(config, null, 2));
|
||||
|
||||
if (options.tailwindcss) {
|
||||
const cssPath = path.join(dest, 'src/app/global.css');
|
||||
|
||||
await fs.writeFile(
|
||||
cssPath,
|
||||
(await fs.readFile(cssPath)).toString().replace('../', '../../'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const packageJson = createPackageJson(projectName, options);
|
||||
await fs.writeFile(path.join(dest, 'package.json'), packageJson);
|
||||
await fs.writeFile(
|
||||
path.join(dest, 'package.json'),
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
);
|
||||
|
||||
const readMe = await getReadme(dest, projectName);
|
||||
await fs.writeFile(path.join(dest, 'README.md'), readMe);
|
||||
@@ -140,8 +161,51 @@ async function copy(
|
||||
}
|
||||
}
|
||||
|
||||
function createPackageJson(projectName: string, options: Options): string {
|
||||
const packageJson = {
|
||||
function createPackageJson(projectName: string, options: Options): object {
|
||||
if (options.template === 'react-router') {
|
||||
return {
|
||||
name: projectName,
|
||||
private: true,
|
||||
type: 'module',
|
||||
scripts: {
|
||||
build: 'react-router build',
|
||||
dev: 'react-router dev',
|
||||
start: 'react-router-serve ./build/server/index.js',
|
||||
typecheck: 'react-router typegen && tsc',
|
||||
},
|
||||
dependencies: {
|
||||
...pick(localVersions, [
|
||||
'@fumadocs/mdx-remote',
|
||||
'fumadocs-core',
|
||||
'fumadocs-ui',
|
||||
]),
|
||||
...pick(versionPkg.dependencies, [
|
||||
'@react-router/node',
|
||||
'@react-router/serve',
|
||||
'gray-matter',
|
||||
'isbot',
|
||||
'react',
|
||||
'react-dom',
|
||||
'react-router',
|
||||
'shiki',
|
||||
]),
|
||||
},
|
||||
devDependencies: pick(versionPkg.dependencies, [
|
||||
'@react-router/dev',
|
||||
'@tailwindcss/vite',
|
||||
'@types/node',
|
||||
'@types/react',
|
||||
'@types/react-dom',
|
||||
'react-router-devtools',
|
||||
'tailwindcss',
|
||||
'typescript',
|
||||
'vite',
|
||||
'vite-tsconfig-paths',
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: projectName,
|
||||
version: '0.0.0',
|
||||
private: true,
|
||||
@@ -149,63 +213,54 @@ function createPackageJson(projectName: string, options: Options): string {
|
||||
build: 'next build',
|
||||
dev: 'next dev',
|
||||
start: 'next start',
|
||||
} as Record<string, string>,
|
||||
...(options.template === 'fuma-docs-mdx'
|
||||
? {
|
||||
postinstall: 'fumadocs-mdx',
|
||||
}
|
||||
: null),
|
||||
},
|
||||
dependencies: {
|
||||
...pick(versionPkg.dependencies, ['next', 'react', 'react-dom']),
|
||||
...pick(localVersions, ['fumadocs-ui', 'fumadocs-core']),
|
||||
} as Record<string, string>,
|
||||
devDependencies: pick(versionPkg.dependencies, [
|
||||
'@types/node',
|
||||
'@types/react',
|
||||
'@types/react-dom',
|
||||
'typescript',
|
||||
]) as Record<string, string>,
|
||||
...(options.template === 'content-collections'
|
||||
? {
|
||||
...pick(versionPkg.dependencies, [
|
||||
'@content-collections/mdx',
|
||||
'@content-collections/core',
|
||||
'@content-collections/next',
|
||||
]),
|
||||
...pick(localVersions, ['@fumadocs/content-collections']),
|
||||
}
|
||||
: null),
|
||||
...(options.template === 'fuma-docs-mdx'
|
||||
? pick(localVersions, ['fumadocs-mdx'])
|
||||
: null),
|
||||
},
|
||||
devDependencies: {
|
||||
...pick(versionPkg.dependencies, [
|
||||
'@types/node',
|
||||
'@types/react',
|
||||
'@types/react-dom',
|
||||
'typescript',
|
||||
]),
|
||||
...(options.tailwindcss
|
||||
? pick(versionPkg.dependencies, [
|
||||
'@tailwindcss/postcss',
|
||||
'tailwindcss',
|
||||
'postcss',
|
||||
])
|
||||
: null),
|
||||
...(options.template === 'fuma-docs-mdx'
|
||||
? pick(versionPkg.dependencies, ['@types/mdx'])
|
||||
: null),
|
||||
...(options.eslint
|
||||
? {
|
||||
eslint: '^8',
|
||||
'eslint-config-next': versionPkg.dependencies.next,
|
||||
}
|
||||
: null),
|
||||
},
|
||||
};
|
||||
|
||||
if (options.template === 'content-collections') {
|
||||
Object.assign(
|
||||
packageJson.dependencies,
|
||||
pick(versionPkg.dependencies, [
|
||||
'@content-collections/mdx',
|
||||
'@content-collections/core',
|
||||
'@content-collections/next',
|
||||
]),
|
||||
pick(localVersions, ['@fumadocs/content-collections']),
|
||||
);
|
||||
}
|
||||
|
||||
if (options.template === 'fuma-docs-mdx') {
|
||||
packageJson.scripts.postinstall = 'fumadocs-mdx';
|
||||
|
||||
Object.assign(
|
||||
packageJson.dependencies,
|
||||
pick(localVersions, ['fumadocs-mdx']),
|
||||
);
|
||||
Object.assign(
|
||||
packageJson.devDependencies,
|
||||
pick(versionPkg.dependencies, ['@types/mdx']),
|
||||
);
|
||||
}
|
||||
|
||||
if (options.tailwindcss) {
|
||||
Object.assign(
|
||||
packageJson.devDependencies,
|
||||
pick(versionPkg.dependencies, [
|
||||
'@tailwindcss/postcss',
|
||||
'tailwindcss',
|
||||
'postcss',
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
if (options.eslint) {
|
||||
Object.assign(packageJson.devDependencies, {
|
||||
eslint: '^8',
|
||||
'eslint-config-next': versionPkg.dependencies.next,
|
||||
});
|
||||
}
|
||||
|
||||
return JSON.stringify(packageJson, undefined, 2);
|
||||
}
|
||||
|
||||
function pick<T extends object, K extends keyof T>(
|
||||
|
||||
@@ -32,25 +32,34 @@ async function main(): Promise<void> {
|
||||
}),
|
||||
template: () =>
|
||||
select({
|
||||
message: 'Choose a content source',
|
||||
message: 'Choose a template',
|
||||
initialValue: 'fuma-docs-mdx' as Template,
|
||||
options: [
|
||||
{
|
||||
value: 'fuma-docs-mdx',
|
||||
label: 'Fumadocs MDX',
|
||||
label: 'Next.js: Fumadocs MDX',
|
||||
hint: 'recommended',
|
||||
},
|
||||
{ value: 'content-collections', label: 'Content Collections' },
|
||||
{
|
||||
value: 'content-collections',
|
||||
label: 'Next.js: Content Collections',
|
||||
},
|
||||
{
|
||||
value: 'react-router',
|
||||
label: 'React Router: MDX Remote',
|
||||
},
|
||||
],
|
||||
}),
|
||||
src: () =>
|
||||
confirm({ message: 'Use `/src` directory?', initialValue: false }),
|
||||
tailwindcss: () => confirm({ message: 'Use Tailwind CSS for styling?' }),
|
||||
eslint: () =>
|
||||
confirm({
|
||||
eslint: (v) => {
|
||||
if (v.results.template === 'react-router') return;
|
||||
|
||||
return confirm({
|
||||
message: 'Add default ESLint configuration?',
|
||||
initialValue: false,
|
||||
}),
|
||||
});
|
||||
},
|
||||
installDeps: () =>
|
||||
confirm({
|
||||
message: `Do you want to install packages automatically? (detected as ${manager})`,
|
||||
@@ -83,12 +92,12 @@ async function main(): Promise<void> {
|
||||
info.start(`Deleting files in ${projectName}`);
|
||||
|
||||
await Promise.all(
|
||||
destDir.map((item) =>
|
||||
fs.rm(item, {
|
||||
destDir.map((item) => {
|
||||
return fs.rm(path.join(dest, item), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
}),
|
||||
),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
info.stop(`Deleted files in ${projectName}`);
|
||||
@@ -100,11 +109,11 @@ async function main(): Promise<void> {
|
||||
|
||||
await create({
|
||||
packageManager: manager,
|
||||
tailwindcss: options.tailwindcss,
|
||||
tailwindcss: true,
|
||||
template: options.template,
|
||||
outputDir: dest,
|
||||
installDeps: options.installDeps,
|
||||
eslint: options.eslint,
|
||||
eslint: options.eslint === true,
|
||||
useSrcDir: options.src,
|
||||
|
||||
log: (message) => {
|
||||
|
||||
+1
@@ -2,5 +2,6 @@ export const versions: {
|
||||
'fumadocs-core': string;
|
||||
'fumadocs-ui': string;
|
||||
'fumadocs-mdx': string;
|
||||
'@fumadocs/mdx-remote': string;
|
||||
'@fumadocs/content-collections': string;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
This is a React Router application generated with
|
||||
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
|
||||
|
||||
Run development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'fumadocs-ui/css/neutral.css';
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
|
||||
/* path of `fumadocs-ui` relative to the CSS file */
|
||||
@source '../node_modules/fumadocs-ui/dist/**/*.js';
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { Route } from './+types/page';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import {
|
||||
DocsBody,
|
||||
DocsDescription,
|
||||
DocsPage,
|
||||
DocsTitle,
|
||||
} from 'fumadocs-ui/page';
|
||||
import { source } from '@/source';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import { executeMdxSync } from '@fumadocs/mdx-remote/client';
|
||||
import type { PageTree } from 'fumadocs-core/server';
|
||||
import { createCompiler } from '@fumadocs/mdx-remote';
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: 'New React Router App' },
|
||||
{ name: 'description', content: 'Welcome to React Router!' },
|
||||
];
|
||||
}
|
||||
const compiler = createCompiler({
|
||||
development: false,
|
||||
});
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const slugs = params['*'].split('/').filter((v) => v.length > 0);
|
||||
const page = source.getPage(slugs);
|
||||
if (!page) throw new Error('Not found');
|
||||
|
||||
const compiled = String(
|
||||
await compiler.compileFile({
|
||||
value: page.data.content,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
page,
|
||||
compiled,
|
||||
tree: source.pageTree,
|
||||
};
|
||||
}
|
||||
|
||||
export default function Page(props: Route.ComponentProps) {
|
||||
const { page, compiled, tree } = props.loaderData;
|
||||
const { default: Mdx, toc } = executeMdxSync(compiled);
|
||||
|
||||
return (
|
||||
<DocsLayout
|
||||
nav={{
|
||||
title: 'React Router',
|
||||
}}
|
||||
tree={tree as PageTree.Root}
|
||||
>
|
||||
<DocsPage toc={toc}>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsDescription>{page.data.description}</DocsDescription>
|
||||
<DocsBody>
|
||||
<Mdx components={defaultMdxComponents} />
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Route } from './+types/search';
|
||||
import { createSearchAPI } from 'fumadocs-core/search/server';
|
||||
import { source } from '@/source';
|
||||
import { structure } from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
const server = createSearchAPI('advanced', {
|
||||
indexes: source.getPages().map((page) => ({
|
||||
id: page.url,
|
||||
url: page.url,
|
||||
title: page.data.title ?? '',
|
||||
description: page.data.description,
|
||||
structuredData: structure(page.data.content),
|
||||
})),
|
||||
});
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
return server.GET(request);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from 'react-router';
|
||||
import { RootProvider } from 'fumadocs-ui/provider/base';
|
||||
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
|
||||
import type { Route } from './+types/root';
|
||||
import './app.css';
|
||||
|
||||
export const links: Route.LinksFunction = () => [
|
||||
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://fonts.gstatic.com',
|
||||
crossOrigin: 'anonymous',
|
||||
},
|
||||
{
|
||||
rel: 'stylesheet',
|
||||
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
|
||||
},
|
||||
];
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<ReactRouterProvider>
|
||||
<RootProvider>{children}</RootProvider>
|
||||
</ReactRouterProvider>
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
let message = 'Oops!';
|
||||
let details = 'An unexpected error occurred.';
|
||||
let stack: string | undefined;
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
message = error.status === 404 ? '404' : 'Error';
|
||||
details =
|
||||
error.status === 404
|
||||
? 'The requested page could not be found.'
|
||||
: error.statusText || details;
|
||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||
details = error.message;
|
||||
stack = error.stack;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="pt-16 p-4 container mx-auto">
|
||||
<h1>{message}</h1>
|
||||
<p>{details}</p>
|
||||
{stack && (
|
||||
<pre className="w-full p-4 overflow-x-auto">
|
||||
<code>{stack}</code>
|
||||
</pre>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type RouteConfig, index, route } from '@react-router/dev/routes';
|
||||
|
||||
export default [
|
||||
index('routes/home.tsx'),
|
||||
route('docs/*', 'docs/page.tsx'),
|
||||
route('api/search', 'docs/search.ts'),
|
||||
] satisfies RouteConfig;
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Route } from './+types/home';
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: 'New React Router App' },
|
||||
{ name: 'description', content: 'Welcome to React Router!' },
|
||||
];
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<HomeLayout
|
||||
className="text-center"
|
||||
nav={{
|
||||
title: 'React Router',
|
||||
}}
|
||||
>
|
||||
<div className="py-12">
|
||||
<h1 className="text-xl font-bold mb-2">Fumadocs on React Router.</h1>
|
||||
<p className="text-fd-muted-foreground mb-8">
|
||||
The truly flexible docs framework on React.js.
|
||||
</p>
|
||||
<Link
|
||||
className="text-sm bg-fd-primary text-fd-primary-foreground rounded-full font-medium px-4 py-2.5"
|
||||
to="/docs"
|
||||
>
|
||||
Open Docs
|
||||
</Link>
|
||||
</div>
|
||||
</HomeLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
loader,
|
||||
type MetaData,
|
||||
type PageData,
|
||||
type Source,
|
||||
type VirtualFile,
|
||||
} from 'fumadocs-core/source';
|
||||
import matter from 'gray-matter';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const files = Object.entries(
|
||||
import.meta.glob<true, 'raw'>('/content/docs/**/*', {
|
||||
eager: true,
|
||||
query: '?raw',
|
||||
import: 'default',
|
||||
}),
|
||||
);
|
||||
|
||||
const virtualFiles: VirtualFile[] = files.flatMap(([file, content]) => {
|
||||
const ext = path.extname(file);
|
||||
const virtualPath = path.relative(
|
||||
'content/docs',
|
||||
path.join(process.cwd(), file),
|
||||
);
|
||||
|
||||
if (ext === '.mdx' || ext === '.md') {
|
||||
const parsed = matter(content);
|
||||
|
||||
return {
|
||||
type: 'page',
|
||||
path: virtualPath,
|
||||
data: {
|
||||
...parsed.data,
|
||||
content: parsed.content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (ext === '.json') {
|
||||
return {
|
||||
type: 'meta',
|
||||
path: virtualPath,
|
||||
data: JSON.parse(content),
|
||||
};
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
export const source = loader({
|
||||
source: {
|
||||
files: virtualFiles,
|
||||
} as Source<{
|
||||
pageData: PageData & {
|
||||
content: string;
|
||||
};
|
||||
metaData: MetaData;
|
||||
}>,
|
||||
baseUrl: '/docs',
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Hello World
|
||||
description: |
|
||||
Your first `document`
|
||||
You'll love it!
|
||||
---
|
||||
|
||||
Hey there! Fumadocs is the docs framework that also works on React Router!
|
||||
|
||||
## Heading
|
||||
|
||||
Hello World
|
||||
|
||||
<Cards>
|
||||
<Card title="Learn more about React Router" href="https://reactrouter.com" />
|
||||
<Card title="Learn more about Fumadocs" href="https://fumadocs.vercel.app" />
|
||||
</Cards>
|
||||
|
||||
```ts
|
||||
console.log('I love React!');
|
||||
```
|
||||
|
||||
### Heading
|
||||
|
||||
#### Heading
|
||||
|
||||
| Head | Description |
|
||||
| ------------------------------- | ----------------------------------- |
|
||||
| `hello` | Hello World |
|
||||
| very **important** | Hey |
|
||||
| _Surprisingly_ | Fumadocs |
|
||||
| very long text that looks weird | hello world hello world hello world |
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Fumadocs
|
||||
description: You can just be minimal.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Fumadocs is a docs framework.
|
||||
@@ -0,0 +1,6 @@
|
||||
.DS_Store
|
||||
/node_modules/
|
||||
|
||||
# React Router
|
||||
/.react-router/
|
||||
/build/
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,9 @@
|
||||
import type { Config } from '@react-router/dev/config';
|
||||
import { source } from './app/source';
|
||||
|
||||
export default {
|
||||
ssr: true,
|
||||
async prerender({ getStaticPaths }) {
|
||||
return [...getStaticPaths(), ...source.getPages().map((page) => page.url)];
|
||||
},
|
||||
} satisfies Config;
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"include": [
|
||||
"**/*",
|
||||
"**/.server/**/*",
|
||||
"**/.client/**/*",
|
||||
".react-router/types/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"types": ["node", "vite/client"],
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"rootDirs": [".", "./.react-router/types"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./app/*"]
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { reactRouter } from '@react-router/dev/vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: ['shiki'],
|
||||
},
|
||||
},
|
||||
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
|
||||
});
|
||||
@@ -3,12 +3,14 @@ import { defineConfig } from 'tsup';
|
||||
import corePkg from '../core/package.json';
|
||||
import uiPkg from '../ui/package.json';
|
||||
import mdxPkg from '../mdx/package.json';
|
||||
import mdxRemotePkg from '../mdx-remote/package.json';
|
||||
import contentCollectionsPkg from '../content-collections/package.json';
|
||||
|
||||
const versions = {
|
||||
'fumadocs-core': corePkg.version,
|
||||
'fumadocs-ui': uiPkg.version,
|
||||
'fumadocs-mdx': mdxPkg.version,
|
||||
'@fumadocs/mdx-remote': mdxRemotePkg.version,
|
||||
'@fumadocs/content-collections': contentCollectionsPkg.version,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user