Skip to content

Commit 37d136a

Browse files
committed
[docs] Display pre-rendered MDX content for official plugins
1 parent 44cdd4b commit 37d136a

2 files changed

Lines changed: 117 additions & 67 deletions

File tree

docs/src/app/(home)/plugins/client.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import type { LucideIcon } from 'lucide-react';
44
import * as lucide from 'lucide-react';
55
import { useCallback, useEffect, useState } from 'react';
66

7-
import { docsContentRoute } from '@/lib/shared';
8-
import { isOfficial, officialPluginSlug, type PluginEntry, ReadmeDrawer, type ReadmeState } from './readme-drawer';
7+
import { isOfficial, type PluginEntry, ReadmeDrawer, type ReadmeState } from './readme-drawer';
98

109
const REGISTRY_URL = 'https://registry.fraq.dev/plugins.json';
1110

@@ -132,7 +131,7 @@ export function PluginMarketplace() {
132131
const openDrawer = useCallback((plugin: PluginEntry) => {
133132
setDrawerPlugin(plugin);
134133
setDrawerOpen(false);
135-
setReadme({ text: null, loading: true, error: false });
134+
setReadme({ text: null, loading: !isOfficial(plugin), error: false });
136135

137136
requestAnimationFrame(() => {
138137
requestAnimationFrame(() => setDrawerOpen(true));
@@ -145,12 +144,10 @@ export function PluginMarketplace() {
145144
}, []);
146145

147146
useEffect(() => {
148-
if (!drawerPlugin) return;
147+
if (!drawerPlugin || isOfficial(drawerPlugin)) return;
149148

150149
const controller = new AbortController();
151-
const contentUrl = isOfficial(drawerPlugin)
152-
? `${docsContentRoute}/plugins/${officialPluginSlug(drawerPlugin)}/content.md`
153-
: `https://cdn.jsdelivr.net/npm/${drawerPlugin.name}@${drawerPlugin.version}/README.md`;
150+
const contentUrl = `https://cdn.jsdelivr.net/npm/${drawerPlugin.name}@${drawerPlugin.version}/README.md`;
154151

155152
fetch(contentUrl, { signal: controller.signal })
156153
.then(async (res) => {

docs/src/app/(home)/plugins/readme-drawer.tsx

Lines changed: 113 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,71 @@
11
'use client';
22

3+
import collections from 'collections/browser';
34
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock';
45
import * as lucide from 'lucide-react';
56
import Markdown, { RuleType } from 'markdown-to-jsx';
6-
import { useEffect, useRef } from 'react';
7+
import {
8+
Component,
9+
type ComponentProps,
10+
type ComponentType,
11+
type PropsWithChildren,
12+
Suspense,
13+
useEffect,
14+
useRef,
15+
} from 'react';
716

8-
import { useMDXComponents } from '@/components/mdx';
17+
import { getMDXComponents, useMDXComponents } from '@/components/mdx';
918
import { gitConfig } from '@/lib/shared';
1019

20+
const officialPluginDocs = collections.docs.createClientLoader<{ slug: string }>({
21+
component(doc, { slug }) {
22+
const MDX = doc.default;
23+
const components = getMDXComponents();
24+
const Link = components.a as ComponentType<ComponentProps<'a'>>;
25+
26+
return (
27+
<MDX
28+
components={getMDXComponents({
29+
a: ({ href, ...props }) => {
30+
if (!href || href.startsWith('#') || href.startsWith('/')) {
31+
return <Link href={href} {...props} />;
32+
}
33+
34+
const base = new URL(`https://fraq.dev/docs/plugins/${slug}.mdx`);
35+
const resolved = new URL(href, base);
36+
if (resolved.origin !== base.origin) {
37+
return <Link href={href} {...props} />;
38+
}
39+
40+
resolved.pathname = resolved.pathname.replace(/\.mdx?$/, '');
41+
return <Link href={`${resolved.pathname}${resolved.search}${resolved.hash}`} {...props} />;
42+
},
43+
})}
44+
/>
45+
);
46+
},
47+
});
48+
49+
function OfficialPluginDoc({ path, slug }: { path: string; slug: string }) {
50+
const MDX = officialPluginDocs.getComponent(path);
51+
return <MDX slug={slug} />;
52+
}
53+
54+
class OfficialPluginDocErrorBoundary extends Component<PropsWithChildren, { failed: boolean }> {
55+
state = { failed: false };
56+
57+
static getDerivedStateFromError() {
58+
return { failed: true };
59+
}
60+
61+
render() {
62+
if (this.state.failed) {
63+
return <p className="text-sm text-fd-muted-foreground">官方文档加载失败,请前往文档页面查看。</p>;
64+
}
65+
return this.props.children;
66+
}
67+
}
68+
1169
export interface PluginEntry {
1270
id: string;
1371
name: string;
@@ -32,13 +90,6 @@ export function officialPluginSlug(plugin: PluginEntry): string {
3290
return plugin.id.slice('fraqjs/'.length);
3391
}
3492

35-
function pluginHref(plugin: PluginEntry): string {
36-
if (isOfficial(plugin)) {
37-
return `/docs/plugins/${officialPluginSlug(plugin)}`;
38-
}
39-
return plugin.repository;
40-
}
41-
4293
function repositorySlug(plugin: PluginEntry): string | null {
4394
if (plugin.repository.startsWith('https://github.com/')) {
4495
const parts = plugin.repository.replace('https://github.com/', '').split('/');
@@ -96,8 +147,8 @@ export function ReadmeDrawer({
96147
const githubUrl = official
97148
? `https://github.com/${gitConfig.user}/${gitConfig.repo}/tree/${gitConfig.branch}/plugins/${slug}`
98149
: plugin.repository;
99-
const docsUrl = official ? pluginHref(plugin) : null;
100150
const npmUrl = `https://www.npmjs.com/package/${npmId}`;
151+
const officialDocPath = slug ? `plugins/${slug}.mdx` : null;
101152

102153
return (
103154
<>
@@ -142,17 +193,6 @@ export function ReadmeDrawer({
142193

143194
{/* Action buttons */}
144195
<div className="flex shrink-0 items-center gap-2 border-b border-fd-border px-5 py-3">
145-
{docsUrl && (
146-
<a
147-
href={docsUrl}
148-
target="_blank"
149-
rel="noopener noreferrer"
150-
className="inline-flex items-center gap-1.5 rounded-md border border-fd-border bg-fd-muted/60 px-3 py-1.5 text-xs text-fd-foreground transition-colors hover:bg-fd-accent"
151-
>
152-
<lucide.BookOpenIcon className="size-3.5" />
153-
文档
154-
</a>
155-
)}
156196
<a
157197
href={npmUrl}
158198
target="_blank"
@@ -181,46 +221,59 @@ export function ReadmeDrawer({
181221

182222
{/* Documentation content */}
183223
<div ref={scrollRef} className="min-h-0 flex-1 overflow-y-auto px-5 py-5">
184-
{readme.loading && (
185-
<div className="flex items-center gap-2 text-sm text-fd-muted-foreground">
186-
<lucide.LoaderCircleIcon className="size-4 animate-spin" />
187-
正在加载 README…
188-
</div>
189-
)}
190-
{readme.error && (
191-
<p className="text-sm text-fd-muted-foreground">README 加载失败,请前往官方文档、npm 或 GitHub 查看。</p>
192-
)}
193-
{readme.text && official ? (
194-
<div className="flex flex-col gap-4">
195-
<p className="text-sm text-fd-muted-foreground">
196-
以下是官方文档的原始 Markdown 内容,请点击“文档”按钮查看完整内容。
197-
</p>
198-
{/* divider */}
199-
<div className="h-px bg-fd-border" />
200-
<pre className="whitespace-pre-wrap break-words font-mono text-xs leading-relaxed text-fd-foreground">
201-
{readme.text}
202-
</pre>
203-
</div>
204-
) : readme.text ? (
205-
<Markdown
206-
options={{
207-
overrides: { ...mdxComponents, img: () => null },
208-
renderRule(next, node, _renderChildren, state) {
209-
if (node.type === RuleType.codeBlock) {
210-
return (
211-
<div key={state.key} className="[&_code]:text-[0.7109375rem]">
212-
<DynamicCodeBlock lang={node.lang || 'text'} code={node.text} />
213-
</div>
214-
);
224+
{official && slug && officialDocPath ? (
225+
Object.hasOwn(collections.docs.raw, officialDocPath) ? (
226+
<OfficialPluginDocErrorBoundary key={officialDocPath}>
227+
<Suspense
228+
fallback={
229+
<div className="flex items-center gap-2 text-sm text-fd-muted-foreground">
230+
<lucide.LoaderCircleIcon className="size-4 animate-spin" />
231+
正在加载官方文档…
232+
</div>
215233
}
216-
return next();
217-
},
218-
}}
219-
className="prose prose-sm dark:prose-invert max-w-none text-fd-foreground [&_a]:text-fd-primary"
220-
>
221-
{readme.text}
222-
</Markdown>
223-
) : null}
234+
>
235+
<div className="prose prose-sm dark:prose-invert max-w-none text-fd-foreground [&_a]:text-fd-primary">
236+
<Markdown options={{ overrides: mdxComponents }}>{`# ${plugin.name}`}</Markdown>
237+
<OfficialPluginDoc path={officialDocPath} slug={slug} />
238+
</div>
239+
</Suspense>
240+
</OfficialPluginDocErrorBoundary>
241+
) : (
242+
<p className="text-sm text-fd-muted-foreground">该官方插件暂时没有可用的文档。</p>
243+
)
244+
) : (
245+
<>
246+
{readme.loading && (
247+
<div className="flex items-center gap-2 text-sm text-fd-muted-foreground">
248+
<lucide.LoaderCircleIcon className="size-4 animate-spin" />
249+
正在加载 README…
250+
</div>
251+
)}
252+
{readme.error && (
253+
<p className="text-sm text-fd-muted-foreground">README 加载失败,请前往 npm 或 GitHub 查看。</p>
254+
)}
255+
{readme.text && (
256+
<Markdown
257+
options={{
258+
overrides: { ...mdxComponents, img: () => null },
259+
renderRule(next, node, _renderChildren, state) {
260+
if (node.type === RuleType.codeBlock) {
261+
return (
262+
<div key={state.key} className="[&_code]:text-[0.7109375rem]">
263+
<DynamicCodeBlock lang={node.lang || 'text'} code={node.text} />
264+
</div>
265+
);
266+
}
267+
return next();
268+
},
269+
}}
270+
className="prose prose-sm dark:prose-invert max-w-none text-fd-foreground [&_a]:text-fd-primary"
271+
>
272+
{readme.text}
273+
</Markdown>
274+
)}
275+
</>
276+
)}
224277
</div>
225278
</div>
226279
</>

0 commit comments

Comments
 (0)