目录

一个差点把我逼疯的 Hugo 坑:LoveIt 里 music 与 typeit 为什么会时灵时不灵

码艺轩|一次从混乱、误判到钉死根因的完整排查记录

很多坑不会一下把人打倒,它只是让你在错误方向上反复消耗。
你以为是目录问题,以为是 shortcode 问题,以为是缓存问题,结果真正动了手脚的,是那行你最不容易起疑的初始化代码。
这篇文章,是一次技术排坑,也是一次把混乱重新理顺的记录。

有些问题,不是不会解。
是它先把你拖进混乱里,再一点点消耗你的判断。
而这一次,我差点就被 Hugo 和 LoveIt 这个坑,磨到怀疑自己。

很多坑不会一下把人打倒,它只是让你在错误方向上反复消耗。
你以为是目录问题,以为是 shortcode 问题,以为是缓存问题,结果真正动了手脚的,是那行你最不容易起疑的初始化代码。
这篇文章,是一次技术排坑,也是一次把混乱重新理顺的记录。

这不是一个普通的小问题

最近在折腾梦行志博客时,我遇到了一个非常恶心、也非常拧巴的问题。

我在文章里用了 musictypeit 这两个 shortcode,正文源码里明明已经输出了 <meting-js>,也已经有 .typeit 容器,可页面就是不生效。播放器不出来,打字动画不启动,像是活了一半,又像是死了一半。

最折磨人的地方不在于它报错,而在于它不痛不痒地坏着。它不给你一个明确的错误信息,不给你一个清晰的失败信号,只是让你不断怀疑:是不是自己又写错了?是不是某个路径又配歪了?是不是 Hugo 又抽风了?

更烦的是,它还不是全站都坏,而是“有的文章正常,有的文章不正常”。这种问题最磨人。因为只要不是全坏,你就总会忍不住去怀疑是不是自己某一篇文章写法不一样,某一个目录结构不一样,某一个 front matter 参数不一样。

而我当时最先怀疑的,就是目录。

因为我测试出来的规律很诡异:

  • hiddenFromHomePage: true 时,所有目录下都正常。
  • hiddenFromHomePage: false 时,只有 Code Art Studio 目录下的文章正常,别的目录下大量失效。

看到这里,人本能就会往“目录差异”“分类模板差异”“某个 section 特殊处理”这些方向猜。说实话,我当时也是这么想的。

但后来我才明白:这整个现象里,目录只是烟雾,真正的火根本不在那里。

/code-art-studio/hugo-loveit-shortcode-debug/images/01-debug-start.webp


先别乱猜,先看结果

我后来强迫自己停下来,不再继续凭感觉猜,而是回到最原始也最有效的一步:直接看最终 HTML 输出。

很多问题,只有当你肯老老实实去看“最后到底生成了什么”,你才会发现,自己前面那些猜测到底有多偏。

我把“生效页”和“失效页”的源码一点点对比之后,发现一个很关键的事实:

shortcode 其实已经执行了

失效页里并不是完全没有内容。

它照样有:

<meting-js auto="https://music.163.com/#/playlist?id=12472829847" theme="#448aff"></meting-js>
<div class="typeit"><h4 id="id-1"></h4></div>

这一步一下就排除了很多错误方向。

因为如果正文里连 <meting-js> 都没有,说明是 music shortcode 没跑;如果连 .typeit 容器都没有,说明是 typeit shortcode 没跑。可现在容器都在,这就说明:

  • shortcode 本身被识别了;
  • shortcode 本身也执行了;
  • 它们确实把容器写进了正文;
  • 问题不在“执行没执行”,而在“执行完以后发生了什么”。

这一步特别关键。它让我把问题从“shortcode 不生效”修正成了“shortcode 只生效了一半”。

也就是:人到了,饭没上来。


真正缺的,不是容器,而是资源

继续往下对比后,我很快发现真正缺失的东西是什么。

生效页底部会正常加载这些资源:

<link rel="stylesheet" href="/lib/aplayer/APlayer.min.css">
<link rel="stylesheet" href="/lib/aplayer/dark.min.css">
<script src="/lib/aplayer/APlayer.min.js"></script>
<script src="/lib/meting/Meting.min.js"></script>
<script src="/lib/typeit/index.umd.js"></script>

并且,生效页的 window.config 里还能看到对应的初始化数据:

window.config = {
  typeit: {
    speed: 100,
    cursorSpeed: 1000,
    cursorChar: "|",
    duration: -1,
    data: { ... }
  }
}

而失效页呢?

  • <meting-js>,但没有 APlayer 和 Meting 的 CSS/JS;
  • .typeit 容器,但没有 TypeIt 的 JS;
  • window.config 里也没有 typeitdata 这些初始化字段。

这时候,方向终于开始清晰了。

问题根本不在于 shortcode 没有输出,而在于:

页面后面的资源注入环节,没有拿到 shortcode 写入的状态。

也就是说,问题不是发生在正文阶段,而是发生在正文之后

/code-art-studio/hugo-loveit-shortcode-debug/images/02-deep-night-debug.webp


LoveIt 的设计,其实早就把坑埋好了

等我开始看 LoveIt 的 shortcode 和 partial 以后,这个问题才真正有了骨架。

LoveIt 对 musictypeit 的处理,不是“执行 shortcode 时,顺手就把所有依赖资源全都输出掉”。它走的是两阶段机制。

第一阶段:shortcode 只负责登记需求

music.html 大概是这样:

<meting-js auto="{{ .Get `auto` }}" theme="{{ $theme }}"></meting-js>
{{- .Page.Scratch.SetInMap "this" "music" true -}}

也就是说,它做两件事:

  1. 输出一个 <meting-js> 容器;
  2. .Page.Scratchthis 里写入 music = true

typeit.html 也是类似套路:

  • 先输出 .typeit 容器;
  • 再把 TypeIt 的分组数据写进 .Page.Scratchthis.typeitMap

第二阶段:assets.html 统一加载依赖

真正决定要不要加载 APlayer、Meting、TypeIt 的,是 assets.html

{{- if (.Scratch.Get "this").music -}}
    {{- /* 加载 APlayer / Meting */ -}}
{{- end -}}

{{- with (.Scratch.Get "this").typeitMap -}}
    {{- /* 加载 TypeIt 并写入 window.config */ -}}
{{- end -}}

看到这里,其实已经能感觉到问题的关键了。

这整套机制成立的前提只有一个:

shortcode 写进 .Scratch 的状态,在 assets.html 读取它的时候,必须还在。

只要中间有人把 this 清空了,整个系统就会出现一种非常诡异的“半生效”状态:

  • 容器还在;
  • 资源没了;
  • 页面看上去像 shortcode 失效;
  • 但本质上,是状态流断了。

真正的变量,不是目录,而是首页

这时候我重新回头看 hiddenFromHomePage 这个线索,才发现它根本不是一个无关紧要的小参数。

它的意义不是“文章显示不显示在首页”这么简单。对这次问题来说,它真正改变的是:

这篇文章会不会先参与首页渲染,再参与 single 页面渲染。

也就是说,真正的变量不是目录,而是:

  • 这篇文章是否进入了首页列表;
  • 它是否因此多走了一条渲染路径;
  • 在这条额外的路径里,页面级状态有没有被破坏。

这一下,我之前关于“目录特殊”“section 特殊”“某个分类模板不同”的那些猜测,基本全都可以丢掉了。

它们不是完全没关系,而是根本不在核心上。

真正的核心,是:

这篇文章在 Hugo 的渲染链里,被处理了几次;而这几次之间,共享状态有没有被错误清空。


我也试过先强制渲染 .Content,结果还是没用

在还没找到根因之前,我一度怀疑是不是 assets.html.Scratch 的时机太早了,于是尝试过一个看起来很合理的修法。

我在 assets.html 顶部加了这样一行:

{{- $noop := .Content -}}

想法很直接:

  • 先把正文强制渲染一遍;
  • shortcode 先执行;
  • .Scratch 先写入;
  • 后面再读取 this.musicthis.typeitMap

按理说,如果问题只是“读取太早”,这一招应该能修好。

结果呢?

完全没有效果。

这一步失败之后,我反而彻底意识到:

问题不是“没写进去”,而是“写进去以后,又被谁抹掉了”。

有时候排查就是这样。你以为自己是在修问题,结果其实是在一步一步缩小嫌疑范围。虽然表面看没修好,但实际上你已经离根因更近了。


真正的元凶,藏在最像没问题的地方

后来继续往上游翻,终于看到了那个真正把我绊住的东西:init.html

在这个初始化模板的最后,有这么两行:

{{- .Scratch.Set "params" $params -}}
{{- .Scratch.Set "this" dict -}}

问题就出在第二行:

{{- .Scratch.Set "this" dict -}}

它看起来特别普通。普通到你第一眼几乎不会觉得它有问题。甚至你还会本能地觉得,这不就是初始化一个字典吗?再正常不过。

可真正的坑,就埋在“看起来再正常不过”里。

这不是初始化,这是重置

这行代码的真实含义不是:

  • 如果 this 不存在,就初始化一个空字典。

而是:

  • 不管 this 里原来有没有数据;
  • 不管前面 shortcode 往里面写了什么;
  • 只要这里跑一次,就无条件用一个新的空字典把它覆盖掉。

也就是说,前面所有写进去的:

  • music = true
  • typeitMap = {...}
  • styleArr = [...]
  • scriptArr = [...]

都可能在这里被一把清空。

/code-art-studio/hugo-loveit-shortcode-debug/images/03-bug-core.webp

看到这里,我前面所有的困惑一下就全接上了。


整个问题终于完全解释通了

把前面的线索全部串起来以后,这个坑的运行轨迹就非常清楚了。

hiddenFromHomePage: false

也就是文章会出现在首页时,流程大概是这样:

  1. 首页开始渲染;

  2. init.html 被调用;

  3. 执行:

    {{- .Scratch.Set "this" dict -}}

    这时候 this = {}

  4. 首页渲染这篇文章的摘要或相关内容,触发 .Content

  5. music.html 执行,写入:

    this.music = true
  6. typeit.html 执行,写入:

    this.typeitMap = {...}
  7. 首页渲染完成;

  8. 然后开始生成这篇文章自己的 single 页面;

  9. init.html 又被调用一次;

  10. 它再次执行:

{{- .Scratch.Set "this" dict -}}
  1. 这一行把首页阶段 shortcode 写进去的状态全部抹平;
  2. 等到 assets.html 再去读取:
(.Scratch.Get "this").music
(.Scratch.Get "this").typeitMap

读到的就是空。

  1. 最终结果就是:
  • 容器还在;
  • 资源不在;
  • window.config 缺字段;
  • 页面看起来像 shortcode 失效;
  • 实际上是共享状态在中间被清空了。

hiddenFromHomePage: true

也就是文章不参与首页渲染时,整个过程就只走 single 页面这一条路径:

  1. init.html 初始化一次;
  2. .Content 渲染一次;
  3. shortcode 写入状态;
  4. assets.html 成功读取状态;
  5. 依赖资源全部正常注入。

所以这个时候就一切正常。

到这一步为止,这个问题才算真正从“诡异现象”变成了“清晰因果链”。


最终修复,只有很小一刀

说到底,这个问题的根因不复杂。真正复杂的,是你在找到它之前,会被它带着绕很多圈。

最后真正的修复,只需要把原来的:

{{- .Scratch.Set "params" $params -}}
{{- .Scratch.Set "this" dict -}}

改成:

{{- .Scratch.Set "params" $params -}}
{{- if not (.Scratch.Get "this") -}}
    {{- .Scratch.Set "this" dict -}}
{{- end -}}

这个改动到底做了什么

逻辑其实很朴素:

  • 如果 this 还不存在,那说明现在确实是第一次初始化,可以安全创建;
  • 如果 this 已经存在,那说明前面某个阶段可能已经写过东西了;
  • 这时候绝不能再覆盖它。

也就是说,我们不是不初始化,而是:

只允许它第一次初始化,不允许它在后面的渲染路径里反复“洗掉现场”。

这就是整个修复的本质。


修复后的完整 init.html

下面是我最后实际可用的版本:

{{- .Scratch.Set "version" "1.0.0" -}}

{{- $params := .Params | merge .Site.Params.page -}}

{{- if eq hugo.Environment "production" -}}
    {{- $cdn := .Site.Params.cdn -}}
    {{- with $cdn.data -}}
        {{- $cdnData := printf "data/cdn/%v" . | resources.Get | transform.Unmarshal -}}
        {{- $cdn = dict "simpleIconsPrefix" $cdnData.prefix.simpleIcons -}}
        {{- $prefix := $cdnData.prefix.libFiles | default "" -}}
        {{- range $key, $value := $cdnData.libFiles -}}
            {{- $cdn = printf "%v%v" $prefix $value | dict $key | merge $cdn -}}
        {{- end -}}
    {{- end -}}
    {{- .Scratch.Set "cdn" $cdn -}}
    {{- .Scratch.Set "fingerprint" .Site.Params.fingerprint -}}
    {{- .Scratch.Set "analytics" .Site.Params.analytics -}}
    {{- .Scratch.Set "comment" $params.comment -}}
    {{- if eq .Params.comment true -}}
        {{- .Scratch.Set "comment" .Site.Params.comment -}}
    {{- else if eq .Params.comment false -}}
        {{- .Scratch.Set "comment" dict -}}
    {{- end -}}
{{- else if eq .Site .Sites.Default -}}
    {{- warnf "Current environment is not "production". The "comment system", "CDN" and "fingerprint" will be disabled.
" -}}
    {{- warnf "当前运行环境不是 "production". "评论系统", "CDN" 和 "fingerprint" 不会启用.
" -}}
{{- end -}}

{{- .Scratch.Set "params" $params -}}
{{- if not (.Scratch.Get "this") -}}
    {{- .Scratch.Set "this" dict -}}
{{- end -}}

{{- partial "plugin/compatibility.html" . -}}

建议把这个文件放在项目根目录:

layouts/_partials/init.html

不要直接魔改主题目录。因为主题一旦更新,你之前的修改很容易被覆盖掉。


这次复盘,真正值钱的不是“修好了”

说实话,最后改的代码很少。

但这次排查对我真正有价值的,不是“又解决了一个技术问题”,而是我再一次看清了:很多坑之所以折磨人,不是因为它有多难,而是因为它太像别的问题了。

它先让你以为是目录问题; 再让你以为是 shortcode 问题; 再让你以为是资源路径问题; 再让你以为是 Hugo 缓存问题; 最后才让你发现,真正的问题居然藏在初始化模板那句你最不容易起疑的代码里。

这和很多现实里的困局其实很像。

真正把人拖住的,往往不是最凶的那一下,而是那些“不致命、但足够让你持续内耗”的东西。它们不一下子把你打倒,而是让你在错误方向上反复消耗,慢慢怀疑自己,慢慢乱掉节奏。

如果这次我一直跟着表象跑,我可能还会继续怀疑:

  • 是不是哪个目录没配对;
  • 是不是哪个 front matter 少了一个字段;
  • 是不是哪个模板分支走偏了;
  • 是不是某个 CDN 资源偶发失效;
  • 是不是浏览器缓存发神经。

可一旦回到“看最终输出、看状态流、看是谁真正改了状态”这条线,很多迷雾就会自己散掉。

写在最后

这篇文章写的是一个 Hugo + LoveIt 的坑,但对我来说,它也不只是一个技术复盘。

它更像是一次提醒:

当你已经被一个问题困住很久的时候,最重要的不是赶紧乱改,而是先让自己稳下来。先去看它到底发生了什么,再去看是谁真正改变了结果,最后再动手。

很多局不是破不了,而是人在急的时候,会先把自己弄乱。

这次也一样。

前面那一大圈怀疑和误判,其实都不值钱。真正值钱的,是最后把那条状态链看清楚以后,我知道自己以后再碰到类似问题,应该优先往哪里看了。

这比修好一个 bug 更重要。

因为 bug 会过去,但这次逼出来的判断力,会留下来。

/code-art-studio/hugo-loveit-shortcode-debug/images/04-ending-mood.webp


又填平一个差点把我逼疯的 Hugo 坑。

表面上看像目录异常、像 shortcode 失效、像缓存抽风,最后真正的元凶,却只是 LoveIt 里 init.html 那一行最不起眼的代码。

我把整个排查过程、因果链和修复方案都完整写下来了。如果你也在用 Hugo + LoveIt,尤其碰到过 music / typeit 容器在、资源却不加载的情况,这篇应该能帮你少走很多弯路。



–全文完–

感谢阅读

“同频之人,终会相遇;同行之路,终有光亮。”

若你有故事想讲、有困惑想聊、或是想找个人说说心里话,甚至只是吐槽发泄一下情绪,都欢迎来找我聊聊:

微信公众号:

梦行志微信公众号

私人微信号(有验证):oklife_1314

梦行志个人微信二维码

我的邮箱📫:hero@oklife.me


希望我写的每一个字,成为我自己和某个人活下去、拼下去的力量。

“技术终归是工具,而我们一次次认真把问题理顺,守住的其实不只是页面样式和代码输出,还有那一点不愿被混乱打败的心气,是每一个深夜仍愿点灯前行的人。”

「码艺轩・以技立身,实干谋生」系列 · 持续更新

本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明来自:https://oklife.me。

文尾配图水墨画图片