为Butterfly添加二级跳转页并适配Pjax-safeGo

前言

偶然看到一个给 hexo 添加二级跳转页的方案,本想直接给破站搬来用的,结果发现不兼容 butterfly 使用的 MoOx/pjax,博主对 js 文件稍作增改 现已兼容 butterfly 的 MoOx/pjax,原代码仓库:

效果预览

代码实现

在 js 文件尾端添加如下代码,实现对 pjax 加载完成事件的监听

document.addEventListener("pjax:complete", function () {
safeGoFun.NzcheckLink(
".post-content a:not(.social-share-icon):not(.fancybox):not(.not-check-link)"
);
})

破站修改后的完整 js 代码如下(修改拼接部分 url 以适配将 html 文件放在 pages 文件夹下):

const safeGoFun = {
// TODO: a链接安全跳转(只对文章页,关于页评论 -- 评论要单独丢到waline回调中)
NzcheckLink: async (domName) => {
// 获取文章页非社会分享的a标签
const links = document.querySelectorAll(domName);
if (links) {
// 锚点正则
let reg = new RegExp(/^[#].*/);
for (let i = 0; i < links.length; i++) {
const ele = links[i];
let eleHref = ele.getAttribute("href"),
eleIsDownLoad = ele.getAttribute("data-download"),
eleRel = ele.getAttribute("rel");

// 如果你的博客添加了Gitter聊天窗,请去掉下方注释 /*|| link[i].className==="gitter-open-chat-button"*/
// 排除:锚点、上下翻页、按钮类、分类、标签
if (
!reg.test(eleHref) &&
eleRel !== "prev" &&
eleRel !== "next" &&
eleRel !== "category" &&
eleRel !== "tag" &&
eleHref !== "javascript:void(0);"
) {
// 判断是否下载地址和白名单,是下载拼接 &type=goDown
if (!(await safeGoFun.NzcheckLocalSite(eleHref)) && !eleIsDownLoad) {
// encodeURIComponent() URI编码
ele.setAttribute(
"href",
"/pages/go.html?goUrl=" + encodeURIComponent(eleHref)
);
} else if (
!(await safeGoFun.NzcheckLocalSite(eleHref)) &&
eleIsDownLoad === "goDown"
) {
ele.setAttribute(
"href",
"/pages/go.html?goUrl=" + encodeURIComponent(eleHref) + "&type=goDown"
);
}
}
}
}
},
// 校验白名单,自己博客,local测试
NzcheckLocalSite: async (url) => {
try {
// 白名单地址则不修改href
const safeUrls = ["localhost:4000", "stardream.online", "blog.stardream.online", "posts"];
let isOthers = false;
for (let i = 0; i < safeUrls.length; i++) {
const ele = safeUrls[i];
if (url.includes(ele)) {
isOthers = true;
break;
}
}
return isOthers;
} catch (err) {
return true;
}
},
};

Object.keys(safeGoFun).forEach((key) => {
window[key] = safeGoFun[key];
});

// 页面dom加载完成后,文章页不是分享按钮,不是图片灯箱,class类名不含有 not-check-link
// not-check-link 是小波自己设计的约定类名class,用来排除不调用跳转方法的链接
document.addEventListener("DOMContentLoaded", function () {
safeGoFun.NzcheckLink(
".post-content a:not(.social-share-icon):not(.fancybox):not(.not-check-link)"
);
});
document.addEventListener("pjax:complete", function () {
safeGoFun.NzcheckLink(
".post-content a:not(.social-share-icon):not(.fancybox):not(.not-check-link)"
);
})

".post-content a:not(.social-share-icon):not(.fancybox):not(.not-check-link)" 中的 post-content 请参照你自己 blog 中的 class name 进行修改(按 f12 打开元素进行查找即可,可以理解为你想让哪部分网页内容对这个二级跳转生效)

html文件没有做增改处理,这里贴出破站的 html(这里请根据自己 blog 的情况对博客名等内容进行修改):

<!DOCTYPE html>
<html data-user-color-scheme="dark">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, shrink-to-fit=no"
/>
<title>
安全中心 | 博客名称 - 欲历观天下色萝.品JS自不厌多
</title>
<link rel="icon" class="icon-favicon" href="/" />
<link
rel="stylesheet"
href="https://lib.baomitu.com/twitter-bootstrap/4.6.1/css/bootstrap.min.css"
/>
<link
rel="stylesheet"
href="https://at.alicdn.com/t/font_1736178_lbnruvf0jn.css"
/>
<style type="text/css">
/* // 向上渐隐显示(主内容使用) */
@-webkit-keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(24px);
}
100% {
opacity: 1;
transform: translateY(-80px);
}
}
@keyframes fadeInUp {
0% {
opacity: 0;
-webkit-transform: translateY(24px);
-ms-transform: translateY(24px);
transform: translateY(24px);
}
100% {
opacity: 1;
-webkit-transform: translateY(-80px);
-ms-transform: translateY(-80px);
transform: translateY(-80px);
}
}
/* // 向上渐隐显示(成功错误提示) */
@-webkit-keyframes alertFadeInUp {
0% {
opacity: 0;
transform: translateY(24px);
}
75% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
}
}
@keyframes alertFadeInUp {
0% {
opacity: 0;
-webkit-transform: translateY(24px);
-ms-transform: translateY(24px);
transform: translateY(24px);
}
75% {
opacity: 1;
-webkit-transform: translateY(0);
-ms-transform: translateY(0);
transform: translateY(0);
}
100% {
opacity: 0;
}
}
@-webkit-keyframes fadeOutUp {
0% {
opacity: 1;
}
to {
opacity: 0;
transform: translate3d(0, -350%, 0);
}
}
@keyframes fadeOutUp {
0% {
opacity: 1;
}
to {
opacity: 0;
-webkit-transform: translate3d(0, -350%, 0);
transform: translate3d(0, -350%, 0);
}
}

:root {
--blue: #007bff;
--indigo: #6610f2;
--purple: #6f42c1;
--pink: #e83e8c;
--red: #dc3545;
--orange: #fd7e14;
--yellow: #ffc107;
--green: #28a745;
--teal: #20c997;
--cyan: #17a2b8;
--white: #fff;
--gray: #6c757d;
--gray-dark: #343a40;
--primary: #007bff;
--secondary: #6c757d;
--success: #28a745;
--info: #17a2b8;
--warning: #ffc107;
--danger: #dc3545;
--light: #f8f9fa;
--dark: #343a40;
--breakpoint-xs: 0;
--breakpoint-sm: 576px;
--breakpoint-md: 768px;
--breakpoint-lg: 992px;
--breakpoint-xl: 1200px;
--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI",
"PingFang SC", Roboto, "Helvetica Neue", Arial, "Noto Sans",
"Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
--font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
}

[data-user-color-scheme="dark"] {
--body-bg-color: #22272e;
--board-bg-color: #2b313a;
--text-color: #adbac7;
--sec-text-color: #b3bac1;
--post-text-color: #adbac7;
--post-heading-color: #adbac7;
--post-link-color: #34a3ff;
--link-hover-color: #30a9de;
--link-hover-bg-color: #22272e;
--line-color: #adbac7;
--navbar-bg-color: #22272e;
--navbar-text-color: #cbd4dc;
--subtitle-color: #cbd4dc;
--scrollbar-color: #30a9de;
--scrollbar-hover-color: #34a3ff;
--button-bg-color: transparent;
--button-hover-bg-color: #46647e;
--highlight-bg-color: #2d333b;
--inlinecode-bg-color: rgba(99, 110, 123, 0.4);
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-corner {
background-color: transparent;
}
::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-color);
border-radius: 6px;
}

html {
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
html,
body {
/* background: #f3f4f5; */
/* font-family: PingFang SC, Hiragino Sans GB, Arial, Microsoft YaHei,
Verdana, Roboto, Noto, Helvetica Neue, sans-serif; */
font-family: var(--font-family-sans-serif);
padding: 0;
margin: 0;
background-color: var(--body-bg-color);
color: var(--text-color);
transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out;
height: 100%;
}
body {
font-size: 1rem;
}
p,
div {
padding: 0;
margin: 0;
}
a {
text-decoration: none;
transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out;
}
body a:hover {
color: var(--link-hover-color);
text-decoration: none;
}
.go-page {
height: 100%;
}
.content {
/* padding-top: 220px; */
width: 450px;
margin: auto;
word-break: break-all;
height: 100%;
}
.content .logo-img {
margin-bottom: 20px;
text-align: center;
padding-top: 220px;
}
.content .logo-img p:first-child {
font-size: 22px;
}
.content .logo-img img {
display: block;
width: 175px;
height: 48px;
margin: auto;
margin-bottom: 16px;
}

.content .loading-item {
background: #fff;
padding: 24px;
border-radius: 12px;
border: 1px solid #e1e1e1;
margin-bottom: 10px;
}
/* 绿色 */
.content .tip1 {
background: #f0f9ea;
}
/* 黄色 */
.content .tip2 {
background: #fdf5e6;
}
/* 红色 */
.content .tip3 {
background: #fef0f0;
}
.content .icon-snapchat-fill {
font-size: 20px;
color: #fc5531;
border: 1px solid #fc5531;
border-radius: 50%;
width: 32px;
text-align: center;
margin-right: 5px;
}
.content .tip1 .icon-snapchat-fill {
color: var(--post-link-color);
border-color: var(--post-link-color);
}
.content .loading-text {
font-size: 16px;
font-weight: 600;
color: #222226;
line-height: 22px;
/* margin-left: 12px; */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.content .flex {
display: flex;
align-items: center;
}
.content .flex-end {
display: flex;
justify-content: flex-end;
}
/* #267dcc 蓝色 */
.content .loading-color1 {
color: var(--post-link-color);
}
.content .loading-color2 {
color: #fc5531;
}
.content .loading-tip {
padding: 12px;
margin-bottom: 16px;
border-radius: 4px;
}
.content .loading-topic {
font-size: 14px;
color: #222226;
line-height: 24px;
margin-bottom: 24px;
}
.loading-topic .flex {
flex-direction: column;
}
.content .loading-img {
width: 24px;
height: 24px;
}
/* #fc5531; #fc5531*/
.content .loading-btn {
font-size: 14px;
color: var(--post-link-color);
border: 1px solid var(--post-link-color);
display: inline-block;
box-sizing: border-box;
padding: 6px 18px;
border-radius: 18px;
margin-left: 8px;
}
.content .loading-btn:hover {
color: var(--link-hover-color);
border-color: var(--link-hover-color);
}
.content .loading-btn-github {
width: 121px;
background: #fc5531;
color: #fff;
}

.hidden {
display: none;
}
.form-control.hidden {
display: none !important;
}
.mp-img-box {
text-align: center;
margin-bottom: 10px;
}
.mp-img {
max-width: 400px;
width: 100%;
box-shadow: 5px 5px 15px rgb(0 0 0 / 8%);
margin-bottom: 5px;
}
.fadeInUp {
-webkit-animation-name: fadeInUp;
animation-name: fadeInUp;
}
.alertFadeInUp {
-webkit-animation-name: alertFadeInUp;
animation-name: alertFadeInUp;
-webkit-animation-duration: 3s;
animation-duration: 3s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.fadeOutUp {
-webkit-animation-name: fadeOutUp;
animation-name: fadeOutUp;
}
.fade-animate {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation-delay: 1s;
animation-delay: 1s;
}
.go-alert {
margin: 0 auto;
width: 110px;
position: absolute;
left: 46%;
top: 5%;
opacity: 0;
text-align: center;
}
.footer {
text-align: center;
position: relative;
margin-bottom: 20px;
}
.footer a {
color: var(--text-color);
}

.flex-box {
display: flex;
height: 100vh;
flex-direction: column;
}
.flex-contain {
flex: 1;
}
.flex-footer {
height: 24px;
}

@media (max-width: 767.98px) {
.content {
width: 94%;
}
.content .logo-img {
padding-top: 120px;
}
}
</style>
</head>
<body class="web-font">
<div id="goPage" class="go-page">
<div class="alert alert-danger go-alert hidden" role="alert">
验证失败
</div>

<div class="content">
<div class="flex-box">
<div class="flex-contain">
<div class="logo-img">
<p class="blog-name">博客名称</p>
<p class="blog-description"></p>
</div>

<!-- 加载ing... -->
<div class="loading-item loading-safe flex">
<i class="iconfont icon-snapchat-fill"></i>
<div class="loading-text">链接安全性检验中 请稍后...</div>
</div>

<div class="go-box"></div>
</div>
<div class="footer flex-footer">
©2021-2024
<a href="https://blog.lolihouse.top" class="blog-name"
><span>博客名称</span></a
>
版权所有
<!-- <span class="blog-name">廿壴(ganxb2)</span> 版权所有 -->
</div>
</div>
</div>
</div>
<!-- goPage end -->

<script src="https://lib.baomitu.com/jquery/3.6.0/jquery.min.js"></script>
<script src="https://lib.baomitu.com/twitter-bootstrap/4.6.1/js/bootstrap.min.js"></script>
<script type="module">
// 请根据自己博客修改
const config = {
// 标题
title:
"さくら荘のタイズ | 安全中心",
// 地址栏图标
iconFavicon: "https://cdn.jsdelivr.net/gh/2427768286/STDM-imgs/images/qm06qq.jpg",
// 二维码地址
mpImgSrc: "https://cdn.jsdelivr.net/gh/2427768286/STDM-imgs/sticker/e53923662b627a645fcd2b0b3feadb3b.gif",
// 博客名称
blogName: "さくら荘のタイズ",
// 博客描述
blogDescription: "欲历观天下色萝,品JS自不厌多。",
// 白名单
safeUrl: [
// 平台 常用平台不用改哈
"github.com",
"gitee.com",
"csdn.net",
"zhihu.com",
"pan.baidu.com",
"baike.baidu.com",
"hexo.io",
"leancloud.cn",
"nodejs.cn",
"jsdelivr.com",
"ohmyposh.dev",
"nerdfonts.com",
"douban.com",
"waline.js.org",
"developer.mozilla.org",
"jinrishici.com",
"hitokoto.cn",
"zhangxinxu.com",
"music.apple.com",

// 好友博客 增加自己的博客友链
],
tipsTextError: "链接错误,关闭页面返回さくら荘のタイズ",
tipsTextDownload:
// "从廿壴(ganxb2)微信公众号获取暗号≖‿≖✧ o‿≖✧(๑•̀ㅂ•́)و✧",
"(๑•̀ㅂ•́)و✧切记网盘压缩文件下载后再解压哦o‿≖✧",
tipsTextDanger: "该网址未在确认的安全范围内",
tipsTextSuccess: "该网址在确认的安全范围内",
textDanger:
"您即将离开さくら荘のタイズ去往如下网址,请注意您的账号隐私安全和财产安全:",
textSuccess: "您即将离开さくら荘のタイズ去往如下网址",
// 后续改成leancloud获取(下载验证码)
wpValidate: "9498",
};

// 获取地址和下载标识
const getQueryString = (name, type) => {
// 构造一个含有目标参数的正则表达式对象
let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"),
regDown = new RegExp("&type=" + type),
// 匹配地址参数
r = window.location.search.substr(1).match(reg),
d = window.location.search.substr(1).match(regDown),
isDownload = false;

// 反编译回原地址 取第3个值,不然就返回 Null
if (r !== null) {
// 如果d不为空,则显示下载提示
if (d !== null) {
isDownload = true;
}
return { url: decodeURIComponent(r[2]), isDownload: isDownload };
}
return null;
};

// xss攻击(绑定值时使用)
const xssCheck = (str, reg) => {
return str
? str.replace(
reg || /[&<">'](?:(amp|lt|quot|gt|#39|nbsp|#\d+);)?/g,
function (a, b) {
if (b) {
return a;
} else {
return {
"<": "&lt;",
"&": "&amp;",
'"': "&quot;",
">": "&gt;",
"'": "&#39;",
}[a];
}
}
)
: "";
};

// 下载按钮点击验证,成功回调把linkUrl绑定给隐藏的a标签模拟点击
const downloadValidate = (config, getLinkUrl) => {
// 下载按钮,下载地址,验证码,警告框
const downloadBtn = document.querySelector(".go-down-btn"),
downloadUrl = document.querySelector(".go-down-url"),
wpValidate = document.querySelector(".wp-validate"),
goAlert = document.querySelector(".go-alert");

downloadBtn.addEventListener(
"click",
function () {
// 显示alert
goAlert.classList.remove("hidden", "alertFadeInUp");
setTimeout(() => {
goAlert.classList.add("alertFadeInUp");
}, 300);
// 暂时给默认值下载后续再改动
wpValidate.value = "9498";
// leancloud回调(node环境则可以利用主机环境变量传入leancloud的参数)
if (
wpValidate &&
wpValidate.value !== "" &&
wpValidate.value === config.wpValidate
) {
// 成功
wpValidate.classList.remove("is-invalid");
wpValidate.classList.add("is-valid");
goAlert.classList.remove("alert-danger");
goAlert.classList.add("alert-success");
goAlert.textContent = "验证成功";
downloadUrl.click();
} else {
// 失败
wpValidate.classList.remove("is-valid");
wpValidate.classList.add("is-invalid");
goAlert.classList.remove("alert-success");
goAlert.classList.add("alert-danger");
goAlert.textContent = "验证失败";
}
},
!1
);
// !1 false 冒泡 是点击子元素,子元素事件先出现在出现父元素事件
// 1 true 捕获 是点击子元素,父元素事件先出现在出现子元素事件
};

// 其他地址校验白名单
const othersValidate = (config, getLinkUrl) => {
let isSafeUrl = false,
safeUrl = config.safeUrl,
url = xssCheck(getLinkUrl.url);

if (safeUrl.length !== 0) {
for (let i = 0; i < safeUrl.length; i++) {
const ele = safeUrl[i];
if (url.includes(ele)) {
isSafeUrl = true;
break;
}
}
}
return isSafeUrl;
};

// 模版基础配置初始
const goInit = (config) => {
// $(function () {
const tplConfig = {
loadingType: "loading-error",
tipType: "tip3",
tipsText: config.tipsTextError,
loadingTopicText: config.textDanger,
loadingColorType: "loading-color2",
goUrl: "/",
},
getLinkUrl = getQueryString("goUrl", "goDown"),
loadingSafe = document.querySelector(".loading-safe"),
goBox = document.querySelector(".go-box"),
title = document.querySelector("title"),
iconFavicon = document.querySelector(".icon-favicon"),
blogName = document.querySelectorAll(".blog-name"),
blogDescription = document.querySelector(".blog-description");

// 初始化:标题,favicon,博客名称,博客描述
title.textContent = config.title;
iconFavicon.setAttribute("href", config.iconFavicon);
blogName.forEach((element) => {
element.textContent = config.blogName;
});
blogDescription.textContent = config.blogDescription;

// 根据地址栏参数判断是下载地址还是纯外链,外链则直接修改a标签按钮url,用户点击跳转
if (getLinkUrl && !getLinkUrl.isDownload) {
// 可参考csdn加入后端请求验证地址是否白名单再进一步给出不同场景状态:是白名单,则绿+蓝,否则黄+红
const isSafeUrl = othersValidate(config, getLinkUrl);
tplConfig.loadingType = "loading-others";
tplConfig.goUrl = xssCheck(getLinkUrl.url);

if (isSafeUrl) {
tplConfig.tipType = "tip1";
tplConfig.tipsText = config.tipsTextSuccess;
tplConfig.loadingTopicText = config.textSuccess;
tplConfig.loadingColorType = "loading-color1";
// 白名单链接直接跳转
setTimeout(() => {
// location.assign(tplConfig.goUrl);
const goUrlBtn = document.querySelector(".go-url-btn");
goUrlBtn.click();
}, 2000);
// location.reload();
// location.replace();
} else {
tplConfig.tipType = "tip2";
tplConfig.tipsText = config.tipsTextDanger;
tplConfig.loadingTopicText = config.textDanger;
tplConfig.loadingColorType = "loading-color2";
}
}
// 如果是下载则按钮事件绑定leancloud请求校验验证码
else if (getLinkUrl && getLinkUrl.isDownload) {
tplConfig.loadingType = "loading-download";
tplConfig.goUrl = xssCheck(getLinkUrl.url);
tplConfig.tipType = "tip1";
tplConfig.tipsText = config.tipsTextDownload;
} else {
// 错误
tplConfig.tipType = "tip2";
tplConfig.tipsText = config.tipsTextError;
}

const othersTpl = `
<div class="loading-topic">
<span
>${tplConfig.loadingTopicText}</span
>
<a class="${tplConfig.loadingColorType} go-url">${tplConfig.goUrl}</a>
</div>
<div class="flex-end">
<a rel="noopener external nofollow noreferrer" class="loading-btn go-url-btn" href="${tplConfig.goUrl}" target="_self">继续</a>
</div>
`;

const downloadTpl = `
<div class="loading-topic">
<div class="flex">
<div class="mp-img-box">
<img class="mp-img" src="${config.mpImgSrc}" alt="qrcode" />
<p>
在线解压有概率造成资源失效<br>
切记下载后再解压哦 ≖‿≖✧
</p>
</div>
<div>
<form class="needs-validation form-inline">
<div class="form-group">
<label class="sr-only" for="wp-validate">验证码</label>
<input
type="text"
class="form-control wp-validate hidden"
id="wp-validate"
placeholder="请输入公众号验证码..."
/>
<input type="text" class="form-control hidden" />
</div>
</form>
<a rel="noopener external nofollow noreferrer" href="${tplConfig.goUrl}" class="go-down-url hidden" target="_self" tittle="go-url"></a>
</div>
</div>
</div>
<div class="flex-end">
<a
class="loading-btn go-down-btn"
href="javascript:void(0);"
target="_self"
>下载</a
>
</div>
`;

const tpl = `
<div class="loading-item ${tplConfig.loadingType} hidden">
<div class="flex loading-tip ${tplConfig.tipType}">
<i class="iconfont icon-snapchat-fill ${
tplConfig.loadingType === "loading-download" && "hidden"
}"></i>
<div class="loading-text">
${tplConfig.tipsText}
</div>
</div>
${
tplConfig.loadingType === "loading-others"
? othersTpl
: tplConfig.loadingType === "loading-download"
? downloadTpl
: ""
}
</div>
`;

// tpl渲染
goBox.innerHTML = tpl;
const loadingItem = document.querySelector(".go-box .loading-item");
loadingSafe.classList.add("fadeOutUp", "fade-animate");
loadingItem.classList.remove("hidden");
loadingItem.classList.add("fadeInUp", "fade-animate");
// 下载按钮事件绑定
if (getLinkUrl && getLinkUrl.isDownload)
downloadValidate(config, getLinkUrl);
// });
};

// -----------------------调用 start 栗子:?goUrl=https%3A%2F%2Fimgod.me&type=goDown
goInit(config);
</script>
</body>
</html>

部署

  1. [Blogroot]\themes\butterfly\source\js\ 下创建一个名为 go.js 的文件,并把上一部分中的 js 代码粘入
  2. _config.butterfly.yml 中引入 js 文件:如下所示
    inject:
    head:
    # - <link rel="stylesheet" href="/xxx.css">
    # 自定义css
    bottom:
    # - <script src="xxxx"></script>
    # 自定义js
    - <script src="/js/go.js"></script>
  3. [Blogroot]\source\ 下创建一个名为 pages 的文件夹
  4. [Blogroot]\source\pages\ 下创建一个名为 go.html 的文件,并把上一部分中的 html 代码粘入
  5. _config.yml 中将我们刚创建的的 pages 文件夹列为不受渲染的对象:
    skip_render:
    - pages/**
  6. 完成

下载链接识别

在 HTML 中的下载链接上添加 data-download="goDown 属性,例:

<a href="https://example.com" target="_blank" rel="noreferrer noopener" data-download="goDown">example</a>

思路来源