这是一个系列博客,记录了我从零开始构建Hugo主题https://github.com/tomowang/hugo-theme-tailwind 的过程。全系列包括四篇文章,这是第二篇:

hugo主题基础知识

在开始构建主题前,我们需要对hugo主题的一些术语、语法及目录结构有初步的了解。

基础语法

hugo使用html/template来渲染内容,方法和变量通过{{ }}进行访问。常用的语法如下:

  • 变量渲染 - {{ $address }}
  • 方法调用 - {{ FUNCTION ARG1 ARG2 .. }}
  • 循环
{{ range $array }}
    {{ . }} <!-- The . represents an element in $array -->
{{ end }}
  • 条件语句 - with if else
{{ with .Params.title }}
    <h4>{{ . }}</h4>
{{ end }}

{{ if (isset .Params "description") }}
    {{ index .Params "description" }}
{{ else }}
    {{ .Summary }}
{{ end }}
  • 管道 - {{ .Title | .RenderString }}
  • 上下文.以及全局上下文$.
  • 使用短横-去除额外的空字符串
<div>
  {{- .Title -}}
</div>

详细的语法描述,可以参考官方文档https://gohugo.io/templates/introduction/

同时hugo还提供很多内置的变量,这些变量通过主题文件的渲染,最终生成静态页面:

  • Site - 站点对象
  • Pages - 页面的聚合
  • Page - 单个页面对象
  • Menu - 菜单列表
  • Menu entry - 单个菜单对象
  • Taxonomy - 单个分类对象

我们在使用主题来渲染页面内容时,需要根据对应的变量,通过属性或者方法调用,最终呈现我们想要的内容。例如我们期望展示单个文章的阅读时长,可以调用Page变量的ReadingTime方法(https://gohugo.io/methods/page/readingtime/ )。部分对象方法较多,具体可以参考官方文档https://gohugo.io/quick-reference/methods/

目录结构

我们创建了基本的hugo主题后,使用命令tree -L 2 -d .可以看到主题目录的基本结构:

.
├── archetypes
├── assets
│   ├── css
│   └── js
├── data
├── i18n
├── layouts
│   ├── _default
│   └── partials
└── static

相关目录含义如下:

  • archetypes - 创建新内容页面的模板
  • assets - 静态资源文件,一般会经由资源管道进行处理
  • data - 用于存储额外的数据,可以参考https://gohugo.io/templates/data-templates/
  • i18n - 存储本地化文件
  • layouts - 核心的模板布局文件
  • static - 静态文件,会被直接复制到最终public目录

其中最核心的为layouts目录,该目录决定了模板支持的页面以及页面的布局。 hugo使用一套规则来查询内容页面应该使用的模板文件。一般站点由以下几种类型的页面构成:

  • 首页
  • 单页
  • 列表页
  • 术语页
  • 术语列表页

其查找顺序可以参考官方文档https://gohugo.io/templates/lookup-order/ 。官方文档介绍了复杂的查找顺序,但主题的大部分页面结构类似,所以实际情况中只需要定义少数页面就能达到期望的结果。

主题内容构建

tailwindcss

在开始构建前,需要安装tailwindcss,我参考官方文档做了一些调整:

# install
pnpm install -D tailwindcss
# init
npx tailwindcss init

通过上面的命令安装tailwindcss并生成tailwind.config.js配置文件, 由于我们的页面内容在layout目录,需要调整tailwind.config.js中的content配置:

content: ["./layouts/**/*.html"],

tailwindcss的引用放在assets/css/main.css中,

@tailwind base;
@tailwind components;
@tailwind utilities;

然后可以通过如下命令对页面内容进行监听生成实际使用的CSS文件:

npx tailwindcss -i assets/css/main.css -o ./static/main.css --watch

基础布局

基于我的需求以及前文对hugo目录结构的描述,最终我定义了如下的基础页面

  • baseof.html - 承载主体页面的布局
  • list.html - 列表页、术语列表页
  • single.html - 单页,主要的内容页
  • terms.html - 术语页

通过baseof.html文件,我们定义基础模板,该模板会应用在所有的页面,其核心内容如下:

<!DOCTYPE html>
<html lang="{{ or site.Language.LanguageCode site.Language.Lang }}" dir="{{ or site.Language.LanguageDirection `ltr` }}">
<head>
  {{ block "title" . }}
    <title>
      {{ if .IsHome }}{{ $.Site.Title }}{{ with $.Site.Params.Subtitle }} —
      {{ . }}{{ end }}{{ else }}{{ .Title }} ::
      {{ $.Site.Title }}{{ with $.Site.Params.Subtitle }} — {{ . }}{{ end }}{{ end }}
    </title>
  {{ end }}
  {{ partial "head.html" . }}
</head>
<body class="w-full bg-slate-50">
  <header class="flex flex-none justify-center">
    {{ partial "header.html" . }}
  </header>
  <main class="flex flex-auto justify-center">
    {{ block "main" . }}{{ end }}
  </main>
  <footer class="flex flex-none justify-center">
    {{ partial "footer.html" . }}
  </footer>
</body>
</html>

我使用{{ partial "head.html" . }}引入head标签需要的内容,并在head.html中引用了前文生成的CSS文件:

<!-- Theme CSS (/static/main.css) -->
<link rel="stylesheet" href="{{ "main.css" | relURL }}" />

HTML body标签中,header标签为头部导航,footer标签为底部菜单,main标签为内容主体。main标签中{{ block "main" . }}{{ end }}的内容,我们需要在其他页面进行定义,如简化版的list.html代码如下:

{{ define "main" }}
  <div class="flex flex-col w-full max-w-4xl lg:max-w-5xl relative">
    <div class="flex flex-row">
      <section class="flex flex-col w-2/3">
        {{ range (.Paginate $pages).Pages }}
          <article class="flex flex-col gap-y-3 p-6 mt-6 rounded-lg shadow-md bg-white">

            <h2 class="text-4xl font-semibold text-slate-800">
              <a href="{{ .RelPermalink }}">{{ .Title | markdownify }}</a>
            </h2>
          </article>
        {{ end }}
      </section>
      <aside class="flex flex-col w-1/3 mx-3 top-0 sticky self-start">
        {{ partial "sidebar.html" . }}
      </aside>
    </div>
    {{ partial "pagination.html" . }}
  </div>
{{ end }}

其中section标签对应文章列表块,aside标签对应侧边术语展示块。单页singhle.html内容类似,包含了文章标题、TOC内容、文章主体等内容。tailwindcss的好处是有现成的typography插件可以直接渲染文章内容,而不必额外定义复杂的CSS样式。

整体的引用关系可以参考下图

hugo theme layout structure

为了复用一些组件,如标签列表、阅读时长、图标等,我们可以将相同的内容进行抽象,放到layouts/partials目录中。以图标为例,我使用https://tabler.io/icons 作为图标来源, 下载对应图标的SVG文件放到assets目录,然后定义partial页面:

{{- $iconFile := resources.GetMatch (printf "icons/%s.svg" .) -}}
{{- if $iconFile -}}
    {{- $iconFile.Content | safeHTML -}}
{{- else -}}
    {{- errorf "Error: icon '%s.svg' is not found under 'assets/icons' folder" . -}}
{{- end -}}

其中.符号为传递的图标名称参数,这样我们就能快速的在页面中进行图标的引用,如

{{ partial "icon" "calendar" }}

通过大概5天业余时间的开发(#bf54ab9 ), 最终呈现的效果如下:

hugo theme basic layout

当然,这是第一阶段的内容,只有白色主题(主题切换按钮点击无反应),没有多语言,也没有响应式。但是主体的页面结构已经有了,tailwindcss在处理CSS样式时也表现了其高效、灵活的特性。