- 后端
- 前端
- 云原生
【免费下载链接】hedgedoc
HedgeDoc - Ideas grow better together
HedgeDoc 内置了经典的MotD(Message of the Day,每日消息)功能,用于在用户打开实例时展示一条可定制的公告。本指南以仓库中 frontend/public/public/motd.md(与 backend/public/motd.md 内容一致)为核心入口,完整讲解 MotD 文件的默认内容、后端静态服务的提供方式、前端获取与缓存去重逻辑、Markdown 渲染管线,以及覆盖该功能的端到端测试,让读者既能直接上手自定义公告,也能理解其底层工作原理。
一、MotD 文件:默认内容与文件定位
motd.md是 HedgeDoc 中 MotD 消息的载体文件,当前仓库中前后端各保留了一份完全相同的副本,默认内容只有两行:
This is the test motd text :smile:- 第一行是一段普通的纯文本公告内容,语义上等价于“这是一条测试用 MotD 文本”;
- 第二行的
:smile:是 GitHub 风格的 emoji 短码,表明 MotD 正文以完整的 Markdown 语法解析,emoji 短码会被渲染为对应的表情符号。
两份文件分别位于:
- 后端静态资源目录:backend/public/motd.md
- 前端公共资源目录:frontend/public/public/motd.md
从源码结构看,真正对外提供服务的版本是后端目录中的那份:后端通过 Fastify 静态文件插件把public文件夹整体映射到/public/URL 前缀(详见下文),而前端目录中的副本主要用于 Next.js 构建期的本地化场景。默认内容即为测试文本,实例管理员在实际部署时应将其替换为自己的公告内容。
二、后端如何提供 motd.md:静态文件服务挂载
HedgeDoc 后端(NestJS + Fastify)在应用启动装配阶段注册静态文件服务,相关实现位于 backend/src/app-init.ts:
logger.log(`Serving the local folder 'public' under '/public'`, 'AppBootstrap'); const path = await import('path'); await app.register(import('@fastify/static'), { root: path.resolve('public'), prefix: '/public/', decorateReply: false, });关键配置点:
root:指向后端工作目录下的public文件夹,motd.md即位于该目录内;prefix:/public/,因此motd.md的最终访问 URL 为http(s)://<实例地址>/public/motd.md;decorateReply: false:避免 Fastify 静态插件覆盖 reply 对象上的默认装饰。
代码注释中还保留了开发团队的说明:目前public文件夹的主要使用场景就是intro.md与motd.md两个文件,未来也可能考虑将其改为 API 端点(见 backend/src/app-init.ts 的 TODO 注释)。
得益于@fastify/static对静态文件的处理,浏览器请求该 URL 时,响应头会自动携带Last-Modified(或etag)标识,这为前端判断“MotD 是否更新过”提供了依据。
三、前端获取 MotD:fetch-motd 与变更检测
前端通过 frontend/src/components/global-dialogs/motd-modal/fetch-motd.ts 完成 MotD 的拉取。其核心逻辑如下:
export interface MotdApiResponse { motdText: string lastModified: string } export const fetchMotd = async (baseUrl: string): Promise<MotdApiResponse | undefined> => { if (isBuildTime) { return } const motdUrl = `${baseUrl}public/motd.md` const response = await fetch(motdUrl, { ...defaultConfig, cache: undefined, next: { revalidate: 60 } }) if (response.status !== 200) { return } const lastModified = response.headers.get('Last-Modified') || response.headers.get('etag') if (lastModified === null) { return } return { lastModified, motdText: await response.text() } }可以提炼出如下要点:
- 请求地址:
${baseUrl}public/motd.md,baseUrl为后端实例地址,与后端/public/前缀一一对应; - 构建期短路:
isBuildTime为真时直接返回undefined,避免在静态构建阶段发起网络请求; - 缓存策略:显式设置
cache: undefined并配合next: { revalidate: 60 }(Next.js 增量静态再验证,60 秒),兼顾实时性与缓存开销; - 变更标识:从响应头中优先读取
Last-Modified,缺失时回退到etag;两者皆无则不返回数据; - 响应契约:最终返回
{ motdText, lastModified },其中motdText为原始 Markdown 文本,lastModified供后续去重判断使用。
获取结果通过 frontend/src/components/motd/motd-context.tsx 中定义的MotdProvider与useMotdContextValue注入全局 React Context,供模态框与 About 页面共享。
四、展示与缓存去重:CachedMotdModal 的工作机制
MotD 弹窗的“只在内容变化时展示一次”行为由 frontend/src/components/global-dialogs/motd-modal/cached-motd-modal.tsx 实现,其关键逻辑:
const [cachedLastModified, saveLocalStorage] = useLocalStorage<string>(MOTD_LOCAL_STORAGE_KEY, undefined, { raw: true }) const show = useMemo(() => { const lastModified = contextValue?.lastModified if (cachedLastModified === IGNORE_MOTD && isTestMode) { return false } if (cachedLastModified === lastModified || lastModified === undefined) { return false } return !dismissed }, [cachedLastModified, contextValue?.lastModified, dismissed])判断流程:
- 若
localStorage中缓存的上次修改标识与当前lastModified相同,说明 MotD 未更新,弹窗不展示; - 若后端未返回
lastModified,同样不展示; - 测试模式下可通过特殊值
IGNORE_MOTD强制屏蔽弹窗。
用户点击“Dismiss(关闭)”后,doDismiss会将当前lastModified写入 localStorage,此后即使刷新页面也不再弹窗,直到管理员修改了motd.md导致Last-Modified变化。相关存储键定义在 frontend/src/components/global-dialogs/motd-modal/local-storage-keys.ts:
export const MOTD_LOCAL_STORAGE_KEY: string = 'motd.lastModified' export const IGNORE_MOTD: string = 'IGNORE_MOTD'弹窗本体由 frontend/src/components/global-dialogs/motd-modal/motd-modal.tsx 渲染:使用CommonModal承载,标题引用 i18n 键motd.title,正文区包裹EditorToRendererCommunicatorContextProvider后渲染MotdContent,底部提供common.dismiss翻译键对应的成功样式按钮;同时只有motdText非空时才真正显示(show && (contextValue?.motdText.length ?? 0) > 0)。
五、Markdown 渲染:MotdContent 与 RendererIframe
MotD 之所以支持 Markdown 与 emoji 短码,是因为它复用了 HedgeDoc 的渲染器。核心组件 frontend/src/components/motd/motd-content.tsx 的实现:
const lines = useMemo(() => { const rawLines = contextValue?.motdText.split('\n') if (rawLines === undefined || rawLines.length === 0) { return [] } return rawLines }, [contextValue?.motdText]) return ( <RendererIframe frameClasses={'w-100'} rendererType={RendererType.SIMPLE} markdownContentLines={lines} adaptFrameHeightToContent={true} showWaitSpinner={true} /> )要点:
- 将
motdText按\n拆分为行数组传入RendererIframe; rendererType使用RendererType.SIMPLE,即简化渲染模式;adaptFrameHeightToContent={true}让 iframe 高度自适应内容,避免出现滚动条;- 渲染在 iframe 中完成,正文区域加载时显示等待指示器。
这意味着用户可以在motd.md中自由使用标题、列表、链接、粗体等 Markdown 语法,包括:smile:这类 emoji 短码,其渲染能力与普通笔记保持一致。此外,frontend/src/components/about-page/motd-card.tsx 在“关于(About)”页面中以卡片形式复用了同一MotdContent,让管理员和用户在不打开弹窗时也能查看当前公告。
六、测试验证:从单元测试到端到端
MotD 功能拥有完整的测试覆盖,可作为理解其行为契约的权威参考:
端到端测试frontend/cypress/e2e/motd.spec.ts 验证了完整用户旅程:
const motdMockHtml = 'This is the test motd text' describe('Motd', () => { it("shows, dismisses and won't show again a motd modal", () => { window.localStorage.removeItem(MOTD_LOCAL_STORAGE_KEY) cy.visitHistory() cy.getSimpleRendererBody().should('contain.text', motdMockHtml) cy.getByCypressId('motd-dismiss').click() cy.getByCypressId('motd-modal').should('not.exist') cy.reload() cy.get('main').should('exist') cy.getByCypressId('motd-modal').should('not.exist') }) })该用例完整覆盖了“首次访问显示弹窗 → 点击关闭 → 弹窗消失 → 刷新后不再显示”的完整流程,与CachedMotdModal中基于lastModified的去重逻辑相互印证。测试断言正文中包含This is the test motd text,与默认motd.md的测试文本完全对应。
单元测试frontend/src/components/global-dialogs/motd-modal/fetch-motd.spec.ts 则针对请求地址拼接、响应状态码判断、Last-Modified/etag读取等分支进行验证。
七、运维实践:如何自定义你的 MotD
综合以上机制,实例管理员自定义公告的操作路径非常清晰:
- 修改文件:编辑后端目录下的 backend/public/motd.md,替换默认的测试文本;支持完整的 Markdown 语法与 emoji 短码;
- 重新部署:由于静态文件由后端进程直接读取,修改后需要重新构建/重启后端容器(参考 backend/docker/Dockerfile 与根目录 README.md 中的部署说明),或根据部署方式挂载该文件为外部卷;
- 生效与去重:用户端会在请求时通过响应头
Last-Modified/etag感知到文件变更,即使老用户已关闭过旧公告,也会因为标识变化而再次看到新内容; - 测试模式:前端测试环境下可通过 localStorage 特殊值
IGNORE_MOTD屏蔽弹窗,不影响开发调试。
结语
HedgeDoc 的 MotD 功能虽然入口文件只有寥寥两行,但其背后是一套完整的“静态文件服务 → 前端拉取 → Context 分发 → localStorage 去重 → iframe Markdown 渲染”链路。理解 motd.md 与 backend/src/app-init.ts、fetch-motd.ts、cached-motd-modal.tsx 等实现之间的协作关系,既能帮助管理员快速定制公告,也能为二次开发提供清晰的切入点。
- 后端
- 前端
- 云原生
【免费下载链接】hedgedoc
HedgeDoc - Ideas grow better together
相关推荐
Ceph Dashboard MOTD(Message of the Day)插件:配置、过期机制与前端展示完全指南
Ceph Dashboard MOTD(Message of the Day)插件:配置、过期机制与前端展示完全指南 导读 Ceph Dashboard 的 M
存储分布式文件系统对象存储后端高可用Instructor 原生缓存机制全解析:从 `AutoCache` 到自定义缓存后端的零配置性能优化
Instructor 原生缓存机制全解析:从 AutoCache 到自定义缓存后端的零配置性能优化 Instructor 从 v1.9.1 起内置了覆盖所有 P
人工智能大模型AI 应用HedgeDoc 前端 Changelog 解读:从 HedgeDoc 1 到 2 的功能演进、弃用与迁移指南
HedgeDoc 前端 Changelog 解读:从 HedgeDoc 1 到 2 的功能演进、弃用与迁移指南 本指南以 frontend/CHANGELOG.
后端前端云原生
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考