前言
这几天有时间就把主题升级一下,顺便总结一下修改和添加内容。我很蠢,但是蠢人有蠢人的办法,一样能实现功能就好了。
更新记录
添加 SWPP 开启 PWA
适配 Waline 文章热度
添加即刻短文图片的描述
添加评论协议至评论区
修改Tab标签生成逻辑
首稿,solitude版本 v3.0.15
修改了什么
修改内容
1. 加载动画
切换页面时展现。
- 修改 themes/solitude/source/css/_layout/fullpage.styl 文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14// ...
.loading-bg
display flex
width 100%
height 100%
position fixed
background var(--loading-bg) // 修改这一行
// ...
.loading-img
width 100px
height 100px
margin auto
animation-duration 0.5s // 修改这一行,复制完后请删除注释
// ... - 修改 themes/solitude/source/css/_mode/index.styl 文件:
1
2
3
4
5
6[data-theme=dark]
--loading-bg #000000dd // 添加这一行
// ...
[data-theme=light]
--loading-bg #ffffffdd // 添加这一行
// ...
参考 | 原帖 |
---|---|
关于本站|Solitude主题魔改内容 | ❖星港◎Star☆ |
2. Tab标签
点击按钮可以跳转页面。
- 修改 themes/solitude/scripts/tags/tabs.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;
function postTabs([name, active], content) {
// 定义正则表达式,用于匹配选项卡块
const tabBlock = /<!--\s*tab (.*?)(?:\s*\[(.*?)\])?\s*-->\n([\s\S]*?)<!--\s*endtab\s*-->/g;
// 使用 matchAll 来获取所有匹配的选项卡
const matches = [...content.matchAll(tabBlock)];
// 将一个字符串转换为数字,设置默认选中的标签索引
active = Number(active) || 0;
// 生成选项卡项函数
const generateTabItems = (matches, name, active, parentId) => {
return matches.map((match, tabId) => {
// 捕获选项卡标题和图标
const [tabCaption = '', tabIcon = ''] = match[1].split('@');
// 获取可选的链接,如果有的话
const link = match[2] ? match[2].trim() : '';
// 渲染内容,使用 Hexo 的 Markdown 渲染引擎
const postContent = renderMarkdown(match[3]);
// 生成唯一的 id,用于对应选项卡内容区域
const tabHref = `${parentId}-${tabId}`;
// 处理图标 HTML
const iconHtml = tabIcon ? `<i class="${tabIcon.trim()} tab solitude"></i>` : '';
// 判断是否为活动状态(选中状态)
const isActive = active === tabId ? ' active' : '';
// 根据是否存在链接来生成导航项
return {
nav: createNavItem(link, tabHref, iconHtml, tabCaption, isActive),
// 选项卡的内容部分
content: `<div class="tab-item-content${isActive}" id="${tabHref}">${postContent}</div>`
};
});
};
const renderMarkdown = (text) => {
return hexo.render.renderSync({ text, engine: 'markdown' }).trim();
};
const createNavItem = (link, tabHref, iconHtml, tabCaption, isActive) => {
const buttonContent = `${iconHtml}${tabCaption.trim() || `${name} ${tabHref}`}`;
return link ? `
<li class="tab${isActive}">
<button onclick="location.href='${link}'" data-href="#${tabHref}" type="button">
${buttonContent}
</button>
</li>` : `
<li class="tab${isActive}">
<button type="button" data-href="#${tabHref}">
${buttonContent}
</button>
</li>`;
};
// 生成选项卡项
const tabItems = generateTabItems(matches, name, active, name.toLowerCase().replace(/\s+/g, '-'));
// 创建完整选项卡结构(包含导航和内容)
const createTabStructure = (tabItems) => {
const tabNav = `<ul class="nav-tabs">${tabItems.map(item => item.nav).join('')}</ul>`;
const tabContent = `<div class="tab-contents">${tabItems.map(item => item.content).join('')}</div>`;
return { tabNav, tabContent };
};
// 获取生成的选项卡导航和内容
const { tabNav, tabContent } = createTabStructure(tabItems);
// 返回最终的选项卡 HTML 结构
return `<div class="tabs" id="${name.toLowerCase().replace(/\s+/g, '-')}">${tabNav}${tabContent}</div>`;
}
// 注册自定义标签,以便在 Hexo 中使用
hexo.extend.tag.register('tabs', postTabs, { ends: true });
hexo.extend.tag.register('subtabs', postTabs, { ends: true });
hexo.extend.tag.register('subsubtabs', postTabs, { ends: true }); - 在 source/custom/css/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/* tab标签的链接跳转 */
.tab-to-top {
position: relative;
display: block;
margin: 16px 0 0 auto;
color: var(--efu-lighttext);
}
.tabs .nav-tabs .tab button {
width: 100%;
padding: 0 0.3rem;
height: 100%;
font-size: 14px;
}
.tabs .nav-tabs .tab.active {
background: var(--light-grey);
}
.tabs .nav-tabs .tab.active i,
.tabs .nav-tabs .tab.active button {
color: var(--efu-lighttext);
}
.tabs .nav-tabs .tab i {
font-size: 14px;
margin-right: 0.3rem;
} - 用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19{% tabs 唯一名称, [index] %}
<!-- tab [唯一Tab] [@icon] [链接] -->
任何内容(也支持内联标签)。
<!-- endtab -->
{% endtabs %}
{% tabs test, 1 %}
<!-- tab test @fas fa-book [https://test.com] -->
任何内容(也支持内联标签)。
<!-- endtab -->
{% endtabs %}
参考 | 原帖 |
---|---|
外挂标签使用 | 伍十七 |
3. 微信公众号卡片
左边侧边栏,点击它可以跳转wechatOA。
- 修改 themes/solitude/layout/includes/widgets/aside/asideFlipCard.pug 文件:
1
2
3
4
5
6
7//- 添加这一行,位于第一行
a(href="/rss/wechatOA/",data-pajx)
.card-widget.card-platform
#flip-wrapper
#flip-content
.face
.back.face
4. 友链、留言页
改为点击按钮跳转页面。
- 修改 themes/solitude/languages/ 下所有文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# default.yml
link:
banner:
toComment: 申请/修改友链 # 修改这一行
# en.yml
link:
banner:
toComment: Application/Modification link
# zh-CN.yml
link:
banner:
toComment: 申请/修改友链
# zh-TW.yml
link:
banner:
toComment: 申請/修改友鏈 - 修改 themes/solitude/layout/includes/widgets/page/links/banner.pug 文件:
1
2
3
4
5
6
7//- ...
if theme.comment.use
//- 替换链接跳转
a.banner-button(onclick="window.open('https://github.com/ymxblog/friends/',' _blank')")
i.solitude.fas.fa-circle-chevron-right
span.solitude.banner-button-text=_p('link.banner.toComment')
//- ... - 修改 themes/solitude/layout/includes/widgets/page/message/content.pug 文件:
1
2
3
4
5
6
7
8
9
10
11
12.article-container
!= page.content
if theme.envelope.enable
.switch_message
button.open(type='button' onclick="document.getElementById('barrage').classList.remove('hide')")
span(aria-hidden='true')= __('message.open')
button.close(type='button' onclick="document.getElementById('barrage').classList.add('hide')")
span(aria-hidden='true')= __('message.close')
//- 添加这两行,链接可改
button(type='button' onclick="window.open('https://github.com/ymxblog/friends/',' _blank')")
span(aria-hidden='true')= __('link.banner.toComment')
#barrage
5. waline评论计数
无法获取waline的评论数据,返回undefined,翻了一下官方文档进行修改。
修改 themes/solitude/layout/includes/widgets/sidebar/waline.pug 如下:
1 | .length-num#waline_allcount |
原帖 | 作者 |
---|---|
API-Waline | 官网 |
6. 添加评论协议
评论区可看。
- 修改 themes/solitude/layout/includes/widgets/third-party/comments/comment.pug 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13//- ...
if use.length > 1
.comment-switch
span.first=use[0]
span#switch-btn
span.second=use[1]
a.plxycss(href="/comment/")
i.solitude.fas.fa-file-lines
span 评论协议
//- ... - 在 source/custom/css/custom.css 最后添加(没有就新建):
1
2
3
4
5
6/* 评论协议样式 */
a.plxycss {
float: right;
font-weight: bold;
font-size: 20px;
}
7. 添加即刻短文图片的描述
即刻短言可看,点效果跳转
- 修改 themes/solitude/layout/includes/page/brevity.pug 如下:
1
2
3
4
5
6
7
8
9
10
11//- ...
if item.image
.bber-content-img
each img in item.image
if typeof img === 'string'
img(src=img, alt=item.content || "图片暂无描述")
else
img(src=img.url, alt=(img.alt || item.content || "图片暂无描述"))
//- ... - 用法两者皆可
1
2
3
4
5
6
7
8
9
10
11- content: 这次做的椰奶冻粉还不错,就是糖放多了,好吃(╯▽╰ )好香~~
date: 2025-03-27 00:01:00
location: 家
image:
- url: https://images.418121.xyz/file/blog/essay/2025/03/27/01.webp
alt: 按着教程来的,事不过三,终于成功了
- content: 我弟弟妹妹画的,很有天赋,像我小时候一样 b( ̄▽ ̄)d ~
date: 2025-03-26 21:30:00
location: 家
image:
- https://images.418121.xyz/file/blog/essay/2025/03/26/01.webp
8. 适配Waline文章热度
超过五条评论即可显示”多人互动“,点效果跳转。
- 修改 themes/solitude/layout/includes/widgets/third-party/hot/index.pug 如下:
1
2
3
4
5
6
7
8
9
10
11
12- const { count } = theme.comment.hot_tip
- const { use } = theme.comment
if use
case use[0]
when 'Twikoo'
include ./twikoo
when 'Artalk'
include ./artalk
//- 添加下面两行
when 'Waline'
include ./waline - 在 themes/solitude/layout/includes/widgets/third-party/hot/ 下添加 waline.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
47script.
function updatePostsBasedOnComments() {
const location = window.location.origin;
const posts = Array.from(document.querySelectorAll('.recent-post-item[onclick] .post_cover a'))
.map(item => item.href.replace(location, ''));
const fetchCommentsCount = () => {
const requests = posts.map(url => {
return fetch(`#{theme.waline.envId}/api/comment?type=count&url=${encodeURIComponent(url)}`)
.then(response => response.json())
.then(data => ({
url: url,
count: data.errno === 0 ? data.data[0] : 0
}));
});
Promise.all(requests)
.then(handleCommentsResponse)
.catch(error => console.error("Error fetching comments:", error));
};
const handleCommentsResponse = (response) => {
response.forEach(({ url, count }) => {
if (count > #{count}) {
const postElement = document.querySelector(`.recent-post-item[onclick*="${url}"]`);
if (postElement) updatePostElement(postElement);
}
});
};
const updatePostElement = (postElement) => {
const infoTopTips = postElement.querySelector(".recent-post-info-top-tips");
const originalSpan = infoTopTips?.querySelector(".original");
const existingHotTip = infoTopTips?.querySelector(".hot-tip");
if (!existingHotTip && originalSpan) {
const hotTip = createHotTipElement();
infoTopTips.insertBefore(hotTip, originalSpan);
}
};
const createHotTipElement = () => {
const hotTip = document.createElement("span");
hotTip.classList.add("hot-tip");
const icon = document.createElement("i");
icon.classList.add("solitude", "fas", "fa-fire-flame-curved");
hotTip.appendChild(icon);
const commentCount = document.createTextNode("#{_p('hot-tip')}");
hotTip.appendChild(commentCount);
return hotTip;
};
fetchCommentsCount();
} - 确认功能是否开启
1
2
3
4
5
6
7
8# ...
# Hot comment tips
# 热评提示
hot_tip:
enable: true
# Number of hot comments
count: 5
# ...
参考了本主题适配Twikoo的用法。
添加了什么
添加内容
1. Do you like me
右边侧边栏有显示你可以点来看看,手机不展示。
原帖 | 作者 |
---|---|
为你的网站添加 Do you like me 小组件 | I Am I |
2. 恋爱墙
主页可以看,点效果即可跳转。
原帖 | 作者 |
---|---|
侧边栏 | yuang01 |
3. 弹幕留言板
类似于🍭Akilarの糖果屋信笺样式留言板,点效果即可跳转。
原帖 | 作者 |
---|---|
信笺样式留言板 | 🍭Akilarの糖果屋 |
4. 添加建站时间置页脚
左边控件点评论即可到页脚。
- 新建 source/custom/js/jz.min.js 文件填入内容(可改):
1
2document.addEventListener("DOMContentLoaded",(function(){const startTime=new Date("2024-05-12T00:00:00Z");function padZero(num){return num<10?"0"+num:String(num)}function calculateElapsedTime(start){const elapsedMilliseconds=new Date-start,totalSeconds=Math.floor(elapsedMilliseconds/1e3);return{days:Math.floor(totalSeconds/86400),hours:Math.floor(totalSeconds%86400/3600),minutes:padZero(Math.floor(totalSeconds%3600/60)),seconds:padZero(totalSeconds%60)}}function updateDisplay(){const{days:days,hours:hours,minutes:minutes,seconds:seconds}=calculateElapsedTime(startTime),runtimeElement=document.getElementById("runtime");runtimeElement&&(runtimeElement.textContent=`花期:${days} 天 ${hours} 小时 ${minutes} 分 ${seconds} 秒`),requestAnimationFrame(updateDisplay)}updateDisplay()}));
- 修改主题配置文件(url可改):
1
2
3
4
5# 页脚信息文字
# 请不要删除主题信息,这是对作者的尊重
links:
- name: <span id="runtime"></span>
url: /history/
5. 文章内插入地图
点击效果,即可跳转,如无显示请后退重新点一次。
- 安装插件
1
npm install hexo-tag-map --save
- 在想用的地方使用这外挂标签即可
1
2
3
4
5插入一个混合地图的示例: {% map %}
{% + 标签值 + 经度 + 纬度 + 文本 + 缩放等级 + 宽 + 高 + 默认图层 + %}
{% map 120.101101,30.239119, 这里是西湖灵隐寺,据说求姻缘很灵验哦!, 15, 100%, 360px, 1 %}
其它地图用法请参考原帖
6. 足迹图
点击效果,即可跳转。
- 下载zip并解压到 source/footmap/ 目录下
- 经过我的修改可以适配本主题,你想改可以自定义。
- 修改根目录 _config.yml 文件:
1
2
3
4
5
6
7
8# ...
# Include / Exclude file(s)
## include:/exclude: options only apply to the 'source/' foldinger
include:
exclude:
ignore:
- source/footmap/* #添加这一句
#... - 打开 source/footmap/data/config.json 设置数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15[
{
"latLng": [22.354887,110.946866], //为足迹的经纬度,可以通过 https://jingweidu.bmcx.com查询得到
"name": "广东 · 茂名 · 信宜", //足迹地点的名称
"articleUrl": "/posts/63e1fc9e.html", //文章地址
"desc": "老家", //足迹地点的描述, \n 为换行符
"photos":[
"https://photo.tuchong.com/20342439/f/1276790136.jpg",
"https://photo.tuchong.com/20342439/f/712590584.jpg",
"https://photo.tuchong.com/20342439/f/888292716.jpg",
"https://photo.tuchong.com/20342439/f/1184318812.jpg"
], //足迹地点的照片链接,为一组图片 url 数据
"freq": 2 //足迹地点的到访次数,范围为 [1, 10]
} // 写下一个的时候记得加逗号,最后一个不要加。
]
原帖 | 作者 |
---|---|
给自己做一个旅行地图吧(2.0升级版) | 扶苏 |
7. 订阅页面
点击效果,即可跳转。
- 新建 source/rss/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---
title: 订阅本站
date: 2024-11-16 23:10:28
update: 2024-11-17 02:21:48
desc: 推送全部文章推送简介
container: true
---
<center><h1>订阅本站</h1></center>
<p style="text-align: right;">最新更新时间为:2024-11-27</p>
<br>
<div class="rss-plan-list"><a class="rss-plan-item rss-plan-wechat" href="./wechatOA/" title="公众号" target="_blank">
<div class="rss-plan-description">推送精选文章<br>推送全文</div>
<div class="rss-plan-info-group">
<div class="rss-plan-title">公众号订阅</div>
<div class="rss-plan-info">推荐的订阅方式</div><img class="rss-plan-icon no-lightbox entered loaded"
src="https://images.418121.xyz/file/blog/page/wechat.webp" data-lazy-src="https://images.418121.xyz/file/blog/page/wechat.webp"
data-ll-status="loaded">
</div>
</a><a class="rss-plan-item rss-plan-mail" title="邮箱订阅" href="https://github.com/yeminxi/Friend-Circle-Lite/issues/new?assignees=&labels=&projects=&template=%E9%82%AE%E7%AE%B1%E8%AE%A2%E9%98%85.md&title=%5B%E9%82%AE%E7%AE%B1%E8%AE%A2%E9%98%85%5D" target="_blank">
<div class="rss-plan-description">推送全部文章<br>更新推送简介</div>
<div class="rss-plan-info-group">
<div class="rss-plan-title">邮件订阅</div>
<div class="rss-plan-info">推荐的订阅方式</div><img class="rss-plan-icon no-lightbox entered loaded"
src="https://images.418121.xyz/file/blog/page/mail.webp" data-lazy-src="https://images.418121.xyz/file/blog/page/mail.webp"
data-ll-status="loaded">
</div>
</a><a class="rss-plan-item rss-plan-rss" href="/rss2.xml" title="rss" target="_blank">
<div class="rss-plan-description">推送全部文章<br>推送简介</div>
<div class="rss-plan-info-group">
<div class="rss-plan-title">RSS</div>
<div class="rss-plan-info">备用订阅方式</div><img class="rss-plan-icon no-lightbox entered loaded"
src="https://images.418121.xyz/file/blog/page/rss.webp" data-lazy-src="https://images.418121.xyz/file/blog/page/rss.webp"
data-ll-status="loaded">
</div>
</a></div>
# 订阅本站
首先感谢你对本站的文章产生一些兴趣,本站的主要内容风格为日常及教程分享、电脑骚操作,如果你对这方面内容感兴趣,欢迎关注。
## 全部文章订阅方式
### 1. 公众号订阅
公众号订阅可以收到我觉得有价值,很重要,比较精彩的文章。这些文章将通过微信公众号发送。并不是所有博客文章都会发到公众号中。建议所有用户订阅。
### 2. RSS订阅(文章摘要)
你可以使用第三方RSS客户端接收到博客的文章摘要通知。
[https://blog.418121.xyz/atom.xml](https://blog.418121.xyz/atom.xml)
[https://blog.418121.xyz/rss2.xml](https://blog.418121.xyz/rss2.xml)
{% link ATOM,订阅链接,https://blog.418121.xyz/atom.xml %}
{% link RSS,订阅链接,https://blog.418121.xyz/rss2.xml %}
### 3. Follow订阅
你可以使用[Follow客户端](https://follow.is/)接收到博客的文章通知。
{% link Follow,订阅链接,https://app.follow.is/share/users/yeminxi %}
### 4. 邮箱订阅
* 如果你更喜欢通过邮箱接收更新,本站提供了邮箱订阅功能。此功能通过[Friend Circle Lite](https://github.com/willow-god/Friend-Circle-Lite)支持。只需简单的填写你的邮箱提交到GitHub Issue,即可开始接收本站的更新邮件,可以通过删除issue进行取消订阅!
1. 访问 [GitHub Issue](https://github.com/yeminxi/Friend-Circle-Lite/issues/new?assignees=&labels=&projects=&template=%E9%82%AE%E7%AE%B1%E8%AE%A2%E9%98%85.md&title=%5B%E9%82%AE%E7%AE%B1%E8%AE%A2%E9%98%85%5D) 页面。
2. 填写你的邮箱信息。
3. 提交 Issue,即可完成订阅!
* 期待与你的互动!如果你有任何问题或建议,欢迎随时联系我。
## 邮箱订阅功能提供鸣谢:
{% link 清羽飞扬,柳影曳曳,清酒孤灯,扬笔撒墨,心境如霜,https://blog.liushen.fun/ %}
## 本站是如何维持的
做公众号是为了保护文章版权,因为发布在个人博客站点对于版权保护能力比较弱,所以发在公众号并且标记为原创可以对文章版权有很好的保护。 - 新建 source/rss/wechatOA/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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116---
layout: false
---
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>关注「叶泯希」微信公众号</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<meta name="apple-mobile-web-app-title" content="叶泯希">
<link rel="bookmark" href="/favicon.ico">
<meta name="description" content="关注叶泯希信公众号,获取更多最新内容。">
<meta name="theme-color" content="#f6f7f8">
<style>
body {
background: #f6f7f8;
margin: 0;
padding: 0
}
.wechatOA {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
height: var(--vh)
}
.wechat-card {
background: #fff;
max-width: 428px;
max-height: 569px;
border-radius: 16px;
padding: 24px 36px 20px 36px;
display: flex;
flex-direction: column;
margin: auto;
align-items: center
}
.qrcode {
width: 256px;
height: 256px;
display: flex
}
.tips {
font-size: 14px;
color: #000;
opacity: .5;
margin-top: 8px
}
.wechatOA-card {
width: 100%;
display: flex;
margin-top: 16px;
background: #f7f7f7;
padding: 18px;
border-radius: 16px;
align-items: center
}
.wechatOA-head {
width: 48px;
height: 48px;
background-image: url(/images/avatar.webp);
border-radius: 68px;
background-size: contain;
margin-right: 16px
}
.wechatOA-name {
font-weight: 700;
color: #000;
}
.wechatOA-description {
font-size: 12px;
color: #000;
opacity: .5
}
a.help {
font-size: 14px;
color: #000;
opacity: .5;
text-decoration: none;
margin: 20px 0
}
a.help:visited {
color: #000
}
</style>
</head>
<body>
<div class="wechatOA">
<div class="wechat-card">
<img src="gzh.webp" class="qrcode" alt="">
<div class="tips">扫描二维码关注公众号</div>
<div class="wechatOA-card">
<div class="wechatOA-head"></div>
<div class="wechatOA-info">
<div class="wechatOA-name">叶泯希</div>
<div class="wechatOA-description">意义是自己赐予的</div>
</div>
</div>
</div>
<a class="help" href="https://kf.qq.com/touch/wxappfaq/1208117b2mai141113jaqAnU.html?platform=14" title="帮助" target="_blank">不会扫码?</a>
</div>
<script>
const vh = 1 * window.innerHeight;
document.documentElement.style.setProperty("--vh", `${vh}px`),
window.addEventListener("resize", (()=>{
let e = 1 * window.innerHeight;
document.documentElement.style.setProperty("--vh", `${e}px`)
}
))
</script>
</body>
</html> - 在 source/custom/css/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
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/* 卡片订阅样式 */
#post .tag_share + #follow {
margin-top: 40px;
}
.rss-plan-list {
display: flex;
width: 100%;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -4px;
position: relative;
justify-content: space-between;
}
.rss-plan-list a {
border-bottom: none;
border-radius: 12px;
}
.rss-plan-item:visited {
color: var(--efu-white);
}
.rss-plan-item.rss-plan-wechat {
background: #27c125;
}
.rss-plan-item.rss-plan-mail {
background: var(--efu-blue);
}
.rss-plan-item.rss-plan-rss {
background: var(--efu-orange);
}
.rss-plan-item {
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 240px;
height: 240px;
margin: 6px 0;
overflow: hidden;
text-decoration: none;
width: calc(100% / 3 - 8px);
filter: brightness(1);
transition: .3s;
border: var(--style-border);
box-shadow: var(--efu-shadow-border);
}
.rss-plan-item:hover {
filter: brightness(1.1)
}
@media screen and (max-width: 1024px) {
.rss-plan-item {
width:calc(100% / 2 - 4px)
}
.rss-plan-item:first-child {
width: 100%
}
}
@media screen and (max-width: 768px) {
.rss-plan-item {
width:100%
}
}
/* 卡片大小 */
.rss-plan-description {
font-size: 16px;
color: var(--efu-white);
margin: 26px 0 0 30px;
line-height: 20px;
}
.rss-plan-info-group {
position: relative;
margin: 0 0 26px 30px;
color: var(--efu-white);
}
.rss-plan-title {
font-size: 36px;
font-weight: 700;
width: fit-content;
line-height: 1;
}
.rss-plan-info {
width: fit-content;
opacity: .6;
}
img.rss-plan-icon {
position: absolute;
bottom: -50px;
right: -20px;
height: 140px;
-webkit-user-drag: none;
user-select: none;
-webkit-transition: all 1.2s cubic-bezier(.39,.575,.565,1) ;
-moz-transition: all 1.2s cubic-bezier(.39,.575,.565,1) ;
-o-transition: all 1.2s cubic-bezier(.39,.575,.565,1) ;
-ms-transition: all 1.2s cubic-bezier(.39,.575,.565,1) ;
transition: all 1.2s cubic-bezier(.39,.575,.565,1) ;
transform-origin: bottom right;
filter: blur(8px);
opacity: .6;
}
.rss-plan-item:hover img.rss-plan-icon {
bottom: -40px;
right: -10px;
filter: blur(0);
opacity: 1
}
.rss-plan-item:hover {
text-decoration: none ;
}
8. 重构404页
点击效果,即可跳转。
- 新建 source/404.md 文件,在文件内输入以下内容:
1
2
3
4---
layout: false
---
<script src="//cdn.dnpw.org/404/v1.min.js" maincolor="#000" jumptime="-1" jumptarget="/" tips="404" error="" charset="utf-8"></script> - 修改主题配置文件
1
2
3
4
5
6# Page default settings
# 页面默认设置
page:
# 404 page
# 404 页面
error: false
原帖 | 作者 |
---|---|
小代码,大公益 | 域名公益 |
9. 自定义link标签
- 新建 themes/solitude/scripts/tags/link.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;
const { parse } = require('psl');
// 定义不同域名对应的头像URL
const avatarMap = new Map([
['418121.xyz', '/images/avatar.webp'],
['github.com', 'https://images.418121.xyz/file/blog/page/git.webp']
]);
// 定义白名单域名
const whitelist = new Set([
'418121.xyz',
'yeminxi.github.io'
]);
// 获取URL的根域名
function getRootDomain(url) {
try {
const hostname = new URL(url).hostname;
const parsed = parse(hostname);
return parsed.domain || hostname;
} catch {
return url.split('/')[0]; // Fallback
}
}
function link(args) {
try {
// 参数解析(支持转义逗号)
const parsedArgs = args.join(' ')
.split(/(?<!\\),/)
.map(s => s.replace(/\\,/g, ',').trim());
const [title = '', sitename = '', rawLink = ''] = parsedArgs;
const link = rawLink.startsWith('http') ? rawLink : `https://${rawLink}`;
// 域名处理
const rootDomain = getRootDomain(link);
const imgUrl = avatarMap.get(rootDomain) ||
`https://api.xinac.net/icon/?url=${encodeURIComponent(rootDomain)}`; //使用api获取网站的ico
// 白名单校验
const isSafe = whitelist.has(rootDomain) || rootDomain.endsWith('.418121.xyz');
const tipMessage = isSafe
? '🛡️ 来自本站地址,本站可确保其安全性,请放心点击跳转'
: '⚠️ 引用站外地址,不保证站点的可用性和安全性,慎重点';
return `
<div class="liushen-tag-link">
<a class="tag-Link" target="_blank" rel="noopener" href="${link}">
<div class="tag-link-tips">${tipMessage}</div>
<div class="tag-link-bottom">
<div class="tag-link-left"
style="background-image: url(${imgUrl})"
onerror="this.style.backgroundImage='url(/images/default-avatar.webp)'"></div>
<div class="tag-link-right">
<div class="tag-link-title">${hexo.extend.helper.get('escape_html')(title)}</div>
<div class="tag-link-sitename">${hexo.extend.helper.get('escape_html')(sitename)}</div>
</div>
<i class="fa-solid fa-angle-right"></i>
</div>
</a>
</div>`;
} catch (error) {
console.error('Link tag error:', error);
return `<div class="liushen-error">链接卡片生成失败:${error.message}</div>`;
}
}
hexo.extend.tag.register('link', link, { ends: false }); - 新建 themes/solitude/source/css/_tags/link.styl 文件,在文件内输入内容:
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#content-inner
.tag-Link
background var(--efu-secondbg)
border-radius 12px
display flex
border 1px solid var(--style-border)
flex-direction column
padding 0.5rem 1rem
margin-top 1rem
text-decoration none
color var(--efu-fontcolor)
margin-bottom 10px
transition background-color 0.3s, border-color 0.3s, box-shadow 0.3s
&:hover
border-color var(--style-border-hover)
background-color var(--efu-main)
box-shadow 0 0 5px rgba(0, 0, 0, 0.2)
.tag-link-tips
color var(--efu-fontcolor)
border-bottom 1px solid var(--efu-gray)
padding-bottom 4px
font-size 0.6rem
font-weight normal
.tag-link-bottom
display flex
margin-top 0.5rem
align-items center
justify-content space-around
.tag-link-left
width 60px
min-width 60px
height 60px
background-size cover
border-radius 25%
background-color var(--efu-card-bg)
.tag-link-right
margin-left 1rem
.tag-link-title
font-size 1rem
line-height 1.2
.tag-link-sitename
font-size 0.7rem
color var(--efu-fontcolor)
font-weight normal
margin-top 4px
transition color 0.3s
&:hover .tag-link-sitename
color var(--efu-main)
i
margin-left auto
color: var(--efu-fontcolor) - 用法
1
{% link 标题,描述,链接 %}
原帖 | 作者 |
---|---|
魔改笔记七:分类条及外链卡片 | LiuShen |
10. 安全跳转页面
- 安装
1
2npm install cheerio --save
npm install hexo-safego --save - 在 Hexo 根目录 _config.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# hexo-safego安全跳转插件
# see https://blog.liushen.fun/posts/1dfd1f41/
hexo_safego:
general: # 基本功能设置
enable: true # 启用插件
enable_base64_encode: true # 使用 Base64 编码
enable_target_blank: true # 从新窗口打开跳转页面
security: # 安全设置
url_param_name: 'u' # URL 参数名
html_file_name: 'go.html' # 重定向页面的文件名
ignore_attrs: # 忽略处理的 HTML 结构
- 'data-fancybox'
scope: # 容器与页面设置
apply_containers: # 应用的容器选择器
- '#post'
apply_pages: # 应用的页面路径
- "/posts/"
exclude_pages: # 排除的页面路径
whitelist: # 域名白名单
domain_whitelist: # 允许的白名单域名,通过字符串匹配实现
- "yeminixi.github.io"
- "418121.xyz"
appearance: # 页面外观设置
avatar: ./images/avatar.webp # 跳转页面头像路径
title: "叶泯希" # 跳转页面标题
subtitle: "安全中心提示你不要乱点,它与本站不存在法律裙带关系ヽ(≧□≦)ノ" # 跳转页面副标题
darkmode: false # 是否启用深色模式
countdowntime: -1 # 跳转页面倒计时秒数,如果设置为负数则为不自动跳转
debug: # 调试设置
enable: false # 启用调试模式
原帖 | 作者 |
---|---|
安全跳转页面·插件版 | LiuShen |
11. Waline评论链接安全跳转
点击效果,即可跳转,下面有链接点点看。
- 在你 Github 中 Fork 的 Waline 仓库下修改 index.cjs 文件内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24const Application = require('@waline/vercel');
const LinkInterceptor = require('waline-link-interceptor'); // 添加这一行
module.exports = Application({
forbiddenWords: ['唱跳', 'rap篮球'], //词汇限制
disallowIPList: ['8.8.8.8', '4.4.4.4'],//黑名单IP
plugins: [
LinkInterceptor({
whiteList: [
'yeminxi.github.io',
'418121.xyz'
],
// blackList: [],
// interceptorTemplate: `hello __URL__ `, // 如果下面自定义了跳转地址,那么此处模板不生效
redirectUrl: "https://blog.418121.xyz/go.html", // 填写中间页的具体 html 地址。
encodeFunc: (url) =>{
return "u="+Buffer.from(url).toString('base64'); // 填入一个外链 url 的处理函数
}
})
],
async postSave(comment) {
// do what ever you want after comment saved
},
}); - 修改 package.json 文件:
1
2
3
4
5
6
7
8
9{
"name": "template",
"version": "1.32.3",
"private": true,
"dependencies": {
"@waline/vercel": "latest",
"waline-link-interceptor": "^0.1.2" //添加这一句
}
}
原帖 | 作者 |
---|---|
Hexo 博客与 Waline 评论区实现外链跳转中间页 | wuanqin |
12. 添加统计页面
点击效果,即可跳转。
- 新建 **themes/solitude/sscripts/helper/charts.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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397const cheerio = require('cheerio')
const moment = require('moment')
hexo.extend.filter.register('after_render:html', function (locals) {
const $ = cheerio.load(locals)
const post = $('#posts-chart')
const tag = $('#tags-chart')
const category = $('#categories-chart')
const htmlEncode = false
if (post.length > 0 || tag.length > 0 || category.length > 0) {
if (post.length > 0 && $('#postsChart').length === 0) {
if (post.attr('data-encode') === 'true') htmlEncode = true
post.after(postsChart(post.attr('data-start')))
}
if (tag.length > 0 && $('#tagsChart').length === 0) {
if (tag.attr('data-encode') === 'true') htmlEncode = true
tag.after(tagsChart(tag.attr('data-length')))
}
if (category.length > 0 && $('#categoriesChart').length === 0) {
if (category.attr('data-encode') === 'true') htmlEncode = true
category.after(categoriesChart(category.attr('data-parent')))
}
if (htmlEncode) {
return $.root().html().replace(/&#/g, '&#')
} else {
return $.root().html()
}
} else {
return locals
}
}, 15)
function postsChart (startMonth) {
const startDate = moment(startMonth || '2020-01')
const endDate = moment()
const monthMap = new Map()
const dayTime = 3600 * 24 * 1000
for (let time = startDate; time <= endDate; time += dayTime) {
const month = moment(time).format('YYYY-MM')
if (!monthMap.has(month)) {
monthMap.set(month, 0)
}
}
hexo.locals.get('posts').forEach(function (post) {
const month = post.date.format('YYYY-MM')
if (monthMap.has(month)) {
monthMap.set(month, monthMap.get(month) + 1)
}
})
const monthArr = JSON.stringify([...monthMap.keys()])
const monthValueArr = JSON.stringify([...monthMap.values()])
return `
<script id="postsChart">
var color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
var postsChart = echarts.init(document.getElementById('posts-chart'), 'light');
var postsOption = {
title: {
text: '文章发布统计图',
x: 'center',
textStyle: {
color: color
}
},
tooltip: {
trigger: 'axis'
},
xAxis: {
name: '日期',
type: 'category',
boundaryGap: false,
nameTextStyle: {
color: color
},
axisTick: {
show: false
},
axisLabel: {
show: true,
color: color
},
axisLine: {
show: true,
lineStyle: {
color: color
}
},
data: ${monthArr}
},
yAxis: {
name: '文章篇数',
type: 'value',
nameTextStyle: {
color: color
},
splitLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
show: true,
color: color
},
axisLine: {
show: true,
lineStyle: {
color: color
}
}
},
series: [{
name: '文章篇数',
type: 'line',
smooth: true,
lineStyle: {
width: 0
},
showSymbol: false,
itemStyle: {
opacity: 1,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(128, 255, 165)'
},
{
offset: 1,
color: 'rgba(1, 191, 236)'
}])
},
areaStyle: {
opacity: 1,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(128, 255, 165)'
}, {
offset: 1,
color: 'rgba(1, 191, 236)'
}])
},
data: ${monthValueArr},
markLine: {
data: [{
name: '平均值',
type: 'average',
label: {
color: color
}
}]
}
}]
};
postsChart.setOption(postsOption);
window.addEventListener('resize', () => {
postsChart.resize();
});
postsChart.on('click', 'series', (event) => {
if (event.componentType === 'series') window.location.href = '/archives/' + event.name.replace('-', '/');
});
</script>`
}
function tagsChart (len) {
const tagArr = []
hexo.locals.get('tags').map(function (tag) {
tagArr.push({ name: tag.name, value: tag.length, path: tag.path })
})
tagArr.sort((a, b) => { return b.value - a.value })
const dataLength = Math.min(tagArr.length, len) || tagArr.length
const tagNameArr = []
for (let i = 0; i < dataLength; i++) {
tagNameArr.push(tagArr[i].name)
}
const tagNameArrJson = JSON.stringify(tagNameArr)
const tagArrJson = JSON.stringify(tagArr)
return `
<script id="tagsChart">
var color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
var tagsChart = echarts.init(document.getElementById('tags-chart'), 'light');
var tagsOption = {
title: {
text: 'Top ${dataLength} 标签统计图',
x: 'center',
textStyle: {
color: color
}
},
tooltip: {},
xAxis: {
name: '标签',
type: 'category',
nameTextStyle: {
color: color
},
axisTick: {
show: false
},
axisLabel: {
show: true,
color: color,
interval: 0
},
axisLine: {
show: true,
lineStyle: {
color: color
}
},
data: ${tagNameArrJson}
},
yAxis: {
name: '文章篇数',
type: 'value',
splitLine: {
show: false
},
nameTextStyle: {
color: color
},
axisTick: {
show: false
},
axisLabel: {
show: true,
color: color
},
axisLine: {
show: true,
lineStyle: {
color: color
}
}
},
series: [{
name: '文章篇数',
type: 'bar',
data: ${tagArrJson},
itemStyle: {
borderRadius: [5, 5, 0, 0],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(128, 255, 165)'
},
{
offset: 1,
color: 'rgba(1, 191, 236)'
}])
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(128, 255, 195)'
},
{
offset: 1,
color: 'rgba(1, 211, 255)'
}])
}
},
markLine: {
data: [{
name: '平均值',
type: 'average',
label: {
color: color
}
}]
}
}]
};
tagsChart.setOption(tagsOption);
window.addEventListener('resize', () => {
tagsChart.resize();
});
tagsChart.on('click', 'series', (event) => {
if(event.data.path) window.location.href = '/' + event.data.path;
});
</script>`
}
function categoriesChart (dataParent) {
const categoryArr = []
let categoryParentFlag = false
hexo.locals.get('categories').map(function (category) {
if (category.parent) categoryParentFlag = true
categoryArr.push({
name: category.name,
value: category.length,
path: category.path,
id: category._id,
parentId: category.parent || '0'
})
})
categoryParentFlag = categoryParentFlag && dataParent === 'true'
categoryArr.sort((a, b) => { return b.value - a.value })
function translateListToTree (data, parent) {
let tree = []
let temp
data.forEach((item, index) => {
if (data[index].parentId == parent) {
let obj = data[index];
temp = translateListToTree(data, data[index].id);
if (temp.length > 0) {
obj.children = temp
}
if (tree.indexOf())
tree.push(obj)
}
})
return tree
}
const categoryNameJson = JSON.stringify(categoryArr.map(function (category) { return category.name }))
const categoryArrJson = JSON.stringify(categoryArr)
const categoryArrParentJson = JSON.stringify(translateListToTree(categoryArr, '0'))
return `
<script id="categoriesChart">
var color = document.documentElement.getAttribute('data-theme') === 'light' ? '#4c4948' : 'rgba(255,255,255,0.7)'
var categoriesChart = echarts.init(document.getElementById('categories-chart'), 'light');
var categoryParentFlag = ${categoryParentFlag}
var categoriesOption = {
title: {
text: '文章分类统计图',
x: 'center',
textStyle: {
color: color
}
},
legend: {
top: 'bottom',
data: ${categoryNameJson},
textStyle: {
color: color
}
},
tooltip: {
trigger: 'item'
},
series: []
};
categoriesOption.series.push(
categoryParentFlag ?
{
nodeClick :false,
name: '文章篇数',
type: 'sunburst',
radius: ['15%', '90%'],
center: ['50%', '55%'],
sort: 'desc',
data: ${categoryArrParentJson},
itemStyle: {
borderColor: '#fff',
borderWidth: 2,
emphasis: {
focus: 'ancestor',
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(255, 255, 255, 0.5)'
}
}
}
:
{
name: '文章篇数',
type: 'pie',
radius: [30, 80],
roseType: 'area',
label: {
color: color,
formatter: '{b} : {c} ({d}%)'
},
data: ${categoryArrJson},
itemStyle: {
emphasis: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(255, 255, 255, 0.5)'
}
}
}
)
categoriesChart.setOption(categoriesOption);
window.addEventListener('resize', () => {
categoriesChart.resize();
});
categoriesChart.on('click', 'series', (event) => {
if(event.data.path) window.location.href = '/' + event.data.path;
});
</script>`
} - 修改主题配置文件
1
2
3
4
5
6
7# Extend
# 扩展
extends:
# Insert in head
# 插入到 head
head:
- <script src="https://npm.elemecdn.com/echarts@4.9.0/dist/echarts.min.js"></script> - 用法,在任意位置插入(记得开启 container: true ):
1
2
3
4
5
6<!-- 文章发布时间统计图 -->
<div id="posts-chart" data-start="2021-01" style="border-radius: 8px; height: 300px; padding: 10px;"></div>
<!-- 文章标签统计图 -->
<div id="tags-chart" data-length="10" style="border-radius: 8px; height: 300px; padding: 10px;"></div>
<!-- 文章分类统计图 -->
<div id="categories-chart" data-parent="true" style="border-radius: 8px; height: 300px; padding: 10px;"></div>
原帖 | 作者 |
---|---|
关于本站|Solitude主题魔改内容 | ❖星港◎Star☆ |
13. 页面预加载
打开浏览器开发者模式切换到网络,然后找个本站页面把鼠标放上去,会进行预加载。
- 修改主题配置文件
1
2
3
4
5
6
7# Extend
# 扩展
extends:
# Insert in head
# 插入到 head
head:
- <script src="//instant.page/5.2.0" type="module"></script>
原帖 | 作者 |
---|---|
使您网站的页面即时在 1 分钟内并显着提高您的转化率 | Alexandre Dieulot |
14. Vercel 加速
- 在 source/ 目录下新建一个 vercel.json 文件,在文件内输入以下内容:
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{
"headers": [
{
"source": "/sw.js",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=0, must-revalidate"
}
]
},
{
"source": "(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, s-maxage=86400, max-age=86400"
}, {
"key": "Vercel-CDN-Cache-Control",
"value": "max-age=31536000"
}
]
}
]
}
原帖 | 作者 |
---|---|
Vercel 加速,快,不止更快 | Mystic Stars |
15. 友链状态检测
点击效果,即可跳转。
- 这里只讲前端配置,完整教程请看参考。
- 动态获取json,适配 solitude主题 的方法,根目录下新建 link.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
81const fs = require('node:fs');
const https = require('node:https');
const dataUrl = 'https://cdn.418121.xyz/links/active.json'; // 替换成你的数据来源的URL
const outputFilePath = './source/friend.json'; // 替换成你想要保存json文件的路径
const forbiddenNames = [
'Hexo',
'开往-友链接力',
'笔墨迹BLOGS·CN',
'博客录boke.lu',
'博友圈',
'集博栈',
'博客圈',
'叶泯希-',
'叶泯希~',
'博客我们BlogWe',
'博客驿站',
'十年之约',
'个站商店',
'BlogFinder'
]; // 想要禁止的name,可以修改
function generateFriendJson(dataUrl, outputFilePath, forbiddenNames) {
https.get(dataUrl, (response) => {
let data = '';
// 检查响应状态
if (response.statusCode !== 200) {
console.error(`请求失败,状态码: ${response.statusCode}`);
return;
}
// 监听数据块
response.on('data', (chunk) => {
data += chunk;
});
// 完成请求
response.on('end', () => {
try {
const jsonData = JSON.parse(data);
const friends = [];
console.log("获取的数据:", jsonData); // 输出获取的数据以确认结构
jsonData.content.forEach(item => {
// 打印每个 item 的名字,看是否在 forbiddenNames 中
console.log(`正在处理: ${item.title}`);
if (!forbiddenNames.includes(item.title)) {
friends.push({
name: item.title,
link: item.link,
avatar: item.avatar
});
} else {
console.log(`禁用项: ${item.title}`); // 如果是禁用项,打印提示
}
});
// 根据规定的格式构建 JSON 数据
const friendData = { friends: friends.map(item => [item.name, item.link, item.avatar]) };
// 将 JSON 对象转换为字符串
const friendJSON = JSON.stringify(friendData, null, 2);
// 写入 friend.json 文件
fs.writeFileSync(outputFilePath, friendJSON);
console.log(`创建文件成功 friend.json at ${outputFilePath}`);
} catch (error) {
console.error('解析数据失败:', error);
}
});
}).on('error', (error) => {
console.error('请求错误:', error);
});
}
// 调用函数生成 JSON 文件
generateFriendJson(dataUrl, outputFilePath, forbiddenNames); - 使用命令生成 friend.json
1
node link.js
- 我使用 Github Action 构建站点,在前面加上:
1
2
3
4
5
6
7
8
9
10
11
12- name: 安装依赖
if: steps.cache.outputs.cache-hit != 'true'
run: |
npm install gulp-cli -g #全局安装gulp
npm install --save
npm install yamljs --save //加入这一句
- name: 更新友链朋友圈
run: |
node link.js //加入这一句
node links.js
node autolinks.js - 在友链的页面 /links/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
99
100
101
102
103
104
105---
title: 友情链接
date: 2024-11-17 06:08:33
type: links
desc: 与数百名博主共同进步
data: links
banner: true
comment: false
---
<style>
.status-tag {
position: absolute;
bottom: 0px;
right: 0px;
padding: 3px 8px;
border-radius: 12px 0px 12px 0px;
font-size: 12px;
color: white;
font-weight: bold;
transition: font-size 0.3s ease-out, width 0.3s ease-out, opacity 0.3s ease-out;
}
.flink-list-item:hover .status-tag {
font-size: 0px;
opacity: 0;
}
/* 固态颜色 */
.status-tag-green {
background-color: #005E00; /* 绿色 */
}
.status-tag-light-yellow {
background-color: #FED101; /* 浅黄色 */
}
.status-tag-dark-yellow {
background-color: #F0B606; /* 深黄色 */
}
.status-tag-red {
background-color: #B90000; /* 红色 */
}
</style>
<script>
function addStatusTagsWithCache(jsonUrl) {
const cacheKey = "statusTagsData";
const cacheExpirationTime = 30 * 60 * 1000; // 半小时
function applyStatusTags(data) {
const linkStatus = data.link_status;
document.querySelectorAll('.cf-friends-link').forEach(card => { // 一定要注意这里的类名,小心匹配不上
if (!card.href) return;
const link = card.href.replace(/\/$/, '');
const statusTag = document.createElement('div');
statusTag.classList.add('status-tag');
let matched = false;
// 查找链接状态
const status = linkStatus.find(item => item.link.replace(/\/$/, '') === link);
if (status) {
let latencyText = '未知';
let className = 'status-tag-red'; // 默认红色
if (status.latency === -1) {
latencyText = '未知';
} else {
latencyText = status.latency.toFixed(2) + ' s';
if (status.latency <= 2) {
className = 'status-tag-green';
} else if (status.latency <= 5) {
className = 'status-tag-light-yellow';
} else if (status.latency <= 10) {
className = 'status-tag-dark-yellow';
}
}
statusTag.textContent = latencyText;
statusTag.classList.add(className);
matched = true;
}
if (matched) {
card.style.position = 'relative';
card.appendChild(statusTag);
}
});
}
function fetchDataAndUpdateUI() {
fetch(jsonUrl)
.then(response => response.json())
.then(data => {
applyStatusTags(data);
const cacheData = {
data: data,
timestamp: Date.now()
};
localStorage.setItem(cacheKey, JSON.stringify(cacheData));
})
.catch(error => console.error('Error fetching test-flink result.json:', error));
}
const cachedData = localStorage.getItem(cacheKey);
if (cachedData) {
const { data, timestamp } = JSON.parse(cachedData);
if (Date.now() - timestamp < cacheExpirationTime) {
applyStatusTags(data);
return;
}
}
fetchDataAndUpdateUI();
}
setTimeout(() => {
addStatusTagsWithCache('https://check.api.418121.xyz/result.json');
}, 0);
</script>
原帖 | 作者 |
---|---|
Github Action实现友链状态检测 | LiuShen |
16. 朋友圈
点击效果,即可跳转,往下拖到最后。
- 这里只讲前端配置,完整教程请看参考。
- 动态获取json,适配 solitude主题 的方法,根目录下新建 links.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
64const fs = require('fs');
const https = require('https');
const dataUrl = 'https://cdn.418121.xyz/links/active.json'; // 替换成你的数据来源的URL
const outputFilePath = './source/flink_count.json'; // 替换成你想要保存json文件的路径
function generateLinkCountJson(dataUrl, outputFilePath) {
https.get(dataUrl, (response) => {
let data = '';
// 检查响应状态
if (response.statusCode !== 200) {
console.error(`请求失败,状态码: ${response.statusCode}`);
return;
}
// 监听数据块
response.on('data', (chunk) => {
data += chunk;
});
// 完成请求
response.on('end', () => {
try {
const jsonData = JSON.parse(data);
if (!Array.isArray(jsonData.content)) {
console.error("错误: JSON 文件中没有有效的 content 数组");
return;
}
const ls = [];
console.log(`链接列表条目数: ${jsonData.content.length}`);
for (const item of jsonData.content) {
console.log(`正在处理链接: ${item.title}`);
ls.push({
name: item.title,
link: item.link,
avatar: item.avatar || '',
descr: item.descr || '无描述',
topimg: item.topimg || null
});
}
// 计数
const length = ls.length;
console.log(`总链接数: ${length}`);
// 将结果写入到 JSON 文件中
fs.writeFileSync(outputFilePath, JSON.stringify({ link_list: ls, length: length }, null, 2));
console.log(`创建文件成功 flink_count.json at ${outputFilePath}`);
} catch (error) {
console.error('解析数据失败:', error);
}
});
}).on('error', (error) => {
console.error('请求错误:', error);
});
}
// 调用函数生成 JSON 文件
generateLinkCountJson(dataUrl, outputFilePath); - 使用命令生成 flink_count.json
1
node links.js
- 我使用 Github Action 构建站点,在前面加上:
1
2
3
4
5
6
7
8
9
10
11
12- name: 安装依赖
if: steps.cache.outputs.cache-hit != 'true'
run: |
npm install gulp-cli -g #全局安装gulp
npm install --save
npm install yamljs --save //加入这一句
- name: 更新友链朋友圈
run: |
node link.js
node links.js //加入这一句
node autolinks.js - 在友链的页面 /pyq/index.md 添加以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22---
title: 朋友圈
date: 2024-11-17 06:08:33
type: banner
container: true
---
<div id="friend-circle-lite-root"></div>
<script>
if (typeof UserConfig === 'undefined') {
var UserConfig = {
// 填写你的fc Lite地址
private_api_url: 'https://fc.api.418121.xyz/',
// 点击加载更多时,一次最多加载几篇文章,默认20
page_turning_number: 20,
// 头像加载失败时,默认头像地址
error_img: 'https://api.vvhan.com/api/avatar/niche',
}
}
</script>
<link rel="stylesheet" href="https://fc.api.418121.xyz/main/fclite.css">
<script src="https://fc.api.418121.xyz/main/fclite.js">//https://cdn.jsdelivr.net/gh/yeminxi/Friend-Circle-Lite/main/fclite.js
</script>
原帖 | 作者 |
---|---|
Friend-Circle-Lite:轻量友链朋友圈 | LiuShen |
17. 动态友链结合友链状态检测及朋友圈
- 动态获取json生成 links.yml 友链文件,适配 solitude主题 的方法,根目录下新建 autolinks.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
154const fs = require('fs');
const https = require('https');
const yaml = require('js-yaml');
// 定义输入 URL 和输出文件名
const inputUrls = [
'https://cdn.418121.xyz/links/花架.json',
'https://cdn.418121.xyz/links/花友.json',
'https://cdn.418121.xyz/links/花族.json',
'https://cdn.418121.xyz/links/花主.json',
'https://cdn.418121.xyz/links/suspend.json',
'https://cdn.418121.xyz/links/404.json',
];
const outputFileName = './source/_data/links.yml';
// 自定义名称和描述字典,可以根据 URL 或文件名进行变更
const customNames = {
'花架': '花架',
'花友': '花友',
'花族': '花族',
'花主': '花主',
'suspend': '花单',
'404': '花失',
};
const descriptions = {
'花架': '鸣谢提供框架及主题、技术支持',
'花友': '造物主自玫瑰诞生起便策划了这场名为救赎的邂逅',
'花族': '本站已加入的族织',
'花主': '许是太久没有惊艳了,花也会忘了它是什么样子',
'suspend': '单向友链,请添加本站链接后在GitHub提交issue留言',
'404': '失效友链,请在GitHub提交issue修改后留言',
};
// 自定义类型字典
const customTypes = {
'花架': 'item',
'花友': 'item',
'花族': 'item',
'花主': 'item',
'suspend': 'discn',
'404': 'discn',
};
// 用于存储所有分类的数组
const allCategories = [];
// 读取每个 JSON URL 并处理数据
const readJsonUrls = async () => {
for (let index = 0; index < inputUrls.length; index++) {
const url = inputUrls[index];
try {
const jsonObj = await fetchJson(url);
// 提取 URL 最后一部分作为分类名称(去掉扩展名)
const className = url.split('/').pop().replace('.json', '');
// 获取对应的描述、自定义名称和类型,如果不存在则使用默认
const descr = descriptions[className] || `来自 ${className} 的链接`;
const customName = customNames[className] || `默认名称 ${className}`;
const customType = customTypes[className] || 'default'; // 默认类型
// 将当前文件内容转换为 YAML 格式的数据结构
let links = jsonObj.content.map(item => ({
name: item.title,
link: item.link,
avatar: item.avatar,
descr: item.descr,
topimg: item.topimg || '',
tag: item.tag || '',
color: item.color || ''
}));
// 如果是第二个 URL,我们进行乱序处理
if (index === 1) { // 注意这里的 index 从 0 开始,所以2号的位置是1
links = shuffleArray(links); // 乱序
}
links.reverse(); // 逆序处理
// 创建分类对象并推入分类数组
allCategories.push({
class_name: customName,
descr: descr,
type: customType,
suffix: '',
link_list: links
});
} catch (err) {
console.error(`读取 URL ${url} 时出错:`, err.message);
}
}
};
// 随机打乱数组的函数
const shuffleArray = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]]; // 交换元素
}
return array;
};
// 使用 https 模块获取 JSON 数据
const fetchJson = (url) => {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
let data = '';
// 收集数据
res.on('data', chunk => {
data += chunk;
});
// 请求结束后解析 JSON
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(new Error('无法解析 JSON'));
}
});
}).on('error', (err) => {
reject(err);
});
});
};
// 生成 YAML 数据并写入文件
const generateYaml = () => {
const yamlData = {
banner_suffix: '',
links: allCategories
};
// 将生成的数据转换为 YAML 格式,强制使用双引号来防止折叠格式
const generatedYaml = yaml.dump(yamlData, {
noRefs: true,
lineWidth: -1 // 设置行宽为 -1,避免自动折行
});
// 写入到 YAML 文件
fs.writeFileSync(outputFileName, generatedYaml, 'utf8');
console.log('YAML 文件已成功生成。');
};
// 执行读取和写入流程
const processUrls = async () => {
await readJsonUrls();
generateYaml();
};
processUrls(); - 请参考原教程到
5. 配置 config.yml
,后接着下来我的步骤,或者直接Fork我的仓库进行修改。1
2
3
4
5
6
7
8
9
10
11# 网络请求设置
request:
timeout: 10 # 超时设置
ssl: false # ssl设置
# 要抓取的 issues 配置
issues:
repo: ymxblog/friends # 仓库持有者/仓库名
label:
groups: ['active','checklist','suspend','404','花架','花友','花族','花主'] #标签有多少个就都写进去
sort: - 修改 /generator/main.py 文件:
1
outputdir = os.path.join(version, 'links') # 第十行。输出文件结构变化时,更新输出路径版本
- 修改 .github/workflows/generator.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
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
100name: Generator
on:
issues:
watch:
types: [started]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- name: Install requirements #安装requests
run: |
pip install -r requirements.txt
- name: Update links #更新
run: |
python generator/main.py
- name: Commit & Push
uses: action-x/commit@v2.9
with:
github-token: ${{ secrets.PAT }}
push-branch: 'output'
force-push: 'true'
commit-message: 'Generate Json'
name: ${{ secrets.GITHUBUSERNAME }}
email: ${{ secrets.GITHUBEMAIL }}
- name: Checkout CDN repository
run: |
git clone https://${{ secrets.GITHUBUSERNAME }}:${{ secrets.PAT }}@github.com/${{ secrets.GITHUBUSERNAME }}/CDN.git
cd CDN
# 确保没有未加入分支的内容
git fetch origin
# 如果需要删除完整的 links 目录,请使用 -r 参数
git rm -r links/ # 删除整个 links 目录(如有必要)
# 配置用户信息
git config --global user.name "${{ secrets.GITHUBUSERNAME }}"
git config --global user.email "${{ secrets.GITHUBEMAIL }}"
git commit -m "Delete links directory"
git push origin main
- name: 延迟30s执行
run: |
echo "Starting delay..."
sleep 30 # 延迟 30 秒
echo "Delay completed."
- name: Push changes to CDN repository
run: |
cd ./v2
# 设置默认主分支为 'main'
git config --global init.defaultBranch main
if [ ! -d ".git" ]; then
git init
echo "Initialized new Git repository."
fi
# 配置用户信息
git config --global user.name "${{ secrets.GITHUBUSERNAME }}"
git config --global user.email "${{ secrets.GITHUBEMAIL }}"
git remote add origin https://${{ secrets.GITHUBUSERNAME }}:${{ secrets.PAT }}@github.com/${{ secrets.GITHUBUSERNAME }}/CDN.git
# 更新本地分支以确保所有更改都已拉取
git fetch origin
# 合并最新的主分支
git merge origin/main --allow-unrelated-histories || { echo "Merge conflict detected"; exit 1; }
# 显示当前状态
git status
# 列出特定目录的内容
ls -la ./links/
# 添加文件并提交
git add .
if [ -n "$(git status --porcelain)" ]; then
git commit -m "友链更新" || echo "No changes to commit"
else
echo "No changes to commit."
fi
# 推送到远程
git push -u origin main || { echo "Push failed, please resolve the issues."; exit 1; }
- name: 发送请求至本仓库的另外一工作流
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.GITHUB_TOKEN }} # 使用默认的 GITHUB_TOKEN
repository: ${{ github.repository }} # 指向当前仓库
event-type: linkschange
client-payload: '{"branch": "output"}' - 创建 .github/workflows/autolinks.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
27name: 更新网站友链
on:
repository_dispatch:
types: [linkschange] # 本工作流由 change 事件触发
jobs:
build:
runs-on: ubuntu-latest
steps:
# 步骤1:检出代码(根据需求可选)
- name: Checkout Code
uses: actions/checkout@v4
with:
ref: ${{ github.event.client_payload.branch || 'main' }}
# 步骤2:执行本地任务
- name: Run Tasks
run: echo "成功触发,当前分支:${{ github.event.client_payload.branch }}"
# 步骤3:触发账号B的私有仓库工作流(关键修改部分)
- name: 触发更新
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.MY_SECRET_TOKEN }} # 必须使用账号B的PAT
repository: yeminxi/hexo_code # 格式:owner/repo
event-type: linkspush # 与目标仓库的事件类型严格匹配
client-payload: '{"branch": "output"}' # 简化参数,移除冗余字段 - 我使用 Github Action 构建站点,在前面加上就可以实现友链检测,朋友圈,友链的三同步:
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
105name: 自动部署
# 当有改动推送到master分支时,启动Action
on:
repository_dispatch:
types: [linkspush] # 本工作流由 change 事件触发
push:
branches:
- solitude
#2020年10月后github新建仓库默认分支改为main,注意更改
release:
types:
- published
jobs:
deploy:
permissions:
actions: write
checks: write
contents: write
runs-on: ubuntu-latest
steps:
- name: 延迟三十秒执行
run: |
echo "Starting delay..."
sleep 30 # 延迟 30 秒
echo "Delay completed."
- name: 检查分支
uses: actions/checkout@v4
with:
ref: solitude
- name: 安装 Node
uses: actions/setup-node@v4
with:
node-version: "20.x" #action使用的node版本,建议大版本和本地保持一致。可以在本地用node -v查询版本号。
- name: 安装 Hexo
run: |
export TZ='Asia/Shanghai'
npm install hexo-cli -g
- name: 缓存 Hexo
uses: actions/cache@v4
id: cache
with:
path: |
node_modules
.deploy_git
public
key: ${{runner.OS}}-${{hashFiles('**/package-lock.json')}}
- name: 安装依赖
if: steps.cache.outputs.cache-hit != 'true'
run: |
npm install gulp-cli -g #全局安装gulp
npm install --save
npm install yamljs --save
- name: 清除缓存
run: |
hexo clean
- name: 更新友链朋友圈
run: |
node link.js
node links.js
node autolinks.js
- name: 生成静态文件
run: |
hexo generate
- name: 提交索引至Algolia
run: |
hexo algolia
- name: 压缩文件
run: |
gulp
# - name: 部署
# uses: JamesIves/github-pages-deploy-action@v4
# with:
# token: ${{ secrets.GITHUBTOKEN }}
# repository-name: ${{ secrets.GITHUBUSERNAME }}/${{ secrets.GITHUBUSERNAME }}.github.io
# branch: v3.0.0
# foldinger: public
# commit-message: "${{ github.event.head_commit.message }} Updated By Github Actions"
- name: 部署到Cloudfare Vercel Pages #此处master:master 指从本地的master分支提交到远程仓库的master分支,若远程仓库没有对应分支则新建一个。如有其他需要,可以根据自己的需求更改。
run: |
cd ./public
git init
git config --global user.name '${{ secrets.GITHUBUSERNAME }}'
git config --global user.email '${{ secrets.GITHUBEMAIL }}'
git branch -M solitude
git add .
git commit -m "${{ github.event.head_commit.message }} $(date -u +"%Y-%m-%dT%H:%M:%SZ") Updated By Github Actions"
git push --force --quiet "https://${{ secrets.GITHUBUSERNAME }}:${{ secrets.GITHUBTOKEN }}@github.com/${{ secrets.GITHUBUSERNAME }}/hexo_code_Solitude.git" solitude:v3.0.0
git push --force --quiet "https://${{ secrets.GITHUBUSERNAME }}:${{ secrets.GITHUBTOKEN }}@github.com/${{ secrets.GITHUBUSERNAME }}/hexo_code_Solitude.git" solitude:main
- name: 推送谷歌url
run: |
hexo deploy - 需要在仓库添加的环境变量,至于怎么获取token这里不多介绍,我这里是用了两个账号所以有两个token,一个只用设置PAT就好。
name value GITHUBUSERNAME 你Github的用户名 GITHUBEMAIL 你Github的邮箱 PAT 包含repo和work的权限token MY_SECRET_TOKEN 另一账号仓库包含repo和work的权限token - 说一下是什么原理
获取issues的内容 -> 按label生成对应的json -> 推送至output分支和CDN仓库 -> 发送请求至本仓库的另外一工作流 -> 收到请求后,发送请求至另外账号仓库的构建站点的工作流 -> 实现动态友链更新
原教程只能生成json文件到本仓库的 *output 分支的v2文件夹里,这里我加了一个推送到我的CDN仓库的操作。然后通过Github Action获取CDN的json内容生成友链文件,友链检测文件,朋友圈文件,实现三端同步实时更新。
同理可得动态即可短文。
原帖 | 作者 |
---|---|
Stellar动态友链教程 | 星日语 |
18. 添加回复时引用该评论
点击效果,即可跳转,随便找条评论试试,只适用Waline。
- 新建 source/custom/js/replycontent.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(function() {
let walineContainer = null;
let observer = null;
const initWalineReply = () => {
// 移除旧的事件监听器
if (walineContainer) {
walineContainer.removeEventListener('click', handleClick);
}
// 获取新的评论容器
walineContainer = document.getElementById('waline-wrap');
if (!walineContainer) {
return; // 如果没有找到容器则退出
}
// 添加点击事件监听器
walineContainer.addEventListener('click', handleClick, { passive: true });
// 如果旧观察者存在,则断开连接
if (observer) {
observer.disconnect();
}
// 创建新的 MutationObserver 实例并开始观察
observer = new MutationObserver(() => {
// 在 DOM 变化时重新初始化
initWalineReply();
});
observer.observe(walineContainer, {
childList: true,
subtree: true
});
};
const handleClick = (e) => {
const replyBtn = e.target.closest('button.wl-reply');
if (!replyBtn || walineContainer.dataset.pjaxLoading === 'true') return;
// 精准定位当前评论内容
const commentItem = replyBtn.closest('.wl-card-item');
const contentNode = commentItem.querySelector('.wl-card > .wl-content');
if (!contentNode) {
return;
}
// 沙盒处理(保留链接结构)
const sandbox = contentNode.cloneNode(true);
// 移除干扰元素
sandbox.querySelectorAll(
'span, .wl-meta, svg, img, .wl-quote, blockquote, a[href^="#"]'
).forEach(n => n.remove());
// 转换普通链接为Markdown格式(保留非锚点链接)
sandbox.querySelectorAll('a:not([href^="#"])').forEach(a => {
const text = a.textContent.trim();
const href = a.getAttribute('href') || '';
a.replaceWith(`[${text}](${href})`);
});
// 提取纯净文本(增加过滤@符号)
const pureText = sandbox.textContent
.replace(/@\S+/g, '') // 新增@提及过滤
.replace(/\n{3,}/g, '\n\n')
.trim()
.substring(0, 500);
// 获取编辑器实例
const editor = walineContainer.querySelector('.wl-editor');
if (!editor) {
return;
}
// 执行插入
insertEditorContent(editor, pureText);
};
// 通用插入方法(支持Markdown)
const insertEditorContent = (editor, text) => {
const insertText = `> ${text}\n\n`;
const inputEvent = new Event('input', { bubbles: true });
if (editor.tagName === 'TEXTAREA' || editor.tagName === 'INPUT') {
// 原生文本域处理
const start = editor.selectionStart;
editor.value =
editor.value.slice(0, start) +
insertText +
editor.value.slice(editor.selectionEnd);
editor.selectionEnd = start + insertText.length;
} else if (editor.isContentEditable) {
// 富文本编辑器处理
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(editor);
range.collapse(false); // 光标移到最后
range.insertNode(document.createTextNode(insertText));
selection.removeAllRanges();
selection.addRange(range);
}
editor.dispatchEvent(inputEvent);
editor.focus();
};
// 初始化及事件监听
document.addEventListener('pjax:complete', initWalineReply);
initWalineReply(); // 初次调用以进行初始化
})(); - 修改主题配置文件:
1
2
3
4
5
6
7# Extend
# 扩展
extends:
# Insert in head
# 插入到 head
head:
- <script data-pjax src="/custom/js/replycontent.js"></script>
19. 弹出式欢迎界面
首次进入本站即显示,点击效果即刻跳转。
- 新建 source/custom/js/welcome.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
56document.addEventListener("DOMContentLoaded", (function() {
if (!getCookie("agreementAccepted")) {
const referrer = document.referrer ? new URL(document.referrer).hostname : "直接访问";
swal({
title: "欢迎来到 叶泯希 <( ̄︶ ̄)↗[GO!]",
content: {
element: "div",
attributes: {
innerHTML: `
<img src='/images/avatar.webp' alt='自定义图标'
style='width:80px; height:auto; border-radius: 50%;
display: block; margin: 0 auto;' /><br />
您来自: ${referrer}<br />
请您在继续浏览本站之前,仔细阅读以下协议:<br /><br />
1. <a href='/privacy/' title='隐私协议' data-pjax-state=''>隐私协议</a>
2. <a href='/disclaimer/' title='免责声明' data-pjax-state=''>免责声明</a> <br />
3. <a href='/copyright/' title='版权协议' data-pjax-state=''>版权协议</a>
4. <a href='/comment/' title='评论协议' data-pjax-state=''>评论协议</a>
5. <a href='/cookies/' title='Cookies' data-pjax-state=''>Cookies</a> <br /><br />
点击“同意”表示您已阅读并同意遵守以上协议。
`
}
},
buttons: {
cancel: "不同意",
confirm: "同意"
},
text: "若未获取新内容请清浏览器缓存。"
}).then(willProceed => {
willProceed ? setCookie("agreementAccepted", "true", 30) :
window.history.length > 1 ? window.history.back() :
window.location.href = "https://xxx";// 替换为希望重定向的 URL
});
}
function setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + 24 * days * 60 * 60 * 1000); //添加30天的cookie
const expires = "expires=" + date.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/";
}
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (" " === c.charAt(0)) c = c.substring(1, c.length);
if (0 === c.indexOf(nameEQ)) return c.substring(nameEQ.length, c.length);
}
return null;
}
})); - 在 source/custom/css/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/* 弹出框样式 */
.swal-modal,.swal-text,.swal-title {
font-family: var(--efu-font) ;
background-color: var(--efu-secondbg);
color: var(--efu-fontcolor) ;
}
.swal-text {
font-size: 14px;
}
.swal-content a:hover {
background-color: var(--efu-theme-op-deep);
}
.swal-content a:not(.fancybox) {
color: var(--efu-fontcolor);
font-weight: 700;
text-decoration: none;
border-bottom: 2px dotted var(--efu-lighttext);
padding: 0px 4px;
border-radius: 4px 4px 0px 0px;
}
.swal-button--confirm {
color: var(--efu-fontcolor);
background-color: var(--efu-theme-op-deep);
} - 修改主题配置文件:
1
2
3
4
5
6
7
8
9# Extend
# 扩展
extends:
# Insert in head
# 插入到 head
head:
- <link rel="stylesheet" href="/custom/css/custom.css">
- <script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
- <script src="/custom/js/welcome.js"></script>
20. 添加小米字体
所见即所得。
- 修改主题配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# Font
# 字体
font:
font-size: 20px
code-font-size: 16px
# Global font
# 全局字体
font-family: "MiSans, sans-serif"
# Extend
# 扩展
extends:
# Insert in head
# 插入到 head
head:
- <link rel="stylesheet" href="https://font.sec.miui.com/font/css?family=MiSans:400,700:MiSans">
21. 配置swpp和PWA
进入页面即可显示。特别感谢Liushen的帮助!
- 安装
1
2npm install swpp-backends@3.0.0-alpha.906 --save
npm install hexo-swpp@4.0.0-alpha.131 --save - 在Hexo配置文件最后添加:
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# swpp
# npm install swpp-backends@3.0.0-alpha.902 --save
# npm install hexo-swpp@4.0.0-alpha.131 --save
# https://kmar.top/posts/b70ec88f/
# https://github.com/EmptyDreams/hexo-swpp
swpp:
# 是否启用,默认 false
enable: true
# 配置文件路径,以 `/` 结尾表示加载指定文件夹下的所有文件,注意文件夹中只能有配置文件,不能有其它文件及文件夹
config_path: 'swpp.config.ts'
# 是否生成 sw
serviceWorker: true
# 是否向所有 HTML 插入注册 sw 的代码
auto_register: true
# 是否生成 DOM 端的 JS 文件并在 HTML 中插入 script
gen_dom: true
# 是否追踪链接,默认 false
# ⚠️警告!!!未启用链接追踪时不要使用永久缓存,否则会导致被缓存的数据永远不会更新,在您不再使用该资源时,缓存也不会被删除!!!
track_link: true
# 生成的 diff 文件的路径(可以是绝对路径也可以是相对路径,使用相对路径时相对于网站发布目录),留空表示不生成(默认为 null)
gen_diff: './diff.json'
# 是否在执行 hexo deploy 时自动执行 swpp 指令
# auto_exec: false
# 检查更新的网址,默认 "https://registry.npmjs.org",注意不能以斜杠结尾
npm_url: 'https://registry.npmmirror.com'
#
# 排序规则。
# 该配置项是为了对 hexo 中的一些变量进行排序,避免每次生成 HTML 时由于这些变量的顺序变动导致生成结果不完全相同。
# 示例:
# ```yaml
# # 下面给出的值为插件的缺省值,用户设置该项不会直接覆盖这些值,只有用户也声明 posts、pages 或 tags 时才会覆盖对应的值。
# swpp:
# sort_rules:
# posts: 'title'
# pages: 'title'
# tags: 'name'
# ```
# 其中 key 值为要排序的变量的名称,value 为变量排序时的依据,
# 填 false 表示禁用该项排序,填 true 表示以 value 本身为键进行排序,填字符串表示以 value[tag] 为键进行排序。
# sort_rules: - 在根目录下创建 swpp.config.ts 文件,把我的域名改成你自己的(不懂就看参考里的教程):
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
88import {
defineConfig
} from 'swpp-backends'
defineConfig({
compilationEnv: {
DOMAIN_HOST: new URL('https://blog.418121.xyz'),
SERVICE_WORKER: "sw",
JSON_HTML_LIMIT: 10,
isStable: (url: URL) => {
return [
/^(https?:\/\/|\/\/)(cdn|fastly)\.jsdelivr\.net\/npm\/.*@\d+\.\d+\.\d+\//,
/^(https?:\/\/|\/\/)jsdelivr\.topthink\.com\/.*@\d+\.\d+\.\d+\//,
/^(https?:\/\/|\/\/)cdn\.jsdmirror\.com\/.*@\d+\.\d+\.\d+\//,
/^(https?:\/\/|\/\/)cdn\.staticfile\.org\/.*\/\d+\.\d+\.\d+\//,
/^(https?:\/\/|\/\/)lf\d+-cdn-tos\.bytecdntp\.com\/.*\/\d+\.\d+\.\d+\//,
/^(https?:\/\/|\/\/)npm\.elemecdn\.com\/.*@\d+\.\d+\.\d+\//
].some(it => it.test(url.href))
},
VERSION_LENGTH_LIMIT: 512,
NETWORK_FILE_FETCHER: {
referer: "https://blog.418121.xyz",
getStandbyList(url: string | URL): (string | URL)[] {
if (typeof url === 'string') url = new URL(url)
if (url.hostname === 'npm.elemecdn.com') {
return [`https://fastly.jsdelivr.net${url.pathname}`]
}
return [url]
}
}
},
crossEnv: {
CACHE_NAME: "BlogCache",
VERSION_PATH: "https://id.v3/",
ESCAPE: 15,
},
runtimeDep: {
getStandbyRequests: (request: Request): {t: number, l: () => Request[]} | void => {
const srcUrl = request.url
const {host, pathname} = new URL(srcUrl)
// noinspection SpellCheckingInspection
const commonCdnList = ['jsdelivr.topthink.com', 'cdn.jsdmirror.com', 'fastly.jsdelivr.net']
// noinspection SpellCheckingInspection
const elme = 'npm.elemecdn.com'
const urlMapper = (it: string) => new Request(it, request)
if (host === elme) {
return {
t: 2000,
l: () => [...commonCdnList.map(it => `https://${it}/npm${pathname}`)].map(urlMapper)
}
}
if (host === 'jsdelivr.topthink.com') {
commonCdnList.splice(0, 1)
return {
t: 2000,
l: () => [...commonCdnList.map(it => `https://${it}${pathname}`)].map(urlMapper)
}
}
}
},
crossDep: {
matchCacheRule: {
runOnBrowser: (url: URL) => {
let { host, pathname } = url;
// 处理省略index.html的情况
if (pathname.endsWith('/')) pathname += 'index.html';
// 仅仅对于blog.418121.xyz')) { 处理 html 和 json
if (host.endsWith('blog.418121.xyz')) {
if (pathname.endsWith('.json')) return 2592000000; // 30 dyas
if (pathname.endsWith('.html')) return 2592000000; // 30 dyas
if (pathname.endsWith('.webp') || pathname.endsWith('.jpg') || pathname.endsWith('.png') || pathname.endsWith('.gif')) return 2592000000; // 30 dyas
}
if (/\.(js|css|woff2|woff|ttf|cur|json)$/.test(url.pathname)) return 2592000000; // 30 dyas
// return ((url.host.endsWith('blog.418121.xyz')) {') && /(\/|\.json)$/.test(url.pathname)) || /\.(js|css|woff2|woff|ttf|cur)$/.test(url.pathname)) ? 86400000 : false
},
runOnNode(url: URL) {
// @ts-ignore
return this.runOnBrowser(url)
}
}
},
}) - 敲 hexo swpp , 使用Github Action 可以在压缩之前加上:
1
2
3
4
5
6
7
8
9# ...
- name: 缓存swpp
run: |
hexo swpp
- name: 压缩文件
run: |
gulp
# ... - 配置 PWA ,修改主题文件 _config_solitude.yml(按自己的来):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# ...
# --------------------------- start ---------------------------
# PWA
# Progressive Web App
pwa:
enable: true
manifest: /manifest.json # manifest.json
theme_color: "#ff8080" # Theme colort
mask_icon: /img/pwa/favicon.png # Mask icon
apple_touch_icon: /img/pwa/favicon.png # Apple touch icon
bookmark_icon: /img/pwa/favicon.png # Bookmark icon
favicon_32_32: /img/pwa/favicon_32.png # 32x32 icon
favicon_16_16: /img/pwa/favicon_16.png # 16x16 icon
# --------------------------- end ---------------------------
# ... - 新建 source/manifest.json 文件(并准备好对应的图片):
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{
"lang": "zh",
"name": "叶泯希",
"short_name": "叶泯希",
"description": "blog.418121.xyz",
"theme_color": "#ff8080",
"background_color": "#ff8080",
"display": "standalone",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "/img/pwa/favicon_36.png",
"sizes": "36x36",
"type": "image/png"
},
{
"src": "/img/pwa/favicon_48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "/img/pwa/favicon_72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/img/pwa/favicon_96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/img/pwa/favicon_144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/img/pwa/favicon_192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/img/pwa/favicon_256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "/img/pwa/favicon_384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/img/pwa/favicon_512.png",
"sizes": "512x512",
"type": "image/png"
}
]
} - 网址右边出现安装应用,及成功。
原帖 | 作者 |
---|---|
Hexo配置SWPP实现PWA | Liushen |
Swpp Backends 官方文档 | 空梦 |
Github仓库 | EmptyDreams |
结尾
至此,全篇结束。希望对你有所帮助,有任何问题请在下方留言。可以关注我的 公众号以及订阅我的文章 ,感谢你的支持,是对我最大的动力,当然了,更多的是因为热爱。