mirror of
https://github.com/nocobase/nocobase.git
synced 2026-10-16 15:07:08 +08:00
* docs: serve v2 documentation from v2.docs.nocobase.com * docs: read the site origin from DOCS_SITE_ORIGIN and rewrite legacy docs links - Read the site origin from DOCS_SITE_ORIGIN (default https://v2.docs.nocobase.com). - Docs assistant accepts citations on the site host and on docs.nocobase.com. - Rewrite absolute docs.nocobase.com links in content to the site origin at build time. - Emit robots.txt with the sitemap location next to sitemap.xml. - gen_rag.sh derives its domains from DOCS_SITE_ORIGIN.
657 lines
25 KiB
TypeScript
657 lines
25 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 {
|
||
pluginSiteOriginLinks,
|
||
remarkSiteOriginLinks,
|
||
} from './plugins/pluginSiteOriginLinks';
|
||
import { siteHost, siteOrigin } from './shared/site';
|
||
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 docsAiApiUrl = process.env.DOCS_AI_API_URL || '';
|
||
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',
|
||
},
|
||
};
|
||
|
||
const docsAiMessages: Record<string, Record<string, string>> = {
|
||
en: {
|
||
docsAiOpen: 'Ask AI',
|
||
docsAiTitle: 'Ask',
|
||
docsAiClose: 'Close documentation assistant',
|
||
docsAiExpand: 'Expand assistant',
|
||
docsAiCollapse: 'Collapse assistant',
|
||
docsAiQuestionPlaceholder: 'Ask a question...',
|
||
docsAiSubmit: 'Ask',
|
||
docsAiQuestionLabel: 'Your question',
|
||
docsAiProgress: 'Progress',
|
||
docsAiLoading: 'Searching the official documentation…',
|
||
docsAiProgressSubmitted:
|
||
`Question submitted; limiting search to ${siteHost}`,
|
||
docsAiProgressCallingModel: 'Calling DeepSeek',
|
||
docsAiProgressParsingCitations:
|
||
'Response received; parsing official documentation references',
|
||
docsAiProgressVerifyingSources: 'Verifying official documentation pages',
|
||
docsAiProgressSourcesVerified: 'Official documentation sources verified',
|
||
docsAiProgressRetrying: 'The response was incomplete; retrying',
|
||
docsAiSources: 'References',
|
||
docsAiAppliesWhen: 'Notes',
|
||
docsAiSuggestions: 'Common questions',
|
||
docsAiSuggestionOne: 'How do I install and upgrade NocoBase?',
|
||
docsAiSuggestionTwo: 'How do I create collections and pages?',
|
||
docsAiSuggestionThree: 'How do I configure roles and permissions?',
|
||
docsAiError:
|
||
'The AI service is unavailable. Please use documentation search and try again later.',
|
||
docsAiRequestId: 'Request ID',
|
||
docsAiEmptyQuestion: 'Enter a question first.',
|
||
},
|
||
zh: {
|
||
docsAiOpen: '询问 AI',
|
||
docsAiTitle: '询问',
|
||
docsAiClose: '关闭文档 AI 助手',
|
||
docsAiExpand: '展开助手',
|
||
docsAiCollapse: '收起助手',
|
||
docsAiQuestionPlaceholder: '询问一个问题...',
|
||
docsAiSubmit: '提问',
|
||
docsAiQuestionLabel: '你的问题',
|
||
docsAiProgress: '处理进度',
|
||
docsAiLoading: '正在检索官方文档…',
|
||
docsAiProgressSubmitted: `问题已提交,正在限定搜索 ${siteHost}`,
|
||
docsAiProgressCallingModel: '正在调用 DeepSeek',
|
||
docsAiProgressParsingCitations: '已收到回答,正在解析官方文档引用',
|
||
docsAiProgressVerifyingSources: '正在验证官方文档页面',
|
||
docsAiProgressSourcesVerified: '已验证官方文档来源',
|
||
docsAiProgressRetrying: '本次响应不完整,正在重试',
|
||
docsAiSources: '参考文档',
|
||
docsAiAppliesWhen: '补充说明',
|
||
docsAiSuggestions: '常见问题',
|
||
docsAiSuggestionOne: '如何安装和升级 NocoBase?',
|
||
docsAiSuggestionTwo: '如何创建数据表和页面?',
|
||
docsAiSuggestionThree: '如何配置用户角色与权限?',
|
||
docsAiError: 'AI 服务暂时不可用,请使用文档搜索并稍后重试。',
|
||
docsAiRequestId: '请求编号',
|
||
docsAiEmptyQuestion: '请先输入问题。',
|
||
},
|
||
ja: {
|
||
docsAiOpen: 'AI に質問',
|
||
docsAiTitle: '質問',
|
||
docsAiClose: 'ドキュメント AI アシスタントを閉じる',
|
||
docsAiExpand: 'アシスタントを展開',
|
||
docsAiCollapse: 'アシスタントを折りたたむ',
|
||
docsAiQuestionPlaceholder: '質問を入力...',
|
||
docsAiSubmit: '質問する',
|
||
docsAiQuestionLabel: 'あなたの質問',
|
||
docsAiProgress: '処理状況',
|
||
docsAiLoading: '公式ドキュメントを検索しています…',
|
||
docsAiProgressSubmitted:
|
||
`質問を送信しました。${siteHost} 内を検索しています`,
|
||
docsAiProgressCallingModel: 'DeepSeek を呼び出しています',
|
||
docsAiProgressParsingCitations:
|
||
'回答を受信しました。公式ドキュメントの参照を解析しています',
|
||
docsAiProgressVerifyingSources: '公式ドキュメントのページを確認しています',
|
||
docsAiProgressSourcesVerified: '公式ドキュメントの参照元を確認しました',
|
||
docsAiProgressRetrying: '回答が不完全なため、再試行しています',
|
||
docsAiSources: '参考ドキュメント',
|
||
docsAiAppliesWhen: '補足',
|
||
docsAiSuggestions: 'よくある質問',
|
||
docsAiSuggestionOne: 'NocoBase をインストール、アップグレードするには?',
|
||
docsAiSuggestionTwo: 'コレクションとページを作成するには?',
|
||
docsAiSuggestionThree: 'ユーザーロールと権限を設定するには?',
|
||
docsAiError:
|
||
'AI サービスを利用できません。ドキュメント検索を使用し、後でもう一度お試しください。',
|
||
docsAiRequestId: 'リクエスト ID',
|
||
docsAiEmptyQuestion: '先に質問を入力してください。',
|
||
},
|
||
es: {
|
||
docsAiOpen: 'Preguntar a la IA',
|
||
docsAiTitle: 'Preguntar',
|
||
docsAiClose: 'Cerrar el asistente de documentación',
|
||
docsAiExpand: 'Ampliar el asistente',
|
||
docsAiCollapse: 'Contraer el asistente',
|
||
docsAiQuestionPlaceholder: 'Escribe una pregunta...',
|
||
docsAiSubmit: 'Preguntar',
|
||
docsAiQuestionLabel: 'Tu pregunta',
|
||
docsAiProgress: 'Progreso',
|
||
docsAiLoading: 'Buscando en la documentación oficial…',
|
||
docsAiProgressSubmitted:
|
||
`Pregunta enviada; buscando solo en ${siteHost}`,
|
||
docsAiProgressCallingModel: 'Consultando DeepSeek',
|
||
docsAiProgressParsingCitations:
|
||
'Respuesta recibida; analizando las referencias oficiales',
|
||
docsAiProgressVerifyingSources:
|
||
'Verificando páginas de la documentación oficial',
|
||
docsAiProgressSourcesVerified: 'Fuentes oficiales verificadas',
|
||
docsAiProgressRetrying: 'La respuesta está incompleta; reintentando',
|
||
docsAiSources: 'Referencias',
|
||
docsAiAppliesWhen: 'Notas',
|
||
docsAiSuggestions: 'Preguntas frecuentes',
|
||
docsAiSuggestionOne: '¿Cómo instalo y actualizo NocoBase?',
|
||
docsAiSuggestionTwo: '¿Cómo creo colecciones y páginas?',
|
||
docsAiSuggestionThree: '¿Cómo configuro roles y permisos?',
|
||
docsAiError:
|
||
'El servicio de IA no está disponible. Usa la búsqueda de documentación e inténtalo más tarde.',
|
||
docsAiRequestId: 'ID de solicitud',
|
||
docsAiEmptyQuestion: 'Escribe una pregunta primero.',
|
||
},
|
||
pt: {
|
||
docsAiOpen: 'Perguntar à IA',
|
||
docsAiTitle: 'Perguntar',
|
||
docsAiClose: 'Fechar o assistente de documentação',
|
||
docsAiExpand: 'Expandir o assistente',
|
||
docsAiCollapse: 'Recolher o assistente',
|
||
docsAiQuestionPlaceholder: 'Digite uma pergunta...',
|
||
docsAiSubmit: 'Perguntar',
|
||
docsAiQuestionLabel: 'Sua pergunta',
|
||
docsAiProgress: 'Progresso',
|
||
docsAiLoading: 'Pesquisando na documentação oficial…',
|
||
docsAiProgressSubmitted:
|
||
`Pergunta enviada; pesquisando apenas em ${siteHost}`,
|
||
docsAiProgressCallingModel: 'Consultando o DeepSeek',
|
||
docsAiProgressParsingCitations:
|
||
'Resposta recebida; analisando as referências oficiais',
|
||
docsAiProgressVerifyingSources:
|
||
'Verificando páginas da documentação oficial',
|
||
docsAiProgressSourcesVerified: 'Fontes oficiais verificadas',
|
||
docsAiProgressRetrying: 'A resposta está incompleta; tentando novamente',
|
||
docsAiSources: 'Referências',
|
||
docsAiAppliesWhen: 'Observações',
|
||
docsAiSuggestions: 'Perguntas frequentes',
|
||
docsAiSuggestionOne: 'Como instalar e atualizar o NocoBase?',
|
||
docsAiSuggestionTwo: 'Como criar coleções e páginas?',
|
||
docsAiSuggestionThree: 'Como configurar funções e permissões?',
|
||
docsAiError:
|
||
'O serviço de IA não está disponível. Use a pesquisa da documentação e tente novamente mais tarde.',
|
||
docsAiRequestId: 'ID da solicitação',
|
||
docsAiEmptyQuestion: 'Digite uma pergunta primeiro.',
|
||
},
|
||
de: {
|
||
docsAiOpen: 'KI fragen',
|
||
docsAiTitle: 'Fragen',
|
||
docsAiClose: 'Dokumentationsassistent schließen',
|
||
docsAiExpand: 'Assistenten erweitern',
|
||
docsAiCollapse: 'Assistenten einklappen',
|
||
docsAiQuestionPlaceholder: 'Frage eingeben...',
|
||
docsAiSubmit: 'Fragen',
|
||
docsAiQuestionLabel: 'Deine Frage',
|
||
docsAiProgress: 'Fortschritt',
|
||
docsAiLoading: 'Offizielle Dokumentation wird durchsucht…',
|
||
docsAiProgressSubmitted:
|
||
`Frage gesendet; Suche auf ${siteHost} beschränkt`,
|
||
docsAiProgressCallingModel: 'DeepSeek wird aufgerufen',
|
||
docsAiProgressParsingCitations:
|
||
'Antwort erhalten; offizielle Verweise werden ausgewertet',
|
||
docsAiProgressVerifyingSources:
|
||
'Seiten der offiziellen Dokumentation werden geprüft',
|
||
docsAiProgressSourcesVerified: 'Offizielle Quellen wurden geprüft',
|
||
docsAiProgressRetrying: 'Die Antwort ist unvollständig; erneuter Versuch',
|
||
docsAiSources: 'Referenzen',
|
||
docsAiAppliesWhen: 'Hinweise',
|
||
docsAiSuggestions: 'Häufige Fragen',
|
||
docsAiSuggestionOne: 'Wie installiere und aktualisiere ich NocoBase?',
|
||
docsAiSuggestionTwo: 'Wie erstelle ich Collections und Seiten?',
|
||
docsAiSuggestionThree: 'Wie konfiguriere ich Rollen und Berechtigungen?',
|
||
docsAiError:
|
||
'Der KI-Dienst ist nicht verfügbar. Nutze die Dokumentationssuche und versuche es später erneut.',
|
||
docsAiRequestId: 'Anfrage-ID',
|
||
docsAiEmptyQuestion: 'Gib zuerst eine Frage ein.',
|
||
},
|
||
fr: {
|
||
docsAiOpen: 'Interroger l’IA',
|
||
docsAiTitle: 'Interroger',
|
||
docsAiClose: 'Fermer l’assistant de documentation',
|
||
docsAiExpand: 'Agrandir l’assistant',
|
||
docsAiCollapse: 'Réduire l’assistant',
|
||
docsAiQuestionPlaceholder: 'Saisissez une question...',
|
||
docsAiSubmit: 'Interroger',
|
||
docsAiQuestionLabel: 'Votre question',
|
||
docsAiProgress: 'Progression',
|
||
docsAiLoading: 'Recherche dans la documentation officielle…',
|
||
docsAiProgressSubmitted:
|
||
`Question envoyée ; recherche limitée à ${siteHost}`,
|
||
docsAiProgressCallingModel: 'Interrogation de DeepSeek',
|
||
docsAiProgressParsingCitations:
|
||
'Réponse reçue ; analyse des références officielles',
|
||
docsAiProgressVerifyingSources:
|
||
'Vérification des pages de documentation officielles',
|
||
docsAiProgressSourcesVerified: 'Sources officielles vérifiées',
|
||
docsAiProgressRetrying: 'La réponse est incomplète ; nouvelle tentative',
|
||
docsAiSources: 'Références',
|
||
docsAiAppliesWhen: 'Remarques',
|
||
docsAiSuggestions: 'Questions fréquentes',
|
||
docsAiSuggestionOne: 'Comment installer et mettre à niveau NocoBase ?',
|
||
docsAiSuggestionTwo: 'Comment créer des collections et des pages ?',
|
||
docsAiSuggestionThree:
|
||
'Comment configurer les rôles et les autorisations ?',
|
||
docsAiError:
|
||
'Le service d’IA est indisponible. Utilisez la recherche dans la documentation et réessayez plus tard.',
|
||
docsAiRequestId: 'ID de la requête',
|
||
docsAiEmptyQuestion: 'Saisissez d’abord une question.',
|
||
},
|
||
ru: {
|
||
docsAiOpen: 'Спросить ИИ',
|
||
docsAiTitle: 'Спросить',
|
||
docsAiClose: 'Закрыть помощника по документации',
|
||
docsAiExpand: 'Развернуть помощника',
|
||
docsAiCollapse: 'Свернуть помощника',
|
||
docsAiQuestionPlaceholder: 'Введите вопрос...',
|
||
docsAiSubmit: 'Спросить',
|
||
docsAiQuestionLabel: 'Ваш вопрос',
|
||
docsAiProgress: 'Ход выполнения',
|
||
docsAiLoading: 'Поиск в официальной документации…',
|
||
docsAiProgressSubmitted:
|
||
`Вопрос отправлен; поиск ограничен сайтом ${siteHost}`,
|
||
docsAiProgressCallingModel: 'Обращение к DeepSeek',
|
||
docsAiProgressParsingCitations:
|
||
'Ответ получен; разбор ссылок на официальную документацию',
|
||
docsAiProgressVerifyingSources: 'Проверка страниц официальной документации',
|
||
docsAiProgressSourcesVerified: 'Официальные источники проверены',
|
||
docsAiProgressRetrying: 'Ответ неполный; повторная попытка',
|
||
docsAiSources: 'Ссылки',
|
||
docsAiAppliesWhen: 'Примечания',
|
||
docsAiSuggestions: 'Частые вопросы',
|
||
docsAiSuggestionOne: 'Как установить и обновить NocoBase?',
|
||
docsAiSuggestionTwo: 'Как создать коллекции и страницы?',
|
||
docsAiSuggestionThree: 'Как настроить роли и разрешения?',
|
||
docsAiError:
|
||
'Сервис ИИ недоступен. Используйте поиск по документации и повторите попытку позже.',
|
||
docsAiRequestId: 'ID запроса',
|
||
docsAiEmptyQuestion: 'Сначала введите вопрос.',
|
||
},
|
||
id: {
|
||
docsAiOpen: 'Tanya AI',
|
||
docsAiTitle: 'Tanya',
|
||
docsAiClose: 'Tutup asisten dokumentasi',
|
||
docsAiExpand: 'Perluas asisten',
|
||
docsAiCollapse: 'Ciutkan asisten',
|
||
docsAiQuestionPlaceholder: 'Masukkan pertanyaan...',
|
||
docsAiSubmit: 'Tanya',
|
||
docsAiQuestionLabel: 'Pertanyaan Anda',
|
||
docsAiProgress: 'Progres',
|
||
docsAiLoading: 'Mencari di dokumentasi resmi…',
|
||
docsAiProgressSubmitted:
|
||
`Pertanyaan dikirim; pencarian dibatasi ke ${siteHost}`,
|
||
docsAiProgressCallingModel: 'Menghubungi DeepSeek',
|
||
docsAiProgressParsingCitations:
|
||
'Jawaban diterima; mengurai referensi dokumentasi resmi',
|
||
docsAiProgressVerifyingSources: 'Memverifikasi halaman dokumentasi resmi',
|
||
docsAiProgressSourcesVerified:
|
||
'Sumber dokumentasi resmi telah diverifikasi',
|
||
docsAiProgressRetrying: 'Jawaban belum lengkap; mencoba lagi',
|
||
docsAiSources: 'Referensi',
|
||
docsAiAppliesWhen: 'Catatan',
|
||
docsAiSuggestions: 'Pertanyaan umum',
|
||
docsAiSuggestionOne: 'Bagaimana cara memasang dan meningkatkan NocoBase?',
|
||
docsAiSuggestionTwo: 'Bagaimana cara membuat koleksi dan halaman?',
|
||
docsAiSuggestionThree: 'Bagaimana cara mengatur peran dan izin?',
|
||
docsAiError:
|
||
'Layanan AI tidak tersedia. Gunakan pencarian dokumentasi dan coba lagi nanti.',
|
||
docsAiRequestId: 'ID permintaan',
|
||
docsAiEmptyQuestion: 'Masukkan pertanyaan terlebih dahulu.',
|
||
},
|
||
vi: {
|
||
docsAiOpen: 'Hỏi AI',
|
||
docsAiTitle: 'Hỏi',
|
||
docsAiClose: 'Đóng trợ lý tài liệu',
|
||
docsAiExpand: 'Mở rộng trợ lý',
|
||
docsAiCollapse: 'Thu gọn trợ lý',
|
||
docsAiQuestionPlaceholder: 'Nhập câu hỏi...',
|
||
docsAiSubmit: 'Hỏi',
|
||
docsAiQuestionLabel: 'Câu hỏi của bạn',
|
||
docsAiProgress: 'Tiến trình',
|
||
docsAiLoading: 'Đang tìm kiếm trong tài liệu chính thức…',
|
||
docsAiProgressSubmitted:
|
||
`Đã gửi câu hỏi; chỉ tìm kiếm trên ${siteHost}`,
|
||
docsAiProgressCallingModel: 'Đang gọi DeepSeek',
|
||
docsAiProgressParsingCitations:
|
||
'Đã nhận câu trả lời; đang phân tích nguồn tài liệu chính thức',
|
||
docsAiProgressVerifyingSources:
|
||
'Đang xác minh các trang tài liệu chính thức',
|
||
docsAiProgressSourcesVerified: 'Đã xác minh các nguồn tài liệu chính thức',
|
||
docsAiProgressRetrying: 'Câu trả lời chưa đầy đủ; đang thử lại',
|
||
docsAiSources: 'Tài liệu tham khảo',
|
||
docsAiAppliesWhen: 'Ghi chú',
|
||
docsAiSuggestions: 'Câu hỏi thường gặp',
|
||
docsAiSuggestionOne: 'Làm cách nào để cài đặt và nâng cấp NocoBase?',
|
||
docsAiSuggestionTwo: 'Làm cách nào để tạo collection và trang?',
|
||
docsAiSuggestionThree: 'Làm cách nào để cấu hình vai trò và quyền?',
|
||
docsAiError:
|
||
'Dịch vụ AI hiện không khả dụng. Hãy dùng chức năng tìm kiếm tài liệu và thử lại sau.',
|
||
docsAiRequestId: 'ID yêu cầu',
|
||
docsAiEmptyQuestion: 'Hãy nhập câu hỏi trước.',
|
||
},
|
||
};
|
||
|
||
const docsAiI18nSource = Object.fromEntries(
|
||
Object.entries(docsAiMessages.en).map(([key]) => [
|
||
key,
|
||
Object.fromEntries(
|
||
Object.entries(docsAiMessages).map(([language, messages]) => [
|
||
language,
|
||
messages[key],
|
||
]),
|
||
),
|
||
]),
|
||
);
|
||
|
||
function withRspressI18nAliases(
|
||
source: Record<string, Record<string, string>>,
|
||
) {
|
||
return Object.fromEntries(
|
||
Object.entries({
|
||
...source,
|
||
...searchI18nSource,
|
||
...docsAiI18nSource,
|
||
}).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 and robots.txt after build
|
||
async afterBuild(config: any, isProd: boolean) {
|
||
if (!isProd) {
|
||
return;
|
||
}
|
||
|
||
if (lang !== 'en') {
|
||
return;
|
||
}
|
||
|
||
const baseDomain = siteOrigin;
|
||
|
||
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');
|
||
|
||
const robotsTxt = [
|
||
'User-agent: *',
|
||
'Allow: /',
|
||
'',
|
||
`Sitemap: ${baseDomain}/sitemap.xml`,
|
||
'',
|
||
].join('\n');
|
||
await fs.writeFile(path.join(outDir, 'robots.txt'), robotsTxt, '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="${siteOrigin}/${lang}${canonicalRoute}" />`;
|
||
}
|
||
return `<link rel="canonical" href="${siteOrigin}${canonicalRoute}" />`;
|
||
},
|
||
(route) => {
|
||
const links = [];
|
||
links.push(
|
||
...indexLanguages.map((language) => {
|
||
if (language === 'en') {
|
||
return `<link rel="alternate" hreflang="en-US" href="${siteOrigin}${route.routePath}" />`;
|
||
}
|
||
const hreflang = langMap[language as keyof typeof langMap];
|
||
return `<link rel="alternate" hreflang="${hreflang}" href="${siteOrigin}/${language}${route.routePath}" />`;
|
||
}),
|
||
);
|
||
links.push(
|
||
`<link rel="alternate" hreflang="x-default" href="${siteOrigin}${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'),
|
||
define: {
|
||
'import.meta.env.DOCS_AI_API_URL': JSON.stringify(docsAiApiUrl),
|
||
'import.meta.env.DOCS_SITE_ORIGIN': JSON.stringify(siteOrigin),
|
||
},
|
||
},
|
||
plugins: [pluginSass(), pluginNodePolyfill()],
|
||
resolve: {
|
||
aliasStrategy: 'prefer-tsconfig',
|
||
},
|
||
tools: {
|
||
rspack(config) {
|
||
config.module.rules.push({
|
||
test: /zxing_reader\.wasm$/,
|
||
type: 'asset/resource',
|
||
});
|
||
},
|
||
},
|
||
},
|
||
markdown: {
|
||
link: {
|
||
checkDeadLinks,
|
||
},
|
||
},
|
||
// Rspress ships built-in Simplified Chinese strings under `zh`, while this site uses `/cn/`.
|
||
i18nSource: (value) => withRspressI18nAliases(value),
|
||
plugins: [
|
||
pluginPreview(),
|
||
// The llms.txt markdown files are generated by a separate remark pipeline, so the link rewrite is applied there too.
|
||
pluginLlms({
|
||
mdFiles: { mdxToMd: false, remarkPlugins: [remarkSiteOriginLinks] },
|
||
}),
|
||
pluginSchema(),
|
||
pluginOgDescription(),
|
||
pluginRemoveGenerator(),
|
||
pluginCrossRefSidebar(),
|
||
pluginSearchSections(),
|
||
pluginSearchIndex(),
|
||
pluginSiteOriginLinks(),
|
||
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',
|
||
},
|
||
],
|
||
},
|
||
});
|