更换到 hugo 也有段时间了,之前使用的好歹还是个类似 next 之于 hexoPaperMod 主题作为切换到 hugo 的第一个主题。但是毕竟自己魔改了一大堆东西,然后全部是用 js 操作各种 dom ,虽然看上去挺炫酷的,但是代价是 iOS 支持直接没做。 以及稀烂的可维护性。这时候想起来 Maupassant 的好了,hexo 虽然核心程序一坨,但是主题足够轻快,自己定义维护修改了好些年也可用度相当高。那没办法,心一横自己重新写好了。

但至于怎么写,和怎么设计,这次也没多考虑,换 hugo 以前用什么理论上照着写就行,毕竟都是模板,css 也比较好迁移,只要 css 库不用太奇葩的都能应对。但缺点是没用到比如 hugo 的 og (opengraph) 部分,还是用一些老办法,可能过于守旧,而且不利于再以后的一些迁移工作。不过话又说回来能完成需求的代码就是好代码,过去的代码就足够稳健,能用就行,既然是博客,除了应该是技术的 playground 还应该出产内容才对,不应该只关注技术更新忽略内容产出。

结构研究

hugo 文件目录结构

关于 hexo 的部分可以参考以前的文章。hugo 虽然默认模块有点反直觉,但是其实还是老一套的东西。直接先生成一套模板:

1
hugo new theme "<theme-name>"

然后就会在 themes/<theme-name> 生成对应内容模板。如果这时候去查看一下目录下的文件:

1
2
❯ ls
archetypes/  assets/  content/  data/  hugo.toml  i18n/  layouts/  LICENSE  README.md  static/  theme.toml

很容易就发现其实就是个 hugo site 的结构。那么如果发现了这点,就很容易联想到 hugo 本身当使用三方主题时候的一些技巧。就是当三方主题被 git clone 或者作为 mod 引入时候,如果需要更改则不需要更改主题本身的文件,由于 hugo 的查找顺序是优先匹配,大概的查找顺序如下,比如针对 layout/test.html

1
2
<site>/layout/test.html
<site>/themes/<theme>/layout/test.html

所以如果存在和主题位置结构相同,但是同名的文件,会优先采用 site 中的覆盖主题的部分。这样也不会导致主题难以更新以及更新后难以合并变更。

回到正题上,我们编写主题主要需要关注 assets/, i18n/, layouts/, static/ 这四个目录。

  • assets: 存放 css 和 js 或者其他库文件
  • i18n: 国际化文件
  • layouts: 布局文件,对应到 hexo 就是模板文件
  • static: 静态文件,下面的内容会直接部署到 public/ 下面而不经过渲染,大部分图片等索引位置都在此

而对于 layouts 部分,结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
❯ tree .
.
├── _default  -- 默认模板
│   ├── baseof.html  -- 基础布局,全部其他部件和模板都以此为基础
│   ├── home.html  -- 主页
│   ├── list.html  -- 所有列表页,包括诸如 tag,category, article 等的列表
│   └── single.html  -- 文章页,渲染后在里面渲染文章内容,所有自定义页其实也是使用该页渲染
└── partials
    ├── footer.html  -- footer
    ├── head  -- head.html 的引用部分,js 和自定义css 等
    │   ├── css.html
    │   └── js.html
    ├── header.html -- header 部分
    ├── head.html -- 头信息
    ├── menu.html -- 默认的页面 navigation 菜单
    └── terms.html -- taxonomy 默认的模板,用于渲染 tags 等

试看一下内容,大概也能发现一些:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
❯ cat _default/baseof.html 
<!DOCTYPE html>
<html lang="{{ or site.Language.LanguageCode site.Language.Lang }}" dir="{{ or site.Language.LanguageDirection `ltr` }}">
<head>
  {{ partial "head.html" . }}
</head>
<body>
  <header>
    {{ partial "header.html" . }}
  </header>
  <main>
    {{ block "main" . }}{{ end }}
  </main>
  <footer>
    {{ partial "footer.html" . }}
  </footer>
</body>
</html>

乍一看的话和 hexo 的思路几乎没啥很大的区别,使用 {{ partial }} 标签进行组件的引入。定义 block 来将其他模板作为该 block 嵌入到对应位置。那么如果没猜错的话比如 single.html 就需要把 main 先定义出来,事实上也确实如此:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{{ define "main" }}
  <h1>{{ .Title }}</h1>

  {{ $dateMachine := .Date | time.Format "2006-01-02T15:04:05-07:00" }}
  {{ $dateHuman := .Date | time.Format ":date_long" }}
  <time datetime="{{ $dateMachine }}">{{ $dateHuman }}</time>

  {{ .Content }}
  {{ partial "terms.html" (dict "taxonomy" "tags" "page" .) }}
{{ end }}

布局划分

既然文件结构基本搞明白了,现在就可以直接把布局划分出来,先简单的用 html 写一下。考虑到我们还需要做成能够自适应的,那栅格难以避免,基本上除了一个头部以外身体和脚都要塞一块去才能相对比较容易的做出来自适应。

1
2
3
4
5
6
7
8
<body>
<div> header </div>
<div>
    <div> content </div>
    <div> sidebar </div>
    <div> footer </div>
</div>
</body>

其中 footer 平时自己一行,content 和 sidebar 一行,当变为移动设备尺寸时候,sidebar 会被挤下去,三个元素为一列进行排布。至于栅格布局,我懒得自己搞,还是按照原样用 purecss 的 grid 组件来完成。

其他我已经确认需要完成的东西如下:

页面 是否需要侧栏 位置
友链 N main
关于 N main
归档 Y main

那么差不多需要独立写完的只有少部分模板,其他需要更换的使用 frontmatter 来开关就好了。

关于所有独立页面,就需要手动创建一下:

1
2
hugo new content/about.md
hugo new content/frlink.md

创建后的两个文件位于 content根目录下,属于是一个 page (页面),对于 frlink 我们会有单独的模板用于适配,而对于关于页面的话仍然还是需要在 md 文件中添加相应内容的。

这里再引入 hugo 的一个概念: .Kind ,用于指代页面类型,一般分为 sections 和 page 。这里因为需求只用到 sections,所以只对需要的部分进行分析,具体需要参考文档

A section is a top-level content directory, or any content directory with an _index.md file. A content directory with an _index.md file is also known as a branch bundle. Section templates receive one or more page collections in context.

很显然,按照我的目录结构:

1
2
3
4
content -- a section
├── about.md -- not a section, is a page
├── frlink.md -- not a section, is a page
└── posts -- a section

按照我的需求,我需要一个 archives 用于归档以及检索归档内容。显然所有博文会位于 posts 下,我需要给予 posts 一个 sections 属性,那么非常明确了:

1
hugo new content/posts/_index.md

只需要为 posts 目录建立一个 _index 即可。但是这样一来 url 上 archives 作为 posts 出现,url 并不匹配,因此需要给 _index 在 frontmatters 中单独指定 url 为 archives 来变相解决问题。这个问题后面进行分页组件编写时候就会遇到,如此的解决方法节省了大量尝试分页组件可用性的时间。

那就开始写吧。

各部件编写

首先必须声明此部分不会包含任何 SEO 相关。因此编写出来的模板也没有 SEO 优化。

head 部分

进入编写之前,库是需要引入不少的,差不多盘算了一下大体是需要这么几样东西:

  • purecss
  • jquery
  • fuse
  • font-awesome
  • 其他

所以先把 head 部分解决掉:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ if .IsHome }}{{ site.Title }}{{ else }}{{ printf "%s | %s" .Title site.Title }}{{ end }}</title>

{{ with resources.Get "js/lib/jquery-3.7.1.min.js" }}
<script src="{{ .Permalink | safeURL }}"></script>
{{ end }}
{{ with resources.Get "js/lib/fuse.js" }}
<script src="{{ .Permalink | safeURL }}"></script>
{{ end }}
{{ with resources.Get "css/lib/pure-min.css" }}
<link href="{{ .Permalink | safeURL }}" rel="stylesheet">
{{ end }}
{{ with resources.Get "css/lib/grids-responsive-min.css" }}
    <link href="{{ .Permalink | safeURL }}" rel="stylesheet">
{{ end }}
{{ with resources.Get "css/lib/font-awesome.min.css" }}
<link href="{{ .Permalink | safeURL }}" rel="stylesheet">
{{ end }}
{{ partialCached "head/css.html" . }}
{{ partialCached "head/js.html" . }}
{{ if .Param "math" }}
    {{ if ne .Params.mathjax false }}
        {{ partialCached "math.html" . }}
    {{ end }}
{{ end }}

至于最后的部分先不进行解释,是引入mathjax 相关的内容。其他的部分,如果是本地托管的则需要放置在 assets 下面并使用 resources.Get才能获取到。这篇不会讲那么细致,具体仍然需要自己查一下 hugo 文档(编写这个模板时候才发现有版本更新,但是确实不算太好查有哪些新选项更新了 )。css 部分由于我使用 sass ,因此引入部分可能有点区别,需要更改 css.html

1
2
3
4
5
6
7
8
{{ $sass := resources.Get "css/main.scss" }}
{{ $style := $sass | resources.ToCSS | resources.Fingerprint }}

{{- if eq hugo.Environment "development" }}
  <link rel="stylesheet" href="{{ $style.Permalink }}" integrity="{{ $style.Data.Integrity }}">
{{- else }}
  <link rel="stylesheet" href="{{ $style.Permalink }}" integrity="{{ $style.Data.Integrity }}" crossorigin="anonymous">
{{- end }}

baseof 部分

直接就着默认模板开始写,不用 purecss 不要全抄:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang="{{ or site.Language.LanguageCode site.Language.Lang }}" dir="{{ or site.Language.LanguageDirection `ltr` }}">
<head>
  {{ partial "head.html" . }}
</head>
<body>
  <div class="body__container">
    <div id="header">
      {{ partial "header.html" . }}
    </div>
    <div class="pure-g" id="layout">
      <div class="pure-u-1 {{ if ne .Params.sidebar false }} pure-u-md-3-4 {{ else }} pure-u-md-4-4 {{ end }}">
        <div class="content__container">
          {{ block "main" . }}{{ end }}
        </div>
      </div>
      {{ if ne .Params.sidebar false }}
      <div class="pure-u-1-4 hidden_mid_and_down">
        {{ partial "sidebar" . }}
      </div>
      {{ end }}
      <div class="pure-u-1 {{ if ne .Params.sidebar false }} pure-u-md-3-4 {{ else }} pure-u-md-4-4 {{ end }}">
        {{ partial "footer.html" . }}
      </div>
    </div>
  </div>
</body>
</html>

关于侧栏是否开启使用了 frontmatter 里面的自定义的参数来进行控制,使用 .Params 进行参数的获取。

header 部分

默认 menu 是很好,但是也没必要分那么细碎…首先看下配置文件,这个地方肯定是不应该硬编码的,还是需要走配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
languages:
    zh:
        languageName: "Chinese"
        weight: 1
        taxonomies:
          category: categories
          tag: tags
          series: series
        menu:
            main:
                - name: 首页
                  url: /
                  weight: 1
                  params:
                    icon: fa-home
                - name: 归档
                  url: archives/
                  weight: 5
                  params:
                    icon: fa-archive
                - name: 关于
                  url: about/
                  weight: 25
                  params:
                    icon: fa-user
                - name: 友链
                  url: frlink/
                  weight: 20
                  params:
                    icon: fa-link
                - name: 订阅
                  url: index.xml
                  weight: 30
                  params:
                    icon: fa-rss

这是我所使用的一份简要的目录菜单的配置,对于单个菜单项目,hugo 默认支持了包括 pre 在内的很多新标签,但是似乎在模板中调用仍然是硬编码的,不能像是 hexo 等直接调用自定义标签,所以比如 icon 这种仍然需要放在 params 下然后通过 .Params 方法调用对应标签来用。而且虽然默认支持 weight ,但是却不支持通过 weight 进行排序,因此这个地方还是需要手动处理下。因此一个简单的菜单部分如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<div id="nav-menu">
    {{- $currentPage := . }}
    <!-- Iterate over main menu items -->
    <!-- Sorted by weight -->
    {{ $sortedMenu := sort .Site.Menus.main "Weight" }}
    {{- range $sortedMenu }}
    {{- $menu_item_url := (cond (strings.HasSuffix .URL "/") .URL (printf "%s/" .URL) ) | absLangURL }}
    {{- $page_url:= $currentPage.Permalink | absLangURL }}
    {{- $is_search := eq (site.GetPage .KeyName).Layout `search` }}
        <a href="{{ .URL | absLangURL }}" title="{{ .Title | default .Name }}" {{- if eq $menu_item_url $page_url }} class="current" {{- end }}>
           <i class="fa {{ .Params.icon }}">
              {{- .Name -}}
           </i>
        </a>
    {{- end }}
</div>

自己再额外根据需求调整一下比如标题,应该就相对可用了。同样,hugo 不支持很多自定义的参数,因此仍然需要放在 params 下来使用:

1
2
3
4
<div class="site__name" >
    <a id="logo" href="/.">{{ .Site.Title }}</a>
    <p class="description">{{ .Site.Params.description }}</p>
</div>

对应的配置文件就如下了:

1
2
3
title: ""
params:
  description: ""

把主要的部分写完了以后我们先来写页脚,也算是相对简单的部分,我的案例中支持备案域名的备案号通过配置文件配置。首先先定义好配置文件:

1
2
params:
  recordICPInfo: "<备案号>"

剩下部分直接硬编码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{{- if not (.Param "hideFooter") }}
    <footer id="footer">
        {{- if site.Copyright }}
            <span>{{ site.Copyright | markdownify }}</span>
        {{- else }}
            <span>Copyright &copy; {{ now.Year }} | <i class="fa fa-moon-o"></i><a href="{{ "" | absLangURL }}">{{ site.Title }}</a></span></br>
        {{- end }}
        <span>
            Powered by
            <a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> | Theme by
            <a href="https://github.com/weearc" rel="noopener" target="_blank">weearc</a>
        </span></br>
        <script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
        <i class="fa fa-eye"></i><span id="busuanzi_container_site_pv"> 总访问量 <span id="busuanzi_value_site_pv"></span> 次 </span> |
        <i class="fa fa-user-md"></i><span id="busuanzi_container_site_uv"> 访客数 <span id="busuanzi_value_site_uv"></span> 人次 </span></br>
        <span>
            <a href="http://beian.miit.gov.cn/" rel="noopener" target="_blank" style="border:none;">
                {{ site.Params.recordICPInfo }}
            </a>
        </span>
    </footer>
{{- end }}

这里添加了不算子来统计访问量,也算是常规操作了。并通过引用 font-awesome 来使用对应字体图标。

home 部分

主页部分我直接进行了拆分:

1
2
3
{{ define "main" }}
      {{ partial "home-article-list" . }}
{{ end }}

而核心部分就是主页的文章列表。已知我目前可以用于发布的文章有 44 篇上下,那么主页必不可行全部渲染出来,所以分页组件是必需的了。

插件编写

默认写作模板协调

其他非核心三方件引入和配置

其他与 hugo 配置整理和协调

成果