Hugo 的「最后更新时间」为什么每次部署都会变成今天

这个问题一开始不明显。

部署完顺手点开一篇文章,发现「最后更新时间」显示的是当天日期,以为是自己刚改过文件,就没在意。过了几天再看,所有文章的更新时间都变成了最近一次部署的日期——包括那些几个月没动过的。

现象

文章页上有两行时间:

  • 发布时间:来自 front matter 里的 date,正常
  • 最后更新时间:模板里的 {{ .Lastmod }},每次部署后都变成部署当天

也就是说,这个字段不再表示「内容什么时候改过」,而变成了「站点什么时候构建过」。

先看 Lastmod 是怎么算出来的

Hugo 的 .Lastmod 是一套回退顺序,我的 hugo.toml 里原本写着:

[frontmatter]
  lastmod = [":git", "lastmod", ":fileModTime", ":default"]

含义是从左往右找第一个能取到值的来源:git 提交信息 → front matter 里的 lastmod → 文件修改时间 → 默认顺序(也就是 date 等)。

按这个顺序,:git 应该最先命中。用 hugo config 确认配置确实生效了:

hugo config | grep -i lastmod
# lastmod = [':git', 'lastmod', ':filemodtime', 'date', ...]

问题就在这里:如果 :git 命中了,页面上应该是提交日期。既然显示的是文件修改时间,说明 GitInfo 没拿到数据,一路退到了 :fileModTime。

拿 git 自己对一下

挑一篇文章,问 git 它最后一次改动是什么时候:

git log -1 --format='%h  %aI  %s' -- 'content/随笔/hello.md'
# d3a7cd9  2026-05-14T17:34:13+08:00  Vendor site content and theme for Netlify

提交日期是 2026-05-14,页面渲染出来的却是构建当天。GitInfo 确实是空的。

缩小范围

为了分清是环境问题还是仓库问题,我建了一个最小的 Hugo 站点,在同一次提交里放三种文件:

content/a.md          (ASCII 文件名)
content/随笔.md        (中文文件名)
content/技术/ad.md     (中文目录 + ASCII 文件名)

模板里直接打印结果:

{{ if .GitInfo }}{{ .GitInfo.AuthorDate.Format "2006-01-02" }}{{ else }}NIL{{ end }}

跑出来是这样:

a.md        GitInfo=2024-03-05    Lastmod=2024-03-05
随笔.md      GitInfo=NIL           Lastmod=0001-01-01
技术/ad.md   GitInfo=NIL           Lastmod=0001-01-01

ASCII 文件名一切正常,只要路径里带中文,GitInfo 就是空的。

原因

macOS 的文件系统返回的文件名是 NFD 编码(一部分字符被拆成「基字符 + 组合符号」),而 git 仓库里存的是 NFC。

Hugo 拿着系统给的文件路径去问 git「这个文件最后一次提交是什么时候」,两个编码对不上,git 查不到这个文件,Hugo 就当作没有 GitInfo,继续往下一个回退项走。

整个过程不报错、不打日志,页面照常生成,只是时间不对。这个组合——macOS + 中文目录 + Hugo GitInfo——是可以稳定复现的,和 Hugo 版本无关。

还有个容易忽略的点:Netlify 检出代码之后,所有文件的修改时间都是检出时间。所以一旦退到 :fileModTime,线上每篇文章的「最后更新时间」必然等于部署时间;本地预览时则是本地当天日期。两边不一样,机制是同一个。

怎么处理

三个方向,成本和效果不一样:

  1. 把中文目录改成 ASCII 名(content/技术/ → content/tech/)。GitInfo 会恢复,时间变成真实的提交日期,但文章 URL 全变,需要配 alias 或重定向。
  2. 每篇文章手动写 lastmod。最准确,代价是每次改动都要记得维护。
  3. 调整回退顺序,让 date 排在 :fileModTime 前面。改动最小,效果是:没写 lastmod 的文章,更新时间与发布时间一致。

我选了第三种:这个博客的内容不常改,「最后更新时间稳定等于文章日期」比「显示每次部署的日期」诚实。

[frontmatter]
  lastmod = [":git", "lastmod", "modified", "date", ":fileModTime"]

改完以后两个时间会完全一样,看着有点多余,所以顺手让主题在两行相同时不渲染「最后更新时间」:

{{ if and .Lastmod (ne (.Lastmod.Format "2006-01-02") (.Date.Format "2006-01-02")) }}
  <span>最后更新时间: <time datetime="...">{{ .Lastmod.Format ... }}</time></span>
{{ end }}

以后哪篇文章真的改了内容、又补了 lastmod,那一行才会重新出现。

小结

  • 页面上的时间不对,先看它是从哪个回退项来的,不要直接改模板
  • Hugo 的 GitInfo 依赖 git 查得到文件,路径编码不一致就静默失败,不会报错
  • 中文目录在 macOS 上是容易踩的点:内容没问题、构建没问题、页面也正常,只有依赖 git 元数据的功能悄悄失效

一个功能「没坏但结果不对」,通常比直接报错更难发现。