认识 hexo

简单来说,就是一个博客框架。社区很活跃,里面有很懂框架可以直接使用和自定义。
更多详情,直接查看官网: https://hexo.io/zh-cn/docs/index.html

🚀 markdown 语法可以查看

本地搭建 hexo 记录

1.git 安装:略
2.node.js 安装:版本 22,详情略
3.启动终端安装框架:npm install -g hexo-cli
4.初始化 hexo

1
2
3
hexo init <folder>
cd <folder>
npm install

5.在 hexo 根目录终端输入: hexo server,启动程序,访问 http://localhost:4000/ 不报错即成功。

常用命令

1.在终端输入 hexo new page 导航栏名字,如:hexo new page about

配置 github pages

注意: 创建github 仓库的时候,必须要一个 README.md,不然会失败。

详情查看官网文档:https://hexo.io/zh-cn/docs/github-pages

进阶

netlify 尝试

netlify 最大的优点是在GitHub仓库设置为私密的时候,依旧能使用,但是GitHub pages不能。
先说结论,不是很理想,对于githubpages的速度提升不是很大:

  • 优点:netlify.app 可以过移动网的防火墙。
  • 缺点:微信浏览器无法访问。
指标 github pages netlify
国内访问速度(avg) 站长之家 2.044s 3.588s
国内访问速度(avg) itdog 1.356s 1.486s
运营商访问 移动网无法访问 三大isp都可以访问
微信访问 可以访问 无法访问

国内访问速度是通过站长之家测试 站长工具>国内测速

站长之家测试数据详情如下:
github pages
netlify

具体操作步骤

提前说明,很简单,官网注册一个账号,使用github注册,绑定一个已有的blog项目,跟着指引走就可以了。

官网地址:https://app.netlify.com/

时间有点老,但是能用:https://blog.grin.cool/blog/hexo-blog

注意:

github pages 和 netlify 使用同一个分支,会冲突。
尝试将 github pages 关闭,但是由于不想删除 .github\workflows\pages.yml ,导致一直关不掉。

于是重新修改逻辑,将 github pages 所需的静态文件放在分支 blog_pages 里面,再在 仓库中,按如下设置,
修改分支:Settings > Pages > Build and deployment > Branch> Deploy from a branch > blog_pages > /(root)

同时在仓库设置中启用工作流写入权限:

  • 访问仓库的 Settings > Actions > General
  • Workflow permissions 部分:
    • 选择 Read and write permissions
    • 勾选 Allow GitHub Actions to create and approve pull requests

修改page.yml 文件的内容为:

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
name: Pages

on:
push:
branches:
- main # 触发部署的源分支

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: main
submodules: recursive

- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Cache NPM dependencies
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.OS }}-npm-cache

- name: Install Dependencies
run: npm install

- name: Build
run: npm run build # 生成静态文件到 public 目录

- name: Deploy to Pages Branch
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
publish_branch: blog_pages # 目标分支名称
force_orphan: true # 确保分支只包含最新文件

备注:

page.yml 文件内容是 deep seek 生成的,具体的意思和语法需要查看 github actions 的语法。

gitalk 登陆失败

如图:
gitalk auth failed

解决方法:

在 github 的仓库中,Your Avatar > Settings > Developer settings > OAuth Apps > Your_App_Name
里面有 Homepage URL 和 Authorization callback URL ,将其改为 xx.netlify.app 的地址

总结:gitalk 只能绑定一个域名地址;

参考:https://github.com/reportportal/reportportal/issues/2374

Vercel 尝试

由于 Vercel 国内被屏蔽掉了,必须要有域名,由于想白嫖,所以暂时不考虑

使用 theme

详情参考:https://butterfly.js.org/

配置评论系统

giscus

详情参考:https://zhuanlan.zhihu.com/p/603658639

gitalk

giscus 无法使用最新评论外挂模块,转 gitalk,具体详情参考:https://www.cnblogs.com/qisi007/p/13731562.html
https://www.summer889.com/2024/11/12/%E6%9C%89%E7%9A%84%E6%B2%A1%E7%9A%84/hexo%E5%8D%9A%E5%AE%A2%E8%AF%84%E8%AE%BA%E5%8A%9F%E8%83%BD/

注意:

gitalk 文件名不能有中文,不然会失效

twikoo

后面有时间在实践
https://niezicheng.github.io/blog-hexo/posts/a77c77c/

配置文章的路由

Add plugin to Hexo:

1
npm install hexo-abbrlink --save

Modify permalink in config.yml file:

1
2
3
permalink: posts/:abbrlink/ 
# or
permalink: posts/:abbrlink.html

Configs in _config.yml:

1
2
3
4
5
6
7
# abbrlink config
abbrlink:
alg: crc32 # Algorithm used to calc abbrlink. Support crc16(default) and crc32
rep: hex # Representation of abbrlink in URLs. Support dec(default) and hex
drafts: false # Whether to generate abbrlink for drafts. (false in default)
force: false # Enable force mode. In this mode, the plugin will ignore the cache, and calc the abbrlink for every post even it already had an abbrlink. (false in default)
writeback: true # Whether to write changes to front-matters back to the actual markdown files. (true in default)

样本

The generated link will look like the following:

1
2
3
4
5
crc16 & hex
https://post.zz173.com/posts/66c8.html

crc16 & dec
https://post.zz173.com/posts/65535.html
1
2
3
4
5
crc32 & hex
https://post.zz173.com/posts/8ddf18fb.html

crc32 & dec
https://post.zz173.com/posts/1690090958.html

参考

https://github.com/ohroy/hexo-abbrlink?tab=readme-ov-file

遇见的问题

md 文档中图片相对路径失效

网上攻略多数都是,hexo的图片默认不支持相对路径。需要配置 post_asset_folder 选项,设置从false改成true之后支持。但是要求图片目录必须和文件名相同
如:

1
2
3
4
.
├── _posts
| ├── test.md
| └── test

极其不方便,然后发现有人专门写了插件,可以看原文:https://www.cnblogs.com/mlzrq/p/16099460.html

亲测可行

这个插件可以把图片可以为hexo的_posts目录下的任意目录下的资源图片,文章内可以使用相对路径引用,在模板渲染时,插件会将相对路径的图片转为inline模式,以base64编码直接在html内引用。这样就不会出现目录问题。

使用方式

  1. 安装插件:

安装命令如下

1
npm i hexo-filter-inline-image
  1. 配置hexo启用插件

_config.yml 增加以下配置

1
2
3
4
5
inline_image:  
enabled: true
compress: false
remote: false
limit: 2048

compress 是否启用图片压缩
remote 是否转换http和https图片,默认关闭
limit 限制图片大小(kb)在此以内才会进行转换

注意:

由于本身Hexo带有缓存会导致插件第一次使用可能会出现无效果,请使用hexo clean清除缓存后即可正常使用

giscus 评论失败

报错:未找到相关的 Issues 进行评论 请联系 @coder-xuyong 初始化创建

本地尝试登陆评论失败

上传到github 后,使用 https://coder-xuyong.github.io 登陆评论,成功!

特殊的md语法

md中引用其他md文件

文章嵌入视频

添加SEO

参考:

但是毕竟不一样,还是自己做个记录,方便后续排查。和qmike里面的一样,可以先检查自己的域名有没有被搜索引擎搜录:

1
2
site:你的网站
比如我的:site:coder-xuyong.netlify.app

然后永久化url地址,之前做过了,不在赘述。剩下的两步是关键,第一步生成站点地图,这个是要给搜索引擎的,告诉它来抓取我们的页面。

安装百度和 Google 的站点地图生成插件:

1
2
npm install hexo-generator-baidu-sitemap --save
npm install hexo-generator-sitemap --save

然后来到hexo根目录配置文件config.yml,在下面添加:

1
2
3
4
5
# 站点地图
sitemap:
path: sitemap.xml
baidusitemap:
path: baidusitemap.xml

给你的 hexo 网站添加蜘蛛协议 robots.txt, 把 robots.txt 放在你的 hexo 站点的 source 文件下即可。

1
2
3
4
5
6
# hexo robots.txt
User-agent: *
Allow: /

Sitemap: https://qmike.top/sitemap.xml
Sitemap: https://qmike.top/baidusitemap.xml

然后重新推送到服务器,访问如下 URL:

  • https://你的域名/sitemap.xml
  • https://你的域名/baidusitemap.xml

看看网页中有没有出现代码。有的话就成功。到这里第一步就完成了

最重要的一步来了,登陆搜索引擎的网址,去添加我们的域名,并且把刚刚弄好的站点地图给它。由于我使用的免费域名,百度收录达到了上限,转而去了bing的搜索引擎。这个比较简单,登录之后,添加网址,添加站点地图。然后等待过两天在看看是否被收录。其他搜索引擎同理

nofollow标签添加,和qmike的博客写的一样,主要是防止文章里面的链接被抓取。

自定义

更新版本

自己自定义之后更新版本比较麻烦,毕竟修改过源代码。我这里是将butterfly的仓库fork一份,在fork的仓库上修改。后续butterfly升级版本了,在vscode上拉取上游分支,解决冲突就好。

由于这是现在才想出来的,仓库也是刚刚fork的,暂时没有实操。后面实操的时候搜索一下”如何合并上游分支“应该就有了

自定义波浪特效

星雪の小屋
Butterfly

butterfly 右下角悬浮菜单栏

星雪の小屋

主页左边分类卡片自定义

目标,层级分类自定义展开。
找到 themes\butterfly\scripts\helpers\aside_categories.js ,其中找到 hierarchicalList 方法下的这段代码

替换 aside_categories.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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
'use strict'

hexo.extend.helper.register('aside_categories', function (categories, options = {}) {
if (!categories || !Object.prototype.hasOwnProperty.call(categories, 'length')) {
options = categories || {}
categories = this.site.categories
}

if (!categories || !categories.length) return ''

const { config } = this
const showCount = Object.prototype.hasOwnProperty.call(options, 'show_count') ? options.show_count : true
const depth = options.depth ? parseInt(options.depth, 10) : 0
const orderby = options.orderby || 'name'
const order = options.order || 1
const categoryDir = this.url_for(config.category_dir)
const limit = options.limit === 0 ? categories.length : (options.limit || categories.length)
const isExpand = options.expand !== 'none'
const expandClass = isExpand && options.expand === true ? 'expand' : ''
const buttonLabel = this._p('aside.more_button')

// [新增] 是否显示分类下的文章列表及文章数量限制
const showPosts = Object.prototype.hasOwnProperty.call(options, 'show_posts') ? options.show_posts : true
const postLimit = options.post_limit || 30

const categoryMap = new Map()
categories.forEach(cat => {
if (cat.length) {
const parentId = cat.parent || 'root'
if (!categoryMap.has(parentId)) {
categoryMap.set(parentId, [])
}
categoryMap.get(parentId).push(cat)
}
})

const sortFn = (a, b) => {
const valA = a[orderby]
const valB = b[orderby]
if (valA < valB) return -order
if (valA > valB) return order
return 0
}

for (const list of categoryMap.values()) {
list.sort(sortFn)
}

// [新增] 生成分类下文章列表的 HTML
const generatePostList = (posts) => {
let result = ''
const allPosts = posts.toArray()
allPosts.sort((a, b) => {
const orderA = a.order || Number.MAX_SAFE_INTEGER
const orderB = b.order || Number.MAX_SAFE_INTEGER
return orderA - orderB
})
const limitedPosts = allPosts.slice(0, postLimit)
limitedPosts.forEach(post => {
result += `<li class="card-article-list-item">
<a class="card-article-list-link" href="${this.url_for(post.path)}">
<span class="card-article-list-name">${post.title}</span>
</a>
</li>`
})
if (allPosts.length > postLimit) {
const firstPost = allPosts[0]
const catPath = firstPost.path.split('/')[1] || ''
result += `<li class="card-article-list-item">
<a class="card-article-list-link" href="${this.url_for(catPath)}/">
<span class="card-article-list-name">${this._p('aside.more_article')} (${allPosts.length - postLimit})</span>
</a>
</li>`
}
return result
}

const hierarchicalList = (remaining, level = 0, parentId = 'root') => {
let result = ''
if (remaining > 0 && categoryMap.has(parentId)) {
categoryMap.get(parentId).forEach(cat => {
if (remaining > 0) {
remaining -= 1
let child = ''
if (!depth || level + 1 < depth) {
const childList = hierarchicalList(remaining, level + 1, cat._id)
child = childList.result
remaining = childList.remaining
}

// [新增] 判断是否为叶子分类(无子分类且满足深度限制),用于决定是否显示文章列表
const hasChild = categoryMap.has(cat._id) && categoryMap.get(cat._id).length > 0
const isLeaf = !hasChild && (!depth || level >= depth - 1)

let postHtml = ''
if (showPosts && isLeaf && cat.posts && cat.posts.length > 0) {
postHtml = `<ul class="card-article-list child">${generatePostList(cat.posts)}</ul>`
}

// [修改] 展开内容的判断由 child 扩展为 child || postHtml,且不再限制只对顶级生效
const hasExpandContent = child || postHtml
const parentClass = isExpand && hasExpandContent ? 'parent' : ''

result += `<li class="card-category-list-item ${parentClass}">`
result += `<a class="card-category-list-link" href="${this.url_for(cat.path)}">`
result += `<span class="card-category-list-name">${cat.name}</span>`

if (showCount) {
result += `<span class="card-category-list-count">${cat.length}</span>`
}

// [修改] 展开图标条件改为 hasExpandContent,不再限定顶级
if (isExpand && hasExpandContent) {
result += `<i class="fas fa-caret-left ${expandClass}"></i>`
}

result += '</a>'

if (child) {
result += `<ul class="card-category-list child">${child}</ul>`
}
// [新增] 追加文章列表
if (postHtml) {
result += postHtml
}

result += '</li>'
}
})
}
return { result, remaining }
}

const list = hierarchicalList(limit)

const moreButton = categories.length > limit
? `<a class="card-more-btn" href="${categoryDir}/" title="${buttonLabel}">
<i class="fas fa-angle-right"></i></a>`
: ''

return `<div class="item-headline"><i class="fas fa-folder-open"></i><span>${this._p('aside.card_categories')}</span>${moreButton}</div>
<ul class="card-category-list${isExpand && list.result ? ' expandBtn' : ''}" id="aside-cat-list">
${list.result}
</ul>`
})

同时修改自定义的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
39
40
41
42
43
44
45
46
47
48
49
50
51

.card-category-list-link,
.card-article-list-link {
display: flex;
align-items: center;
position: relative;
}

/* 图标样式 */
.card-category-list-link i {
margin-left: auto;
padding: 0 5px;
transition: transform 0.3s ease;
font-size: 14px;
}

/* 展开状态旋转图标 */
.card-category-list-link i.expand {
transform: rotate(-90deg);
}

/* 文章列表样式 */
.card-article-list {
padding-left: 20px;
}

.card-article-list-item {
list-style: none;
position: relative;
}

.card-article-list-item:before {
content: "";
position: absolute;
left: -15px;
top: 50%;
transform: translateY(-50%);
font-size: 16px;
}

/* 文章链接样式 */
.card-article-list-link {
padding: 5px 0;
font-size: 0.9em;
color: #666;
}

/* 子分类缩进 */
.card-category-list .child {
padding-left: 15px;
}

在source/js/main.js中修改,这是控制展开开关DOM的方法,将其修改为点击整行展开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const toggleCardCategory = () => {
const cardCategory = document.querySelector('#aside-cat-list.expandBtn')
if (!cardCategory) return

const handleToggleBtn = e => {
const link = e.target.closest('.card-category-list-link')
if (!link) return

// 让整行都可展开/收起,并阻止链接默认跳转
e.preventDefault()
e.stopPropagation()
link.classList.toggle('expand')
}

btf.addEventListenerPjax(cardCategory, 'click', handleToggleBtn, true)
}

加载动画

https://blog.anheyu.com/posts/52d8.html

替换不蒜子

可以去vercount的官网绑定域名,查看访问信息详情。
这个比较麻烦的是,需要一添加一个目录去放验证文件,同时还需要新增一个js:项目\scripts\vercount-verify.js:

1
2
3
4
hexo.extend.generator.register('vercount-verify', () => ({
path: '.well-known/文件名字.txt',
data: '文件内容'
}))

导航栏居中

在自己定义的custom.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

/* 导航栏居中显示和文字图标大小修改 */

#nav {
justify-content: center;
}

#blog-info {
position: absolute;
left: 36px;
}

#nav .menus_items {
margin: 0 auto;
}

#nav .site-page {
font-size: 0.95em;
}

#nav .menus_item > span > i:first-child,
#nav #search-button i {
font-size: 1.08em;
}

@media screen and (max-width: 768px) {
#nav .site-page {
font-size: 0.86em;
}
}

自定义公告来访者信息

参考的june,但是和我版本有不兼容,因此重写一下:

  • 进入 腾讯位置服务 应用管理界面
  • 点击创建应用,应用名称和类型随便填。
  • 在新创建的应用中点击添加key,产品选择WebServiceAPI,域名白名单不填。
  • 把得到的 key 记下
  • 再去 配额管理 -> 账户额度 里拉满(反正就一个要用,多了的话按需分配)
  • 前往 经纬度查询定位 获取你所在位置的经纬度,记下来。开了梯子可能用不了,换手机试一下。
  1. 修改[blogRoot]\butterfly\layout\includes\widget\card_announcement.pug,将其全部替换:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if theme.aside.card_announcement.enable
.card-widget.card-announcement
.item-headline
i.fas.fa-bullhorn.fa-shake
span= _p('aside.card_announcement')
.announcement_content!= theme.aside.card_announcement.content
#welcome-info
script(data-pjax).
var longitude = '#{config.welcome.longitude}';
var Latitude = '#{config.welcome.Latitude}';
var txkey = '#{config.welcome.key}'
var ipLoacation;
window.onload = () => {
window.welcometxmap?.()
};
document.addEventListener("pjax:complete", () => window.welcometxmap?.())
  1. 添加location.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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
// 获取来访者信息
function welcometxmap() {
// 请求数据
const storage = window.btf?.saveToLocal;
if (!storage) return;

ipLoacation = storage.get('ipLocation');
if (ipLoacation) {
// 使用 ipLocation
showWelcome();
} else {
// 数据已过期或不存在
var script = document.createElement('script');
var url = `https://apis.map.qq.com/ws/location/v1/ip?key=${txkey}&output=jsonp`;
script.src = url;
window.QQmap = function (data) {
ipLoacation = data;
// 将数据保存到 localStorage,过期时间设置为 1 天
storage.set('ipLocation', ipLoacation, 1);
document.body.removeChild(script);
delete window.QQmap;
// 获取到数据后立即显示欢迎信息
showWelcome();
};
document.body.appendChild(script);
}
}

function getDistance(e1, n1, e2, n2) {
const R = 6371;
const { sin, cos, asin, PI, hypot } = Math;
let getPoint = (e, n) => {
e *= PI / 180;
n *= PI / 180;
return { x: cos(n) * cos(e), y: cos(n) * sin(e), z: sin(n) };
};

let a = getPoint(e1, n1);
let b = getPoint(e2, n2);
let c = hypot(a.x - b.x, a.y - b.y, a.z - b.z);
let r = asin(c / 2) * 2 * R;
return Math.round(r);
}

function showWelcome() {
if (!ipLoacation) return; // 确保 ipLoacation 存在

let dist = getDistance(longitude, Latitude, ipLoacation.result.location.lng, ipLoacation.result.location.lat);
let pos = ipLoacation.result.ad_info.nation;
let ip;
let posdesc;

// 根据国家、省份、城市信息自定义欢迎语
switch (ipLoacation.result.ad_info.nation) {
case "日本":
posdesc = "よろしく,一起去看樱花吗";
break;
case "美国":
posdesc = "Let us live in peace!";
break;
case "英国":
posdesc = "想同你一起夜乘伦敦眼";
break;
case "俄罗斯":
posdesc = "Высушите эту бутылку водки!";
break;
case "法国":
posdesc = "C'est La Vie";
break;
case "德国":
posdesc = "Die Zeit verging im Fluge.";
break;
case "澳大利亚":
posdesc = "一起去大堡礁吧!";
break;
case "加拿大":
posdesc = "拾起一片枫叶赠予你";
break;
case "中国":
pos = ipLoacation.result.ad_info.city + " " + ipLoacation.result.ad_info.district; // ipLoacation.result.ad_info.province + " " +
ip = ipLoacation.result.ip;
switch (ipLoacation.result.ad_info.province) {
case "北京市":
posdesc = "北——京——欢迎你~~~";
break;
case "天津市":
posdesc = "讲段相声吧";
break;
case "河北省":
posdesc = "山势巍巍成壁垒,天下雄关铁马金戈由此向,无限江山";
break;
case "山西省":
posdesc = "展开坐具长三尺,已占山河五百余";
break;
case "内蒙古自治区":
switch (ipLoacation.result.ad_info.city) {
case "通辽市":
posdesc = "可汗宇宙中心";
break;
default:
posdesc = "天苍苍,野茫茫,风吹草低见牛羊";
break;
}
break;
case "辽宁省":
posdesc = "我想吃烤鸡架!";
break;
case "吉林省":
posdesc = "状元阁就是东北烧烤之王";
break;
case "黑龙江省":
posdesc = "很喜欢哈尔滨大剧院";
break;
case "上海市":
posdesc = "众所周知,中国只有两个城市";
break;
case "江苏省":
switch (ipLoacation.result.ad_info.city) {
case "南京市":
posdesc = "这是我挺想去的城市啦";
break;
case "苏州市":
posdesc = "上有天堂,下有苏杭";
break;
case "无锡市":
posdesc = "无锡太美,无锡太美";
break;
default:
posdesc = "散装是必须要散装的";
break;
}
break;
case "浙江省":
posdesc = "东风渐绿西湖柳,雁已还人未南归";
break;
case "河南省":
switch (ipLoacation.result.ad_info.city) {
case "郑州市":
posdesc = "豫州之域,天地之中";
break;
case "南阳市":
posdesc = "臣本布衣,躬耕于南阳此南阳非彼南阳!";
break;
case "驻马店市":
posdesc = "峰峰有奇石,石石挟仙气嵖岈山的花很美哦!";
break;
case "开封市":
posdesc = "刚正不阿包青天";
break;
case "洛阳市":
posdesc = "洛阳牡丹甲天下";
break;
default:
posdesc = "可否带我品尝河南烩面啦?";
break;
}
break;
case "安徽省":
posdesc = "蚌埠住了,芜湖起飞";
break;
case "福建省":
posdesc = "井邑白云间,岩城远带山";
break;
case "江西省":
posdesc = "落霞与孤鹜齐飞,秋水共长天一色";
break;
case "山东省":
posdesc = "遥望齐州九点烟,一泓海水杯中泻";
break;
case "湖北省":
switch (ipLoacation.result.ad_info.city) {
case "黄冈市":
posdesc = "红安将军县!辈出将才!";
break;
default:
posdesc = "来碗热干面~";
break;
}
break;
case "湖南省":
switch (ipLoacation.result.ad_info.city) {
case "衡阳市":
posdesc = "老乡见老乡,两眼泪汪汪!";
break;
default:
posdesc = "74751,长沙斯塔克";
break;
}
break;
case "广东省":
switch (ipLoacation.result.ad_info.city) {
case "广州市":
posdesc = "看小蛮腰,喝早茶了嘛~";
break;
case "深圳市":
switch (ipLoacation.result.ad_info.district) {
case "南山区":
posdesc = "好巧!博主也在南山区生活喔~";
break;
default:
posdesc = "今天你996了嘛~";
break;
}
break;
case "阳江市":
posdesc = "阳春合水!博主家乡~ 欢迎来玩~";
break;
default:
posdesc = "来两斤福建人~";
break;
}
break;
case "广西壮族自治区":
posdesc = "桂林山水甲天下";
break;
case "海南省":
posdesc = "朝观日出逐白浪,夕看云起收霞光";
break;
case "四川省":
posdesc = "康康川妹子";
break;
case "贵州省":
posdesc = "茅台,学生,再塞200";
break;
case "云南省":
posdesc = "玉龙飞舞云缠绕,万仞冰川直耸天";
break;
case "西藏自治区":
posdesc = "躺在茫茫草原上,仰望蓝天";
break;
case "陕西省":
posdesc = "来份臊子面加馍";
break;
case "甘肃省":
posdesc = "羌笛何须怨杨柳,春风不度玉门关";
break;
case "青海省":
posdesc = "牛肉干和老酸奶都好好吃";
break;
case "宁夏回族自治区":
posdesc = "大漠孤烟直,长河落日圆";
break;
case "新疆维吾尔自治区":
posdesc = "驼铃古道丝绸路,胡马犹闻唐汉风";
break;
case "台湾省":
posdesc = "我在这头,大陆在那头";
break;
case "香港特别行政区":
posdesc = "永定贼有残留地鬼嚎,迎击光非岁玉";
break;
case "澳门特别行政区":
posdesc = "性感荷官,在线发牌";
break;
default:
posdesc = "带我去你的城市逛逛吧!";
break;
}
break;
default:
posdesc = "带我去你的国家逛逛吧";
break;
}

// 根据本地时间切换欢迎语
let timeChange;
let date = new Date();
if (date.getHours() >= 5 && date.getHours() < 11) timeChange = "<span class='welcome-time'>🌤️ 早上好,一日之计在于晨</span>";
else if (date.getHours() >= 11 && date.getHours() < 13) timeChange = "<span class='welcome-time'>☀️ 中午好,记得午休喔~</span>";
else if (date.getHours() >= 13 && date.getHours() < 17) timeChange = "<span class='welcome-time'>🕞 下午好,饮茶先啦!</span>";
else if (date.getHours() >= 17 && date.getHours() < 19) timeChange = "<span class='welcome-time'>🚶‍♂️ 即将下班,记得按时吃饭~</span>";
else if (date.getHours() >= 19 && date.getHours() < 24) timeChange = "<span class='welcome-time'>🌙 晚上好,夜生活嗨起来!</span>";
else timeChange = "<span class='welcome-time'>夜深了,早点休息,少熬夜</span>";

try {
// 自定义文本和需要放的位置
document.getElementById("welcome-info").innerHTML =
`<span>欢迎来自 <b>${pos}</b> 的小伙伴</span><br>
${timeChange}<br>
<span class="welcome-message">${posdesc}</span><br>
当前位置距<b> coder-xuyong </b>约 <b>${dist}</b> 公里!<br>
<span>IP地址为:<b>${ip}</b></span>`;
} catch (err) {
console.log("无法获取#welcome-info元素", err);
}
}

// 在页面加载时调用welcometxmap
document.addEventListener('DOMContentLoaded', welcometxmap);
  1. 添加location.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
#welcome-info {
overflow: hidden;
--june-card-welcome: #49b1f5;
}
#welcome-info .welcome-time {
width: 100%;
margin: 12px 0 0;
display: inline-flex;
}
#welcome-info .welcome-message {
width: 100%;
margin-bottom: 8px;
display: inline-flex;
}
#welcome-info .welcome-region {
background: var(--june-background);
border: var(--style-border);
color: var(--font-color);
border-radius: 8px;
display: inline-flex;
align-items: center;
padding: 6px 10px;
margin: 6px 0;
}
  1. 在butterfly主题的配置文件中找到inject,将js和css引入
  2. 最后在hexo的配置文件中,添加配置:
1
2
3
4
welcome:
longitude: 104.0668
Latitude: 30.5728
key: 你的腾讯地图Key

需要注意的是不同的版本可能会存在不同的问题,本次使用到是butterfly5.7+hexo7.30
不过有ai,有点问题让ai改一下就好了。

鼠标样式修改

在自定义的css文件中添加:

1
2
3
4
5
6
7
8
9
body {
cursor: url(https://img.june-pj.cn/img/default.cur),
default;
}
a,
img {
cursor: url(https://img.june-pj.cn/img/pointer.cur),
default;
}

添加日历倒计时卡片

参考于:https://blog.june-pj.cn/posts/c8344523/#1-%E6%B7%BB%E5%8A%A0card-calendar-pug%E5%92%8Ccard-schedule-pug

但是不太行,于是让AI给我改了一下,也算符合预期,记录一下:

创建文件:\themes\butterfly\layout\includes\widget\card_schedule.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
26
27
28
29
30
.card-widget#card-schedule
#schedule-area-left
#schedule-title 距离节日
#schedule-days 0
#schedule-date 0000-00-00
#schedule-area-right
.schedule-r0
.schedule-d0 本年
.schedule-d1
span#p_span_year.aside-span1
span.aside-span2 还剩
a 0
| 天
progress#pBar_year(max="365")
.schedule-r1
.schedule-d0 本月
.schedule-d1
span#p_span_month.aside-span1
span.aside-span2 还剩
a 0
| 天
progress#pBar_month(max="30")
.schedule-r2
.schedule-d0 本周
.schedule-d1
span#p_span_week.aside-span1
span.aside-span2 还剩
a 0
| 天
progress#pBar_week(max="7")

添加自定义的
calendar.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
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#card-schedule {
display: flex;
padding: 18px 10px !important;
}

#schedule-area-right a {
pointer-events: none;
color: var(--text-highlight-color);
}


#schedule-area-left, #schedule-area-right {
height: 100px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

#schedule-area-left {
width: 30%;
}

#schedule-area-right {
width: 70%;
padding: 0 5px;
}

.schedule-r0, .schedule-r1, .schedule-r2 {
height: 2rem;
width: 100%;
align-items: center;
display: flex;
}

.schedule-d0 {
width: 30px;
margin-right: 5px;
text-align: center;
font-size: 12px;
}

.schedule-d1 {
width: calc(100% - 35px);
height: 1rem;
position: relative;
align-items: center;
display: flex;
}

#card-schedule progress {
width: 100%;
height: 1rem;
appearance: none;
border: 0;
border-radius: 6px;
background: #dbe3ec;
overflow: hidden;
}

#card-schedule progress::-webkit-progress-bar {
background: #dbe3ec;
border-radius: 6px;
overflow: hidden;
}

#card-schedule progress::-webkit-progress-value {
background: #e6827f;
border-radius: 6px;
}

#card-schedule progress::-moz-progress-bar {
background: #e6827f;
border-radius: 6px;
}

.aside-span1, .aside-span2 {
height: 1.5rem;
font-size: 12px;
line-height: 1;
z-index: 1;
display: flex;
align-items: center;
position: absolute;
}

.aside-span1 {
left: 0;
margin-left: 5px;
color: #172033;
}

.aside-span2 {
right: 5px;
color: var(--june-secondtext);
}

.aside-span2 a {
margin: 0 3px;
}

[data-theme="dark"] #card-schedule progress {
background: #263449;
}

[data-theme="dark"] #card-schedule progress::-webkit-progress-bar {
background: #263449;
}

[data-theme="dark"] #card-schedule progress::-webkit-progress-value,
[data-theme="dark"] #card-schedule progress::-moz-progress-bar {
background: #60a5fa;
}

[data-theme="dark"] .aside-span1 {
color: #f8fafc;
}

#pBar_month, #pBar_week, #pBar_year {
width: 100%;
border-radius: 5px;
height: 100%;
}

#schedule-date, #schedule-days, #schedule-title {
display: flex;
align-items: center;
}

#schedule-title {
height: 25px;
line-height: 1;
font-size: 14px;
font-weight: 700;
}

#schedule-days {
height: 55px;
line-height: 1;
font-size: 35px;
font-weight: 900;
color: var(--june-lighttext);
}

#schedule-date {
height: 20px;
line-height: 1;
font-size: 12px;
color: var(--june-secondtext);
}

calendar.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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
document.addEventListener("DOMContentLoaded", initializeCard);
document.addEventListener("pjax:complete", initializeCard);

let week, date, dates, asideTime, daysInYear;

function initializeCard() {
cardTimes();
cardRefreshTimes();
}

function getNextFestivalDate() {
const now = new Date();
const currentYear = now.getFullYear();
const today = new Date(currentYear, now.getMonth(), now.getDate());

// 常见阳历节日
const solarFestivals = [
{ name: "元旦", month: 1, day: 1 },
{ name: "情人节", month: 2, day: 14 },
{ name: "妇女节", month: 3, day: 8 },
{ name: "清明节", month: 4, day: 4 },
{ name: "劳动节", month: 5, day: 1 },
{ name: "儿童节", month: 6, day: 1 },
{ name: "建党节", month: 7, day: 1 },
{ name: "建军节", month: 8, day: 1 },
{ name: "教师节", month: 9, day: 10 },
{ name: "国庆节", month: 10, day: 1 },
{ name: "万圣节", month: 10, day: 31 },
{ name: "平安夜", month: 12, day: 24 },
{ name: "圣诞节", month: 12, day: 25 },
];

// 常见农历节日
const lunarFestivals = [
{ name: "春节", month: 1, day: 1 },
{ name: "元宵节", month: 1, day: 15 },
{ name: "上巳节", month: 3, day: 3 },
{ name: "端午节", month: 5, day: 5 },
{ name: "七夕节", month: 7, day: 7 },
{ name: "中元节", month: 7, day: 15 },
{ name: "中秋节", month: 8, day: 15 },
{ name: "重阳节", month: 9, day: 9 },
{ name: "寒衣节", month: 10, day: 1 },
{ name: "下元节", month: 10, day: 15 },
{ name: "腊八节", month: 12, day: 8 },
{ name: "小年", month: 12, day: 23 },
];

const upcoming = [];

// 处理阳历节日(今年 + 明年)
[currentYear, currentYear + 1].forEach((y) => {
solarFestivals.forEach(({ name, month, day }) => {
const festDate = new Date(y, month - 1, day);
if (festDate >= today) upcoming.push({ name, date: festDate });
});

// 感恩节:每年十一月第四个星期四。
const novemberFirst = new Date(y, 10, 1);
const thanksgivingDay = 22 + ((11 - novemberFirst.getDay()) % 7);
const thanksgiving = new Date(y, 10, thanksgivingDay);
if (thanksgiving >= today) upcoming.push({ name: "感恩节", date: thanksgiving });
});

// 处理农历节日(今年 + 明年)
[currentYear, currentYear + 1].forEach((y) => {
lunarFestivals.forEach(({ name, month, day }) => {
const solarDate = chineseLunar.lunarToSolar(y, month, day);
const festDate = new Date(
solarDate.getFullYear(),
solarDate.getMonth(),
solarDate.getDate()
);
if (festDate >= today) upcoming.push({ name, date: festDate });
});

// 除夕是下一年春节的前一天,避免农历十二月只有二十九天时转换错误。
const newYearDate = chineseLunar.lunarToSolar(y + 1, 1, 1);
const newYearEve = new Date(
newYearDate.getFullYear(),
newYearDate.getMonth(),
newYearDate.getDate() - 1
);
if (newYearEve >= today) upcoming.push({ name: "除夕", date: newYearEve });
});

// 排序并返回最近节日
if (upcoming.length === 0) return null;
return upcoming.sort((a, b) => a.date - b.date)[0];
}

function cardRefreshTimes() {
const e = document.getElementById("card-schedule");
if (!e) return;

const now = new Date();
const asideDay = (now - asideTime) / 86400000;

const elapsedYearDays = Math.floor(asideDay) + 1;
e.querySelector("#pBar_year").max = daysInYear;
e.querySelector("#pBar_year").value = elapsedYearDays;
e.querySelector("#p_span_year").innerText = ((elapsedYearDays / daysInYear) * 100).toFixed(1) + "%";
e.querySelector(".schedule-r0 .aside-span2 a").innerText = daysInYear - elapsedYearDays;

e.querySelector("#pBar_month").value = date;
e.querySelector("#pBar_month").max = dates;
e.querySelector("#p_span_month").innerText = ((date / dates) * 100).toFixed(1) + "%";
e.querySelector(".schedule-r1 .aside-span2 a").innerText = (dates - date);

const wd = week === 0 ? 7 : week;
e.querySelector("#pBar_week").value = wd;
e.querySelector("#pBar_week").max = 7;
e.querySelector("#p_span_week").innerText = ((wd / 7) * 100).toFixed(1) + "%";
e.querySelector(".schedule-r2 .aside-span2 a").innerText = (7 - wd);
}

function cardTimes() {
const getShanghaiMidnight = (date = new Date()) => {
const shanghaiStr = date.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
return new Date(shanghaiStr.split(" ")[0] + " 00:00:00");
};

const shanghaiNow = getShanghaiMidnight();
year = shanghaiNow.getFullYear();
week = shanghaiNow.getDay();
date = shanghaiNow.getDate();

const month = shanghaiNow.getMonth();
dates = [
31,
(year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 ? 29 : 28,
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
][month];

daysInYear = ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) ? 366 : 365;

asideTime = new Date(`${year}/01/01 00:00:00`);

const nextFestival = getNextFestivalDate();
const isToday = nextFestival.date.toDateString() === shanghaiNow.toDateString();
const diffDays = isToday ? 0 : Math.ceil((nextFestival.date - shanghaiNow) / 86400000);
const festDateStr = formatDateYMD(nextFestival.date);

const schedule = document.getElementById("card-schedule");
if (schedule) {
schedule.querySelector("#schedule-title").innerText = isToday
? `今天是${nextFestival.name}`
: `距离${nextFestival.name}`;
schedule.querySelector("#schedule-date").innerText = festDateStr;
schedule.querySelector("#schedule-days").innerText = diffDays;
}

}

function formatDateYMD(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}


在_config.butterfly.yml中引入创建的js和css。此外还需要引入<script src="https://unpkg.com/chinese-lunar@0.1.4/lib/chinese-lunar.js"></script>

修改\themes\butterfly\layout\includes\widget\index.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
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
-#aside-content.aside-content
- //- post
- if globalPageType === '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 page.series
- include ./card_post_series.pug
- !=partial('includes/widget/card_recent_post', {}, {cache: true})
- !=partial('includes/widget/card_ad', {}, {cache: true})
- else
- //- page
- !=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
+ -
+ const asideCards = [
+ ['card_schedule', theme.aside.card_schedule],
+ ['card_recent_post', theme.aside.card_recent_post],
+ ['card_ad', theme.ad],
+ ['card_newest_comment', theme.aside.card_newest_comments],
+ ['card_categories', theme.aside.card_categories],
+ ['card_tags', theme.aside.card_tags],
+ ['card_archives', theme.aside.card_archives],
+ ['card_webinfo', theme.aside.card_webinfo],
+ ['card_bottom_self', theme.aside.card_bottom_self]
+ ].map((card, index) => ({
+ name: card[0],
+ config: card[1] || {},
+ index,
+ order: Number.isFinite(card[1] && card[1].sort_order) ? card[1].sort_order : null
+ })).sort((a, b) => {
+ if (a.order === null && b.order === null) return a.index - b.index
+ if (a.order === null) return 1
+ if (b.order === null) return -1
+ return a.order - b.order || a.index - b.index
+ })
+ each card in asideCards
+ !=partial(`includes/widget/${card.name}`, {}, {cache: true})

还是第一次用diff这个标签,+号就是修改的地方,-号就是原样。

修改themes\butterfly\source\css_layout\aside.styl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
      > div:first-child
width: 80%

+ #aside-content
- .sticky_layout
- display: flex
- flex-direction: column
-
+ if hexo-config('aside.card_schedule.sort_order')
+ #card-schedule
+ order: hexo-config('aside.card_schedule.sort_order')
+
- if hexo-config('aside.card_recent_post.sort_order')
- .card-recent-post
- order: hexo-config('aside.card_recent_post.sort_order')

修改butterfly下的_config.yml文件的配置:

1
2
3
4
5
6
-  card_announcement:
- enable: true
- content: This is my Blog
+ card_schedule:
+ enable: true
+ sort_order: null

最后再在外层的_config.butterfly.yml中添加上面的配置,设置排序号

随机访问文章

参考自:https://zml3589110.github.io/posts/883659004.html

1
2
npm install object-assign
npm install hexo-generator-random --save

然后再照常 hexo cleanhexo g -d,public 下就会有个 random.html 的文件了

菜单栏可以添加上该地址

1
2
menu:
随机: /random.html || fas fa-random

修改hexo下_config的site.url为自己的域名地址。但是这也本地调试的时候无法跳转,因此在hexo的根目录新增文件random-template.html,添加内容:

1
2
3
4
5
6
7
8
9
10
11
12
<body onload="javascript:dogo();">
<script>
function dogo(){
var urls=[
{% for post in posts %}
"{{ config.root }}{{ post.path | uriencode }}",
{% endfor %}
];
n = Math.floor(Math.random()*urls.length);
location.href = urls[n];
}
</script>

同时在_config的site.url的位置添加

1
2
urls:
template: random-template.html

即可本地访问了。

听说开启了 pjax,使用这个插件会有问题。这时候可以查看张洪Heo的改造。

参考