refactor(docs) parse and render docs mdx files by fumadocs

This commit is contained in:
javayhu 2025-06-16 00:51:36 +08:00
parent 3b741b3b98
commit f4d8a09ab6
10 changed files with 379 additions and 281 deletions

3
.gitignore vendored
View File

@ -43,6 +43,9 @@ next-env.d.ts
# content collections # content collections
.content-collections .content-collections
# fumadocs
.source
# OpenNext build output # OpenNext build output
.open-next .open-next

View File

@ -1,11 +1,7 @@
import path from 'path'; import path from 'path';
import { DEFAULT_LOCALE, LOCALES } from '@/i18n/routing'; import { DEFAULT_LOCALE, LOCALES } from '@/i18n/routing';
import { defineCollection, defineConfig } from '@content-collections/core'; import { defineCollection, defineConfig } from '@content-collections/core';
import { import { transformMDX } from '@fumadocs/content-collections/configuration';
createDocSchema,
createMetaSchema,
transformMDX,
} from '@fumadocs/content-collections/configuration';
/** /**
* 1. Content Collections documentation * 1. Content Collections documentation
@ -16,25 +12,25 @@ import {
* 2. Use Content Collections for Fumadocs * 2. Use Content Collections for Fumadocs
* https://fumadocs.dev/docs/headless/content-collections * https://fumadocs.dev/docs/headless/content-collections
*/ */
const docs = defineCollection({ // const docs = defineCollection({
name: 'docs', // name: 'docs',
directory: 'content/docs', // directory: 'content/docs',
include: '**/*.mdx', // include: '**/*.mdx',
schema: (z) => ({ // schema: (z) => ({
...createDocSchema(z), // ...createDocSchema(z),
preview: z.string().optional(), // preview: z.string().optional(),
index: z.boolean().default(false), // index: z.boolean().default(false),
}), // }),
transform: transformMDX, // transform: transformMDX,
}); // });
const metas = defineCollection({ // const metas = defineCollection({
name: 'meta', // name: 'meta',
directory: 'content/docs', // directory: 'content/docs',
include: '**/meta**.json', // include: '**/meta**.json',
parser: 'json', // parser: 'json',
schema: createMetaSchema, // schema: createMetaSchema,
}); // });
/** /**
* Blog Author collection * Blog Author collection
@ -324,5 +320,5 @@ function extractLocaleAndBase(fileName: string): {
} }
export default defineConfig({ export default defineConfig({
collections: [docs, metas, authors, categories, posts, pages, releases], collections: [authors, categories, posts, pages, releases],
}); });

View File

@ -1,4 +1,4 @@
import { withContentCollections } from '@content-collections/next'; import { createMDX } from 'fumadocs-mdx/next';
import type { NextConfig } from 'next'; import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin'; import createNextIntlPlugin from 'next-intl/plugin';
@ -69,8 +69,9 @@ const nextConfig: NextConfig = {
const withNextIntl = createNextIntlPlugin(); const withNextIntl = createNextIntlPlugin();
/** /**
* withContentCollections must be the outermost plugin * https://fumadocs.dev/docs/ui/manual-installation
* * https://fumadocs.dev/docs/mdx/plugin
* https://www.content-collections.dev/docs/quickstart/next
*/ */
export default withContentCollections(withNextIntl(nextConfig)); const withMDX = createMDX();
export default withMDX(withNextIntl(nextConfig));

View File

@ -6,6 +6,7 @@
"dev": "concurrently \"content-collections watch\" \"next dev\"", "dev": "concurrently \"content-collections watch\" \"next dev\"",
"build": "content-collections build && next build", "build": "content-collections build && next build",
"start": "next start", "start": "next start",
"postinstall": "fumadocs-mdx",
"lint": "biome check --write .", "lint": "biome check --write .",
"lint:fix": "biome check --fix --unsafe .", "lint:fix": "biome check --fix --unsafe .",
"format": "biome format --write .", "format": "biome format --write .",

510
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

27
source.config.ts Normal file
View File

@ -0,0 +1,27 @@
import { defineDocs, frontmatterSchema, metaSchema } from 'fumadocs-mdx/config';
import { z } from 'zod';
const customDocsSchema = frontmatterSchema.extend({
preview: z.string().optional(),
index: z.boolean().default(false),
});
const customMetaSchema = metaSchema.extend({
description: z.string().optional(),
});
/**
* frontmatterSchema.extend causes error: Type instantiation is excessively deep,
* so we define the schema manually.
*
* https://fumadocs.dev/docs/mdx/collections#schema-1
*/
export const docs = defineDocs({
dir: 'content/docs',
docs: {
schema: customDocsSchema,
},
meta: {
schema: customMetaSchema,
},
});

View File

@ -1,5 +1,5 @@
import { getMDXComponents } from '@/components/custom/mdx-components';
import * as Preview from '@/components/docs'; import * as Preview from '@/components/docs';
import { CustomMDXContent } from '@/components/shared/custom-mdx-content';
import { import {
HoverCard, HoverCard,
HoverCardContent, HoverCardContent,
@ -87,6 +87,8 @@ export default async function DocPage({ params }: DocPageProps) {
const preview = page.data.preview; const preview = page.data.preview;
const MDX = page.data.body;
return ( return (
<DocsPage <DocsPage
toc={page.data.toc} toc={page.data.toc}
@ -102,9 +104,8 @@ export default async function DocPage({ params }: DocPageProps) {
{preview ? <PreviewRenderer preview={preview} /> : null} {preview ? <PreviewRenderer preview={preview} /> : null}
{/* MDX Content */} {/* MDX Content */}
<CustomMDXContent <MDX
code={page.data.body} components={getMDXComponents({
customComponents={{
a: ({ href, ...props }: { href?: string; [key: string]: any }) => { a: ({ href, ...props }: { href?: string; [key: string]: any }) => {
const found = source.getPageByHref(href ?? '', { const found = source.getPageByHref(href ?? '', {
dir: page.file.dirname, dir: page.file.dirname,
@ -133,7 +134,7 @@ export default async function DocPage({ params }: DocPageProps) {
</HoverCard> </HoverCard>
); );
}, },
}} })}
/> />
</DocsBody> </DocsBody>
</DocsPage> </DocsPage>

View File

@ -0,0 +1,45 @@
import { ImageWrapper } from '@/components/docs/image-wrapper';
import { Wrapper } from '@/components/docs/wrapper';
import { YoutubeVideo } from '@/components/docs/youtube-video';
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
import { Callout } from 'fumadocs-ui/components/callout';
import { File, Files, Folder } from 'fumadocs-ui/components/files';
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { TypeTable } from 'fumadocs-ui/components/type-table';
import defaultMdxComponents from 'fumadocs-ui/mdx';
import * as LucideIcons from 'lucide-react';
import type { MDXComponents } from 'mdx/types';
import type { ComponentProps, FC } from 'react';
/**
* Enhanced MDX Content component that includes commonly used MDX components
* It can be used for blog posts, documentation, and custom pages
*/
export function getMDXComponents(components?: MDXComponents): MDXComponents {
// Start with default components
const baseComponents: Record<string, any> = {
...defaultMdxComponents,
...LucideIcons,
// ...((await import('lucide-react')) as unknown as MDXComponents),
YoutubeVideo,
Tabs,
Tab,
TypeTable,
Accordion,
Accordions,
Steps,
Step,
Wrapper,
File,
Folder,
Files,
blockquote: Callout as unknown as FC<ComponentProps<'blockquote'>>,
img: ImageWrapper,
};
return {
...baseComponents,
...components,
};
}

View File

@ -28,7 +28,7 @@ export default function Example() {
className="bg-transparent px-4 py-2 text-sm focus-visible:outline-none" className="bg-transparent px-4 py-2 text-sm focus-visible:outline-none"
/> />
</div> </div>
<DynamicCodeBlock lang={lang} code={code} options={{}} /> <DynamicCodeBlock lang={lang} code={code} />
</div> </div>
); );
} }

View File

@ -1,12 +1,12 @@
import { createMDXSource } from '@fumadocs/content-collections';
import { allDocs, allMetas } from 'content-collections';
import { loader } from 'fumadocs-core/source'; import { loader } from 'fumadocs-core/source';
import * as LucideIcons from 'lucide-react'; import * as LucideIcons from 'lucide-react';
import { createElement } from 'react'; import { createElement } from 'react';
import { docs } from '../../../.source';
import { docsI18nConfig } from './i18n'; import { docsI18nConfig } from './i18n';
/** /**
* Turn a content source into a unified interface * Turn a content source into a unified interface
* .source folder is generated by `fumadocs-mdx`
* *
* https://fumadocs.dev/docs/headless/source-api * https://fumadocs.dev/docs/headless/source-api
* https://fumadocs.dev/docs/headless/content-collections * https://fumadocs.dev/docs/headless/content-collections
@ -14,7 +14,7 @@ import { docsI18nConfig } from './i18n';
export const source = loader({ export const source = loader({
baseUrl: '/docs', baseUrl: '/docs',
i18n: docsI18nConfig, i18n: docsI18nConfig,
source: createMDXSource(allDocs, allMetas), source: docs.toFumadocsSource(),
icon(iconName) { icon(iconName) {
if (!iconName) { if (!iconName) {
return undefined; return undefined;