Hugo | 大工程-同人博客02-基础结构与分类法:分类法深入研究、分页、partials、瀑布流图片展示


基础:三级结构的调整、分类法页面(Categories、Tags、Series)的汇总页面和单独页面的的装修。哦,还有短代码包裹的瀑布流展示图片画廊,附带灯箱效果。

上一篇我们总结了大概的需求,那我们现在就开始动手吧!

其实这篇本来 5 月初要发的,但是博主突然被公司的大裁员袭击,随后又被消化系统袭击,进入新公司以后早九晚十了一整个月总之现在整个人都活人微死,总之接连匆忙的被生活的绞肉机打飞……叹气。总之,回到正题。

对了,代码里的注释是我手打的,因为想让小白(也就是之后把一切都忘了的我自己)看懂。用到的部分 Hugo 术语是跟着英文文档走的;官方的英文文档更新一点也更细一点。部分说明会写比较长,因为也想记录一下我踩过的坑。

三级结构

根据 Hugo 自己的官方文档,三级结构本身在 Hugo 里面并不难实现。根据官方文档给出的说明,Page bundles,只需要在一个文件夹里创建一个 _index.md 的文件,Hugo 就会把这整个文件夹识别成一个 page bundle(页面包?),之后则会根据查找顺序下指定的 list 和 single 模板,直接生成对应的章节列表和单章节页面。(其中 _index.md 按照 list 模板,剩下的按照 single 模板)。

按照这个逻辑,都在 posts 文件夹下面,短篇文章我可以直接建立一个 md 文件并标注 CP、原著等信息;长篇我可以在文件夹下面先新建一个以长篇小说标题为名称的文件夹,并在新的文件夹里创建 _index.md 作为目录页,并在文件夹里给长篇的每一个章节单独建立 markdown 文件。_index.md 对应的目录页 url 会是 网站域名/posts/长篇小说标题 ,而我会在 目录页 的 frontmatter 里指定标题、作者、CP、原著等信息, 在文件夹里具体章节的 frontmatter 里标注。这样我通过标签、原作之类的信息进行检索的时候,就只会检索到这个顶着长篇文章标题的目录页,不会搜到单独的章节页,完美地符合了我的需求。

其实三级结构本身的部分处理起来是很快的,但是如果我的别的需求和这个结构产生交叉的时候,呃,就会涉及到许多……复杂的地方。

小插曲:tempAuthor 参数

因为我的网页里有邀请 guest 老师写的文章,但是因为这个文章不是我写的,因此我会希望在做字数统计相关的工作内容时去除所有的 guest 文,因此我设计了一个在 frontmatter 里用于标注 guest 文作者的参数,叫作 tempAuthor。

一般情况下,如果一个文章 frontmatter 里没有这个参数,那么默认显示这个文章的作者是我,并把文章列入相关的字数统计计算里,如果文章有这个参数,那么作者就要显示为这个作者,并且在进行字数统计的时候不计算这篇文章。

开始!Taxonomy 页面

我其实觉得原本的 cactus 主题的分类页面不好看的。所以我重新定制了一下主题的各个 taxonomy 页面。不过要开 taxonomy,首先要修改 config.toml

toml
1[taxonomies]
2  tag = "tags"
3  category = "categories"
4  series = "series"

这一步主要是告诉主题我要用这个分类法。右边的引号里的是总体的大分类,左边的大分类下面小分类的名字。如,假设我一篇文章的 frontmatter 里面有 tags : ["Hugo","博客装修","大工程","多级结构"] 这样一行,那么就是这篇文章在 tags 这个分类法下面分别有 4 个 tag,“Hugo”是一个 tag,“博客装修”也是一个 tag。

分类法可以左右一样(比如 series),也可以不一样。我 series 用一样的拼写主要是为了呃,符合英文语法?

注意我下面会用的名词,就是 taxonomy 分类法,对应的是 categories、tags、series 这些分类的类别,而 Term 就是具体的类别名。比如,在上面的例子里,“博客装修”就是一个分类法为标签(tags),Term 叫“博客装修”的 tag。

#1 原作 | Categories

原作页面我想要一看就知道原作 IP 长什么样,所以我做了卡片式的设计;我同时想要知道我在一个 IP 写了多少篇文、写了多少字,所以在设计时做了一些改动。

总体 Categories 页面

content 文件夹下面(也就是和 posts 文件夹平行的位置)建立 categories 文件夹,并在文件夹里建立 _index.md。这个 _index.md 里面写下的内容会成为 网站域名/categories 这个页面的内容。我在这个文件的 frontmatter 指定了一些关键的信息。

toml
1title = "原作 Fandoms"  //指定标题
2type = "categories" //去 layouts/categories 文件夹里面找模板
3layout = "index" //去 type 指定的文件夹下找index.html作为模板

那么我们现在已经指定了页面的内容和使用的模板,我们接下来就创建这个模板。

1 Categories 页面模板 | HTML

layouts 文件夹下面建立 categories 文件夹,并建立 index.html,这个 index.html 会成为整体 Categories 页面的模板,也就是 你的网站域名/categories 这个地址下的页面。

go-html-template
 1{{ define "main" }}
 2
 3<article class="post" itemscope itemtype="http://schema.org/BlogPosting">
 4
 5  <h1>  {{ .Title }} </h1>
 6  {{ .Content }}
 7
 8  <div class="content" itemprop="articleBody">
 9    <div id="categories">
10      {{ if (eq (len .Data.Terms) 0) }} 
11      <div class="category-list-title"> No categories </div>
12      {{ end }}
13
14      <div class="category-list">
15        <div class="row" data-masonry='{"percentPosition": true }'>
16        {{ $topaginate := slice }}
17        {{ range $k, $v := .Data.Terms.Alphabetical }}
18        {{ $topaginate = $topaginate | append  (site.GetPage (print $.Data.Plural "/" $v.Term )) }}
19        {{ end }} <!--生成名为 $topaginate 的数组,数组内包含所有的category,并且保留了它们作为分类法的数据结构-->
20
21        {{ range (.Paginate $topaginate ).Pages.ByDate}} <!--遍历 $topaginate 并分页,一般默认分页是10个1页-->
22        <div class="col-12 col-md-6 col-xl-6 pb-3 px-2">
23        <div class="categories-card"> <!--生成卡片-->
24          {{ if .Params.BGPic }} <!--如果指定了卡片头图,显示图片,没有算了-->
25          <div class="categories-pic">
26          <a href="{{ .Page.Permalink }}"> <img src="{{ .Params.BGPic | absURL }}" /> </a>
27          </div>
28          {{ end }}
29
30          {{$scratch := newScratch}}
31          {{ range .Pages }} <!--因为这一结构在range $topaginte 结构下面,所以这句话的意思是遍历categories分类法包含当前Term的所有页面;比如当前$topaginte遍历到了“灌篮高手”,这句range就是遍历所有categories里面包含“灌篮高手”的词条的页面-->
32          {{$scratch.Add "total1" 1}}
33          {{ end }}
34
35          <div class="categories-text">
36          <a href="{{ .Page.Permalink }}">{{ .Page.Title }}</a>
37          <span>({{ $scratch.Get "total1" }})</span> 
38          </div><!--呈现当前这个IP的名字、添加链接,并且显示作者写了多少篇这个IP的文-->
39        </div>
40        </div>
41        {{ end }}    
42        </div>
43      </div>
44    </div>
45  </div>
46</article>
47{{ end }}

在这个代码里 data-masonry='{"percentPosition": true }'<div class="col-12 col-md-6 col-xl-6 pb-3 px-2"> 这些句子是不能直接使用的;我自己做瀑布流图片展示的时候单独引用过 bootstrap 的库,这些句子是这个库里面的语法,没有声明的前提下直接照抄可能会出问题。如果不打算引用 bootstap 库,可以删除这两句和对应的代码包裹层,在 css/scss 文件里面定义 categories-card 的类加一行 width:50% 就可以保证原作界面的卡片宽度是页面布局宽度的 50%。你如果也打算引用 bootstrap 库,可以在自己的 partials/head.html 里加上下面对应的语句。

go-html-template
1<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">`

可能这个 bootstrap 库在你看到的时候已经不是最新的了,如果有最新的可以直接搜索 bootstrap 然后去引用最新的库。有些主题自带 bootstrap 库的引用,所以使用的时候需要检查一下,不要重复或者冲突了库的版本引用,有可能会造成问题。

2 Categories 页面样式 | SCSS

加完模板以后,我在 assets/scss/custom.scss 里面增加样式定义。CSS 我就不注释了大家自己玩吧。注意 -o-object-fit: cover; object-fit: cover; 这两句是保证无论图片原始尺寸和长宽比是什么样都会等比缩放来自动适应指定大小框体的句子,想要的话不要删掉哦。

scss
 1.categories-card{
 2  border-radius: 0.25rem;
 3  border-width: 0px;
 4  word-wrap: break-word;
 5  align-items: start;
 6  box-shadow: 0 1px 5px #999999;
 7  justify-content: center;
 8}
 9
10.categories-pic {
11  display: block;
12  height: 12rem;
13  img {
14    height:100% !important;
15    width: 100%;
16    -o-object-fit: cover;
17    object-fit: cover;
18    border-top-left-radius: $border-radius;
19    border-top-right-radius: $border-radius;
20  }
21}
22
23.categories-text {
24  display: block;
25  padding-top: 1rem;
26  padding-bottom: 1rem;
27  color: $color-text;
28  text-align: center;
29  justify-content: center;
30}

此外,$border-radius$color-text 是主题自带的变量,如果你使用的不是 cactus 主题的话,需要改一下。

这个页面现在定义应该是一个在宽视窗时并列两排的卡片,上面是图片,下面是 IP 名称和写的文章数。图片高度固定为 12rem,宽度为页面宽度的一半。需要注意,此页的所有卡片里图片的地址是在遍历 categories 里面具体的 category term 对应的 _index.md 页面 frontmatter 里拿出来的,所以如果后续没有配套指定的话,图片会显示不出来哦。

单独的 Category 页面

具体单独 category 页面效果可以看→灌篮高手 | 雪景球

在刚刚建立的 content/categories 文件夹里建立名为 IP 原作的文件夹。因为我的同人只写了名侦探柯南和灌篮高手的,所以我建立了两个文件夹,一个叫名侦探柯南,一个叫灌篮高手。在这两个文件夹下面分别建立 _index.md,并用来指定一些具体的页面内容,示例如下。

md
 1+++
 2title = "灌篮高手"
 3subtitle = "Slam Dunk"
 4date = "2023-05-21"
 5BGPic = "img/categories/SlamDunk.jpg"
 6+++
 7
 8《灌篮高手》是日本漫画家井上雄彦以高中篮球为题材的少年漫画,漫画原作的最终章《全国大赛篇——湘北VS山王工业》被改编为电影《灌篮高手》(THE FIRST SLAM DUNK),由原作者井上雄彦编剧和执导,新增了宫城良田等角色的成长和心理描写。
 9
10我的同人主要围绕漫画原作以及电影里展现的角色形象展开,没有考虑过TV的塑造。

其中 BGPic 的地址就是我要显示在 categories 总页面卡片里的图片的地址,图片是在 static 文件夹后面的路径。

接下来我们来创建具体一个 category term 的展示模板。在 layouts 文件夹创建 taxonomy 文件夹,并在这个文件夹里创建 category.html 文件。在这里可以指定单独的 category 页面的样子。我的代码如下:

go-html-template
 1{{ define "main"}}
 2{{$scratch1 := newScratch}}
 3{{ partial "category_title.html" . }}
 4
 5<div id="archive">
 6  {{ .Content }}
 7  
 8  {{ $pages := .Paginate .Pages.ByDate.Reverse }}
 9    {{ if .Site.Params.showAllPostsArchive }}
10      {{ $pages = .Pages }}
11    {{ end }}
12
13    {{ range $pages.Pages.GroupByDate "2006" }}
14      <h2>{{.Key }}</h2> 
15
16  <ul class="post-list">
17    {{ range .Pages }}
18    <li class="post-item">
19      <div class="meta">
20        <time datetime="{{ .Date }}" itemprop="datePublished">{{ .Date.Format (.Site.Params.dateFormat | default "2006-01-02") }}</time>
21      </div>
22
23      <span>
24      {{ $value4 := .Params.tags }}
25      {{ $firstTag1 := index $value4 0 }}
26      <a class="tag-link-indie" href="{{ "/tags/" | relLangURL }}{{ $firstTag1 | safeHTML }}" rel="tag">{{ $firstTag1 }}</a>&nbsp;&nbsp;</span> 
27
28      <span><a class="" href="{{ .Permalink }}">{{ if .Title }}{{ .Title }}{{ else }} Untitled {{ end }}</a></span>
29      
30      {{ if .IsPage }}
31        &nbsp;{{ .WordCount }}32      {{ else if .IsSection }}
33        {{ $scratch1 := newScratch }}
34        {{ $scratch2 := newScratch }}
35          {{ range .Pages.GroupBy "Section" }}
36             {{ range .Pages }}
37             {{ $scratch1.Add "total" 1 }}
38             {{ $scratch2.Add "total" .WordCount }}
39             {{ end }}
40           &nbsp;{{ $scratch1.Get "total" }}&nbsp;{{ $scratch2.Get "total" }}41          {{ end}}
42      {{ end }}
43    </li>
44    {{ end }}
45  </ul>
46  {{ end }}
47  {{ if eq .Site.Params.showAllPostsArchive false }}
48    {{ partial "pagination.html" . }}
49  {{ end }}
50</div>
51{{ end }}

需要注意的是,我这个页面主要取用列表形式,大部分列表 class 的呈现样式都用的 cactus 自带的 scss 里的样式,这里就不单独放 scss 的文件了。

1 单独的标题模板 | Partials

我的 {{ partial "category_title.html" . }} 这一句是因为我想要页面的标题行能够显示我在这一个 IP 总共写了多少篇文章,总共多少字,所以单独写了一个 partials/category_title.html 的模板。没这个需求的可以不要,就直接 {{ partial "title.html" . }} 或者 {{ .Title }} 就行。

partial 里的模板代码如下:

go-html-template
 1{{$scratch1 := newScratch}}
 2  {{ range .Pages }}
 3    {{ if .Params.tempAuthor}}
 4    {{ else }}
 5      {{ if .IsPage }}
 6        {{$scratch1.Add "total" .WordCount}}
 7      {{ else if .IsSection }}
 8        {{ $year := .Date.Format "2006" }}
 9        {{ $section := print $year "-" .Title }}
10        {{ $path := print "/posts/" $section }}
11
12          {{ with .GetPage $path }}
13            {{ range .Pages }}
14              {{$scratch1.Add "total" .WordCount}}
15            {{ end }}
16          {{ end }}
17      {{ end }}
18    {{ end }}
19  {{ end }}  
20
21    <h1><a href="/categories">{{ partial "title.html" . }}:{{ len (.Pages) }}{{ $scratch1.Get "total" }}字 </a></h1>

因为是统计自己的文章字数,所以首先排除有 tempAuthor 参数的文章,之后再进行计算。

接下来,我在计算的部分主要用了 scratch 相关的逻辑和语句(newScratch $scratch.Add);这个逻辑和语句在更高版本的 Hugo 中似乎被 store 取代了,语法有些许不同,如果用的 hugo 版本较高需要注意一下。

计算的方式首先当然是遍历当前分类法下面的页面合集。如果当前被遍历的页面是单独的短篇页面时,直接统计字数;如果被遍历的页面是一个长篇的目录页,那么就直接去这个目录页所在的文件夹的路径下面,去遍历这个文件夹里所有的章节页面,并记录字数之和。

后续的目录页路径获取代码会这么写,是因为我的所有的文章命名格式都是年份 - 标题。比如我的《幻觉动物》一文的文件命名为 2024-幻觉动物,这篇文章 _index.md 文件的 frontmatter 里面会指定 title 为“幻觉动物”,date 也指定为开始写作的时间。

{{ $year := .Date.Format "2006" }} 是在获取年份,{{ $section := print $year "-" .Title }} 是在打印出文件夹的名称,也就是 2024-幻觉动物{{ $path := print "/posts/" $section }} 这一句实际上就是直接指定了路径。按照我的举例,那就是指定了让程序去 /posts/2024-幻觉动物 这个地址去找页面,并让程序遍历找到的 Page Bundle,之后遍历 Bundle 里的页面并把字数统计加总起来。

这个地方代码写的比较笨,是硬写的,但是我也不知道有啥好方法。因为 。Permalink 在这里不生效,可能是因为 partials 里的模板文件一般被当做独立页面来看待,无法直接继承页面链接这些元信息?或者 .GetPath 有特殊的语法有求?(但 partials 模板好像又可以继承分类页面对.Pages 合集定义的 context,所以我也有点不太清楚,也有可能是我不太熟悉 Hugo 对于路径和链接的引用方式?)如果有人知道有什么聪明方法可以告诉我。

之所以单独开一个 partials 里面的模板出来,而不是直接和 category.html 写在一起,是因为 Hugo 在同一个模板文件内部只允许对同一个页面合集(page collection)按照一种排列方式遍历/分页。而我在计算总篇数总字数的时候就已经需要遍历一遍页面合集,在后面呈现文章列表的时候又有分页、时间倒序等需求,就对包含了所有 categories 含有当前 category term(如:categories 参数含有灌篮高手)的页面集合遍历了两次,排序要求还不一样,会发生冲突,有一部分不生效。而把这个部分拎到 partial 里以后,partial 会把这里的逻辑当成独立的页面模块,单独处理,就不算一个页面里用两个排列方式遍历了集合。

当然之所以会遍历两次是因为:我标题的地方要总数,下面又要呈现文章列表。如果有更简洁的处理可以告诉我因为我……菜菜的。

好了那我们继续回去看主体模板。

2 分页 | Paginate

最开始 Paginate 的部分是在分页,把文章列表按时间倒叙排列后,每 10 篇文一页来进行分页。需要注意的是 Hugo 有两种分页方法,一种是直接调用 paginator 语句,如 {{ range .Paginator.Pages }},一种是 paginate,使用方式如下:

go-html-template
1{{ $pages := where .Site.RegularPages "Section" "articles" }}
2{{ $pages = $pages.ByTitle }}
3{{ range (.Paginate $pages 7).Pages }}

这两种分页方式的区别是,Paginator 代码简单快捷,但是不允许你对 .Pages 集合里的页面做过滤(filter)(我理解是删除或者去除满足某些要素的页面),也不允许你在分页之前对文章顺序做整理(sort)(比如按照时间顺序倒序、按照标题字母顺序倒序排列等等),只会按照默认的 Pages 集合调用的默认顺序来对页面排序,排序后再分页(目前来看可能是权重或者时间正序顺序,有一个查找优先级列表,具体可以翻文档)。

“不允许页面预处理后分页,只允许分页后处理”这种规则,看起来似乎只是操作顺序的问题,影响不大,但是实操中还是有区别的。我举一个我自己遇到的例子:假设我有 39 篇文章,按照发布时间从前往后的顺序排列下来应当是 1、2、3……39。我的需求是按照时间倒序排列分页,按照十篇文章一页的规则,我的目标分页结果是,p1 为{39,38,37,…,30};p2 为{29,28,27,…,20};p3 为{19,28,27,…,10};p4 为{9,8,7,…,1}。当我使用目前代码使用的 Paginate 规则时,我会得到我想要的结果。

但当我调用 Paginator 的时候,我要求 Paginator 按照时间倒序来呈现结果(sort),导致我得到的最终分页结果是,p1 为{10,9,8,…,1};p2 为{20,19,18,…,11};p3 为{30,29,28,…,20};p4 为{39,38,37,…,31}。相当于 Paginator 首先按照 1-39 的顺序为我 10 个一页分出了 4 页,分别是 1-10,11-20,21-30,31-39;之后在每页的列表展示的时候才按照时间倒序展示。

这就是因为 Paginator 不允许你处理原始的页面合集会直接先分页,而 Paginate 允许你预处理你的 Pages 合集。我自己写的时候踩了这个坑,研究了一会儿才研究出来。

3 遍历 | Range

Hugo 的 range 语法与其说是循环,更准确的说法是遍历?当我们使用 range 的时候,range 到 end 之间包裹的代码逻辑,是我们让程序去遍历 range 的集合里面的每一个页面去拿到的信息,相当于我们获取参数已经脱离了当前的这个列表集合页面,而去到了我们指定范围的页面里。

在 Taxonomy 的模板列表管辖范围里,默认的.Pages 集合是 frontmatter 参数里所有 categories 参数含有当前 category 的文章的集合。(因为我先前设定了长篇文章只有目录页有 categories、tags、series 等参数,所以单独的章节页面不会显示在这个集合里,只有短篇文章,或者长篇文章的目录页。)

因为已经分完了页,目前我们在第一页。 {{ range $pages.Pages.GroupByDate "2006" }} 是想在每一页展示的文章里,把年份相同的文章攒成一个小的合集,并通过 <h2>{{.Key }}</h2> 显示了年份。之后遍历每一个年份小集合。

而在这一行之后的第二次出现的 range 代码 {{ range .Pages }} (在 <ul class="post-list"> 下面一行的)则是遍历这个分页后(第 1 页)又攒成了年份小集合(2024 年)里,要求遍历这个小集合的每一个页面(Page)。

4 CP 信息与笨拙的链接引用

对于详细的文章章节信息,我首先希望引用时间信息,之后引用 CP 信息。我没有给 CP 单独设定一个 taxonomy 的分类法,把它并在了众多标签的集合里。我在自己写标签的时候,规定自己必须把每一篇文章的第一个 Tag 用作 CP 标记,所以这里的代码就这么写了。就是显示这篇文章的第一个 Tag,

{{ $value4 := .Params.tags }} 的意思是把当前被遍历的页面 frontmatter 里标注的 Tags 后面的集合赋予变量 $value4。因为 Tags 是一个数组,所以 $value4 现在也是一个数组。而 {{ $firstTag1 := index $value4 0 }} 的意思是把 $value4 数组里第一个元素的值赋予 $firstTag1 变量。我们就取到了一篇文的 CP 值。

之后就是显示这个 cp 名字并加上超链接。似乎在声明这个是 taxonomy 参数的情况下可以用 .Permalink 来写标签的链接?但是也可能因为这个页面是 Category 的页面所以没法识别出别的 taxonomy 对应的超链接?总之我自己用 .Permalink 的时候似乎出现了一些问题,所以我的网页链接是硬拼出来了标签对应网址的链接,换言之,硬写的比较笨(……)语句是 href="{{ "/tags/" | relLangURL }}{{ $firstTag1 | safeHTML }}"

5 长篇文章的字数统计

每篇文章标题之后,我想跟随的信息是(如果是长篇)有多少章节,以及文章的总字数。

在这里还是用了 Scratch 相关的逻辑。先声明两个 Scratch 变量 {{ $scratch1 := newScratch }}``{{ $scratch2 := newScratch }},在 $scratch1 统计章节数,在 scratch2 统计总字数。

总之我承认我代码水平有限所以我硬写了。嗯嗯。具体的 scss 样式好像因为我用了原 cactus 主题的设置就不单列出来了,大家自己看样子写写就行,嗯。

#2 标签 | Tags

我对于标签汇总页面的想法是:我想要标签云!写的文章越多,字体越大的那种!具体的单独 tag 逻辑和 category 的逻辑比较像,只不过把呈现第一个标签的地方变成了呈现原作 IP,其他内容基本一致,就不单独展示了。只写一些 tags 页面的流程。

content 文件夹下面(也就是和 posts 文件夹平行的位置)建立 tags 文件夹,并在文件夹里建立 _index.md。文件的 frontmatter 这么指定:

toml
1title = "标签 Tags"
2type = "tags"
3layout = "index"

随后写模板。在 layouts 文件夹下面建立 tags 文件夹,并在文件夹里建立 index.html

go-html-template
 1{{ define "main" }}
 2<article class="post" itemscope itemtype="http://schema.org/BlogPosting">
 3  <h1>  {{ partial "title.html" . }} </h1>
 4  {{ .Content }}
 5  <div class="content" itemprop="articleBody">
 6    <div id="tag-cloud">
 7      {{ if (eq (len .Data.Terms) 0) }}
 8      <div class="tag-cloud-title"> No tags </div>
 9      {{ end }}
10        
11      <div class="tag-cloud-tags">
12      {{ $AllRegularPagesCount := len .Site.RegularPages }}
13      {{ range $elem := .Site.Taxonomies.tags.Alphabetical }}
14      <a style="font-size: {{ (add 1 (mul 10 (div (float $elem.Count) $AllRegularPagesCount))) }}rem;" href="{{ $elem.Page.Permalink }}"> {{- .Page.Title -}} </a>
15      {{ end }}
16      </div>
17    </div>
18  </div>
19</article>
20{{ end }}

如果不喜欢呈现的方式,可以调整一下中间的计算公式。标签云的具体 scss 的一些样式是主题自带的,这里的算法主要是在改字号大小,其他的具体修改定制就好!

因为我的文章标签比较多,我就没有做头图,每个标签点开也只有包含标签的文章数量、字数和文章列表,也不需要单独建立 Tag Term 的文件夹和 _index.md 文件,大体逻辑和 category 相同的。我也做了个 partials/tag_title.html 的模板,没有需要 Tag Term 页面显示总篇数和总字数需求的可以不要。

#3 成册 | Series

创建文件的流程和思路与前面相同,不过代码上有不同。

首先声明,因为我的 Series 这个参数不是单纯用来标文章的分类法,也是想用来做本子相关策划内容展示的,所以我在文章的 fronttmatter 里会有一些系列相关的特殊的参数设置。

比如,在我的长篇同人文的 markdown 文件的 frontmatter 里,我会放这种参数:

toml
1series : ["栖息地","孤独悸动"]
2subSeries: 恶土 · Badlands
3serial_Num: 6

其中 series 是用来标识该篇文章归属的本子名称的。考虑到策划情况很多变化,一篇文可能收录在不同的本子里,允许标注多个 series 参数;所以把这个分类法当成“系列”来用也是可以的。

subSeries 是因为我有的本子策划比较细,分上下卷会有单独的卷名,为了在本子页面显示出来做了。没这个需求的可以不看,因为实在是 series 参数没法数组套数组,我没法每个 Series 都设置 subSeries 参数,所以目前设计的这个参数就是只跟着 series 数组里面第一个走的,也就是我这个 frontmatter 里“恶土·Badlands”这个参数其实只有“栖息地”这个本子在用。

serial_Num 是系列序号,用于手动调整文章在本子页面显示的先后顺序的,只有相对大小有意义,绝对大小没有意义。使用方法举例:我的《栖息地》这个本子有 8 篇文,那么排序就是取 8 篇文的 serial_Num(假设分别是 1-8),按照从小到大顺序排列,那么这篇文章就是《栖息地》的第六篇文章;与此同时《孤独悸动》这个本子只有两篇文,那么如果我另一篇文的 serial_Num 是 5,那篇文在《孤独悸动》这个本子里就在这篇文的前面,如果 serial_Num 是 7,那篇文在《孤独悸动》这个本子里就在这篇文的后面。

总体 Series 页面

这个页面样式设计比较受 Hugo 主题 Vnovel 的影响,具体主题可以看这个 VNovel | Hugo Themes

具体创建流程还是老操作。在 content 文件夹下面(也就是和 posts 文件夹平行的位置)建立 series 文件夹,并在文件夹里建立 _index.md。文件的 frontmatter 这么指定:

toml
1+++
2title = "文本集合 Series"
3type = "series"
4layout = "index"
5+++
6
7一些有主题策划的文本集合。
1 Series 页面模板 | HTML

随后写模板。在 layouts 文件夹下面建立 series 文件夹,并在文件夹里建立 index.html。网页模板内容如下:

go-html-template
 1{{ define "main" }}
 2<article class="post" itemscope itemtype="http://schema.org/BlogPosting">
 3<h1>  {{ partial "title.html" . }} </h1>
 4  {{ .Content }}
 5  
 6  <div class="content" itemprop="articleBody">
 7  
 8  <div class="grid">
 9    {{ $topaginate := slice }}
10    {{ range $k, $v := .Data.Terms.Alphabetical }}
11    {{ $topaginate = $topaginate | append  (site.GetPage (print $.Data.Plural "/" $v.Term )) }}
12    {{ end }}
13
14    {{ range (.Paginate $topaginate ).Pages.ByDate}}
15      <div class="book-card">
16      <a href="{{ .Permalink }}">
17        {{ if .Params.BookCover }}<img class="book-cover" src="{{ .Params.BookCover | absURL }}" alt="thumbnail" />
18        {{ else }}<img class="book-cover" src="{{ "/img/series/novel.png" | absURL }}" alt="thumbnail" />
19        {{ end }}
20      </a>
21
22        <div class="book-text">
23        <h4><a href="{{ .Permalink }}">{{ .Title }}</a></center></h4>
24          <div class="book-oneline-meta">
25          {{ $cat := .Params.BookCategory }}
26          {{ if $cat }}<a href="{{ "/categories/" | relURL }}{{ $cat | safeHTML }}">{{ $cat }}</a>
27          {{ end }}
28          {{ $tag := .Params.BookCP }}
29          {{ if $tag }}<a href="{{ "/tags/" | relURL }}{{ $tag | safeHTML }}">{{ $tag }}</a>
30          {{ end }}
31          </div>
32
33          <div class="book-description">
34          <p>{{ if .Description }}{{ .Description }}
35               {{ else }}{{ .Summary }}{{ end }}
36          </p>
37          </div>
38            
39        </div>
40      </div>
41    {{ end }}
42  </div>
43  {{ partial "pagination.html" . }}
44  </div>
45</article>
46{{ end }}

还是,这里面用到的大多数参数是遍历具体的 series 页面的时候拿过来的参数,比如 BookCoverBookCategory,BookCP,Description。这些参数都是后续需要建立单独的 Series Term 文件夹和对应的 _index.md 之后写在 frontmatter 里的。

  • BookCover:这个是本子封面,如果有指定图片就会加载指定图片,如果没有就加载默认图片,默认图片的位置是在 static 文件夹里的位置。
  • BookCategory 和 BookCP:这个分别用来标识原作和 CP,是给本子单独写的一个参数,超链接的跳转地址也是硬写的,因为在 Series 页面调用不来其他分类法的 Term 就干脆这么写了。或许会有更聪明的直接用{{.Permalink}}的做法但我不在乎。
  • Description:是本子的一句话简介,要非常非常短。
2 Series 页面样式 | SCSS

scss 的样式是这样的:

scss
 1.grid{
 2  display: grid;
 3  padding: $padding-8;
 4  gap: $padding-16;
 5  grid-template-columns: repeat(2, 1fr);
 6}
 7
 8@media (max-width: 576px) { /* 小屏幕 */
 9  .grid { grid-template-columns: 1fr; /* 1列 */ }
10}
11
12@media (min-width: 577px) and (max-width: 767px){
13  .grid {
14    grid-template-columns: 1fr; /* 1列 */
15    justify-items: center;
16  }
17  .book-card{ max-width: calc(min(375px, 75%)); }/* 改变宽度 */
18}
19
20.book-card{
21  border-radius: $border-radius;
22  border-width: 0px;
23  word-wrap: break-word;
24  display: flex;
25  align-items: start;
26  padding: $padding-16;
27  a{ background-image: none !important; }
28}
29
30.book-cover {
31  display: block;
32  left: 0;
33  border-radius: $border-radius;
34  -o-object-fit: cover;
35  object-fit: cover;
36  width: 6rem;
37  height: 10rem;
38  box-shadow: 0 1px 5px #999999;  
39}
40
41.book-text {
42  flex: 1;
43  margin-left: $padding-16;
44  margin-right: $padding-16;
45  margin-top: 0;
46  margin-bottom: 0;
47  color: $color-text;
48  h4{
49    margin-top: 0;
50    margin-bottom: 0;
51    font-size: larger;
52    font-weight: 600;
53    color: $color-text;
54  }
55  
56  .book-oneline-meta{
57    margin-top: 0;
58    margin-bottom: 0;
59    font-size: 14px;
60    color: $color-meta;
61    a{ background-image: linear-gradient(transparent, transparent 5px, #383838 5px, #383838) !important; }
62  }
63
64  .book-description{
65    margin-top: 0;
66    margin-bottom: 0;
67    text-overflow: ellipsis;
68    font-size: 14px;
69    p{
70      margin-block-start: $padding-4;
71      margin-block-end: $padding-4;
72      line-height: 1.2;
73      text-indent: 0;
74      }
75   }
76}

如果想要定制屏幕宽度较小时页面元素的呈现样式,可以使用诸如 @media (max-width: 576px) 之类的语句来写小屏幕宽度的定制呈现方式。在我这一段里面,我就改了一些规则:窗口宽度较小和中等时只呈现一列。注意 scss 里还是用了很多 cactus 固有的变量,如果你的主题不是 cactus 的话还是需要单独写一下的。

单独的 Series 页面

这个页面我做的相当浓墨重彩,因为,有,瀑布流图片展示!!不过,首先还是老流程。在刚刚建立的 content/series 文件夹里建立名为 Series Term 的我文件夹。比如我有个本子名为《凌晨四点钟的婚姻》,那我就建立名为凌晨四点钟的婚姻的文件夹,并在里面建立 _index.md。frontmatter 如下:

toml
 1+++
 2title = "凌晨四点钟的婚姻"
 3subtitle = "Marriage at 4 A.M."
 4date = "2023-11-19"
 5BookCover = "img/series/MarriageAt4AM/cover.jpg"
 6BookCategory = "灌篮高手"
 7BookCP = "洋良"
 8BookShopLink = "https://weidian.com"
 9description = "凌晨四点钟,是个一切尚且被黑暗笼罩,但天色已经开始变亮,为日出做准备的时间段。"
10detailedDescription="洋良形婚平行宇宙,讨论爱,自我,与死亡。爱情笨蛋们在圈圈绕绕中被心牵引,找到从未问出口问题的答案。"
11+++

可以看到,前面代码里用到的 BookCover、BookCategory、BookCP、BookShopLink、description 等参数都是在这个地方读取和指定的。

接下来我们来看模板。建立 layouts/taxonomy/series.html 文件。

go-html-template
 1{{ define "main"}}
 2<div id="archive">
 3  <div class="book-Showpage">
 4    {{ if .Params.BookCover }}
 5    <img class="book-Showpage-SeriesCover" src="{{ .Params.BookCover | absURL }}">
 6    {{ else }}
 7    <img class="book-Showpage-SeriesCover" src="{{ "/img/series/novel.png" | absURL }}">
 8    {{ end }}
 9
10    <div class="book-Showpage-text">
11      <h1>  {{ partial "title.html" . }}</h1>
12      <h2 class="subtitle">  {{ if .Params.subtitle }}{{ .Params.subtitle }}{{ end }}</h2>
13      {{ $cat := .Params.BookCategory }}
14      {{ if  $cat }}<a href="{{ "/categories/" | relURL }}{{ $cat | safeHTML }}">{{ $cat }}</a>{{ end }}
15      {{ $tag := .Params.BookCP }}
16      {{ if $tag }}<a href="{{ "/tags/" | relURL }}{{ $tag | safeHTML }}">{{ $tag }}</a>{{ end }}
17      {{ $link := .Params.BookShopLink }}
18      {{ if $link }}<a class="ShopLink" href="{{ $link }}">购买链接</a>{{ end }}
19      
20      {{ $dataFormat := .Site.Params.dateFormat | default "2006-01-02" }}
21      <span><time datetime="{{ .Date }}" itemprop="datePublished">{{ .Date.Format $dataFormat }}</time></span>
22      
23      <p>{{ .Description }}</p>
24      {{ if .Params.detailedDescription }}
25      <p>{{ .Params.detailedDescription }}</p>
26      {{ end }}
27
28    {{ if le (len .Pages) 1}} 
29      {{ range .Pages }}
30      <ul class="post-list">
31      {{ range .Pages.ByDate | first 10 }}
32        <li class="post-item">
33        <div class="meta">
34        <time datetime="{{ time .Date }}" itemprop="datePublished">{{ .Date.Format (.Site.Params.dateFormat | default "2006-01-02") }}</time>
35        </div>
36        <span>
37        {{ if .Params.chapter_Num }}{{ .Params.chapter_Num }}章 | {{ end }}
38        <a class="" href="{{ .Permalink }}">{{ if .LinkTitle }} {{ .LinkTitle }} {{ else }} Untitled {{ end }}</a>
39        </span>
40        </li>
41      {{ end }}
42
43      {{ if gt (len .Pages) 10 }}
44      <div class="more-link">>>><a href="{{ .Permalink }}">查看全部 See All</a><<<</div>
45      {{ end }}
46      </ul>
47      {{ end }}
48    {{ else }}
49      <ul class="post-list">
50      {{ range sort .Pages "Params.serial_Num" "asc" }}
51      {{ $series := .Params.series }}
52      {{ $seriesIndex := newScratch}}
53      {{ range $index, $series_A := $series }}
54        {{ if eq $series_A  $.Title }}
55        {{ $seriesIndex.Add "index" $index }}
56        {{ end }}
57      {{ end }}
58      {{ if eq ($seriesIndex.Get "index") 0 }}
59        {{ $subSeries := .Params.subSeries }}
60        {{ if $subSeries }}
61          {{ if (ne $subSeries ($.Scratch.Get "subSeries")) }}
62          {{ $.Scratch.Set "subSeries" $subSeries }}
63          <h2>{{ $subSeries }}</h2>
64          {{ end }}
65        {{end}}
66      {{ end }}
67        <li class="post-item">
68          <div class="meta">
69          <time datetime="{{ time .Date }}" itemprop="datePublished">{{ .Date.Format (.Site.Params.dateFormat | default "2006-01-02") }}</time>
70          </div>
71          <span><a class="" href="{{ .Permalink }}">{{ if .Title }} {{ .Title }} {{ else }} Untitled {{ end }}</a></span>
72        </li>
73      {{ end }}
74      </ul>
75    {{ end }}
76    </div>
77  </div> 
78</div>
79
80{{ if .Content}}
81<div class="page-gallery">
82  <h1>插图 Gallery</h1>
83  {{ .Content }}
84</div>
85{{ end }}
86{{ end }}

这里面我处理了几个问题。首先读取 frontmatter 里的封面路径 BookCover 参数,如果读到了就加载图片,如果没读到就加载默认图片。然后分栏,左侧为本子封面,右侧为基础信息。标题、原作、CP 和链接,等基本信息。

1 填空用的

如果看过我的 frontmatter 参数和代码,会发现我做了两个参数 descriptiondetailedDescription, 这是因为 description 是在 Series 汇总页面的一句话简述,需要比较短,但有时候我的书籍页面列表太短了,就会空一截,所以我会多用一段 detailedDescription 文字来填充一点空隙。

subtitle 也是如此。

2 章节/文章列表

这里的处理逻辑相对复杂。造成这种复杂现象的原因主要是我做这个页面考虑的是做本子的思路,从呈现本子内容的角度出发的。

因为我在做本子的时候有两种本子,一种是多篇文本攒成的一个文本集合,一个是一个长篇文本单独出本,这两种本子的文本列表内容不同。对于多篇文本攒成的文本集合,我认为文本列表需要呈现的内容应当是每一篇文的标题、编纂信息(比如分卷等),文章排布顺序等;而单独长篇出本,列表里需要呈现的就不是文章的标题信息了,而是章节信息。因为博客本身存在三级结构,很明显,二者对于需要呈现的信息不属于一个层级,所以这里的逻辑就比较复杂了。

  1. {{ if le (len .Pages) 1}} 一上来判断,该本子是是否只由一个长篇组成;
  2. 如果该本子是只由一个长篇组成的,遍历本子集合后,在文章层面上继续遍历章节的 Pages 组合;
    1. 显示前 10 章的标题、章节数、日期等信息;(比 10 章少的就空着)
    2. 如果文章的章节数比 10 章多,展示前 10 章的章节标题列表后,显示“查看全部”的链接,该链接点击后可以来到文章的目录页,查看全部章节列表;
  3. 如果本子是多个长篇或短篇组成的本子,那么按照 serial_Num 参数从小到大来排列文章顺序并显示信息;
    1. 考虑到一个文章可能有多个 Series 参数,认为文章/目录页里面的 subSeries 参数只对第一个 Series Term 生效,所以额外做了当前 Series Term 是否是当前文章所有 Series Term 集合排列里第一个 Term 的判断;如果本子展示页面不涉及一篇文章重复收录的行为或者分卷这种稍微复杂的需求,这一部分可以忽略;
      1. 如果当前本子是当前文章的 Series 集合里的第一个 Term,则在文章标题前面显示分卷 subSeries 参数和信息;
      2. 如果当前本子不是当前文章 Series 集合里的第一个 Term,则不显示分卷参数和信息
    2. 接下来显示文章写作时间、标题,并附上链接;

当然,做本子肯定还有别的情况,比如有两篇文章、三篇文章等,但是这些页面的列表结构需求无外乎我给出的示例的两种层级的混合:比如两篇文章的时候可能既想要显示两篇文章的标题,也想要显示具体文章内的章节列表,只需要在我给出的这两种层级结构上进行组合修改就好。

比如,我想要显示两篇文章组成的本子,同时每篇文章只显示前三个章节名称以及列表以及显示全部的按钮,那么我只需要在前面的 {{ if le (len .Pages) 1}}{{ else }} 中间加如下代码:

go-html-template
 1{{ else  if le (len .Pages) 2}}
 2  {{ range .Pages.ByDate }}
 3  
 4  <h2><a class="" href="{{ .Permalink }}">{{ .Title }} | {{.Date.Format "2006-01-02" }}</a></h2>
 5  
 6  <ul class="post-list">
 7  {{ range .Pages.ByDate | first 3 }}
 8    <li class="post-item">
 9      <div class="meta">
10      <time datetime="{{ time .Date }}" itemprop="datePublished">{{ .Date.Format (.Site.Params.dateFormat | default "2006-01-02") }}</time>
11      </div>
12      <span>
13      {{ if .Params.chapter_Num }}{{ .Params.chapter_Num }}章 | {{ end }}
14      <a class="" href="{{ .Permalink }}">{{ if .LinkTitle }} {{ .LinkTitle }} {{ else }} Untitled {{ end }}</a>
15       </span>
16     </li>
17   {{ end }}
18
19   {{ if gt (len .Pages) 3 }}
20     <div class="more-link">>>>
21       <a href="{{ .Permalink }}">查看全部 See All</a><<<</div>
22   {{ end }}
23   </ul>
24   {{ end }}

这一段代码里一篇文章最多显示 3 章的目录,算是一个延伸。

额外 Extra 之思考:

在这个页面的单篇文章的章节列表部分,我曾经想过用 partial 的方式,通过模板化,来把章节列表进行分页呈现,在这个小模块里面允许读者翻页。我其实部分实现了想要的效果。效果如下:

fanfic_series_in_page_list_test

我的操作是:在 series.html 模板页加入 partial 模板。

go-html-template
1{{ if le (len .Pages) 1}}
2
3  {{ range .Pages }}
4    {{ partial "in-page-list.html" . }}
5  {{ end }}
6{{ else }}
7……

之后在 partials 文件夹里面新建 in-page-list.html,加入如下代码:

go-html-template
 1 <ul class="post-list">
 2   {{ $pages := .Paginate .Pages.ByDate }}
 3   {{ range $pages.Pages }}
 4      <li class="post-item">
 5      <div class="meta"><time datetime="{{ time .Date }}" itemprop="datePublished">{{ .Date.Format (.Site.Params.dateFormat | default "2006-01-02") }}</time></div>
 6      <span>
 7      {{ if .Params.chapter_Num }}{{ .Params.chapter_Num }}章 | {{ end }}
 8      <a class="" href="{{ .Permalink }}">{{ if .LinkTitle }} {{ .LinkTitle }} {{ else }} Untitled {{ end }}</a>
 9      </span>
10      </li>
11  {{ end }}
12  {{ partial "pagination.html" . }}
13</ul>

看起来都很好,但是问题出现在我点击下一页按钮的时候。这时候,页面会显示 Page Not Found,而看地址,我原来网页的地址为 http://localhost:1313/series/结局之后/,点击跳转后地址为 http://localhost:1313/posts/2020-结局之后/page/2/

而之所以这个地址会显示 Page Not Found,是因为我的《结局之后》这篇文的目录页本身没有分页,只有一个页面,地址为 http://localhost:1313/posts/2020-结局之后/。我发现,当我把我的《结局之后》文章的章节目录页代码改为有分页的时候,点击按钮就会直接跳转到文章的章节目录的第 2 页,而不是在原本页面的框架下只把列表部分替换为列表的第 2 页。

为此,我查阅了一下文档→ Pagination

Regardless of pagination method, the initial invocation is cached and cannot be changed. If you invoke pagination more than once for a given list page, subsequent invocations use the cached result. This means that subsequent invocations will not behave as written.

一个页面集合的分页方式和产生的相关链接是固定的,分页一次后结果就缓存了起来了,链接就是分页页面的后面加上如 /page/2/ 这种来显示。如果对一个页面集合进行多次分页,后续的分页操作都会沿用第一次分页缓存的结果。而因为我 Series 模板的章节列表分页行为是在处于《结局之后》这篇文目录页面的层级来进行分页的,它只会调用第一次的分页结果。 那么也就不奇怪为啥我点击按钮的时候会跳转到如右边的界面了,http://localhost:1313/posts/2020-结局之后/page/2/

显然,我不可能舍弃我的文章章节目录页来做这个本子的框架,所以我这个设想最终功亏一篑。我也曾经想过换一个分页方式,但是尝试后没有成功,所以这个设想就只能遗憾止步于此了。(而且想了一下如果是本子有两篇文章,那显示两个分页还有点怪,也不算太遗憾……大概。)

如果有朋友有办法达到我想做的效果欢迎在评论区提出?(←贼心不死

3 SCSS 样式

Series 页面比较重要的结构就是这些!这里先放一下页面除了瀑布流图片相关的 scss 样式。

scss
 1.book-Showpage{
 2  margin-top: 0;
 3  display: flex;
 4  align-items: start;
 5  width: 100%;
 6}
 7
 8.book-Showpage-SeriesCover{
 9  display: block;
10  left: 0;
11  margin-top: 3rem;
12  border-radius: $border-radius;
13  -o-object-fit: cover;
14  object-fit: cover;
15  width: 22rem;
16  height: auto;
17  box-shadow: 0 1px 5px #999999;
18}
19
20.book-Showpage-text{
21  display: block;
22  margin-left: $padding-32;
23  margin-top: $padding-4;
24  .subtitle {
25    text-align: right;
26  }
27  .post-list .post-item .meta{
28      margin-right: 0 !important;
29  }
30  p{
31    text-indent: 0 !important;
32    line-height: 1.2;
33  }
34  .ShopLink{
35    color: $color-link;
36  }
37  h2 {
38    margin-top: 1rem;
39    margin-bottom: .25rem;
40  }
41}
42
43@media (max-width: 768px){
44  .book-Showpage{
45    display:block;
46  }
47  .book-Showpage-SeriesCover{
48    display: block;
49    width: 60%;
50    margin: 0 auto;
51  }
52  .book-Showpage-text{
53    text-align: center;
54    margin-left: 0rem;
55    margin-top: $padding-16;
56    p{
57      text-align: center;
58    }
59    li{
60      text-align: center !important;
61    }
62  }
63}
4 短代码实现的瀑布流图片展示

其实现在网上有很多的,呃,瀑布流图片展示的 gallery 教程。但为什么我自己要从头搓,因为我这个图片不是想单纯展示的,我还想标识 credits 和原作者,并且阐述这些图片和文本创作背后的联系和表达,等等等。总之就是我有很多废话想讲,于是这就让我的图片展示不单纯只是给大家看我 CP 美图,而需要我单独去 address 每一张图。

于是我的需求就是比起用一个自动化的模板瀑布流展示图片,一个有两层嵌套结构的短代码更能符合我的需求。而我也希望我的图片不仅是瀑布流排列,还要能够点开放大显示文本信息。

我最后使用的方案是调用 bootstrap 库的瀑布流实现方式,配上 fancybox 的灯箱效果来进行实现。

瀑布流的实现方法我是参考的这个:Tutorial - Creating Masonry/Pinterest style image gallery with Hugo and Bootstrap 5 | Front End Dev

Fancybox 的使用方法可以看文档:Fancybox | Fancyapps

有探索精神的朋友们可以自己拼拼图!很好拼的。还可以锻炼对层级拆分的理解!拼完了可以来和我对答案(?

不喜欢拼的朋友继续看。嗯。总之首先第一步还是要调用库,先在 head.html 里引用 bootstrap 和 fancybox…… bootstrap 似乎也有允许下载的,如果不喜欢自己的博客必须联网才能显示一些样式也可以下载,fancybox 有没有大家自己看吧我忘了(忘了!)

go-html-template
1<script src="https://cdn.jsdelivr.net/npm/@fancyapps/[email protected]/dist/fancybox/fancybox.umd.js"></script>
2
3<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/[email protected]/dist/fancybox/fancybox.css"/>
4
5<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">

如果觉得自己不是每一页都要调用这些库可以加一些 if else 判断句来限制引用的情况。

接下来创建短代码模板。在 layouts/shortcodes 里面创建两个 html 文件。masonry-gallery.html 用来放相册的包裹层,标识这一系列的图片属于一个瀑布流的 gallery 以及附加灯箱效果,masonry-img.html 用来放单个图片的格式、文字说明。

masonry-gallery.html 代码如下:

go-html-template
 1<div class="{{ .Get "class" }}">
 2  <div class="row" data-masonry='{"percentPosition": true }'>
 3  {{ .Inner }}
 4  </div>
 5
 6  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
 7<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/masonry.pkgd.min.js" integrity="sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D" crossorigin="anonymous" async></script>
 8
 9<script>
10    Fancybox.bind("[data-fancybox]", {
11      compact: false
12  });
13</script>
14
15</div>

嗯可以看到常规的短代码格式后面跟了一个 bootstrap 引用一个 masonry-layout 的引用,然后绑了一套 fancybox 告诉程序这些图片都要灯箱。

masonry-img.html 代码如下:

go-html-template
 1{{- $src := .Get "src" -}}
 2{{- $caption := .Get "caption" -}}
 3
 4{{- $baseURL := .Site.BaseURL -}}
 5{{- $src1 := print $baseURL "/" $src -}}
 6{{- $src2 := print "/static/" $src -}}
 7
 8{{ $ic := images.Config $src2 }}
 9{{ $width := $ic.Width }}
10{{ $height := $ic.Height }}
11
12{{- with .Parent -}}
13<div class="col-6 col-md-4 col-xl-3 pb-3 px-2">
14<a href="{{ $src1 }}" data-fancybox="series-gallery" data-caption="{{ $caption }}">
15<img src="{{ $src1 }}"
16      width="{{ $width }}"   height="{{ $height }}"
17      alt="Gallery Image" class="img-fluid"></a>
18      </div>
19{{- else -}}
20  <img src="{{.Get "src" }}">
21{{- end -}}

其实这一段我写的时候很明显的发现是我直接用类似于 {{ .Permalink }} 或者 {{ .Width }} 等参数的时候会报错,会找不到,会没法直接引用放进来的资源的数据。推测是因为短代码本身是封装的,所以不会在拿到资源地址的时候自动传递资源的 context 进代码,所以需要我们直接声明资源的信息。

所以在这里可以看到我在强行写怎么取资源的地址,手动拼出来了一个告诉程序去哪里找资源的信息(宽和高),之后再传递到代码里的这样一个过程。宽高之所以需要是因为,在瀑布流展示的时候其实是希望图片宽度固定,然后保持原本图片的长宽比,通过这样一个流程来做出瀑布流的,所以需要直接传进去图片本身的长宽,比较好计算。

好了,现在短代码的部分结束了,我们来看这个短代码怎么用。在你想要呈现瀑布流图片库的页面对应的 markdown 文件里放入如下示例的代码。

md
1{。{< masonry-gallery class="masonry-gallery" >}}
2
3{。{< masonry-img src="img/series/TheAftermaths/cover.jpg" caption="<h1>封面:结局之后</h1>排版:米画师@清明雨Cis <hr><p>这是我最早开始动笔的一个长篇完整的同人文,刚开始写的时候还很青涩,整个人精神状态也很差,充满了自我怀疑,而且从现在回看文笔上也确实有很多缺点。</p>" >}}
4
5{。{< masonry-img src="img/series/TheAftermaths/backcover.jpg" caption="<h1>封底</h1>排版:米画师@清明雨Cis <hr><p>我当时给自己的目标定的是每个月更新一章,因此这篇文也是陪伴我时间最长的一篇文,帮助我走过了很长的时间,也算是记录了我的成长。到现在我仍然很感激我能写下、写完这篇文,所以最后决定出本。出本的时候修订了很多文本,把本子从10万字删节到6万多,改得很痛苦,但最后效果我比较满意。也希望大家享受这篇文。</p>" >}}
6
7{。{< /masonry-gallery >}}

还是老规矩,复制后使用时需要把每个 {。{< 里的中文句号删除,使得每行开头变成短代码标准的标签开头。

需要注意的是:

  1. 我的图片是全放在 /static 文件夹里的,所以这里填的图片实际上的路径地址是 /staticimg/series/TheAftermaths/cover.jpg
  2. 比较好的是 fancybox 的这个 caption 渲染是可以保留 html 标签的,所以写的时候可以自己加格式进去,不会显得那么 plain。

最后附上一个我自己调试的 SCSS 样式。需要注意的仍然是 cactus 主题的 scss 自带变量,注意替换调整。

scss
 1//瀑布流展示照片
 2.masonry-gallery{
 3  text-align: center;
 4  margin-top: $padding-32;
 5  padding: $padding-16;
 6  h1{
 7    text-align: center;
 8  }
 9  img{
10    border-radius: $border-radius;
11    box-shadow: 0 1px 5px #999999;
12  }
13}
14
15.page-gallery{
16  margin-top: $padding-16;
17  h1{
18    text-align: center;
19  }
20}
21
22//灯箱效果的custom修改
23.fancybox__slide.has-caption {
24  flex-direction: row;
25}
26
27.fancybox__content{
28  max-height: 100%;
29  max-width: 100%;
30}
31
32.fancybox__caption {
33  min-width: 200px;
34  max-width: 350px;
35  height: auto;
36  word-wrap: break-word;
37  padding: 1rem 1rem 1rem 1rem;
38  p{
39    margin-block-start: $padding-1;
40    line-height: 1.5;
41  }
42}
43
44@media (max-width: 768px){
45  .fancybox__slide.has-caption {
46    flex-direction: column;
47  }
48  .fancybox__caption {
49    word-wrap: break-word;
50    padding: 1rem 1rem 1rem 1rem;
51    h1{
52      margin-top: 0;
53      margin-bottom: 0;
54      font-size: $font-size;
55    }
56    hr{
57      margin-top: 0;
58      margin-bottom: 0;
59    }
60    p{
61      margin-top: 0;
62      font-size: 0px;
63      margin-bottom: 0;
64    }
65  }
66}

恭喜!结束了

呃,没有全部结束。只是结束了所有的,分类页面的装修。辛苦了!过去的我!(……)和看到现在的大家!

下次应该写一些章节和单独文章页面的装修的 features,也会涉及到和 taxonomy 相关的内容,所以觉得先写 taxonomy 的简单一点,不过下次更新不知道又是猴年马月了……

鼓掌鼓掌!再见再见!