使用 Hugo 制作静态博客

2021-04-29 hugo html

Hugo 采用开源的 goldmark 作为 markdown 的解析器,兼容 GitHub-Flavored Markdown 标准规范,很多的静态网站都是使用的Hugo,例如 K8S 的主页,这里介绍常见的使用技巧。

简介

Hugo 是通过 GoLang 编写的静态网站页面,效率要比 Ruby 编写的 Jekyll 要高很多。可以直接从 Github 上下载二进制包,然后解压添加到 PATH 路径下即可。

建议安装 hugo-extended 版本,其支持 SCSS 功能,适合于对主题的开发。另外,通过 goldmark 实现了很多扩展,最常用的就是任务列表,详细的清单可以参考 MarkdownGuide 中的介绍。

通过 Hugo 可以高度化定制,在设计时尽量能做到 Markdown 语法的兼容。

基本使用

通过主题可以对页面进行定制,有很多主题可以选择,相见 themes.gohugo.io,以 hugo-geekdoc 为例,下载后直接放到项目的 themes 目录下即可。

----- 检查安装是否成功
hugo version

----- 当前目录下新建项目
hugo new site hugo

----- 主题模板加到themes目录下,在配置文件config.toml中增加theme="doks"或者通过--theme hyde参数启动
git clone --depth 1 --recursive https://github.com/h-enk/doks.git themes

----- 创建一篇文章
hugo --verbose new post/create-hugo-blog.md

----- 启动服务
hugo server --buildDrafts --bind="0.0.0.0" --port=38787

----- 生成静态文件(含draft),保存在buildDir目录下,默认是public
hugo -D

配置文件

默认采用根目录下的 hugo.toml hugo.yaml hugo.json 等配置,可以通过 --config a.toml,b.toml 方式指定配置文件。不过推荐使用目录,默认为 config 目录,可以在配置文件中使用 configDir 配置项修改。

hugo.toml
config/
 |-_default/ 
 | |-hugo.toml
 | |-languages.toml
 | |-menus.en.toml
 | |-menus.cn.toml
 | `-params.toml
 |-develop/
 | |-hugo.toml
 | `-params.toml
 `-production/
   `-hugo.toml

目录下第一层是环境,包括 _default 默认配置,常见一般还会配置 production 目录,例如,通过 --environment production 启动时,会将 _default 中的配置与 production 合并,在模板中可以通过如下方式检查。

{{ if eq hugo.Environment "production" }}
<div> 生产模式 </div>
{{ else }}
<div> 开发模式 </div>
{{ end }}

第二层对应了配置项的顶层,常见的如 [Params] 对应 params.toml[Menu] 对应了 menu.toml 包括了不同语言。

基本概念

Section

基于 content 目录结构定义的页面集合,该目录下的第一级子目录都是一个 section,如果想让一个子目录成为 section 需要在目录下面定义 _index.md 文件,这样,所有的 section 就构成一个 section tree

content/
 `-blog/               <-- Section, 因为是content的子目录
   |-funny-cats/
   | |-mypost.md
   | `-kittens/        <-- Section, 因为包含_index.md
   |   `-_index.md
   `-tech/             <-- Section, 同上
       `-_index.md

另外,除了通过上述 Section 对内容进行分类外,还允许 Type 自定义,如果页面中不存在,那么就会默认使用 Section 对应的值,使用时可以参考如下的查找顺序,可见模板 Type 优先级高于 Section,从而允许更好的自定义。

还可以根据不同的页面类型进行处理,包括了 page section term home taxonomy 等,也就是 .Kind 变量,可以参考官方 Methods Kind 中的介绍。

模板

Hugo 使用 GoLang 的 html/template 库作为模版引擎,分成了三种类型模版:

  • single 用于单个页面的渲染。
  • list 渲染一组相关内容,例如一个目录下的所有内容。
  • partial 可被其它模版引用,作为模版级别的组件,例如页面头部、页面底部等。

其中 baseof.html 作为不同 Section 的根模板,有套模版查找机制,如果找不到与内容完全匹配的模板,它将向上移动一级并从那里搜索,其中,基本模版 baseof.html 的查找规则如下。

01. /layouts/section/<TYPE>-baseof.html
02. /themes/<THEME>/layouts/section/<TYPE>-baseof.html   <--- 不同Type的模板
03. /layouts/<TYPE>/baseof.html
04. /themes/<THEME>/layouts/<TYPE>/baseof.html
05. /layouts/section/baseof.html
06. /themes/<THEME>/layouts/section/baseof.html          <--- 不同Section模板
07. /layouts/_default/<TYPE>-baseof.html
08. /themes/<THEME>/layouts/_default/<TYPE>-baseof.html
09. /layouts/_default/baseof.html
10. /themes/<THEME>/layouts/_default/baseof.html         <--- 默认值

在模板中,通过 {{ partial "xx" . }} 引入 Partials 模块,模块中对应的页信息,可通过 {{ . }} 查看,另外,参数可以通过如下方式添加 {{ partial "header.html" (dict "current" . "param1" "1" "param2" "2" ) }}

除了上述的 Partials 引入方式之外,还可以通过 {{ define "main" }} ... {{ end }} 在不同的类型中(例如 single list 等)自定模块,然后以 {{ block "main" . }} ... {{ end }} 方式使用。

变量引用

在模板中通过 {{ xxx }} 方式引用变量,使用变量方式如下:

  • 全局配置,如 .Site.Params.titile 对应 hugo.toml 中的 [Params]config/_default/params.toml 中配置。
  • 页面参数,可以在开始指定,并通过 .Params.xxx 方式引用。
  • 其它参数,包含了一些内置的参数,可以直接使用,例如 .Title .Section .Content .Page
  • 本地化参数,常见如 i18n "global-identifier" 指定,或者 i18n .Site.Params.xxx 转换。
  • 使用函数,例如 hugo.Environment hugo.IsExtended 等,其它可以参考 Functions 内容。

除了上述的变量,还可以在 data 目录下保存 json yaml toml xml 等格式的数据文件,并通过 .Site.Data.xxx 方式在模板中进行引用。

基本语法

模板通过 {{ }} 包裹,其中的内容称为动作 (Action),一般包含了两类:A) 数据求值,会直接输出到模板,包括直接使用变量;B) 控制结构,包括了条件、循环、函数等。

另外,可以对每行控制是否换行,通过 - 控制,例如 {{- -}} 为开始和结束均不换行,不过也可以最后压缩。

----- 注释
{{/* comment */}}
----- 访问已经存在的变量,自定义变量
{{ .Title }}
{{ $var }}
{{ $var := "Value" }}
{{ $var := `Hello
World` }}

Slice VS. Map

其中 Slice 就对应了数组,常见操作如下。

{{ $fruit := slice "Apple" "Banana" }}

{{ $fruit = append "Cherry" $fruit }}
{{ $fruit = append $fruit (slice "Pear") }}
{{ $fruit = append (slice "Cherry" "Peach") $fruit }}

{{ $fruit = uniq $fruit }}

{{ range $fruit }}
  I love {{ . }}
{{ end }}

{{ range $index, $value := $fruit }}
  {{ $value }} or {{ . }} is at index {{ $index }}
{{ end }}

{{ $first := first 2 $fruit }}
{{ $last := last 3 $fruit }}
{{ $third := first 3 $fruit | last 1 }}
{{ $third := index $fruit 2 }}

字典的使用方式如下。

{{ $hero := dict "firstame" "John" "lastname" "Lennon" }}

{{ $hero = merge $hero (dict "birth" "1940") }}

{{ $hero.firstame }}
{{ $firstname := index $hero "firstname" }}
{{ range $key, $value := $hero }}
  {{ $key }}: {{ $value }}
{{ end }}

{{ $basis := "lastname" }}
{{ if eq $relation "friend" }}
  {{ $basis = "firstname" }}
{{ end }}
Hello {{ index $hero $basis }}!

{{ range slice "firstname" "lastname" }}
  {{ . }}: {{ index $hero . }}
{{ end }}

使用时,可以通过 {{ if reflect.IsSlice $value }} 或者 IsMap 判断具体的类型。

逻辑判断

通过 if 语句来判断某个值的真假,不过建议使用 with,此时会在范围内重新绑定上下文。

{{ if .IsHome }} xxx {{ end }}          // 当 IsHome 为 true 会调用,否通过 not
{{ if eq .Title "Home" }} xxx {{ end }} // 判断变量是否相等,不等 ne
{{ if and .IsHome .Params.show }} xxx {{ end }}   // 多个条件同时满足,某个满足 or
{{ if strings.Contains "hugo" "go" }} xxx {{end}} // 判断是否包含指定字符串

// 如下两种方式等价,如果为空则会跳过
{{ if isset .Params "title" }}
    <h4>{{ index .Params "title" }}</h4>
{{ end }}
{{ with .Params.title }}
    <h4>{{ . }}</h4>
{{ end }}

// 但是 if 可是使用 else if 语句
{{ if (isset .Params "description") }}
    {{ index .Params "description" }}
{{ else if (isset .Params "summary") }}
    {{ index .Params "summary" }}
{{ else }}
    {{ .Summary }}
{{ end }}

// 如下是一个稍微复杂的逻辑
{{ if (and (or (isset .Params "title") (isset .Params "caption")) (isset .Params "attr")) }}
    <div class="caption {{ index .Params "attr" }}">
        {{ if (isset .Params "title") }}
            <h4>{{ index .Params "title" }}</h4>
        {{ end }}
        {{ if (isset .Params "caption") }}
            <p>{{ index .Params "caption" }}</p>
        {{ end }}
    </div>
{{ end }}

如果 Param 设置了 description 属性,那么输出 Paramdescription 内容,否则输出 Summary 的内容。

{{ with .Param "description" }}
    {{ . }}
{{ else }}
    {{ .Summary }}
{{ end }}

迭代

对于字典数据可以通过 {{ range $idx, $var := .Site.Data.xxx }} 遍历,而数组则 {{ range $arr }} 方式遍历,同样以上述的 Data 为例,可以通过如下方式排序、过滤、获取数据。

// 这里上下文访问的是数组元素,要访问全局上下文需要使用 $. 访问
{{ range $array }}
    {{ . }}
{{ end }}

// 可以声明变量、元素索引
{{ range $val := $array }}
    {{ $val }}
{{ end }}
{{ range $idx, $val := $array }}
   {{ $idx }} -- {{ $val }}
{{ end }}

// 为 map 元素的索引和值声明变量
{{ range $key, $val := $map }}
   {{ $key }} -- {{ $val }}
{{ end }}

// 当传入的参数为空时,执行 else 语句
{{ range $array }}
    {{ . }}
{{else}}
    // 在 $array 为空时才会执行
{{ end }}

另外,还可以使用如下方式。

<ul>
  {{ range sort .Site.Data.books.fiction "title" }}
    <li>{{ .title }} ({{ .author }})</li>
  {{ end }}
</ul>

{{ range where .Site.Data.books.fiction "isbn" "978-0140443530" }}
  <li>{{ .title }} ({{ .author }})</li>
{{ end }}

{{ index .Site.Data.books "historical-fiction" }}

这样就可以根据不同变量进行过滤等,对于 .Site.Pages 等内置变量也相同。

如下是条件过滤的处理。

{{- if and (isset .Params "math") (eq .Params.math true) }}
{{- end -}}

过滤页面

如下是一个先过滤当前 Section 的数据,同时页面需要设置 class: "page" 选项,先按照年份聚合,然后再按照日期的倒序进行排序,显示日期以及对应的标题信息。

{{ $blogs := where (where .Site.Pages "Section" .Section) "Params.Class" "==" "page" -}}
{{ range $blogs.GroupByDate "2006" "desc" }}
<h1>{{ .Key }}</h1>
<ul>
{{ range .Pages.ByDate.Reverse }}
  <li><span>{{ .Date.Format "2006-01-02" }}</span> <a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
{{ end }}
</ul>
{{ end }}

页面上可用变量可以通过 Page Variables 查看,如果直接通过 {{ . }} 打印会显示文件所在路径。

其它常用使用示例如下。

{{ range .Data.Pages }}                        // 遍历 Data.Pages
{{ range where .Data.Pages "Section" "blog" }} // 遍历 Data.Pages,过滤 Section 为 blog 的数据
{{ range first 10 .Data.Pages }}               // 遍历 Data.Pages,取前10条数据
{{ range last 10 .Data.Pages }}                // 遍历 Data.Pages,取后10条数据
{{ range after 10 .Data.Pages }}               // 遍历 Data.Pages,取第10条数据之后的数据
{{ range until 10 .Data.Pages }}               // 遍历 Data.Pages,取第10条数据之前的数据

短代码

ShortCodes 主要是为处理一些 Markdown 不方便表示的处理逻辑,从而省略了一些原始 html 代码的编写,官方 提供了一些默认的实现,例如一些视频网站的链接、relref 等,源码可以参考 Github

假设有个 foobar 的 ShortCode,可以通过如下方式使用,注意,参数的包裹方式,当前暂未找到比较好的禁止渲染方法。

{{ foobar "foo" "bar" }}
Some short codes
{{ /foobar }}

上述的参数在 ShortCode 模板中有几种获取方式:通过 with .Get 0 获取,最简单直接;或者,通过 index .Params 0 获取,而其中的内容,则可以通过 .Inner 方式读取,而且会要求必须要调用。

另外,还可以参考 Hugo ShortCodes 中的一些示例。

常用函数

在模板中可以通过 {{ with .Site.Data.Resume . }} {{ .SomeData }} {{ end }} 方式引用。

高级进阶

静态文件

包括了图片、CSS、Javascript 等,通常是现有的文件,例如三方库 Bootstrap、FontAwesome 等,引用时需要放到 static 目录下,模板中需要放到对应目录下,会自动复制。

可通过 {{ "css/bootstrap.min.css" | absURL }} 这种方式引用,此时访问 http://foobar.com/css/bootstrap.min.css 是,会影射到 static/css/bootstrap.min.css 文件。

Mounts

可以通过 npm 管理三方的 JS 包,不过此时需要在配置文件中通过 module.mounts 配置。

[module]
  [[module.mounts]]
    source = "node_modules/katex/dist"
    target = "static/katex"

然后在模板中可以通过如下方式使用。

<script type="text/javascript" src="{{ "katex/katex.min.js" | absURL }}"></script>`
<link rel="stylesheet" href="{{ "katex/katex.min.css" | absURL }}"/>

CSS

从 0.43 版本之后,Hugo 就支持了 SASS 的编译,不过只能把源文件放到 /assets/scss//themes/<NAME>/assets/scss/ 目录下,然后,通过如下方式引入。

{{ $opts := dict "transpiler" "libsass" "targetPath" "css/style.css" }}
{{ with resources.Get "sass/main.scss" | toCSS $opts | minify | fingerprint }}
  <link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{ end }}

通过 resouces.Get 获取 SCSS 文件内容,接着通过管道编译、压缩、生成指纹,这样可以给生成文件加上 hash 文件名,这样可以从 CDN 拉取最新的 CSS 而不是缓存的老文件。另外,在编译 CSS 时的配置选项除了上述,还可参考如下。

{{ $opts := (dict "outputStyle" "compressed" "enableSourceMap" true "includePaths" (slice "node_modules")) -}}

代码高亮可以通过如下命令生成,示例可以参考 Style Longer 以及 Style

hugo gen chromastyles --style=monokai > syntax.css

注意,配置参数要设置 codeFences=true,否则行号等信息会通过表格方式显示,会导致显示异常。

JavaScript

与 CSS 类似,可以通过如下方式引入 JS 脚本

{{ $params := dict "api" "https://example.org/api" }}
{{ with resources.Get "js/main.js" }}
  {{ if hugo.IsDevelopment }}
    {{ with . | js.Build }}
      <script src="{{ .RelPermalink }}"></script>
    {{ end }}
  {{ else }}
    {{ $opts := dict "minify" true "params" $params }}
    {{ with . | js.Build $opts | fingerprint }}
      <script src="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous"></script>
    {{ end }}
  {{ end }}
{{ end }}

在脚本中可以通过 import * as params from '@params'; 方式引用参数,甚至可以通过 Shims 方式引入 React 代码,更多功能可以参考 Hugo JS

图片渲染

在 hugo 中定制图片格式比较麻烦,不支持类似 ![name](uri){: width="420"} 这种方式,因为支持直接使用 html,所以,可以通过如下的方式进行配置。

<img src="picture.png" alt="some message" with="50%" />

可以在 css 中定制 img 对齐方式,不过此时就只能使用一种了。另外,官方提供了 figure 的 shortcodes 代码,可以使用,但是会导致不同的平台的兼容性问题。

还有一种方式,在 layouts/_default/_markup/render-image.html 中定制图片的渲染方式,然后通过 ![name](uri?width=100px) 这种类似方式使用,不过支持的参数需要在上述文件中配置。

更多其它的渲染 Hooks 可以参考 Render Hooks 内容。

数据传递

通过 Scratch 可以传递数据,用于在 PageShortCodes 间传递数据,如果要在模板中使用临时的数据传递,那么可以通过 newScratch 新建,如下以 Hugo 自动生成的 Scratch 为例,如下使用 .Page.Scratch.Scratch 的作用相同。

{{ .Scratch.Set "hey" "Hello" }} # 还可以是 3 或者 (slice "Hello" "World")
{{ .Scratch.Get "hey" }}         # 获取
{{ .Scratch.Add "hey" "Welcome" }} # 执行加法,类似Go语言,字符串拼接、数值相加、数组拼接
{{ .Scratch.GetSortedMapValues "hey" }} # 除了通过Get获取map之外,还可以返回以key排序的value值
{{ .Scratch.Delete "hey" }}      # 删除

{{ .Scratch.SetInMap "hey" "Hello" "World" }} # 设置map,对应 key:value 为 Hello:World
{{ .Scratch.SetInMap "hey" "Hey" "Andy" }}
{{ .Scratch.Get "hey" }} # map[Hello:World Hey:Andy]

模板调试

可以通过 {{ printf "%#v" .Permalink }} 打印当前的变量信息。另外,如果要调试页面布局,可以在 <head> 中添加如下的内容,方便查看。

<style> 
div { 
  border: 1px solid black;
  background-color: LightSkyBlue; 
} 
</style>

相关文章

Hugo 默认提供了 Related Content 配置相关文章,默认通过 keywords date tags 进行相关性的匹配。

最佳实践

其中顶层目录包含了 archetypes assets content data i18n static layouts 几个,

archetypes/          通过new子命令新建文章时的模板
config/              默认使用hugo.[toml|yaml|json]作为配置,可以使用如下目录方式
 |-_default/         默认配置
 `-production/       全局配置
i18n/                本地化
themes/
 |-halo/             对应的模板名
   |-assets/         模板中的资源文件
   | |-images/
   | |-js/           JavaScript相关脚本,详见 footer/script-footer.html
   | |-scss/         SCSS文件
   | | `-app.scss    顶层的SCSS文件,会包含其它目录下的文件,详见 head/head.html
   | `-static/       静态文件
   |   `-syntax.css  上述通过hugo gen chromastyles命令生成的CSS文件,详见 head/head.html
   |-layouts/        布局模板,其中 _default 是默认,而 partials 是不同模板的引用
     |-_default/
     |-blog/         对应 blog Section 模板
     |-docs/         对应 docs Section 模板
     |-partials/     不同模板中的引用
     |-resume/       对应 resume Section 模板
     | |-baseof.html 渲染用的根页面
     |-shortcodes/   短代码
     |-slide/        对应 slide Section 模板
     |-404.html      生成404页面

其中 CSS 相关内容依赖如下几个文件:A) syntax.css 语法高亮,只需要压缩即可;B) main.css 核心的自定义配置,为了使用 bootstrap 变量,同时会在 scss 模板中引用,所以,无需再单独引入 bootstrap.min.css 即可;C) fontawesome.min.css 使用的图标,可以参考 Icons 中的内容。

另外,Bootstrap 的相关变量保存在 bootstrap/scss/_variables.scss 中。

标签

通过默认的 tags keywords 配置关联文章,其中 keywords 按照文章类型添加,而 tags 则包含如下常见的分类。

  • topic 专题文章,会将某些文章集中梳理、展示。
  • language 编程相关,细分为 c/cpp lua bash rust java python golang web css html 具体语言。
  • database 数据库相关,细分为 mysql 等。
  • linux 操作系统相关,细分为 kvm network command vim ebpf 等。
  • security 安全相关,细分为 ssh tls/ssl 等。
  • container 容器相关内容,细分为 dockerk8s 等。
  • warehouse 大数据相关内容,细分为 hudi 等。
  • devops 运维开发相关工具,细分为 git 等。
  • algorithm structure 算法数据结构相关。
  • example 包含相关的示例代码、cheatsheet 常用命令行整理、software 相对命令行要更系统。

每篇文章标题下方的标签列表可以用来跳转,此时会通过模板中的 layouts/_default/list.html 进行渲染。

其它

很多项目中可以通过 Markdown 维护文档,这里的模板也支持,使用方式如下。

cd content/cn
ln -s /your/code/docs/path foobar

然后通过 /cn/foobar/ 访问即可,此时文档的维护层级如下。

|-_index.md      必须存在,会使用 layouts/_default/list.html 渲染
|-basic.md       basic/ 可以使用这种方式,相对链接可通过 ../metric/nginx 类似方式引用
|-metric/
| |-nginx.md     metric/nginx/ 一般就是底层页面了
| `-system/      无法引用,也可以增加 system.md 文件
|   |-cpu.md     metric/system/cpu/
|   `-load.md    metric/system/load/
|-metric.md      metric/ 可以使用相对引用,引用 metric/ 目录下的文件
`-query/
  `-files.md

TODO

  • 侧边栏 TOC,点击锚点元素时默认是到页面顶部,如果顶部有 fixed 定位,就会遮挡部分,支持 ScrollSpy 功能。

参考