Docs 定制化完全指南:运行时主题、脚本注入与主题定制文件实战解析
【免费下载链接】docsDocs is an open-source text editor: web-native, made for real-time collaboration, cleanly structured documents and sub-documents with full ownership of your data. Built to scale with Django and React.项目地址: https://gitcode.com/GitHub_Trending/docs150/docs
Docs 作为一款基于 Django + React 构建的开源协同文档编辑器,为部署者提供了三层渐进式的定制能力:通过FRONTEND_CSS_URL/FRONTEND_JS_URL实现零代码的运行时主题与行为注入,通过THEME_CUSTOMIZATION_FILE_PATH指向的 JSON 主题定制文件实现图标、页脚、翻译、Waffle 服务网格等品牌化配置。本文将以官方定制指南 documentation/customization.md 为骨架,结合后端settings.py、配置接口ConfigView与前端ConfigProvider的源码实现,给出完整可落地的配置方案与参数说明。
读完本文,你将掌握:如何在不停机、不重新构建的前提下为 Docs 更换品牌皮肤与注入自定义脚本,如何通过一份 JSON 文件统一配置站点图标、多语言页脚、界面文案翻译与 Waffle 服务网格,并理解这些配置从前端请求到后端加载的完整链路。
一、定制化能力全景与工作机制
Docs 的定制化体系可以分为两条主线:
- 运行时注入(Runtime Injection):通过
FRONTEND_CSS_URL与FRONTEND_JS_URL两个环境变量,指向自托管的 CSS / JS 文件 URL。前端应用启动后会以<link rel="stylesheet">和<script>标签的方式动态加载它们,无需任何代码改动或重新编译,适合快速换肤、注入第三方分析脚本、挂载自定义菜单等场景。 - 主题定制文件(Theme Customization File):通过
THEME_CUSTOMIZATION_FILE_PATH指向一份 JSON 文件,集中配置header.icon(头部图标)、footer(页脚)、translations(翻译覆盖)、waffle(服务网格)、favicon、home、help、onboarding等结构化选项,由后端在启动/运行时读取并提供给前端。
两条主线的配置最终都会汇聚到同一个入口:前端启动时请求GET /api/v1.0/config/获取公共配置,再交由ConfigProvider分发消费。这也是理解 Docs 定制化架构的关键:配置值在服务端定义,经配置 API 下发,由前端运行时应用,因此绝大部分定制都不需要重启前端或重新构建镜像。
二、运行时主题定制:FRONTEND_CSS_URL
2.1 配置方法
在 Docs 后端运行环境中设置环境变量,将其指向一个可公开访问的 CSS 文件地址:
FRONTEND_CSS_URL=http://anything/custom-style.css设置后,Docs 前端会在页面<head>中注入<link rel="stylesheet" href="...">加载该样式表并应用到整个前端应用。从源码看,该变量在 settings.py 中被定义为可空配置项:
FRONTEND_CSS_URL = values.Value( None, environ_name="FRONTEND_CSS_URL", environ_prefix=None )未设置时该值为None,前端不会注入任何外部样式表,行为与默认一致。
2.2 前端如何加载
在 ConfigProvider.tsx 中,前端通过 Next.js 的Head组件注入样式链接:
{conf?.FRONTEND_CSS_URL && ( <Head> <link rel="stylesheet" href={conf?.FRONTEND_CSS_URL} /> </Head> )}也就是说,只要配置接口返回的FRONTEND_CSS_URL非空,样式表就会被插入页面头部,并按照 CSS 层叠规则覆盖默认样式。该配置字段同样出现在ConfigResponse类型定义中(见 useConfig.tsx)。
2.3 典型用例:自定义背景色
假设你想将整个应用的背景色替换为品牌色,可以创建一份自定义 CSS 文件并托管到任意静态服务器:
body { background-color: #3498db; }然后设置FRONTEND_CSS_URL指向该文件。应用加载后,背景色即被覆盖为#3498db。由于是运行时加载,你可以随时替换 CSS 内容并在刷新后生效,无需重启任何服务。
2.4 优势与适用边界
- 零代码定制:不需要修改或重新构建前端仓库;
- 灵活换肤:可用任意 CSS 规则打造符合组织品牌的主题;
- 运行时生效:修改 CSS 文件内容即可热切换主题,无需重启或重新编译。
需要注意的是,运行时 CSS 覆盖的是"样式层"而非"组件层":它能改变颜色、尺寸、布局等外观,但如果要调整 DOM 结构或交互行为,则需要配合下一节的 JavaScript 注入。
三、运行时 JavaScript 注入:FRONTEND_JS_URL
3.1 配置方法
与 CSS 类似,设置环境变量指向一个可公开访问的 JS 文件:
FRONTEND_JS_URL=http://anything/custom-script.jsDocs 前端会用 Next.js 的Script组件以afterInteractive策略加载该脚本(见 ConfigProvider.tsx):
{conf?.FRONTEND_JS_URL && ( <Script src={conf?.FRONTEND_JS_URL} strategy="afterInteractive" /> )}afterInteractive表示脚本在页面完成水合(hydration)后执行,适合注入交互增强类逻辑。对应后端配置定义见 settings.py。
3.2 典型用例:向头部注入自定义菜单
官方文档给出的示例脚本会在应用头部追加一个自定义菜单按钮。脚本需要等待 DOM 就绪后再操作,确保目标元素存在:
(function() { 'use strict'; function initCustomMenu() { // Wait for the page to be fully loaded const header = document.querySelector('header'); if (!header) return false; // Create and inject your custom menu const customMenu = document.createElement('div'); customMenu.innerHTML = '<button>Custom Menu</button>'; header.appendChild(customMenu); console.log('Custom menu added successfully'); return true; } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initCustomMenu); } else { initCustomMenu(); } })();设置FRONTEND_JS_URL指向该文件后,刷新页面即可在头部看到自定义菜单。同样的思路可以用于接入第三方埋点、注入辅助工具条、修改现有功能行为等,覆盖"样式无法触及"的动态定制需求。
3.3 优势与边界
- 动态定制:无需代码改动即可修改行为与外观;
- 高度灵活:可新增功能、改造既有特性或集成第三方服务;
- 运行时注入:无需重启或重新编译。
边界提醒:注入脚本在浏览器端执行,属于"运行时增强"而非"后端逻辑修改",涉及数据面或权限面的功能仍需在 Django 后端实现;同时该脚本应具备幂等性,避免重复加载导致重复插入 DOM。
四、主题定制文件核心机制:THEME_CUSTOMIZATION_FILE_PATH
图标、页脚、翻译与 Waffle 配置都依托于同一套机制:主题定制文件(Theme Customization File)。这是理解 Docs 品牌化定制的钥匙。
4.1 环境变量与默认值
THEME_CUSTOMIZATION_FILE_PATH=<path>后端定义见 settings.py:
THEME_CUSTOMIZATION_FILE_PATH = values.Value( os.path.join(BASE_DIR, "impress/configuration/theme/default.json"), environ_name="THEME_CUSTOMIZATION_FILE_PATH", environ_prefix=None, ) THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.IntegerValue( 60 * 60 * 24, environ_name="THEME_CUSTOMIZATION_CACHE_TIMEOUT", environ_prefix=None, )要点:
- 默认路径:
BASE_DIR/impress/configuration/theme/default.json,即仓库中的 src/backend/impress/configuration/theme/default.json。该文件是 Docs 出厂默认的主题定制内容。 - 缓存超时:默认
86400秒(1 天)。主题定制 JSON 会被缓存,修改文件后最长可能需要一个缓存周期才会被重新读取(或清空缓存后立即生效)。 - 若将该变量设为空字符串,后端会返回空的主题定制对象
{}(见 viewsets.py)。
4.2 后端加载链路与缓存策略
主题定制文件由ConfigView._load_theme_customization()加载(见 viewsets.py),完整流程如下:
- 若
THEME_CUSTOMIZATION_FILE_PATH未设置,直接返回{}; - 以文件路径的 slug 生成缓存键
theme_customization_<slug>,先查 Django 缓存; - 缓存未命中则用
utf-8编码读取 JSON 文件并json.load解析; - 若文件不存在(
FileNotFoundError)或 JSON 非法(JSONDecodeError),记录 error 日志并返回{},不会导致服务崩溃; - 解析成功后写入缓存,缓存时长为
THEME_CUSTOMIZATION_CACHE_TIMEOUT。
这套容错设计意味着:一个损坏或缺失的定制文件只会让定制项失效为默认值,而不会拖垮整个配置接口,相关行为也被后端测试覆盖(见 test_api_config.py 中针对不存在文件与非法 JSON 的用例)。
4.3 配置下发:GET /api/v1.0/config/
ConfigView是一个AllowAny权限、带config节流作用域的公开接口(见 viewsets.py)。它把FRONTEND_CSS_URL、FRONTEND_JS_URL、FRONTEND_THEME、LANGUAGES等公共设置打包返回,并附上theme_customization(即主题定制文件解析结果)与RELEASE_VERSION:
dict_settings["theme_customization"] = self._load_theme_customization() dict_settings["RELEASE_VERSION"] = settings.RELEASE return drf.response.Response(dict_settings)前端通过 getConfig 请求config/并缓存到localStorage(键docs_config),随后useConfig以 5 分钟 staleTime 的 TanStack Query 持有该配置。前端ThemeCustomization接口(见 useConfig.tsx)声明了以下可用键:
| 键 | 类型 | 用途 |
|---|---|---|
favicon | { light, dark } | 浅色/深色模式站点图标(<link rel="icon">属性) |
footer | FooterType | 页脚配置(支持多语言 +default兜底) |
header | HeaderType | 头部图标/Logo 配置 |
help | 对象 | 帮助文档 URL、支持邮箱、法务链接 |
home | 对象 | 首页配置(如with-proconnect、icon-banner) |
onboarding | 对象 | 新手引导开关与链接 |
translations | Resource | i18next 翻译资源覆盖 |
waffle | LaGaufreV2Props | Waffle(La Gaufre)服务网格 |
ConfigProvider在拿到配置后分别处理:应用翻译覆盖、设置主题、注入 CSS/JS、设置 Sentry/PostHog、处理 favicon 与版本刷新逻辑(见 ConfigProvider.tsx)。
五、自定义 Docs 头部图标:header.icon
头部图标可以从主题定制文件中配置。以仓库自带的开发环境示例 src/helm/env.d/dev/configuration/theme/demo.json 为例:
{ "header": { "logo": {}, "icon": { "src": "/assets/icon-docs.svg", "style": { "width": "32px", "height": "auto" }, "alt": "", "withTitle": true } } }字段说明:
src:图标资源路径,可指向仓库静态资源(如/assets/icon-docs.svg)或任意可访问的 URL;style:行内样式,控制图标的渲染尺寸(如width/height);alt:无障碍替代文本;withTitle:是否连同产品标题一起展示;header.logo:Logo 配置,示例中为空对象表示不启用。
该配置是可选的——如果主题定制文件中没有header.icon,前端将回退到默认图标。仓库中的默认文件 src/backend/impress/configuration/theme/default.json 使用的即是/assets/icon-docs.svg(宽高 40px)。
六、页脚配置:footer
页脚同样来自主题定制文件,且支持按语言分发的完整配置结构。
6.1 配置结构
{ "footer": { "default": { "logo": { "src": "/assets/icon-docs.svg", "width": "54px", "alt": "Docs Logo", "withTitle": true }, "externalLinks": [ { "label": "GitHub", "href": "https://github.com/suitenumerique/docs/" }, { "label": "DINUM", "href": "https://www.numerique.gouv.fr/dinum/" }, { "label": "ZenDiS", "href": "https://zendis.de/" }, { "label": "BlockNote.js", "href": "https://www.blocknotejs.org/" } ], "bottomInformation": { "label": "Unless otherwise stated, all content on this site is under", "link": { "label": "licence etalab-2.0", "href": "https://github.com/etalab/licence-ouverte/blob/master/LO.md" } } }, "en": { "legalLinks": [ { "label": "Legal Notice", "href": "#" }, { "label": "Personal data and cookies", "href": "#" }, { "label": "Accessibility", "href": "#" } ], "bottomInformation": { "label": "Unless otherwise stated, all content on this site is under", "link": { "label": "licence MIT", "href": "https://github.com/suitenumerique/docs/blob/main/LICENSE" } } }, "fr": { "...": "同结构,法语文案" }, "de": { "...": "同结构,德语文案" }, "nl": { "...": "同结构,荷兰语文案" } } }6.2 关键规则
footer.default是语言不匹配时的兜底配置:当用户当前语言在footer中没有对应键(如en/fr/de/nl)时,前端会回退到footer.default,保证任何语言环境下页脚都不会空白;legalLinks:法务链接组(法律声明、隐私与 Cookie、无障碍声明等),可在各语言块内独立配置;externalLinks:页脚展示的外部链接列表,label+href成对出现;bottomInformation:页脚底部信息,支持label文本 + 内嵌link(文本与地址)。
官方文档同时给出了配置后的视觉效果示例(见 documentation/assets/footer-configurable.png),配置正确后页脚会按所选语言展示对应的法律链接与版权信息。
七、自定义翻译:translations
Docs 的界面文案可以部分覆盖——即基于现有翻译资源,仅对需要改动的键做增量覆盖,而不必维护整份语言文件。
7.1 配置方法
在主题定制文件中加入translations键,结构遵循 i18next 资源格式:
{ "translations": { "en": { "translation": { "Docs": "MyDocs", "New doc": "+" } } } }- 顶层键是语言代码(如
en、fr、zh-CN等); - 语言下再套一层
translation,内部是原始文案键 → 自定义文案值的映射; - 未覆盖的键继续使用内置翻译,因此这是一种部分覆盖机制。
7.2 前端如何应用
在 ConfigProvider.tsx 中,配置返回后调用customizeTranslations()将覆盖资源合并进当前 i18next 实例:
useEffect(() => { if (!conf?.theme_customization?.translations) { return; } customizeTranslations(conf.theme_customization.translations); }, [conf?.theme_customization?.translations, customizeTranslations]);该逻辑位于features/language模块,与 Docs 的同步语言机制配合:当用户语言确定后,覆盖翻译会即时生效,无需刷新页面。
7.3 实战提示
- 文案键必须与前端实际使用的 i18n key 完全一致(包括大小写与空格),例如示例中的
"Docs"、"New doc",可通过源码中的useTranslation()调用点确认键名; - 由于翻译是前端 i18next 资源,覆盖范围受限于前端已声明的键,无法凭空新增翻译命名空间;
- 多语言场景下,为每个语言代码各写一份
translation块即可实现按语言的品牌化文案。
八、Waffle(La Gaufre)配置:waffle
Waffle(法语俗称 "La Gaufre",即"华夫饼")是 Docs 中展示**服务网格(services grid)**的小部件,典型用于把同一组织内的多个服务入口聚合到一个下拉/弹层网格中,方便用户在不同服务间跳转。
8.1 配置方法
在主题定制文件中使用waffle键,结构对齐前端 UI 组件库的LaGaufreV2Props。参考仓库示例 src/helm/env.d/dev/configuration/theme/demo.json:
{ "waffle": { "data": { "services": [ { "name": "Docs", "url": "https://docs.numerique.gouv.fr/", "maturity": "stable", "logo": "https://lasuite.numerique.gouv.fr/assets/products/docs.svg" }, { "name": "Visio", "url": "https://visio.numerique.gouv.fr/", "maturity": "stable", "logo": "https://lasuite.numerique.gouv.fr/assets/products/visio.svg" }, { "name": "Fichiers", "url": "https://fichiers.numerique.gouv.fr/", "maturity": "stable", "logo": "https://lasuite.numerique.gouv.fr/assets/products/fichiers.svg" } ] }, "showMoreLimit": 9 } }8.2 可用属性(LaGaufreV2Props)
waffle的值直接对应前端 UI 组件库LaGaufreV2的 props(可在前端依赖@gouvfr-lasuite/ui-components的LaGaufreV2.tsx中查看完整定义),常见属性包括:
| 属性 | 说明 |
|---|---|
data.services | 静态服务列表,每个服务包含name、url、maturity、logo等字段 |
showMoreLimit | 网格中直接展示的服务数量上限,超出部分收进"更多"折叠区(示例为 9) |
apiUrl | 动态获取服务的数据接口地址 |
8.3 行为规则
官方文档明确了 Waffle 的两种数据来源行为:
- 静态模式:如果提供了
data.services,Waffle 直接展示这些服务,不发起额外请求; - 动态模式:如果不提供
data,可以通过apiUrl属性从后端 API 端点动态拉取服务列表。
按需二选一:内部服务清单固定时用静态配置(如上例),服务由中台系统统一管理、需要实时变更时用apiUrl动态获取。
九、完整示例与实战清单
9.1 开箱即用的演示配置
仓库为 Helm 开发环境提供了完整的主题定制示例 src/helm/env.d/dev/configuration/theme/demo.json,它一次性覆盖了本文讲到的全部能力:
translations:把 "Docs" 文案替换为 "MyDocs"、把 "New doc" 替换为 "+";footer:default+en/fr/de/nl四语言页脚;header.icon:自定义头部图标;waffle:静态展示 Docs / Visio / Fichiers 三个服务;- 额外还包含
home(首页 ProConnect 开关与横幅图标)与favicon(浅色/深色图标)。
9.2 上线前的检查清单
- 文件可达:确认
THEME_CUSTOMIZATION_FILE_PATH指向的文件在容器/后端进程内真实存在,且可被读取(建议先手动执行python -c "import json;json.load(open('<path>'))"验证 JSON 合法性); - 缓存预期:主题定制 JSON 默认缓存 1 天(
THEME_CUSTOMIZATION_CACHE_TIMEOUT),上线后想立即生效可调小该值或清空缓存; - 配置接口验证:部署后请求
GET /api/v1.0/config/,检查theme_customization是否按预期返回; - 资源可访问:
FRONTEND_CSS_URL、FRONTEND_JS_URL以及图标/Logo 引用的资源地址必须对浏览器端可访问(注意 CSP、跨域与鉴权策略); - 回退安全:任何一项配置异常(文件缺失、JSON 非法、字段缺失)都会优雅回退到默认值,不会阻断应用启动——这由后端
_load_theme_customization的容错分支保证; - 版本一致性:
ConfigProvider会比对前后端RELEASE_VERSION,不一致时自动触发一次刷新,确保定制配置与新版本代码匹配。
9.3 配置链路速查
环境变量(FRONTEND_CSS_URL / FRONTEND_JS_URL / THEME_CUSTOMIZATION_FILE_PATH ...) │ ▼ Django settings.py 定义与解析(src/backend/impress/settings.py) │ ▼ GET /api/v1.0/config/ → ConfigView 组装公共设置 + 加载主题定制 JSON(src/backend/core/api/viewsets.py#L3086-L3167,带缓存) │ ▼ 前端 useConfig 拉取并缓存到 localStorage(src/frontend/apps/impress/src/core/config/api/useConfig.tsx) │ ▼ ConfigProvider 分发:注入 CSS/JS、应用翻译、设置主题/favicon、初始化 Analytics(src/frontend/apps/impress/src/core/config/ConfigProvider.tsx)通过这条链路,Docs 将"品牌化定制"从代码层剥离到了配置层:无论是部署在裸机、Docker Compose 还是 Kubernetes/Helm(对应模板见 src/helm/impress/templates),只需调整环境变量与主题定制 JSON,即可在不改动一行业务代码的前提下完成整套视觉与行为定制。
【免费下载链接】docsDocs is an open-source text editor: web-native, made for real-time collaboration, cleanly structured documents and sub-documents with full ownership of your data. Built to scale with Django and React.项目地址: https://gitcode.com/GitHub_Trending/docs150/docs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考