Aug 21, 2021 / Hugo

Hugo Modules 使用笔记

这两天在整理博客的源代码结构时用到了 Hugo Modules。这个功能最早出现在 2019 年发布的 Hugo v0.56.0 当中,基于 Go Modules,用于引入外部依赖。

每个 Hugo 站点可以拆分成七个组件,对应根目录的七个文件夹:

  • static / 静态文件
  • content / 站点内容
  • layouts / 模板
  • data / 数据文件
  • assets / 附件
  • i18n / 翻译文件
  • archetypes / 内容模板

模块可以自定义组件内容,例如在 layouts/shortcodes 文件夹里面添加新的短代码,或是在 assets 下挂载前端库。加载模块后自定义内容便会被挂载到站点的对应目录下供 Hugo 使用,也支持同时加载多个模块(有优先度)。主题其实就是一个 Hugo 模块。

具体的使用方法可以看上面的官方文档,我在这里就说下自己的使用场景。

加载评论系统的脚本

这个博客的源代码托管在 GitHub,用到了两个 Git Submodule(注意这个和 Hugo Module 不一样):评论系统和主题。我一开始是这样做的:

  • 评论系统的前端存放在 assets 文件夹下,方便加载对应的脚本
  • 主题存放在 themes/papyrus 文件夹下

但我发现第一点的做法并不够好:评论系统前端文件夹里有 node_modules 黑洞,而 Hugo 启动开发服务器时会监控 assets 下所有文件的变化并热刷新页面,这很浪费资源。我只需要 Hugo 监控 dist 文件夹下的内容变化,不需要管其他文件。

所以这个时候就可以使用 Hugo Module 提供的挂载(Mount)功能来解决。我评论系统的文件夹结构大概是这样的:

├───rediscuss
│   ├───dist
│   ├───node_modules
│   ├───src
│   ├───...

主题只会用到 dist 文件夹下编译好的 JS 和 CSS 文件,所以只要挂载这个文件夹到 assets 下就可以了。在 config.yaml 添加:

module:
    mounts:
        - source: assets
          target: assets
        - source: rediscuss/dist
          target: assets/rediscuss

这样就可以把博客根目录下的 rediscuss/dist 挂载到 assets/rediscuss 文件夹下。需要注意的是,在添加自己的规则后,默认的会被覆盖,所以需要先手动挂载 assets 文件夹。(When you add a mount, the default mount for the concerned target root is ignored: be sure to explicitly add it.

完成后主题可以用 resources.Get "rediscuss/rediscuss.es.js" 来获取 rediscuss/dist/rediscuss.es.js 文件。也可以在 SCSS 文件里面使用 @import 来引入相应的样式文件。

代替软连接

前几天有考虑过把博客搬到服务器上,并使用 Google Drive 来同步并自动更新站点。可以方便写作和加快站点编译速度(不用等 CI)。网盘需要同步以下文件夹:

  • contents
  • assets
  • layouts
  • static
  • data

我一开始的想法是把这些文件夹同步到一个目录下,~/blog_source。接着在另一个目录,~/hugo_blog,存放主题文件,并使用软连接把上面提到的文件夹都关联到这个目录下。但 Hugo 对软连接的支持并不是很好,我测试的时候似乎弹出了不支持软连接的错误。Hugo Mount 挂载的文件夹也可以在站点目录之外,所以在这里可以用来代替软连接:

# config.yaml
module:
    mounts:
        - source: ~/blog_source/content
          target: content
        - source: ~/blog_source/static
          target: static
        - source: ~/blog_source/layouts
          target: layouts
        - source: ~/blog_source/data
          target: data
        - source: ~/blog_source/assets
          target: assets
        - source: ~/blog_source/archetypes
          target: archetypes

这样就能达到我想要的效果了。

引入 PhotoSwipe 文件

前两天给博客加了图片灯箱,用了 PhotoSwipe v5 版本。因为还是 Beta 版,作者没有在 npm 上发包,也就不能直接用 JSDelivr。文档推荐的安装方法是:

npm install --save git://github.com/dimsemenov/photoswipe#v5-beta

# or yarn
yarn add git://github.com/dimsemenov/photoswipe#v5-beta

(和 Git clone 也差不多,就是升级方便点)

作者有把编译好的文件提交到仓库的 dist 文件夹下,主题需要引入的文件有:

  • dist/photoswipe.css
  • dist/photoswipe.esm.min.js
  • dist/photoswipe-lightbox.esm.min.js

其实这里也可以不使用 Mount 来解决:Hugo 支持在 JS 里面 import npm 依赖(得益于 ESBuild),CSS 的话也可以在编译时添加 includePaths 参数来加载。

不过我想使用 ES6 Modules + 按需加载,而在 HTML 模板里面是不支持直接引用 node_modules 下的文件的。我希望只在有图片的文章下加载脚本。

# config.yaml
module:
    mounts:
        - source: assets
          target: assets
        - source: node_modules/photoswipe/dist
          target: assets/photoswipe

HTML 模板里面可以这样写:

{{ $style := resources.Get "photoswipe/photoswipe.css" }}
{{ $core := resources.Get "photoswipe/photoswipe.esm.min.js" }}
{{ $lightbox := resources.Get "photoswipe/photoswipe-lightbox.esm.min.js" }}

<script type="module">
    const shouldLoad = document.querySelectorAll('.article-content figure').length > 0;
    if (shouldLoad) {
        const PhotoSwipeLightbox = (await import(`{{ $lightbox.RelPermalink | safeJS }}`)).default;
        const styleHref = `{{ $style.RelPermalink | safeJS }}`;

        const styleTag = document.createElement('link');
        styleTag.rel = 'stylesheet';
        styleTag.href = styleHref;
        document.head.appendChild(styleTag);

        const lightbox = new PhotoSwipeLightbox({
            gallerySelector: '.article-content',
            childSelector: 'figure a',
            pswpModule: '{{ $core.RelPermalink }}'
        });
        lightbox.init();
    }
</script>

现代浏览器都支持 ES6 Modules,具体数据可以参考 caniuse


感觉这个功能挺有用的,不过好像没有很多人讨论过,所以就写了这篇文章记录下。

CC BY-NC-SA 4.0