Files
nocobase/docs/rspress.config.ts

305 lines
8.4 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
import { pluginSass } from '@rsbuild/plugin-sass';
import { defineConfig, type RspressPlugin } from '@rspress/core';
import { pluginLlms } from '@rspress/plugin-llms';
import { pluginSchema } from './plugins/pluginSchema';
import { pluginOgDescription } from './plugins/pluginOgDescription';
import { pluginRemoveGenerator } from './plugins/pluginRemoveGenerator';
import { pluginPreview } from '@rspress/plugin-preview';
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill';
import {
pluginCrossRefSidebar,
crossRefCanonicalMap,
} from './plugins/pluginCrossRef';
import { pluginSearchSections } from './plugins/pluginSearchSections';
import { pluginSearchIndex } from './plugins/pluginSearchIndex';
import * as path from 'node:path';
import * as fs from 'node:fs/promises';
const lang = process.env.DOCS_LANG || 'en';
const base = process.env.DOCS_BASE || lang === 'en' ? '/' : `/${lang}/`;
const checkDeadLinks = process.env.CHECK_DEAD_LINKS !== 'false';
const rspressI18nAliases: Readonly<Record<string, string>> = {
cn: 'zh',
};
const locales = {
en: {
title: 'NocoBase Documentation',
description: 'Learn and master NocoBase quickly',
},
cn: {
title: 'NocoBase 文档',
description: '快速学习和掌握 NocoBase',
},
};
const currentLocale = locales[lang as keyof typeof locales] || locales.en;
const indexLanguages = [
'en',
'cn',
'ja',
'es',
'pt',
'de',
'fr',
'ru',
'id',
'vi',
];
const langMap = {
en: 'en-US',
cn: 'zh-CN',
ja: 'ja-JP',
es: 'es-ES',
pt: 'pt-PT',
de: 'de-DE',
fr: 'fr-FR',
ru: 'ru-RU',
id: 'id-ID',
vi: 'vi-VN',
};
/**
* 搜索结果分组标题里两个固定分组的文案。其余分组的标签由 `pluginSearchSections` 从 `_nav.json` 和
* 各目录 `index.md` 里读出来,本来就是本地化好的,不需要在这里维护。
*
* 缺失语言由 Rspress 自动回落到 `en`,所以只写常用语种即可。
*/
const searchI18nSource: Record<string, Record<string, string>> = {
searchSectionPlugins: {
en: 'Plugins',
zh: '插件',
ja: 'プラグイン',
de: 'Plugins',
es: 'Plugins',
fr: 'Plugins',
pt: 'Plugins',
ru: 'Плагины',
id: 'Plugin',
vi: 'Plugin',
},
searchSectionOthers: {
en: 'Others',
zh: '其他',
ja: 'その他',
de: 'Sonstiges',
es: 'Otros',
fr: 'Autres',
pt: 'Outros',
ru: 'Прочее',
id: 'Lainnya',
vi: 'Khác',
},
};
function withRspressI18nAliases(
source: Record<string, Record<string, string>>,
) {
return Object.fromEntries(
Object.entries({ ...source, ...searchI18nSource }).map(
([key, translations]) => {
const nextTranslations = { ...translations };
for (const [alias, original] of Object.entries(rspressI18nAliases)) {
if (!nextTranslations[alias] && nextTranslations[original]) {
nextTranslations[alias] = nextTranslations[original];
}
}
return [key, nextTranslations];
},
),
);
}
function sitemap(): RspressPlugin {
const routes = new Set<string>();
return {
name: '@nocobase/custom-sitemap',
// Collect all route paths during build
async extendPageData(pageData: any, isProd: boolean) {
if (!isProd) {
return;
}
if (lang !== 'en') {
return;
}
if (pageData?.routePath) {
routes.add(pageData.routePath as string);
}
},
// Generate sitemap.xml after build
async afterBuild(config: any, isProd: boolean) {
if (!isProd) {
return;
}
if (lang !== 'en') {
return;
}
const baseDomain = 'https://docs.nocobase.com';
const urlEntries = Array.from(routes)
.sort()
.map((routePath) => {
const links: string[] = [];
// <loc> uses the canonical English URL
const loc = `${baseDomain}${routePath}`;
// Alternate links for each language (same logic as head canonical/alternate)
for (const language of indexLanguages) {
if (language === 'en') {
links.push(
` <xhtml:link rel="alternate" hreflang="en-US" href="${baseDomain}${routePath}" />`,
);
} else {
const hreflang = langMap[language as keyof typeof langMap];
links.push(
` <xhtml:link rel="alternate" hreflang="${hreflang}" href="${baseDomain}/${language}${routePath}" />`,
);
}
}
// x-default points to the English URL
links.push(
` <xhtml:link rel="alternate" hreflang="x-default" href="${baseDomain}${routePath}" />`,
);
return [
' <url>',
` <loc>${loc}</loc>`,
...links,
' </url>',
].join('\n');
})
.join('\n');
const sitemapXml = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"',
' xmlns:xhtml="http://www.w3.org/1999/xhtml">',
urlEntries,
'</urlset>',
'',
].join('\n');
const outDir: string = config.outDir;
const sitemapPath = path.join(outDir, 'sitemap.xml');
await fs.mkdir(outDir, { recursive: true });
await fs.writeFile(sitemapPath, sitemapXml, 'utf-8');
},
};
}
export default defineConfig({
head: [
[
'meta',
{
name: 'robots',
content: indexLanguages.includes(lang)
? 'index,follow'
: 'noindex,nofollow',
},
],
(route) => {
// 跨模块虚拟路由通过 frontmatter canonicalPath 指向原始页面
const canonicalRoute =
crossRefCanonicalMap?.[route.routePath] || route.routePath;
if (lang !== 'en') {
return `<link rel="canonical" href="https://docs.nocobase.com/${lang}${canonicalRoute}" />`;
}
return `<link rel="canonical" href="https://docs.nocobase.com${canonicalRoute}" />`;
},
(route) => {
const links = [];
links.push(
...indexLanguages.map((language) => {
if (language === 'en') {
return `<link rel="alternate" hreflang="en-US" href="https://docs.nocobase.com${route.routePath}" />`;
}
const hreflang = langMap[language as keyof typeof langMap];
return `<link rel="alternate" hreflang="${hreflang}" href="https://docs.nocobase.com/${language}${route.routePath}" />`;
}),
);
links.push(
`<link rel="alternate" hreflang="x-default" href="https://docs.nocobase.com${route.routePath}" />`,
);
return links.join('\n');
},
],
root: path.join(__dirname, `docs/${lang}`),
outDir: path.join(__dirname, lang === 'en' ? 'dist' : `dist/${lang}`),
themeDir: path.join(__dirname, 'theme'),
base,
title: currentLocale.title,
description: currentLocale.description,
icon: 'https://www.nocobase.com/images/favicon/apple-touch-icon.png',
logo: {
light: 'https://static-docs.nocobase.com/20260119193433.png',
dark: 'https://static-docs.nocobase.com/20260119193447.png',
},
route: {
cleanUrls: true,
},
builderConfig: {
html: {
tags: [
{
tag: 'script',
// 通过 window.RSPRESS_THEME 变量来指定默认的主题模式
children: "window.RSPRESS_THEME = 'light';",
},
],
},
source: {
tsconfigPath: path.join(__dirname, 'tsconfig.json'),
},
plugins: [pluginSass(), pluginNodePolyfill()],
resolve: {
aliasStrategy: 'prefer-tsconfig',
},
},
markdown: {
link: {
checkDeadLinks,
},
},
// Rspress ships built-in Simplified Chinese strings under `zh`, while this site uses `/cn/`.
i18nSource: (value) => withRspressI18nAliases(value),
plugins: [
pluginPreview(),
pluginLlms(),
pluginSchema(),
pluginOgDescription(),
pluginRemoveGenerator(),
pluginCrossRefSidebar(),
pluginSearchSections(),
pluginSearchIndex(),
sitemap(),
],
search: {
// 自定义搜索:结果按文档区分组,插件元信息页沉底。见 theme/search/searchHooks.ts。
searchHooks: path.join(__dirname, 'theme/search/searchHooks.ts'),
},
lang,
themeConfig: {
darkMode: false,
socialLinks: [
{
icon: 'github',
mode: 'link',
content: 'https://github.com/nocobase/nocobase',
},
],
},
});