Hexo博客搭建(重构版)

搭建Hexo

Hexo官方文档:https://hexo.io/zh-cn/docs/

  1. 安装 node.js
  1. 安装 git

npm 换源

1
npm config set registry https://registry.npm.taobao.org

安装cnpm

1
2
3
4
5
# 安装
npm install -g cnpm --registry==https://registry.npm.taobao.org

# 验证
cnpm -v

安装
安装Hexo

1
2
3
4
5
# 安装
cnpm install -g hexo-cli

# 验证
hexo -v

Hexo初始化

1
2
3
4
5
# 初始化hexo
hexo init

# 启动
hexo cl;hexo g;hexo s

安装Anzhiyu主题

Anzhiyu:https://docs.anheyu.com/

1
2
3
4
5
# dev
git clone -b dev https://github.com/anzhiyu-c/hexo-theme-anzhiyu.git themes/anzhiyu

# main
git clone -b main https://github.com/anzhiyu-c/hexo-theme-anzhiyu.git themes/anzhiyu

安装插件

1
2
3
4
5
# npm
npm install hexo-renderer-pug hexo-renderer-stylus --save

# cnpm
cnpm install hexo-renderer-pug hexo-renderer-stylus --save

安装 hexo-deployer-git 插件 推送更新

项目地址:https://github.com/hexojs/hexo-deployer-git

1
2
# 安装插件
npm install hexo-deployer-git --save

修改_config.yml文件

1
2
3
4
deploy:
- type: git
repo: git@github.com:libertysea/libertysea.github.io.git # github仓库
branch: master

项目地址:https://github.com/Rozbo/hexo-abbrlink

1
npm install hexo-abbrlink --save

修改_config.yml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
permalink: posts/:abbrlink.html  # 此处可以自己设置

## abbrlink config
abbrlink:
alg: crc32 #support crc16(default) and crc32 进制
rep: hex #support dec(default) and hex 算法
drafts: false #(true)Process draft,(false)Do not process draft. false(default)
## Generate categories from directory-tree
## depth: the max_depth of directory-tree you want to generate, should > 0
auto_category:
enable: true #true(default)
depth: #3(default)
over_write: false
auto_title: false #enable auto title, it can auto fill the title by path
auto_date: false #enable auto date, it can auto fill the date by time today
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 abbrlink.

algolia官网:https://dashboard.algolia.com

1
2
# 安装插件
npm install --save hexo-algolia

修改_config.yml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
algolia:
appId: 'xxx' # 官网获取
apiKey: 'xxx' # 官网获取
adminApiKey: 'xxx' # 官网获取
chunkSize: 5000
indexName: "LibertySea"
fields:
- content:strip:truncate,0,2000
- excerpt:strip
- gallery
- permalink
- photos
- slug
- tags
- title
- categories
- path

通过 hexo algolia 更新

安装 hexo-wordcount 字数统计

1
2
# 安装
npm install hexo-wordcount --save

修改_config.anzhiyu.yml文件

1
2
3
4
5
wordcount:
enable: true
post_wordcount: true
min2read: true
total_wordcount: true

实现 RSS 订阅

安装插件hexo-generator-feed

1
npm install --save hexo-generator-feed

配置_config.yml文件

1
2
3
4
5
#订阅RSS
feed:
type: atom
path: atom.xml
limit: false

修改_config.anzhiyu.yml文件

1
rss: /atom.xml

Twikoo 部署

文档:https://twikoo.js.org/

docker私有部署

1
2
# docker
docker run --name twikoo -e TWIKOO_THROTTLE=1000 -p 8080:8080 -v ${PWD}/data:/app/data -d imaegoo/twikoo

朋友圈配置

朋友圈前端:https://github.com/anzhiyu-c/hexo-circle-of-friends-front
前端配置详见文档:https://docs.anheyu.com/page/fcircle.html

HEO前端配置:https://blog.zhheo.com/p/4e18a507.html

anzhiyu主题前端存在一定bug,修改使用MySQL数据库无法保存,可使用Heo前端进行更改

朋友后端项目地址:https://github.com/Rock-Candy-Tea/hexo-circle-of-friends
文档:https://fcircle-doc.yyyzyyyz.cn/#/

clone项目仓库

1
2
git clone https://github.com/Rock-Candy-Tea/hexo-circle-of-friends
# 若无法下载可手动解压

提前下载镜像

1
2
# docker
docker pull yyyzyyyz/fcircle:latest

编辑/hexo_circle_of_friends/fc_settings.yaml文件

1
2
LINK: [
{ link: "https://blog.wpixiu.cn/link/", theme: "common2" }, # 友链页地址1,修改为你的友链页地址

切换到项目目录下,运行部署脚本

1
2
# 需 python 版本3.8
python3 deploy.py

留言板(弹幕版)

1
hexo new page comments

comment.js

1
class EasyDanmaku{constructor(t){this.container=this.checkParams(t),this.pathname=t.page||null,this.wrapperStyle=t.wrapperStyle||null,this.line=t.line||10,this.speed=t.speed||5,this.runtime=t.runtime||10,this.colourful=t.colourful||!1,this.loop=t.loop||!1,this.hover=t.hover||!1,this.coefficient=t.coefficient||1.38,this.originIndex=0,this.originList=null,this.offsetValue=this.container.offsetHeight/this.line,this.vipIndex=0,this.overflowArr=[],this.clearIng=!1,this.cleartimer=null,this.init(),this.handleEvents(t)}handleEvents(t){this.onComplete=t.onComplete||null,this.onHover=t.onHover||null}init(){this.runstatus=1,this.aisle=[],this.container.style.overflow="hidden",this.hover&&this.handleMouseHover(),"relative"!==Utils.getStyle(this.container,"position")&&"fixed"!==Utils.getStyle(this.container,"position")&&(this.container.style.position="relative");for(let t=0;t<this.line;t++)this.aisle.push({normalRow:!0,vipRow:!0})}checkParams(t){if(!document.querySelector(t.el))throw`Could not find the ${t.el} element`;if(t.wrapperStyle&&"string"!=typeof t.wrapperStyle)throw"The type accepted by the wrapperStyle parameter is string";if(t.line&&"number"!=typeof t.line)throw"The type accepted by the line parameter is number";if(t.speed&&"number"!=typeof t.speed)throw"The type accepted by the speed parameter is number";if(t.colourful&&"boolean"!=typeof t.colourful)throw"The type accepted by the colourful parameter is boolean";if(t.runtime&&"number"!=typeof t.runtime)throw"The type accepted by the runtime parameter is number";if(t.loop&&"boolean"!=typeof t.loop)throw"The type accepted by the loop parameter is boolean";if(t.coefficient&&"number"!=typeof t.coefficient)throw"The type accepted by the coefficient parameter is number";if(t.hover&&"boolean"!=typeof t.hover)throw"The type accepted by the hover parameter is boolean";if(t.onComplete&&"function"!=typeof t.onComplete)throw"The type accepted by the onComplete parameter is function";if(t.onHover&&"function"!=typeof t.onHover)throw"The type accepted by the onHover parameter is function";return document.querySelector(t.el)}send(t,e=null,i=null){if(0==this.runstatus)return void this.overflowArr.push({content:t,normalClass:e});if(t.length<1)return;let n=document.createElement("div"),r=0,s=this.speed,o=null,l=0;n.innerHTML=t,n.style.display="inline-block",n.classList.add("default-style"),(e||this.wrapperStyle)&&n.classList.add(e||this.wrapperStyle),function a(){if(r=Math.round(Math.random()*(this.line-1)),this.aisle[r].normalRow){this.aisle[r].normalRow=!1,this.container.appendChild(n),s+=n.offsetWidth/n.parentNode.offsetWidth*2,n.style.cssText=`\n                    text-align:center;\n                    min-width:130px;\n                    will-change: transform;\n                    position:absolute;\n                    right: -${n.offsetWidth+130}px;\n                    transition: transform ${s}s linear;\n                    transform: translateX(-${n.parentNode.offsetWidth+n.offsetWidth+130}px);\n                    top: ${r*this.offsetValue}px;\n                    line-height:${this.offsetValue}px;\n                    color:${this.colourful?"#"+("00000"+(16777216*Math.random()<<0).toString(16)).substr(-6):void 0}\n                `;let t=(n.parentNode.offsetWidth+n.offsetWidth)/s/60;o=setInterval((()=>{l+=t,l>n.offsetWidth*this.coefficient&&(this.aisle[r].normalRow=!0,clearInterval(o))}),16.66),setTimeout((()=>{1!=n.getAttribute("relieveDel")&&(i&&i({runtime:s,target:n,width:n.offsetWidth}),n.remove())}),1e3*s)}else{this.aisle.some((t=>!0===t.normalRow))?a.call(this):(()=>{this.overflowArr.push({content:t,normalClass:e}),this.clearIng||this.clearOverflowDanmakuArray()})()}}.call(this)}batchSend(t,e=!1,i=null){let n=this.runtime||1.23*t.length;this.originList=t,this.hasAvatar=e,this.normalClass=i;let r=setInterval((()=>{location.pathname!=this.pathname&&clearInterval(r),this.originIndex>t.length-1?(clearInterval(r),this.originIndex=0,this.onComplete&&this.onComplete(),this.loop&&this.batchSend(this.originList,e,i)):(e?this.send(`${t[this.originIndex].url?'<a href="'+t[this.originIndex].url+'">':""}<img src=${t[this.originIndex].avatar}>\n                        <p>${t[this.originIndex].content}</p>${t[this.originIndex].url?"</a>":""}\n                    `,i||this.wrapperStyle):this.send(t[this.originIndex],i||this.wrapperStyle),this.originIndex++)}),n/t.length*1e3)}centeredSend(t,e,i=3e3,n=null){let r=document.createElement("div"),s=0;r.innerHTML=t,(e||this.wrapperStyle)&&r.classList.add(e||this.wrapperStyle),function t(){if(this.aisle[s].vipRow)this.container.appendChild(r),r.style.cssText=`\n                    position:absolute;\n                    left:50%;\n                    transform:translateX(-50%);\n                    top: ${s*this.offsetValue}px;\n                `,this.aisle[s].vipRow=!1,setTimeout((()=>{n&&n({duration:i,target:r,width:r.offsetWidth}),r.remove(),this.aisle[s].vipRow=!0}),i);else{if(s++,s>this.line-1)return;t.call(this)}}.call(this)}play(){const t=this.container.children;for(let e=0;e<t.length;e++)this.controlDanmakurunStatus(t[e],1);this.runstatus=1,0!==this.overflowArr.length&&this.clearOverflowDanmakuArray()}pause(){const t=this.container.children;for(let e=0;e<t.length;e++)this.controlDanmakurunStatus(t[e],0);this.runstatus=0}controlDanmakurunStatus(t,e){const i=0,n=/-(\S*),/;if(e===1){clearTimeout(t.timer);const e=Utils.getStyle(t,"transform").match(n)[1];t.style.transition=`transform ${this.speed}s linear`,t.style.transform=`translateX(-${t.parentNode.offsetWidth+parseInt(e)+t.offsetWidth+130}px)`,t.timer=setTimeout((()=>{t.remove()}),1e3*this.speed)}else if(e===i){clearTimeout(t.timer);const e=Utils.getStyle(t,"transform").match(n)[1];t.style.transition="transform 0s linear",t.style.transform=`translateX(-${e}px)`,t.setAttribute("relieveDel",1)}}handleMouseHover(){Utils.eventDelegation(this.container,"default-style","mouseover",(t=>{t.style["z-index"]=1e3,this.controlDanmakurunStatus(t,0),this.onHover&&this.onHover(t)})),Utils.eventDelegation(this.container,"default-style","mouseout",(t=>{t.style.zIndex=1,1==this.runstatus&&this.controlDanmakurunStatus(t,1)}))}clearOverflowDanmakuArray(){clearInterval(this.cleartimer),this.clearIng=!0;let t=0;this.cleartimer=setInterval((()=>{0===this.overflowArr.length?(t++,t>20&&(clearInterval(this.cleartimer),this.clearIng=!1)):(this.send(this.overflowArr[0].content,this.overflowArr[0].normalClass||this.wrapperStyle),this.overflowArr.shift())}),500)}}class Utils{static getStyle(t,e){return window.getComputedStyle(t,null)[e]}static eventDelegation(t,e,i,n){t.addEventListener(i,(t=>{try{t.target.className.includes(e)?n(t.target):t.target.parentNode.className.includes(e)&&n(t.target.parentNode)}catch(t){}}))}}

comments.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
// todo:留言板地址,twikoo地址
function danmu() {
if (location.pathname != '/comments/' || document.body.clientWidth < 768) return //判断是否是留言板页面
console.log(1);
const Danmaku = new EasyDanmaku({
page: '/comments/', // 即留言板地址
el: '#danmu', //弹幕挂载节点
line: 10, //弹幕行数
speed: 20, //弹幕播放速度
hover: true,
loop: true, //开启循环播放
})
let data = saveToLocal.get('danmu')
if (data) Danmaku.batchSend(data, true);
else {
let ls = []
fetch('https://twikoo.wpixiu.cn/', { // 此处替换成自己的twikoo地址
method: "POST",
body: JSON.stringify({
"event": "GET_RECENT_COMMENTS",
"includeReply": false,
"pageSize": 100
}),
headers: { 'Content-Type': 'application/json' }
}).then(res => res.json()).then(({ data }) => {
data.forEach(i => {
if (i.avatar == undefined) i.avatar = 'https://cravatar.cn/avatar/d615d5793929e8c7d70eab5f00f7f5f1?d=mp'
ls.push({ avatar: i.avatar, content: i.nick + ':' + formatDanmaku(i.comment), url: i.url + '#' + i.id })
});
Danmaku.batchSend(ls, true);
saveToLocal.set('danmu', ls, 0.02)
});
// 格式化评论
function formatDanmaku(str) {
str = str.replace(/<\/*br>|[\s\uFEFF\xA0]+/g, '');
str = str.replace(/<img.*?>/g, '[图片]');
str = str.replace(/<a.*?>.*?<\/a>/g, '[链接]');
str = str.replace(/<pre.*?>.*?<\/pre>/g, '[代码块]');
str = str.replace(/<.*?>/g, '');
return str
}
}
document.getElementById('danmuBtn').innerHTML = `<button class="hideBtn" onclick="document.getElementById('danmu').classList.remove('hidedanmu')">显示弹幕</button> <button class="hideBtn" onclick="document.getElementById('danmu').classList.add('hidedanmu')">隐藏弹幕</button>`
}
danmu()
document.addEventListener("pjax:complete", danmu)

修改_config.yml文件

1
2
3
4
bottom:
# 自定义js
- <script src="/js/comment.js"></script>
- <script src="/js/comments.js"></script>

source/about/index.md 文件下添加如下内容

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
<style>
#article-container a:not(.headerlink, .fancybox, .default-style a) {
font-weight: 700;
color: var(--font-color);
padding: 0 3px;
border-bottom: 2px var(--anzhiyu-main) solid;
}

#article-container a:not(.headerlink, .fancybox, .default-style a):hover {
color: #fff;
border-radius: 5px;
text-decoration: none;
background-color: var(--anzhiyu-main);
}

#danmu {
width: 100%;
height: calc(100% - 60px);
position: fixed;
left: 0;
top: 60px;
z-index: 1;
pointer-events: none;
}

.hidedanmu {
opacity: 0;
}

.hidedanmu * {
pointer-events: none !important;
}

div#danmuBtn {
display: flex;
justify-content: center;
}

div#danmuBtn button {
background: var(--anzhiyu-main);
color: white;
padding: 8px 20px;
margin: 0 20px;
border-radius: 100px;
}

.default-style {
pointer-events: all;
cursor: pointer;
font-size: 16px;
border-radius: 100px;
overflow: hidden;
}

.default-style a {
background-color: rgba(0, 0, 0, 0.5);
transition: .3s;
color: #eee !important;
display: flex;
align-items: center;
justify-content: center;
padding: 6px 16px 6px 6px;
text-decoration: none !important;
}

.default-style a:hover {
background-color: rgba(0, 0, 0, 0.7);
}

.default-style img {
pointer-events: none;
height: 30px;
width: 30px;
margin: 0 5px 0 0 !important;
border-radius: 50% !important;
}

.default-style p {
line-height: 1;
pointer-events: none;
margin: 0 !important;
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

{% note success flat %}
如果有什么 **想说的****想问的** 或者 **发现了本站的BUG**,欢迎留言告知😊。
{% endnote %}

{% note pink 'fa-solid fa-link' flat %}
若想 **添加友链** 请前往 [友情链接](/link/) 页面进行友链申请😄
{% endnote %}

<div id="danmuBtn"></div>
<div id="danmu"></div>

小风扇

在博客根目录执行以下命令

1
hexo new page air-conditioner

然后在会生成source/air-conditioner/index.md,将以下内容替换原内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---
title: 便携小空调 - 为你的夏日带去清凉!
date: 2022-10-20 22:06:17
comments: true
aside: false
top_img: false
---

> 终于为博客安装上了便携小空调

<style>
.copyright-box a {
border-bottom: none !important;
padding: 0 !important;
}
</style>

<div id="air-conditioner-vue"></div>
<script defer data-pjax src='https://npm.elemecdn.com/anzhiyu-air-conditioner@1.0.1/index.3f125bc6.js'></script>

其他部署方式——代码嵌入网站

1
<iframe height="740" src="https://loquacious-bienenstitch-58539b.netlify.app/"></iframe>

自行构建部署
项目地址:https://github.com/anzhiyu-c/air-conditioner-vue

Hexo部署服务器(免密登录)

创建git用户

1
2
3
4
5
6
7
8
# 创建git用户
adduser git

# 使用vim编辑/etc/sudoers
vim /etc/sudoers

# 添加如下内容
git ALL=(ALL:ALL) ALL

image.png

添加秘钥

**git** 用户下操作

1
2
3
4
5
6
7
8
9
10
mkdir -p ~/.ssh 
touch ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
chmod 700 ~/.ssh

# 生成秘钥(此操作在自己电脑中执行),秘钥保存在 id_rsa.pub 中
ssh-keygen -t rsa

# 将密钥保存在服务器,将id_rsa.pub内容复制
vim ~/.ssh/authorized_keys

测试本地连接服务器

1
2
# yourIp为远程服务器的ip地址
ssh -v git@yourIp //yourIp为你的服务器ip

创建git仓库

1
2
3
4
mkdir /www/git/ 
cd /www/git/
git init --bare blog.git #在/www/git下创建新仓库blog.git
chown git:git -R blog.git #给予git用户权限

blog.git/hooks 文件夹下创建一个 post-receive 钩子

1
2
cd blog.git/hooks
vim post-receive

post-receive 文件中输入以下代码

1
2
#!/bin/bash 
git --work-tree=/www/wwwroot/hexo --git-dir=/www/git/blog.git checkout -f

授予 post-receive 文件可执行权限

1
chmod +x /www/git/blog.git/hooks/post-receive

切换git用户下操作

1
2
3
4
sudo mkdir -p /www/wwwroot/hexo
# 设置目录所有权
sudo chown -R $USER:$USER /www/wwwroot/hexo/
sudo chmod -R 755 /www/wwwroot/hexo/

测试

1
ssh -v git@你的 ip 地址

image.png
修改_config.yml

1
2
3
4
deploy:
type: 'git'
repo: git@服务器ip:/www/git/blog.git
branch: master

hexo cl;hexo g;hexo d
image.png

服务器404页面开启

image.png

兰空图床部署

简介

Lsky Pro:一个开源图床,用来最终存放图片的地方,支持第三方云储存,本地、阿里云 OSS、腾讯云 COS、七牛云、又拍云、FTP。Lsky Pro 是一个用于在线上传、管理图片的图床程序,中文名:兰空图床,你可以将它作为自己的云上相册,亦可以当作你的写作贴图库。兰空图床始于 2017 年 10 月,最早的版本由 ThinkPHP 5 开发,后又经历了数个版本的迭代,在 2021 年末启动了新的重写计划并于 2022 年 3 月份发布全新的 2.0 版本。
项目地址:https://github.com/lsky-org/lsky-pro
文档:https://docs.lsky.pro/
特性:

  • 支持本地等多种第三方云储存 AWS S3、阿里云 OSS、腾讯云 COS、七牛云、又拍云、SFTP、FTP、WebDav、Minio
  • 多种数据库驱动支持,MySQL 5.7+、PostgreSQL 9.6+、SQLite 3.8.8+、SQL Server 2017+
  • 支持配置使用多种缓存驱动,Memcached、Redis、DynamoDB、等其他关系型数据库,默认以文件的方式缓存
  • 多图上传、拖拽上传、粘贴上传、动态设置策略上传、复制、一键复制链接
  • 强大的图片管理功能,瀑布流展示,支持鼠标右键、单选多选、重命名等操作
  • 自由度极高的角色组配置,可以为每个组配置多个储存策略,同时储存策略可以配置多个角色组
  • 可针对角色组设置上传文件、文件夹路径命名规则、上传频率限制、图片审核等功能
  • 支持图片水印、文字水印、水印平铺、设置水印位置、X/y 轴偏移量设置、旋转角度等
  • 支持通过接口上传、管理图片、管理相册
  • 支持在线增量更新、跨版本更新
  • 图片广场

安装要求:

  • PHP >= 8.0.2
  • Mysql>=5.7
  • BCMath PHP 扩展
  • Ctype PHP 扩展
  • DOM PHP 拓展
  • Fileinfo PHP 扩展
  • JSON PHP 扩展
  • Mbstring PHP 扩展
  • OpenSSL PHP 扩展
  • PDO PHP 扩展
  • Tokenizer PHP 扩展
  • XML PHP 扩展
  • Imagick 拓展
  • exec、shell_exec 函数
  • readlink、symlink 函数
  • putenv、getenv 函数

安装

将安装包上传至站点目录然后解压,将站点的运行目录指向程序的 public 文件夹。
image.png

Nginx设置伪静态规则。

1
2
3
location / {
try_files $uri $uri/ /index.php?$query_string;
}

image.png

安装fileinfoimagemagickexif扩展。
image.png

取消禁用函数exec、shell_exec 、readlink、symlink 、putenv、getenv。

配置好域名以后,访问站点 首页 ,程序会自动跳转至安装页面,环境检测通过以后即可通过引导进行安装。

原图保护

原图保护的作用是隐藏图片的真实 url 路径,未开启前图片是通过运行环境输出并缓存的,开启后请求该图片会通过 PHP 接管,由 PHP 处理图片的输出。
开启原图保护功能后访问图片 404
这种情况是因为运行环境接管图片资源导致的,请更改 nginx 或 apache 的配置,例如 nginx:

1
2
3
4
5
6
7
8
9
10
# ...

location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
{
expires 30d;
error_log /dev/null;
access_log /dev/null;
}

# ...

将这段代码注释掉即可。

CDN加速

简单图床-EasyImage 部署

项目地址:https://github.com/icret/EasyImages2.0
宝塔软件商店 -> 一键部署 -> 简单图床
image.png

image.png