安全跳转页面·重制版

碎碎念

原本的安全跳转页面糟糕的一塌糊涂,因为当时水平有限,所以只能在别人的基础上修改,导致很多地方都不兼容,比如最简单的fancybox我都没有办法排除,会导致无法点击图片进行放大查看,除此之外无法排除友链页面,也无法排除友情链接的跳转卡片,兼容性也很差,群友想要使用我也没法提供解决方案,很是头疼,所以经过整整一个月的酝酿,我胡汉三又回来啦!此次重构大大简化了代码结构,并解决了前面的问题,为了测试稳定性,我还特意悄悄地上线了六天,好像也没人提出什么bug(也有可能是我的人气太少了呜呜呜),这才正式写出该重制版教程,给予需要的朋友以启发。

功能介绍

  1. 设置替换白名单:按照揽星给出的建议,可以自定义替换白名单匹配,如友链文章引用,好友引用等无需替换,其他链接替换;
  2. 设置页面白名单:如仅匹配文章页面的链接;
  3. 设置元素白名单:如仅匹配id="article-container"的内容;
  4. 设置跳转白名单:如知乎等,使用跳转页面,但显示为安全,可以自动跳转;
  • 注:该教程理论上适用于全部Hexo架构博客,请按照要求修改代码即可,该教程需要有一定的前端水平,如果有问题可以发到评论区,我会尽量解答。

功能实现

这里我还是使用原廿壴博客提供的跳转页模板,但是相关跳转页逻辑完全重构

页面模板

首先需要在source文件夹下创建go.html,写入以下内容:

代码语言:javascript
复制
---
layout: false
---
<!DOCTYPE html>
<html data-user-color-scheme="light">

<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>
安全中心 | LiuShen's Blog
</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, &#34;Segoe UI&#34;,
    &#34;PingFang SC&#34;, Roboto, &#34;Helvetica Neue&#34;, Arial, &#34;Noto Sans&#34;,
    &#34;Liberation Sans&#34;, sans-serif, &#34;Apple Color Emoji&#34;, &#34;Segoe UI Emoji&#34;,
    &#34;Segoe UI Symbol&#34;, &#34;Noto Color Emoji&#34;;
  --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas,
    &#34;Liberation Mono&#34;, &#34;Courier New&#34;, monospace;
}

[data-user-color-scheme=&#34;dark&#34;] {
  --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>

&lt;div class=&#34;content&#34;&gt;
  &lt;div class=&#34;flex-box&#34;&gt;
    &lt;div class=&#34;flex-contain&#34;&gt;
      &lt;div class=&#34;logo-img&#34;&gt;
        &lt;p class=&#34;blog-name&#34;&gt;LiuShen&#39;s Blog&lt;/p&gt;
        &lt;p class=&#34;blog-description&#34;&gt;&lt;/p&gt;
      &lt;/div&gt;

      &lt;!-- 加载ing... --&gt;
      &lt;div class=&#34;loading-item loading-safe flex&#34;&gt;
        &lt;i class=&#34;iconfont icon-snapchat-fill&#34;&gt;&lt;/i&gt;
        &lt;div class=&#34;loading-text&#34;&gt;链接安全性检验中 请稍后...&lt;/div&gt;
      &lt;/div&gt;

      &lt;div class=&#34;go-box&#34;&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&#34;footer flex-footer&#34;&gt;
      ©2021-2024
      &lt;a href=&#34;https://www.qyliu.top&#34; class=&#34;blog-name&#34;&gt;&lt;span&gt;LiuShen&#39;s Blog&lt;/span&gt;&lt;/a&gt;
      版权所有
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

</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:
"安全中心 | LiuShen's Blog",
// 地址栏图标
iconFavicon: "https://cdn.qyliu.top/i/2024/03/21/65fc56832e37d.png",
// 二维码地址
// mpImgSrc: "/img/wxgzh.webp",
// 博客名称
blogName: "LiuShen's Blog",
// 博客描述
blogDescription: "柳影曳曳,清酒孤灯,扬笔撒墨,心境如霜",
// 白名单
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",
"qyliu.top",

    // 好友博客 增加自己的博客友链
  ],
  tipsTextError: &#34;链接错误,关闭页面返回本站&#34;,
  // tipsTextDownload:
    // &#34;从廿壴(ganxb2)微信公众号获取暗号≖‿≖✧ o‿≖✧(๑•̀ㅂ•́)و✧&#34;,
  //   &#34;(๑•̀ㅂ•́)و✧“博客”微信公众号关注走一波o‿≖✧&#34;,
  tipsTextDanger: &#34;该网址未在确认的安全范围内&#34;,
  tipsTextSuccess: &#34;该网址在确认的安全范围内&#34;,
  textDanger:
    &#34;您即将离开博客去往如下网址,请注意您的账号隐私安全和财产安全:&#34;,
  textSuccess: &#34;您即将离开博客去往如下网址&#34;,
  // 后续改成leancloud获取(下载验证码)
  // wpValidate: &#34;9498&#34;,
};
// 获取地址
const getQueryString = (name, type) =&gt; {
  // 构造一个含有目标参数的正则表达式对象
  let reg = new RegExp(&#34;(^|&amp;)&#34; + name + &#34;=([^&amp;]*)(&amp;|$)&#34;),
    regDown = new RegExp(&#34;&amp;type=&#34; + 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) =&gt; {
  return str
    ? str.replace(
      reg || /[&amp;&lt;&#34;&gt;&#39;](?:(amp|lt|quot|gt|#39|nbsp|#\d+);)?/g,
      function (a, b) {
        if (b) {
          return a;
        } else {
          return {
            &#34;&lt;&#34;: &#34;&amp;lt;&#34;,
            &#34;&amp;&#34;: &#34;&amp;amp;&#34;,
            &#39;&#34;&#39;: &#34;&amp;quot;&#34;,
            &#34;&gt;&#34;: &#34;&amp;gt;&#34;,
            &#34;&#39;&#34;: &#34;&amp;#39;&#34;,
          }[a];
        }
      }
    )
    : &#34;&#34;;
};

// 其他地址校验白名单
const othersValidate = (config, getLinkUrl) =&gt; {
  let isSafeUrl = false,
    safeUrl = config.safeUrl,
    url = xssCheck(getLinkUrl.url);
	console.log(&#34;shuchuchuchcu&#34;, safeUrl)
	console.log(&#34;shuchuchuchcu&#34;, url)

  if (safeUrl.length !== 0) {
    for (let i = 0; i &lt; safeUrl.length; i++) {
      const ele = safeUrl[i];
      if (url.includes(ele) 
	  || url.includes(ele + &#39;/&#39;) 
	  || url.includes(&#39;https://&#39; + ele)
	  || url.includes(&#39;https://&#39; + ele + &#39;/&#39;)
	  || url.includes(&#39;http://&#39; + ele)
	  || url.includes(&#39;http://&#39; + ele + &#39;/&#39;)) {
        isSafeUrl = true;
        break;
      }
    }
  }
  return isSafeUrl;
};

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

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

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

    if (isSafeUrl) {
      tplConfig.tipType = &#34;tip1&#34;;
      tplConfig.tipsText = config.tipsTextSuccess;
      tplConfig.loadingTopicText = config.textSuccess;
      tplConfig.loadingColorType = &#34;loading-color1&#34;;
      // 白名单链接直接跳转
      setTimeout(() =&gt; {
        const goUrlBtn = document.querySelector(&#34;.go-url-btn&#34;);
        goUrlBtn.click();
      }, 2000);

    } else {
      tplConfig.tipType = &#34;tip2&#34;;
      tplConfig.tipsText = config.tipsTextDanger;
      tplConfig.loadingTopicText = config.textDanger;
      tplConfig.loadingColorType = &#34;loading-color2&#34;;
    }
  }
  else {
    // 错误
    tplConfig.tipType = &#34;tip2&#34;;
    tplConfig.tipsText = config.tipsTextError;
  }

  const othersTpl = `
      &lt;div class=&#34;loading-topic&#34;&gt;
        &lt;span
          &gt;${tplConfig.loadingTopicText}&lt;/span
        &gt;
        &lt;a class=&#34;${tplConfig.loadingColorType} go-url&#34;&gt;${tplConfig.goUrl}&lt;/a&gt;
      &lt;/div&gt;
      &lt;div class=&#34;flex-end&#34;&gt;
        &lt;a rel=&#34;noopener external nofollow noreferrer&#34; class=&#34;loading-btn go-url-btn&#34; href=&#34;${tplConfig.goUrl}&#34; target=&#34;_self&#34;&gt;继续&lt;/a&gt;
      &lt;/div&gt;
    `;
  const tpl = `
      &lt;div class=&#34;loading-item ${tplConfig.loadingType} hidden&#34;&gt;
        &lt;div class=&#34;flex loading-tip ${tplConfig.tipType}&#34;&gt;
          &lt;i class=&#34;iconfont icon-snapchat-fill ${tplConfig.loadingType === &#34;loading-download&#34; &amp;&amp; &#34;hidden&#34;
    }&#34;&gt;&lt;/i&gt;
          &lt;div class=&#34;loading-text&#34;&gt;
            ${tplConfig.tipsText}
          &lt;/div&gt;
        &lt;/div&gt;
        ${tplConfig.loadingType === &#34;loading-others&#34;
      ? othersTpl
      // : tplConfig.loadingType === &#34;loading-download&#34;
      //   ? downloadTpl
        : &#34;&#34;
    }
      &lt;/div&gt;
    `;

  // tpl渲染
  goBox.innerHTML = tpl;
  const loadingItem = document.querySelector(&#34;.go-box .loading-item&#34;);
  loadingSafe.classList.add(&#34;fadeOutUp&#34;, &#34;fade-animate&#34;);
  loadingItem.classList.remove(&#34;hidden&#34;);
  loadingItem.classList.add(&#34;fadeInUp&#34;, &#34;fade-animate&#34;);
};
goInit(config);

</script>
</body>

</html>

以上代码可能需要修改的部分只有一个地方,白名单,不过这里的白名单都是通用的,可以不进行修改,这里的白名单为跳转白名单,详情请看功能介绍,下面是页面展示:

跳转白名单展示

JS链接替换

下面就是我重构的内容,使用JS脚本,将能匹配上的链接进行替换,请在自定义JS代码部分添加以下内容:

代码语言:javascript
复制
function updateLinks() {
// 定义白名单数组
var whitelist = [
'qyliu.top', // 添加您不想替换链接的域名或路径片段
'zouht.com',
'akilar.top',
……
];

var containerArticle = document.getElementById(&#34;article-container&#34;);
if (containerArticle) {
    var links = containerArticle.getElementsByTagName(&#34;a&#34;);
    for (var i = 0; i &lt; links.length; i++) {
        var link = links[i];
        var hasFancybox = link.hasAttribute(&#34;data-fancybox&#34;);
        var isSafeGo = link.href.startsWith(&#39;/go.html&#39;);
        
        // 使用 Array.prototype.some() 来检查链接的 href 是否包含白名单中的某个元素
        var isWhitelisted = whitelist.some(function(whitelistedItem) {
            return link.href.includes(whitelistedItem);
        });

        // 如果没有特定属性且链接没有安全跳转,且链接不在白名单中
        if (!hasFancybox &amp;&amp; !isSafeGo &amp;&amp; !isWhitelisted) {
            var originalUrl = link.href;
            link.href = &#34;/go.html?goUrl=&#34; + encodeURIComponent(originalUrl) + &#34;&amp;type=goDown&#34;;
        }
    }
}

}

// 在 PJAX 完成时调用函数
document.addEventListener('pjax:complete', function() {
// 检查当前路径是否以 "/posts/" 开头
if (window.location.pathname.startsWith('/posts/')) {
updateLinks();
console.log('pjax||文章页面,准备替换安全链接');
} else {
console.log('pjax||非文章页面无需替换安全链接');
}
});

// 在页面加载完成后调用函数
window.addEventListener('load', function() {
// 检查当前路径是否以 "/posts/" 开头
if (window.location.pathname.startsWith('/posts/')) {
updateLinks();
console.log('load||文章页面,准备替换安全链接');
} else {
console.log('load||非文章页面无需替换安全链接');
}
});

这里需要修改的部分主要有:

  1. 第三行替换白名单,这些网站将默认为安全网站,不会被重定向到安全跳转页面,可以将友链的根域名放到这里,如果在链接中匹配到元素,将不进行替换。
  2. 第十行元素白名单:填写你想替换的页面的某个部分的ID或者类名,查找方式如下:

元素白名单查找方式
  1. 第十六行元素黑名单:比如fancybox,fancybox是图片点击后放大预览的插件,如果链接替换了的话会导致无法正常放大,显示图片异常,这个应该都一样,确定的方式如下:
确认fancybox
  1. 下面两个执行函数,由于我的页面开了pjax,不需要的删除即可,也可以保留,需要替换其中的posts路径,该路径在随便的文章页链接中就能找到:
页面白名单

此时,功能基本实现了,你的文章页的外链卡片应该已经被替换为了安全链接。

评论区

每个评论系统基本上都会有一个回调函数,比如butterfly主题我们定位到文件:[blogroot]themes\butterfly\layout\includes\third-party\comments\twikoo.pug,修改其中的代码:

代码语言:javascript
复制
 - const { envId, region, option } = theme.twikoo
 - const { use, lazyload, count } = theme.comments

script.
(() => {
const getCount = () => {
const countELement = document.getElementById('twikoo-count')
if(!countELement) return
twikoo.getCommentsCount({
envId: '!{envId}',
region: '!{region}',
urls: [window.location.pathname],
includeReply: true
}).then(res => {
countELement.textContent = res[0].count
}).catch(err => {
console.error(err)
})
}

const init = () =&gt; {
  twikoo.init(Object.assign({
    el: &#39;#twikoo-wrap&#39;,
    envId: &#39;!{envId}&#39;,
    region: &#39;!{region}&#39;,
    onCommentLoaded: () =&gt; {
      btf.loadLightbox(document.querySelectorAll(&#39;#twikoo .tk-content img:not(.tk-owo-emotion)&#39;))
  •      document.querySelectorAll(&#39;#twikoo .tk-comments-container a&#39;).forEach(function(aEl){
    
  •        if (!aEl.hasAttribute(&#39;data-fancybox&#39;)) {
    
  •          if (!aEl.href.startsWith(window.location.origin)) {
    
  •            aEl.href = &#39;/go.html?goUrl=&#39; + encodeURIComponent(aEl.href) + &#39;&amp;type=goDown&#39;;
    
  •          }
    
  •        }
    
  •      });
      }
    }, !{JSON.stringify(option)}))
    
    !{count ? &#39;GLOBAL_CONFIG_SITE.isPost &amp;&amp; getCount()&#39; : &#39;&#39;}
    

    }

    const loadTwikoo = () => {
    if (typeof twikoo === 'object') setTimeout(init,0)
    else getScript('!{url_for(theme.asset.twikoo)}').then(init)
    }

    if ('!{use[0]}' === 'Twikoo' || !!{lazyload}) {
    if (!{lazyload}) btf.loadComment(document.getElementById('twikoo-wrap'), loadTwikoo)
    else loadTwikoo()
    } else {
    window.loadOtherComment = loadTwikoo
    }
    })()

去掉加号即为正常缩进,注意第一二行,去掉前面的一格空格,可以看到我加了一些限制条件,和上面同理,这里我就不多说了,有什么问题可以在评论区交流。

最后功能实现。