☰
HedgeDoc MotD 功能全解析:从 motd.md 配置到前端展示与缓存机制
2026/9/28 2:46:17 网站建设 项目流程
  • 后端
  • 前端
  • 云原生

【免费下载链接】hedgedoc

HedgeDoc - Ideas grow better together

项目地址:https://gitcode.com/gh_mirrors/he/hedgedoc
点击查看免费下载

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() } }

可以提炼出如下要点:

  1. 请求地址:${baseUrl}public/motd.md,baseUrl为后端实例地址,与后端/public/前缀一一对应;
  2. 构建期短路:isBuildTime为真时直接返回undefined,避免在静态构建阶段发起网络请求;
  3. 缓存策略:显式设置cache: undefined并配合next: { revalidate: 60 }(Next.js 增量静态再验证,60 秒),兼顾实时性与缓存开销;
  4. 变更标识:从响应头中优先读取Last-Modified,缺失时回退到etag;两者皆无则不返回数据;
  5. 响应契约:最终返回{ 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])

判断流程:

  1. 若localStorage中缓存的上次修改标识与当前lastModified相同,说明 MotD 未更新,弹窗不展示;
  2. 若后端未返回lastModified,同样不展示;
  3. 测试模式下可通过特殊值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

综合以上机制,实例管理员自定义公告的操作路径非常清晰:

  1. 修改文件:编辑后端目录下的 backend/public/motd.md,替换默认的测试文本;支持完整的 Markdown 语法与 emoji 短码;
  2. 重新部署:由于静态文件由后端进程直接读取,修改后需要重新构建/重启后端容器(参考 backend/docker/Dockerfile 与根目录 README.md 中的部署说明),或根据部署方式挂载该文件为外部卷;
  3. 生效与去重:用户端会在请求时通过响应头Last-Modified/etag感知到文件变更,即使老用户已关闭过旧公告,也会因为标识变化而再次看到新内容;
  4. 测试模式:前端测试环境下可通过 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

项目地址:https://gitcode.com/gh_mirrors/he/hedgedoc
点击查看免费下载

相关推荐

上一篇:Winhance界面全导览:软件、优化、自定义三大模块一文看懂
下一篇:React 360 SEO终极指南:让你的VR内容轻松被搜索引擎收录

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询