前段时间写的一个基于 Cloudflare Workers 搭建 Vless 节点访问 ChatGPT 的文章部分内容失效了,节点还是可以正常翻墙,但是由于代码中的 ProxyIP 失效了,所以无法访问 ChatGPT 的网站了,其实是所有托管在 Cloudflare 的网站都访问不了了。这周末更新了一下那篇文章中的代码,算是短暂修复了。

这个问题不是本文要讨论的内容,但是它让我想到我需要对文章内容的时效性加个必要的“免责声明”。

其实文章过期提醒这个 Feature 在很多博客主题中都有,但是我使用的 hexo-theme-yun 已经进入了维护阶段。
所以我需要自己动手实现这个功能。

在这篇博客中,我将会介绍如何在 hexo-theme-yun 主题中添加文章过期提醒功能。

效果如下:

文章过期提醒

A while ago I wrote an article about building a Vless node with Cloudflare Workers to access ChatGPT, and part of its content has gone stale. The node itself can still bypass the firewall fine, but the ProxyIP in the code stopped working, so I could no longer reach ChatGPT — in fact, none of the sites hosted on Cloudflare could be reached. This weekend I updated the code in that article, which is a temporary fix.

That problem isn’t what this article is about, but it made me realize I should add a necessary “disclaimer” about the timeliness of my articles.

Actually, an article expiration notice is a feature that exists in many blog themes, but the hexo-theme-yun I use has entered maintenance mode.
So I had to implement this feature myself.

In this post, I’ll show you how to add an article expiration notice to the hexo-theme-yun theme.

Here’s what it looks like:

Article expiration notice

实现原理

实现原理本身很简单,就是在渲染文章的时候获取到文章的更新时间,然后根据这个时间判断文章是否需要过时提醒。

再加上现在有 AI 的加持,即使是我这种没学过前端的人也能在一天内实现这个功能。

其实,30 分钟就够了,但是我用了一天的时间。其中 80% 的时间用来调试 CSS,这东西真是太恶心了。

我对 yun 主题的架构不是很了解,不过在摸索了一番后,我觉得所有文章以及页面的渲染都会在 post-content.pug 中进行。

所以我把代码加在这个文件里,但请注意这可能不是最好的解法。

实现步骤

总体的设计参考了 butterfly 主题的文章过期提醒功能

  1. 所以,首先在主题的配置文件中加入下面的配置。
1
2
3
4
5
6
7
8
# 文章过期提醒
expire_notice:
enable: true
days: 90
text_before: 注意:这篇文章上次更新于
text_after: 天前,文章内容可能已经过时。
except_tags:
- 结绳记事
  • enable:是否启用文章过期提醒功能。
  • days:文章过期提醒的天数。
  • text_before:文章过期提醒的前缀。
  • text_after:文章过期提醒的后缀。
  • except_tags:不需要过期提醒的标签。

我的博客中没有使用分类功能,我觉得分类和标签的区别不大,两个都用的话会显得有些多余,所以我只使用了标签功能。
于是我就想到实际上不是所有的文章都需要加入过期提醒功能,所以我加入了 except_tags 这个配置项。

  1. 然后修改 post-content.pug 为下面的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
section.post-body(itemprop='articleBody')
//- Outdated notice
- var isExceptTag = false;
- if (post && post.tags) {
- isExceptTag = post.tags.some(tag => theme.expire_notice.except_tags.includes(tag.name));
- }
- var oneDay = 24*60*60*1000; // hours*minutes*seconds*milliseconds
- var firstDate = post && post.updated ? new Date(post.updated) : new Date();
- var secondDate = new Date();
- var diffDays = Math.round(Math.abs((firstDate - secondDate) / oneDay));

if theme.expire_notice.enable && !isExceptTag && diffDays > theme.expire_notice.days
div.expire_notice
p.expire_notice_p #{theme.expire_notice.text_before}
span.diff-days #{diffDays}
span.text-after #{theme.expire_notice.text_after}

//- body include reward, content is only post content
.post-content.markdown-body!= page.content

这段代码首先拿到了一些配置项,比如文章的标签是否在 except_tags 中,文章的更新时间,然后计算了文章更新时间和当前时间的差值。

然后根据这些值判断是否需要显示过期提醒。

  1. 最后,加入 CSS 样式。

我选择自己为这个功能写一个 CSS 文件,然后在主题的配置文件中引入这个 CSS 文件。

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
36
37
38
.expire_notice {
position: relative;
margin: 20px 0 20px 0;
padding: 15px;
border-radius: 3px;
font-size: 16px;
background: #f5f5f5; /* 灰色系列的背景颜色 */
color: #616161; /* 灰色系列的文字颜色 */
display: flex;
flex-wrap: wrap;
align-items: flex-start;
border-left: 5px solid #9e9e9e; /* 灰色系列的左边框 */
}
.expire_notice::before {
content: ""; /* 清空 content 属性 */
display: inline-block;
width: 1.25em;
height: 1.25em;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+PHBhdGggZmlsbD0iI2YyYTYwMCIgZD0ibTU3LjE2IDguNDJsLTUyIDEwNGMtMS45NCA0LjAyLS4yNiA4Ljg1IDMuNzUgMTAuNzljMS4wOC41MiAyLjI1LjggMy40NS44MWgxMDRjNC40Ni0uMDQgOC4wNS0zLjY5IDguMDEtOC4xNWE4LjEyMyA4LjEyMyAwIDAgMC0uODEtMy40NWwtNTItMTA0YTguMDY3IDguMDY3IDAgMCAwLTE0LjQgMCIvPjxwYXRoIGZpbGw9IiNmZmNjMzIiIGQ9Im01My41NiAxNS43MmwtNDguOCA5Ny40Yy0xLjgzIDMuNzctLjI1IDguMzEgMy41MiAxMC4xNGMuOTkuNDggMi4wOC43NCAzLjE4Ljc2aDk3LjVhNy41NSA3LjU1IDAgMCAwIDcuNDgtNy42MmE3LjYwNSA3LjYwNSAwIDAgMC0uNzgtMy4yOGwtNDguNy05Ny40YTcuNDQzIDcuNDQzIDAgMCAwLTkuOTMtMy40N2E3LjQ4NCA3LjQ4NCAwIDAgMC0zLjQ3IDMuNDciLz48cGF0aCBmaWxsPSIjNDI0MjQyIiBkPSJNNjQuMzYgMzQuMDJjNC42IDAgOC4zIDMuNyA4IDhsLTMuNCA0OGMtLjM4IDIuNTQtMi43NCA0LjMtNS4yOCAzLjkyYTQuNjQ2IDQuNjQ2IDAgMCAxLTMuOTItMy45MmwtMy40LTQ4Yy0uMy00LjMgMy40LTggOC04bTAgNjRjMy4zMSAwIDYgMi42OSA2IDZzLTIuNjkgNi02IDZzLTYtMi42OS02LTZzMi42OS02IDYtNiIgb3BhY2l0eT0iMC4yIi8+PGxpbmVhckdyYWRpZW50IGlkPSJub3RvV2FybmluZzAiIHgxPSI2OCIgeDI9IjY4IiB5MT0iLTE4MDguMzYiIHkyPSItMTg4Ny4wNSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAtMy42NCAtMTc3Ni4wOSkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM0MjQyNDIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyMTIxMjEiLz48L2xpbmVhckdyYWRpZW50PjxwYXRoIGZpbGw9InVybCgjbm90b1dhcm5pbmcwKSIgZD0iTTY0LjM2IDM0LjAyYzQuNiAwIDguMyAzLjcgOCA4bC0zLjQgNDhjLS4zOCAyLjU0LTIuNzQgNC4zLTUuMjggMy45MmE0LjY0NiA0LjY0NiAwIDAgMS0zLjkyLTMuOTJsLTMuNC00OGMtLjMtNC4zIDMuNC04IDgtOCIvPjxsaW5lYXJHcmFkaWVudCBpZD0ibm90b1dhcm5pbmcxIiB4MT0iNjQuMzYiIHgyPSI2NC4zNiIgeTE9Ii0xODA4LjM2IiB5Mj0iLTE4ODcuMDUiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgMCAtMTc3Mi4xMSkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM0MjQyNDIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyMTIxMjEiLz48L2xpbmVhckdyYWRpZW50PjxjaXJjbGUgY3g9IjY0LjM2IiBjeT0iMTA0LjAyIiByPSI2IiBmaWxsPSJ1cmwoI25vdG9XYXJuaW5nMSkiLz48cGF0aCBmaWxsPSIjZmZmMTcwIiBkPSJNNTMuNTYgMjMuMDJjLTEuMiAxLjUtMjEuNCA0MS0yMS40IDQxcy0xLjggMyAuNyA0LjdjMi4zIDEuNiA0LjQtLjMgNS4zLTEuOHMxOS4yLTM2LjkgMTkuOS0zOC42Yy42LTEuODcuMTgtMy45MS0xLjEtNS40Yy0xLjMtMS4yLTIuNi0xLTMuNC4xIi8+PGNpcmNsZSBjeD0iMzEuMzYiIGN5PSI3NS4zMyIgcj0iMy4zIiBmaWxsPSIjZmZmMTcwIi8+PC9zdmc+"); /* 设置图标 */
background-size: contain; /* 调整图标的大小 */
background-repeat: no-repeat; /* 防止图标重复 */
background-position: center; /* 调整图标的位置 */
align-self: center;
}
.expire_notice_p {
margin: 0 5px; /* 移除默认的段落边距 */
color: #616161; /* 灰色系列的文字颜色 */
flex: 1;
}
.diff-days {
color: #d81e06; /* 红色文字 */
font-weight: bold;
font-size: 20px; /* 更大的字体大小 */
margin: 0 5px 0 5px;
}
.text-after {
flex: 1; /* 这将使元素占据剩余的空间 */
}

然后在主题的配置文件中引入这段 CSS 就 OK 了。

1
2
3
4
# custom your assets in head
head:
css:
myCss: /css/my.css

至此,这个小功能就实现完成了。效果如开篇图所示。

还是前端好,所见即所得,自己学了什么就能立马写出来看到效果。

不像我现在,干了一年的 WLAN Driver,WLAN 也不懂,Driver 也不会。😅

How it works

The principle is quite simple: when rendering a post, grab the post’s update time and use it to decide whether the post needs an expiration notice.

With AI around to help, even someone like me who never learned frontend can implement this feature within a day.

Actually, 30 minutes would have been enough, but it took me a whole day — 80% of that time was spent debugging CSS. That stuff is just awful.

I’m not very familiar with the yun theme’s architecture, but after poking around, I believe all posts and pages are rendered in post-content.pug.

So I added my code to that file. Note that this may not be the best solution, though.

Implementation steps

The overall design is inspired by the article expiration notice of the butterfly theme.

  1. First, add the following configuration to the theme’s config file.
1
2
3
4
5
6
7
8
# Article expiration notice
expire_notice:
enable: true
days: 90
text_before: Note: this article was last updated
text_after: days ago, the content may be outdated.
except_tags:
- 结绳记事
  • enable: whether to enable the article expiration notice.
  • days: after how many days a post is considered expired.
  • text_before: the prefix of the notice.
  • text_after: the suffix of the notice.
  • except_tags: tags that don’t need the notice.

My blog doesn’t use categories — I think categories and tags aren’t that different, and using both feels redundant, so I only use tags.
That made me realize not every article needs the expiration notice, so I added the except_tags option.

  1. Then modify post-content.pug as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
section.post-body(itemprop='articleBody')
//- Outdated notice
- var isExceptTag = false;
- if (post && post.tags) {
- isExceptTag = post.tags.some(tag => theme.expire_notice.except_tags.includes(tag.name));
- }
- var oneDay = 24*60*60*1000; // hours*minutes*seconds*milliseconds
- var firstDate = post && post.updated ? new Date(post.updated) : new Date();
- var secondDate = new Date();
- var diffDays = Math.round(Math.abs((firstDate - secondDate) / oneDay));

if theme.expire_notice.enable && !isExceptTag && diffDays > theme.expire_notice.days
div.expire_notice
p.expire_notice_p #{theme.expire_notice.text_before}
span.diff-days #{diffDays}
span.text-after #{theme.expire_notice.text_after}

//- body include reward, content is only post content
.post-content.markdown-body!= page.content

This code first reads some config values — for instance, whether the post’s tags are in except_tags, and the post’s update time — then computes the difference between the update time and now.

Based on those values it decides whether to show the expiration notice.

  1. Finally, add the CSS styles.

I chose to write a separate CSS file for this feature and include it via the theme’s config.

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
36
37
38
.expire_notice {
position: relative;
margin: 20px 0 20px 0;
padding: 15px;
border-radius: 3px;
font-size: 16px;
background: #f5f5f5; /* Gray background */
color: #616161; /* Gray text */
display: flex;
flex-wrap: wrap;
align-items: flex-start;
border-left: 5px solid #9e9e9e; /* Gray left border */
}
.expire_notice::before {
content: ""; /* Clear the content property */
display: inline-block;
width: 1.25em;
height: 1.25em;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+PHBhdGggZmlsbD0iI2YyYTYwMCIgZD0ibTU3LjE2IDguNDJsLTUyIDEwNGMtMS45NCA0LjAyLS4yNiA4Ljg1IDMuNzUgMTAuNzljMS4wOC41MiAyLjI1LjggMy40NS44MWgxMDRjNC40Ni0uMDQgOC4wNS0zLjY5IDguMDEtOC4xNWE4LjEyMyA4LjEyMyAwIDAgMC0uODEtMy40NWwtNTItMTA0YTguMDY3IDguMDY3IDAgMCAwLTE0LjQgMCIvPjxwYXRoIGZpbGw9IiNmZmNjMzIiIGQ9Im01My41NiAxNS43MmwtNDguOCA5Ny40Yy0xLjgzIDMuNzctLjI1IDguMzEgMy41MiAxMC4xNGMuOTkuNDggMi4wOC43NCAzLjE4Ljc2aDk3LjVhNy41NSA3LjU1IDAgMCAwIDcuNDgtNy42MmE3LjYwNSA3LjYwNSAwIDAgMC0uNzgtMy4yOGwtNDguNy05Ny40YTcuNDQzIDcuNDQzIDAgMCAwLTkuOTMtMy40N2E3LjQ4NCA3LjQ4NCAwIDAgMC0zLjQ3IDMuNDciLz48cGF0aCBmaWxsPSIjNDI0MjQyIiBkPSJNNjQuMzYgMzQuMDJjNC42IDAgOC4zIDMuNyA4IDhsLTMuNCA0OGMtLjM4IDIuNTQtMi43NCA0LjMtNS4yOCAzLjkyYTQuNjQ2IDQuNjQ2IDAgMCAxLTMuOTItMy45MmwtMy40LTQ4Yy0uMy00LjMgMy40LTggOC04bTAgNjRjMy4zMSAwIDYgMi42OSA2IDZzLTIuNjkgNi02IDZzLTYtMi42OS02LTZzMi42OS02IDYtNiIgb3BhY2l0eT0iMC4yIi8+PGxpbmVhckdyYWRpZW50IGlkPSJub3RvV2FybmluZzAiIHgxPSI2OCIgeDI9IjY4IiB5MT0iLTE4MDguMzYiIHkyPSItMTg4Ny4wNSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAtMy42NCAtMTc3Ni4wOSkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM0MjQyNDIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyMTIxMjEiLz48L2xpbmVhckdyYWRpZW50PjxwYXRoIGZpbGw9InVybCgjbm90b1dhcm5pbmcwKSIgZD0iTTY0LjM2IDM0LjAyYzQuNiAwIDguMyAzLjcgOCA4bC0zLjQgNDhjLS4zOCAyLjU0LTIuNzQgNC4zLTUuMjggMy45MmE0LjY0NiA0LjY0NiAwIDAgMS0zLjkyLTMuOTJsLTMuNC00OGMtLjMtNC4zIDMuNC04IDgtOCIvPjxsaW5lYXJHcmFkaWVudCBpZD0ibm90b1dhcm5pbmcxIiB4MT0iNjQuMzYiIHgyPSI2NC4zNiIgeTE9Ii0xODA4LjM2IiB5Mj0iLTE4ODcuMDUiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgMCAtMTc3Mi4xMSkiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM0MjQyNDIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyMTIxMjEiLz48L2xpbmVhckdyYWRpZW50PjxjaXJjbGUgY3g9IjY0LjM2IiBjeT0iMTA0LjAyIiByPSI2IiBmaWxsPSJ1cmwoI25vdG9XYXJuaW5nMSkiLz48cGF0aCBmaWxsPSIjZmZmMTcwIiBkPSJNNTMuNTYgMjMuMDJjLTEuMiAxLjUtMjEuNCA0MS0yMS40IDQxcy0xLjggMyAuNyA0LjdjMi4zIDEuNiA0LjQtLjMgNS4zLTEuOHMxOS4yLTM2LjkgMTkuOS0zOC42Yy42LTEuODcuMTgtMy45MS0xLjEtNS40Yy0xLjMtMS4yLTIuNi0xLTMuNC4xIi8+PGNpcmNsZSBjeD0iMzEuMzYiIGN5PSI3NS4zMyIgcj0iMy4zIiBmaWxsPSIjZmZmMTcwIi8+PC9zdmc+)
background-size: contain; /* Adjust the icon size */
background-repeat: no-repeat; /* Prevent the icon from repeating */
background-position: center; /* Adjust the icon position */
align-self: center;
}
.expire_notice_p {
margin: 0 5px; /* Remove default paragraph margins */
color: #616161; /* Gray text */
flex: 1;
}
.diff-days {
color: #d81e06; /* Red text */
font-weight: bold;
font-size: 20px; /* Larger font size */
margin: 0 5px 0 5px;
}
.text-after {
flex: 1; /* Make this element take up the remaining space */
}

Then include this CSS in the theme’s config file, and that’s it.

1
2
3
4
# custom your assets in head
head:
css:
myCss: /css/my.css

And that’s the whole feature, as shown in the opening screenshot.

Frontend development is great — what you see is what you get. Whatever you learn, you can immediately write it out and see the result.

Not like me now: after a year of WLAN Driver work, I still don’t understand WLAN, and I can’t write a Driver either. 😅