Fumadocs 草稿支持
结合 vercel 的 preview 环境能力,扩展 draft 字段实现在线阅读草稿
本文主要介绍基于 Next.js 的集成方式,并且基于多语言支持的前提下进行数据处理。没有配置多语言支持时,getPage 等相关代码会有略微不同。如有不理解之处,请参考官方资料:
i18n
Support i18n routing on your Next.js + Fumadocs app
Manual Installation
Setup Fumadocs on Next.js.
Example
A complete example of Next.js + Fumadocs + i18n
依赖版本说明
本文采取的 Fumadocs 相关依赖的版本信息如下:
| 依赖项 | 版本号 |
|---|---|
fumadocs-core | 15.7.7 |
fumadocs-mdx | 11.8.1 |
fumadocs-ui | 15.7.6 |
流程概览
扩展字段和定义环境变量
import { defineDocs, frontmatterSchema, metaSchema } from "fumadocs-mdx/config";
import z from "zod";
export const docs = defineDocs({
docs: {
schema: frontmatterSchema.extend({
// 扩展字段
draft: z.boolean().default(false),
}),
},
meta: {
schema: metaSchema,
},
});启动草稿,支持显示草稿内容。线上环境建议设置为 false。
DRAFT=true然后记得在 mdx 文章中添加 draft 字段,让这篇文章变为草稿。
---
title: hello
description: fumadocs
draft: true
---拦截具备 draft 字段的页面
import { DraftReadme } from "@/components/draft-readme";
import { source } from "@/lib/source";
import { getMDXComponents } from "@/mdx-components";
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
} from "fumadocs-ui/page";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
export default async function Page(
props: PageProps<"/[lang]/docs/[[...slug]]">
) {
const params = await props.params;
const page = source.getPage(params.slug, params.lang);
if (!page) notFound();
const MDXContent = page.data.body;
// 对于不支持阅读草稿内容的环境,将看到这个内容
if (page.data.draft && process.env.DRAFT === "false") {
return <DraftReadme lang={params.lang} />;
}
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle className="flex justify-between items-center">
<span>
{/* 添加一个 🚧,标识内容还在编写 */}
{page.data.draft ? `🚧 ${page.data.title}` : page.data.title}
</span>
</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<MDXContent components={getMDXComponents()} />
</DocsBody>
</DocsPage>
);
}这里的 DraftReadme 是我自定义的一个提醒组件,告知用户内容处于编写状态,暂未发布。你可以自定义自己需要的内容,或者直接执行 notFound 跳转。
搜索过滤
import { source } from "@/lib/source";
import { createTokenizer } from "@orama/tokenizers/mandarin";
import { createFromSource } from "fumadocs-core/search/server";
import { NextRequest, NextResponse } from "next/server";
const formSource = createFromSource(source, {
localeMap: {
cn: {
components: {
tokenizer: createTokenizer(),
},
search: {
threshold: 0,
tolerance: 0,
},
},
},
});
export const GET = async (req: NextRequest) => {
const query = req.nextUrl.searchParams.get("query")!;
const locale = req.nextUrl.searchParams.get("locale")!;
const data = await formSource.search(query, { locale });
const filteredData = data.filter((item) => {
const [slug] = item.url.split("#");
const slugs = slug.split("/").filter(Boolean).slice(2);
const page = source.getPage(slugs, locale);
if (page) {
if (page.data.draft) {
return process.env.DRAFT === "true";
} else {
return true;
}
} else {
return false;
}
});
return NextResponse.json(filteredData);
};Sidebar 过滤
import { filterPageTree } from "@/lib/filterPageTree";
import { baseOptions } from "@/lib/layout.shared";
import { source } from "@/lib/source";
import { DocsLayout } from "fumadocs-ui/layouts/docs";
export default async function Layout({
children,
params,
}: LayoutProps<"/[lang]/docs">) {
const { lang } = await params;
return (
<DocsLayout
tree={filterPageTree(source.pageTree[lang])}
{...baseOptions(lang)}
>
{children}
</DocsLayout>
);
}import { source } from "@/lib/source";
import { PageTree } from "fumadocs-core/server";
export const filterPageTree = (pageTree: PageTree.Root): PageTree.Root => {
const filterNode = (node: PageTree.Node): PageTree.Node | null => {
if (node.type === "page") {
const slug = node.url?.split("/").slice(3);
if (slug) {
const page = source.getPage(slug);
if (page?.data.draft) {
if (process.env.DRAFT === "false") {
return null;
}
}
}
return node;
} else if (node.type === "folder") {
const filteredChildren = node.children
.map(filterNode)
.filter((child): child is PageTree.Node => child !== null);
if (filteredChildren.length === 0) {
return null;
}
return {
...node,
children: filteredChildren,
};
} else if (node.type === "separator") {
return node;
}
return node;
};
const filteredChildren = pageTree.children
.map(filterNode)
.filter((child): child is PageTree.Node => child !== null);
return {
...pageTree,
children: filteredChildren,
};
};关于 vercel
草稿的显示,核心就是控制环境变量里的 DRAFT 字段值。可以设置根据不同的环境来控制它的值。我只会在 Preview 环境设置为 true,vercel 的 Preview 环境本身就只有自己可以访问,不是公开的。所以也不用担心草稿内容会泄露,也不用担心被 SEO 收录,两全其美。如果你高兴,用一个 PRIVATE 来控制显示也挺不错的。