提前过一下愚人节

3k 5 mins
... ...

  虽然四月还没到但我把 blog 的发送时间设成四月了,至于为什么要这么做,请看我新折腾出来的超级可爱的:归档日历
  我觉得我真的可以写一篇很长很长的 blog 记录一下试图奴役 chatGPT 给我写代码的这大半个月……我觉得我才是那个被奴役的人,真的【
  但不是今天,今天还是先记录一下我都干了些什么【。

img-right

  看归档页面不爽很久了【?
  其实这个主题的框架(已经被我改得面目全非的 chan)完全没有问题,只是我太话痨了,建站一个月不到文章数直逼 30,虽然开学以后更新的频率大概会变得正常一点真会吗,但一整年保守估计至少也要 50 篇,在这种情况下一整个的列表实在是没办法满足我「希望在一年结束的时候可以对着 blog 幸福地【?回忆我每个月的点点滴滴」的愿望,所以我决定进行文章列表的拆分。

  最开始的拆分方案是为每个月份添加一个小标题,然后生成目录点击跳转,想想这样页面还是太长了于是放弃了。
  第二个拆分方案是根据月份翻页,但查了下文档 hexo 的翻页器好像只支持根据数量翻页,要实现这个功能有点太复杂了……以及想到后面如果犯懒搞不好也会有一个月只写一两篇的情况,这也单独翻一页就有点难看了【。于是这个方案也被我放弃了。
  最后采用的的方案是:在年归档页面添加指向月归档的链接,在月归档页面里沿用原先的列表样式。完成基本功能以后灵机一动,又把空白的月份也加上了,于是这就变成了一个非常美丽的日历页面……

速成版(伪)

  虽然这个页面折腾了我四个小时,但如果你是个极简主义者而不是像我一样在折腾 blog 的过程中逐渐膨胀出了无法控制的欲望,那么实现这个功能其实只需要一行代码:

1
<%- list_archives([options]) %>

  是的 hexo 其实自带实现「在年归档页面显示月归档子页面列表」的辅助函数,你只需要为年归档页面和月归档页面指定不同的 layout 就可以……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<% if (is_month()) { %>
...
// 在这里放上你原本的归档 layout 文件
<% } else { %>

<% var last; %>
// 我其实没太搞懂这一句是干什么用的,但是不赋这个值会生成一长串标题
<% page.posts.each(function(post){ %>
<% var year = post.date.year(); %>
<% if (last != year){ %>
<% last = year; %>
<div class="archive-year-wrap">
<h2><%= year %></h2>
</div>
<% } %>
<% } %>

<%- list_archives() %>

<% } %>

  这么写完以后刷新年归档页面应该就会出现一个丑丑的月归档列表了,稍微改改 css 也是可以做得很好看的!
  那么我为什么还要折腾这四个小时呢,自然是因为在折腾 blog 的过程中逐渐膨胀出了无法控制的欲望……

  不对我突然反应过来这个列出来的好像不是「在年归档页面显示月归档子页面列表」,而仅仅是「按月显示所有归档页面的列表」,其它年份的月归档页面也会一起列进来。嗯……或许可以加一些 JS 获取标题年份后把年份不匹配的页面链接删除,总之大体上来看还是比我的最终方案简单。
  但如果你还是好奇我做了什么的话↓

基本实现过程

  嗯其实这一段和对象 / 数组相关的代码基本都是 chatGPT 写出来的,我主要负责向它提问题和扣问号。虽然我是个诚实的魔女,不会否认它为最终成果的实现做出的贡献,但我还是要说:一遍又一遍地翻报错 logs 排除你瞎编的 108 个变量名实在是太·痛·苦·了!
  以及我今天才发现原来 ejs 里连续写 JS 代码是不需要每一行都打一对 <% %> 的,但是省略以后的写法不太好对齐缩进,所以演示里还是全打上了【

文章分类收纳

  总之为了创建月分类的链接,我们要把年存档下的所有文章按月分类,而分类之前需要创建一个 对象 用于收纳分类的文章:

1
<% var archives = {}; %>

  这个操作可以理解为新建一个名为 archives文件夹
  然后遍历每一篇文章,获取它们的年份和月份,再按照这两个数据将它们分类:

1
2
3
4
5
6
7
8
9
10
11
12
<% page.posts.forEach(function(post) { %>
<% var year = post.date.year(); %>
// 获取文章年份,设为变量 year
<% var month = post.date.month(); %>
// 获取文章月份,设为变量 month
<% if (!archives[year]) archives[year] = {}; %>
// 检查「archives」内是否有对应「当前文章年份」的文件夹,如果没有,就创建一个
<% if (!archives[year][month]) archives[year][month] = []; %>
// 检查「archives/当前文章年份」内是否有对应「当前文章月份」的文件夹,如果没有,就创建一个
<% archives[year][month].push(post); %>
// 将文章放入对应的文件夹中
<% }); %>

  操作完毕之后我们会得到一个这样的对象,或者可以理解为文件夹的结构

1
2
3
4
5
6
{
2025: {
0: [ {文章 1}, {文章 2} ],
1: [ {文章 3} ]
}
}

  这么一看因为是在年归档页面里执行的代码,所以并不会出现多个不同的年份,生成年份文件夹的这一步好像是多余的……算了不管了先这样吧【。
  当然这里的文章 1234 其实是更加复杂的对象,包含了你在 markdown 内写过的所有信息(比如标题、分类、文章内容),以及由 hexo 生成的那部分(比如文章的 url)。但具体包含了什么并不重要,只要记住 post 加入对象后依然可以像加入前一样自由调用任何变量就可以。

创建月归档链接

  接下来要做的就是把「月份」相关的文件夹的信息显示在页面上,因为这段代码的逻辑非常复杂所以有些注释要写到代码框之外……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<% var monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; %>
// 注释 1-1
<% Object.keys(archives).forEach(function(year) { %>
// 在「archives」文件夹中遍历每一个对象,并存储为变量「year」
<h2><%- year %></h2>
// 显然这里只有一个 year,用它来生成页面标题
<ul>
<% Object.keys(archives[year]).forEach(function(monthIndex) { %>
// 在「archives/年份」文件夹中遍历每一个对象,并存储为变量「monthIndex」
<% var monthName = monthNames[monthIndex]; %>
// 注释 1-2
<% var monthNum = String(Number(monthIndex) + 1).padStart(2, '0'); %>
//注释 2-1
<li>
<a href="<%- url_for('/archives/' + year + '/' + monthNum) %>">
//注释 2-2
<%- monthName %>
// 注释 1-3
</a>
</li>
<% }); %>
</ul>
<% }); %>
注释 1

  为了显示完整的月份名而不是数字,我们需要建立一个数组来储存每个月份的全名,再将月份的数值转换为编号调用。

  • 1-1:如果想显示中文的话可以改成 "一月", "二月", "三月"
  • 1-2:这里获得的 monthIndex 是从「0」开始计算的,即「一月」对应的数字是「0」,「二月」对应的数字是「1」;
  • 1-3:显示完整的月份名。
注释 2

  我的 Hexo 自动生成的月归档页面链接是类似「2025/03」这样的,所以需要将获得的 monthIndex 进行处理,转换成正确的格式。

  • 2-1:padStart() 可以用指定内容将字符串填充至指定长度,这里的 .padStart(2, '0') 即用「0」将字符串填充至两位;
    在填充前需要用 String() 来将 monthIndex 从数值转换为字符串。
    monthIndex 是从「0」开始计数的,需要在原值的基础上 +1 才能获得需要的数字;
    而直接写 String(monthIndex + 1) 的话,两个数值会被当作两个字符串拼接,如三月的数值会输出「21」;
    所以要在 String() 里再包裹一层 Number() 确保以数值的形式进行运算再输出。
  • 2-2:正确处理完数据之后拼接成 url 即可,但因为是在年归档页面上的链接,这里也可以直接写成 href="<%- monthNum %>"

进阶选项

  复制上面的代码之后你就可以获得一个自由的月分类链接了!接下来是一些进阶选项,需要对上述的代码进行一些比较繁琐的修改,建议在完全理解基本原理之后再阅读。

显示月分类文章数

  这个不算很难,但是哎呀先坑着明天再写【。
  最后隔了三天才来,差点忘记自己写了什么了……

  因为在上面的代码中已经将所有的文章按月分类到对象中了,所以想要显示分类下有多少文章的话,只需要获得这个对象的长度即可。
  在 function 的代码中添加一行:

1
<% var postCount = archives[year][monthIndex].length; %>

  之后在月份名之后用 postCount 调用即可:

1
<span class='count'><%- postCount %></span>

  处理完毕后的代码大概长这样:

1
2
3
4
5
6
7
8
9
10
11
<% Object.keys(archives[year]).forEach(function(monthIndex) { %>
<% var monthName = monthNames[monthIndex]; %>
<% var monthNum = String(Number(monthIndex) + 1).padStart(2, '0'); %>
<% var postCount = archives[year][monthIndex].length; %>
<li>
<a href="<%- url_for('/archives/' + year + '/' + monthNum) %>">
<%- monthName %>
</a>
<span class='count'><%- postCount %></span>
</li>
<% }); %>

显示空白月份

  这个真的很复杂,也是哎呀先坑着明天再写【。
  嗯这一段也是坑了三天才来的【

  在之前的代码里我们是通过「查询已有的文件夹」的形式来创建页面元素的,但如果希望显示空白的月份的话,创建的元素就成了固定的 12 个。所以这个方案不是再通过查询「文件夹」来创建元素,而是先创建元素,再查找「文件夹」中是否有匹配的对象。
  具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<% for (var i = 0; i < 12; i++) { %>
// 计数 12 次
<% var monthName = monthNames[i]; %>
<% var monthNum = String(i + 1).padStart(2, '0'); %>
<% var postCount = archives[year][i] ? archives[year][i].length : 0; %>
// 注释 1
<% if (postCount > 0) { %>
// 如果「postCount」大于 0,则是有文章的月份,继续生成正常元素
<li>
<a href="<%- url_for('/archives/' + year + '/' + monthNum) %>">
<%- monthName %>
</a>
<span class='count'><%- postCount %></span>
</li>
<% } else { %>
// 否则就是空白月份
...
// 设计空月份的元素,这里就不需要做链接了
<% } %>
<% } %>
注释 1

  这一行代码的作用是「查询 archives[year] 中是否存在 [i],如果存在则返回 archives[year][i].length,如不存在则返回 0」。
  我一开始很疑惑,每年不空余的月份应该是随机数量的,怎么就能保证 0~11 的数字编号一定能匹配到对应的月份呢?
  后来想了一下这里的 [i] 应该被自动转换成字符串了,也就是说它是通过匹配「文件夹」的「标题」是否与 i 对应(即查询对象内是否存在与 i 同名的 key)来判断的,所以一定会指向对应的月份。

  总之如此修改之后空白的月份应该就出现了!或许还可以举一反三地做出月历界面,不过月历似乎需要研究一下怎么区分每个月的天数的问题……【怎么感觉我又挖新坑了?

折腾预告

  目前年归档与月归档页面直接的互动还不是很顺畅,需要在月归档页面添加回到年归档的链接,如果能在非空余的月归档页面之间直接切换上一月下一月就更好了……
  需求真是越来越刁钻了呢!