本文中编写的内容不再维护,请查看新版教程

更新日志

2022/06/09

  1. 修复文章自我推荐的问题
  2. 相关推荐列表数量不够时随机选取文章填充

魔改思路

  默认的最新文章和相关推荐的生成方式是在构建博客时直接写入到html文件中,而我的SW缓存策略就导致更新博文时也要刷新全部或部分html缓存,以此更新最新文章和相关推荐列表(下文简称“列表”)。

  于是我想到了一个解决方案,我将列表有关的信息直接保存在一个json文件中,然后用户访问页面的时候读取这个json文件,再根据json文件编写列表。这样子的话每次更新博文的时候不就只需要更新修改的博文和这个json文件就可以了。

  接下来就是制定json格式,当前我是用的方案是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"info": {
"[abbrlink]": {
"title": "标题",
"img": "封面",
"time": "日期",
"sort": "最新文章列表用到的日期",
"date": "相关推荐列表用到的日期"
}
},
"recent": ["[abbrlink]", "[abbrlink]"],
"related": {
"[abbrlink]": ["[abbrlink]", "[abbrlink]"]
}
}

  其中,当sortdate值相同时会用time替代,这样设计的目的是为了压缩json体积。

  接下来就是考虑如何生成json文件了,我们选择通过hexo生成,就是在scripts目录下面写一个js用来生成json

教程

  修改:[butterfly]\layout\includes\widget\card_recent_post.pug

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
  if theme.aside.card_recent_post.enable
.card-widget.card-recent-post
.item-headline
i.fas.fa-history
span= _p('aside.card_recent_post')
+ .aside-list(id='recent-list')
- .aside-list
- - let postLimit = theme.aside.card_recent_post.limit === 0 ? site.posts.length : theme.aside.card_recent_post.limit || 5
- - let sort = theme.aside.card_recent_post.sort === 'updated' ? 'updated' : 'date'
- - site.posts.sort(sort, -1).limit(postLimit).each(function(article){
- - let link = article.link || article.path
- - let title = article.title || _p('no_title')
- - let no_cover = article.cover === false || !theme.cover.aside_enable ? 'no-cover' : ''
- - let post_cover = article.cover
- .aside-list-item(class=no_cover)
- if post_cover && theme.cover.aside_enable
- a.thumbnail(href=url_for(link) title=title)
- img(src=url_for(post_cover) onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'` alt=title)
- .content
- a.title(href=url_for(link) title=title)= title
- if theme.aside.card_recent_post.sort === 'updated'
- time(datetime=date_xml(article.updated) title=_p('post.updated') + ' ' + full_date(article.updated)) #[=date(article.updated, config.date_format)]
- else
- time(datetime=date_xml(article.date) title=_p('post.created') + ' ' + full_date(article.date)) #[=date(article.date, config.date_format)]
- - })

  新建:[butterfly]\layout\includes\widget\card-related-post.pug

1
2
3
4
5
6
.card-widget.card-recommend-post
.item-headline
//这个地方的i标签改成你想要的图标
i.iconfont.icon-tuijian
span= _p('post.recommend')
.aside-list(id='related-list')

  修改:[butterfly]\layout\includes\widget\index.pug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  if is_post()
- const tocStyle = page.toc_style_simple
- const tocStyleVal = tocStyle === true || tocStyle === false ? tocStyle : theme.toc.style_simple
if showToc && tocStyleVal
.sticky_layout
include ./card_post_toc.pug
else
!=partial('includes/widget/card_author', {}, {cache: true})
!=partial('includes/widget/card_announcement', {}, {cache: true})
!=partial('includes/widget/card_top_self', {}, {cache: true})
.sticky_layout
if showToc
include ./card_post_toc.pug
if theme.related_post && theme.related_post.enable
+ !=partial('includes/widget/card-related-post', {}, {cache: true})
- != related_posts(page,site.posts)
!=partial('includes/widget/card_recent_post', {}, {cache: true})
!=partial('includes/widget/card_ad', {}, {cache: true})

  删除:[butterfly]\scripts\helpers\related_post.js

  新建:[butterfly]\scripts\customs\posts.js

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
const logger = require('hexo-log')()

hexo.extend.generator.register('buildPostJson', async () => {
const resultJson = {}
const config = hexo.theme.config
const list = hexo.locals.get('posts').data
const sort = config.aside.card_recent_post.sort
const date_type = config.related_post.date_type

const buildAbbrlinkInfo = () => {
const writeTime = sort === date_type ?
(json, post) => json.time = (sort === 'updated' && post.updated ? post.updated : post.date) :
(json, post) => {
if (sort === 'updated') {
json.sort = post.updated ? post.updated : post.date
json.date = post.date
} else {
json.sort = post.date
json.date = post.updated ? post.updated : post.date
}
}
const json = {}
for (let post of list) {
const info = {}
info.title = post.title
writeTime(info, post)
info.img = post.cover
json[post.abbrlink] = info
}
resultJson.info = json
}

/** 构建最新文章信息 */
const buildRecentJsonInfo = () => {
if (!(config.aside.enable && config.aside.card_recent_post.enable)) return
const getTime = sort === 'updated' ? post => post.updated ? post.updated : post.date : post => post.date
const sorted = []
for (let post of list) sorted.push(post)
sorted.sort((a, b) => getTime(b) - getTime(a))
resultJson.recent = {
limit: config.aside.card_recent_post.limit || 5,
list: sorted.map(it => it.abbrlink.toString())
}
}

/** 构建相关推荐信息 */
const buildRelatedJsonInfo = () => {
if (!config.related_post.enable) return
resultJson.related = {}
const maxCount = config.related_post.limit
resultJson.related.list = {}
const categories = hexo.locals.get('categories').data
const tags = hexo.locals.get('tags').data
// 查找对象
const findObj = (src, dist) => {
for (let value of src) {
if (value.name === dist) return value.posts.data
}
return []
}
// 获取指定标签的文章列表
const getPostsByTags = tag => {
const result = []
for (let value of findObj(tags, tag.name)) result.push(value)
return result
}
// 获取指定分类的文章列表
const getPostsByCategories = cat => {
const result = []
for (let value of findObj(categories, cat.name)) result.push(value)
return result
}
/**
* 获取和指定文章相关的文章列表,根据有关程度从大到小排序
* @param post
* @return {[{post, value}]} 其中value是有关程度,post是文章对象
*/

const handle = post => {
const map = new Map()
const plusValue = (post, plus = 1) => {
if (map.has(post)) map.set(post, map.get(post) + plus)
else map.set(post, plus)
}
for (let tag of post.tags.data) {
const list = getPostsByTags(tag)
for (let value of list) plusValue(value)
}
for (let cat of post.categories.data) {
const list = getPostsByCategories(cat)
for (let value of list) plusValue(value, 2)
}
const result = []
map.forEach((value, key) => result.push({post: key, value: value}))
result.sort((a, b) => b.value - a.value)
return result
}

for (let post of list) {
const info = handle(post)
const json = []
for (let value of info) {
if (value.post.abbrlink === post.abbrlink) continue
if (json.length === maxCount) break
json.push(value.post.abbrlink.toString())
}
//如果相关推荐数量不够就随机推一些文章上去
for (; json.length !== maxCount;) {
const index = Math.floor(Math.random() * list.length)
const abbrlink = list[index].abbrlink.toString()
if (abbrlink === post.abbrlink.toString() || json.indexOf(abbrlink) > -1) continue
json.push(abbrlink)
}
resultJson.related.list[post.abbrlink] = json
}
}

const tasks = [buildAbbrlinkInfo, buildRecentJsonInfo, buildRelatedJsonInfo]
await Promise.all(tasks.map(it => new Promise(resolve => {
it()
resolve()
})))
logger.info(`文章JSON构建成功(${list.length})`)
return {
path: 'postsInfo.json',
data: JSON.stringify(resultJson)
}
})

  引入JS:

注意:如果你开启了pjax,请通过no-pjax的方式引入该JS

如果你使用异步方式引入该JS,请使用defer,以此保证JS运行时DOM已经解析完毕

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
syncJsonInfo()

function syncJsonInfo() {
/** 读取某一个文章的信息 */
function readAbbrlink(json, abbrlink) {
return json['info'][abbrlink]
}
/** 构建最新文章 */
function syncRecentPosts(json) {
function create(abbrlink, title, img, date) {
return `<div class="aside-list-item">\<a class="thumbnail" href="/posts/${abbrlink}/" title="${title}"><img src="${img}" onerror="this.onerror=null;this.src='/img/404.jpg'" alt="${title}"></a><div class="content"><a class="title" href="/posts/${abbrlink}/" title="${title}">${title}</a><time title="更新于 ${date.toLocaleString()}">${date.toLocaleDateString()}</time></div></div>`
}
const recent = json.recent
const list = recent.list
const div = document.getElementById('recent-list')
if (!div) return
const length = Math.min(list.length, recent.limit)
for (let i = 0; i !== length; ++i) {
const abbrlink = list[i]
const info = readAbbrlink(json, abbrlink)
const html = create(abbrlink, info['title'], info['img'],
new Date(info['sort'] ? info['sort'] : info['time']))
div.insertAdjacentHTML('beforeend', html)
}
}
/** 构建相关文章 */
function syncRelatedPosts(json) {
function create(abbrlink, title, img, date) {
return `<div class="aside-list-item"><a class="thumbnail" href="/posts/${abbrlink}/" title="${title}"><img alt="${title}" src="${img}"></a><div class="content"><a class="title" href="/posts/${abbrlink}/" title="${title}">${title}</a><time title="发表于 ${date.toLocaleDateString()}">${date.toLocaleDateString()}</time></div></div>`
}
const related = json['related']
const div = document.getElementById('related-list')
if (!div) return
const href = location.href.substring(0, location.href.length - 1)
const abbrlink = href.substring(href.lastIndexOf('/') + 1)
const list = related['list'][abbrlink]
for (let i = 0; i !== list.length; ++i) {
const info = readAbbrlink(json, list[i])
const html = create(list[i], info['title'], info['img'],
new Date(info['date'] ? info['date'] : info['time']))
div.insertAdjacentHTML("beforeend", html)
}
}

const jsonCache = sessionStorage.getItem('postsInfo')
if (jsonCache) {
const json = JSON.parse(jsonCache)
syncRecentPosts(json)
syncRelatedPosts(json)
} else {
fetch(`/postsInfo.json`).then(response => {
response.json().then(json => {
sessionStorage.setItem('postsInfo', JSON.stringify(json))
syncRecentPosts(json)
syncRelatedPosts(json)
})
})
}
}

  然后通过hexo s就能看到效果了。


创作不易,扫描下方打赏二维码支持一下吧ヾ(≧▽≦*)o