安然无恙,各位;
前言
新年快乐!2026年的第一笔~
这段时间难得的一个五天假期,就打算折腾下自己的小站,改动最多的就是安全方面的事情了,由于个人的想法比较多,过度敏感,对这方面做的比较离谱。
现在经常看着有博友不少因为留言的问题被请去喝茶谈心的,连忙转头看看自己的,发现也有不少奇怪的链接,以前也没注意这方面问题,靠自己一点点审查的话很麻烦,就打算弄一个中间页提示。首先在网上找了一下,发现修改的都是hexo文章和构建页面的跳转中间页,而且有的实现起来有些麻烦,当然是指对于不懂的人来说,有很多专业术语或者是技术层面的代购的“对牛弹琴”, 当然,可能没什么实际作用,大家当个乐子看看就行。
插件
不过还是有一个不错的插件实现的:安全跳转页面·插件版 | LiuShen’s Blog 柳哥在很早之前就弄了一个插件一键完成的。
效果如下:支持白昼黑夜主题、

liushen的实现方法就不是简单的使用外挂JS进行实现,因为检测的内容是直接对HTML内容进行检测,并不会经过JavaScript,于是liushen就开发了这么一个插件:hexo-safego
插件功能
- 外部链接跳转:将外部链接替换为自定义的跳转页面,可以自定义文件名称,增加安全性。
- 灵活配置:支持多个容器,使用css选择器进行选择、支持白名单域名和生效页面路径的配置。
- Base64 编码:可选将外链链接编码为Base64加密,在跳转时再使用js转为正常网站地址,增强安全性能。
- 调试模式:调试模式输出详细信息,便于开发和调试。
- 自定义页面:支持设置标题、副标题、头像、暗黑模式,如果有前端基础,还可以自己定义跳转页面进行美化。
留言
但是,柳哥的插件只是支持hexo构建的页面,对于twikoo等外嵌的留言系统来说是没作用的,在探索了一下变量,参考了空白Koobai和老麦笔记的文章终于造出了一堆史山代码。
期间出现了名字跳转或内容跳转失效,如果使用有问题或者其他系统自行看F12看,这个问题的本质是昵称链接的 DOM 选择器不匹配 + 异步加载的昵称链接未被脚本捕获,其他系统我没尝试过各位可以自行测试。
用鼠标点击评论区的昵称,或者Ctrl+Shift+C,此时开发者工具会定位到昵称对应的 HTML 代码;查看昵称链接的 HTML 结构,示例如下(你需要看自己的):
<a href="https://koxiuqiu.cn" target="_blank" rel="noopener noreferrer" class="tk-nick tk-nick-link"><strong>朽丘秋雨</strong></a>可放心食用,一键CV复制粘贴即用!
实现如下:
前往/source目录创捷一个html文件,命名:link.html,当然你可以自己改:
PS: 小提示 如果发现hexo g 构建public目录不存在,可以自己再复制一份过去到public也可以,不使用hexo clean就不用操作了,我个人的习惯就是直接hexo g&d两件套了,clean除非改样式一般不用。
样式参考的是柳哥的(其实一模一样的样式),还是挺喜欢柳哥的样式审美的。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<link rel="icon" href="/xiaoke.png" type="image/x-icon">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="robots" content="noindex, nofollow"/>
<title>😃页面加载中,请稍候...</title>
<style type="text/css">
body {
overflow: hidden;
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
transition: background 0.3s ease-in-out;
}
.container {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
margin: 0;
flex-direction: column;
}
.avatar-placeholder, .avatar {
width: 100px;
height: 100px;
border-radius: 50%;
margin-bottom: 15px;
display: block;
}
.avatar {
display: none;
}
.description {
font-size: 20px;
font-weight: 600;
}
.subtitle {
font-size: 15px;
margin-bottom: 20px;
color: #C4C4C4;
}
.loading {
text-align: center;
padding: 30px;
border-radius: 25px;
animation: fadein 2s;
width: 450px;
max-width: 80%;
transition: all 0.3s ease-in-out;
}
@keyframes fadein {
from { opacity: 0 }
to { opacity: 1 }
}
.content {
margin-bottom: 20px;
}
.url-text {
margin-bottom: 10px;
font-size: 16px;
letter-spacing: 1px;
}
.jump-url {
position: relative;
font-size: 20px;
display: block;
margin-top: 5px;
margin-bottom: 25px;
padding: 15px;
border-radius: 18px;
height: 25px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.copy-btn-container {
position: absolute;
display: flex;
align-items: center;
right: 10px;
top: 50%;
transform: translateY(-50%);
height: 100%;
width: 63px;
flex-direction: row-reverse;
}
.copy-btn {
width: 40px;
height: 40px;
border-radius: 12px;
border: 1px solid #a4a4a4;
transition: all 0.3s ease-in-out;
cursor: pointer;
}
.copy-btn-container svg {
width: 25px;
height: 25px;
fill: #888;
}
.countdown-text {
margin-top: 12px;
font-size: 12px;
}
.button-container {
display: flex;
justify-content: center;
gap: 20%;
margin-top: 20px;
}
.button {
padding: 10px 20px;
border-radius: 16px;
border: none;
cursor: pointer;
font-size: 16px;
width: 120px;
height: 40px;
}
.cancel-button {
color: black;
}
.confirm-button {
color: white;
}
/* 白天模式 */
body.light {
background: linear-gradient(135deg, #E9E9E9, #FFFFFF);
}
body.light .loading {
border: 1px solid #ccc;
background: rgba(255,255,255,0.7);
box-shadow: 0 16px 32px rgba(0,0,0,0.1);
}
body.light .loading:hover {
box-shadow: 0 16px 32px rgba(0,0,0,0.2);
}
body.light .url-text {
color: #333;
}
body.light .jump-url {
border: 1px solid #ccc;
background-color: #F7F9FE;
color: #333;
}
body.light .copy-btn-container {
background: linear-gradient(to left, #F7F9FE 75%,transparent 100%);
}
body.light .copy-btn {
background-color: #F7F9FE;
}
body.light .copy-btn:hover {
box-shadow: 0 16px 32px rgba(100,100,100,0.2);
}
body.light .countdown-text {
color: #515151;
}
body.light .cancel-button {
background-color: #a6e3e9;
}
body.light .confirm-button {
background-color: #3fc1c9;
}
/* 黑夜模式 */
body.dark {
background: linear-gradient(135deg, #364f6b, #222831);
}
body.dark .loading {
border: 1px solid #777;
background: #393e46;
color: #EFEFEF;
box-shadow: 0 16px 32px rgba(100,100,100,0.1);
}
body.dark .loading:hover {
box-shadow: 0 16px 32px rgba(100,100,100,0.2);
}
body.dark .description {
color: #F3F3F3;
}
body.dark .url-text, body.dark .countdown-text {
color: #EFEFEF;
}
body.dark .jump-url {
border: 1px solid #777;
background-color: #333;
color: #EFEFEF;
}
body.dark .copy-btn-container {
background: linear-gradient(to left, #333 75%,transparent 100%);
}
body.dark .copy-btn {
background-color: #222831;
}
body.dark .copy-btn:hover {
box-shadow: 0 16px 32px rgba(100,100,100,0.2);
}
body.dark .cancel-button {
background-color: #872C2C;
color: #FFF;
}
body.dark .confirm-button {
background-color: #28566F;
color: #FFF;
}
</style>
<script type="text/javascript">
// 适配Stellar主题的日夜模式
function detectStellarTheme() {
const body = document.body;
const theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
body.classList.add('dark');
body.classList.remove('light');
} else {
body.classList.add('light');
body.classList.remove('dark');
}
}
// 获取URL参数
function GetQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
var r = window.location.search.substr(1).match(reg);
return r ? decodeURIComponent(r[2]) : null; // 改用decodeURIComponent,兼容URL编码的参数
}
// 安全解码URL参数(优化容错性)
function decodeSafeUrlParam(paramStr) {
try {
// 修复base64替换逻辑,添加补全等号的处理
const base64 = paramStr.replace(/-/g, '+').replace(/_/g, '/');
// 补全base64缺失的等号
const paddedBase64 = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, '=');
const decodedBinary = atob(paddedBase64);
const decoded = decodeURIComponent(Array.from(decodedBinary).map(c =>
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
).join(''));
return decoded;
} catch (e) {
console.warn('URL解码失败,使用原始值:', e);
return paramStr;
}
}
// 跳转逻辑
let jump_url = GetQueryString('url');
// 解码处理
if (jump_url) {
jump_url = decodeSafeUrlParam(jump_url);
}
// 修复核心:正确创建正则表达式对象,添加i忽略大小写
const UrlReg = new RegExp('^((http|https|thunder|qqdl|ed2k|Flashget|qbrowser|ftp|rtsp|mms)://)', 'i');
// 验证URL合法性
if (!jump_url || !UrlReg.test(jump_url)) { // 改用test方法更高效
document.title = '参数错误,正在返回首页...';
jump_url = location.origin;
}
// 手动跳转函数(仅修改此处,从新建窗口改为当前窗口)
function jump() {
location.href = jump_url; // 当前窗口直接跳转
}
// 关闭/返回逻辑
function closeWindow() {
function isWeChat() {
return /MicroMessenger/i.test(navigator.userAgent);
}
function isQQ() {
return /QQ/i.test(navigator.userAgent) && !/MicroMessenger/i.test(navigator.userAgent);
}
if (isWeChat()) {
if (typeof WeixinJSBridge !== "undefined") {
WeixinJSBridge.call('closeWindow');
} else {
document.addEventListener('WeixinJSBridgeReady', function () {
WeixinJSBridge.call('closeWindow');
}, { once: true });
setTimeout(() => fallbackBack(), 500);
}
} else if (isQQ()) {
try {
if (typeof mqq !== "undefined" && mqq.ui && mqq.ui.closeWebView) {
mqq.ui.closeWebView();
} else {
fallbackBack();
}
} catch (e) {
fallbackBack();
}
} else {
fallbackBack();
}
}
// 返回兜底函数
function fallbackBack() {
if (window.history.length <= 1) {
window.location.href = location.origin;
} else {
window.history.back();
}
}
// 复制链接功能
function copyToClipboard() {
const urlText = document.getElementById('jump-url-text').textContent;
// 改用Clipboard API,替代过时的execCommand
navigator.clipboard.writeText(urlText).then(() => {
alert('链接已复制到剪贴板!');
}).catch(() => {
// 降级方案
const tempInput = document.createElement('input');
tempInput.value = urlText;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
alert('链接已复制到剪贴板!');
});
}
// 加载头像
async function loadAvatar() {
const avatarImg = document.querySelector('.avatar');
const placeholder = document.querySelector('.avatar-placeholder');
const img = new Image();
img.src = '/xiaoke.png';
img.onload = function () {
avatarImg.src = img.src;
avatarImg.style.display = 'block';
placeholder.style.display = 'none';
}
img.onerror = function() {
placeholder.style.display = 'none'; // 加载失败隐藏占位符
}
}
// 页面初始化
window.addEventListener('load', function () {
detectStellarTheme();
loadAvatar();
// 显示目标链接
const jumpUrlElement = document.getElementById('jump-url-text');
jumpUrlElement.textContent = jump_url;
// 更新提示文本
const countdownText = document.querySelector('.countdown-text');
countdownText.textContent = "💡请自行确认链接安全性,手动点击跳转";
// 监听主题切换
window.addEventListener('storage', (e) => {
if (e.key === 'theme') detectStellarTheme();
});
});
</script>
</head>
<body>
<div class="container">
<div class="avatar-placeholder"></div>
<img src="" alt="头像" class="avatar">
<div class="description">朽丘秋雨</div>
<div class="subtitle">安全提示</div>
<div class="loading">
<div class="content">
<div class="url-text">您即将离开本站,跳转到:</div>
<div class="jump-url" id="jump-url">
<span id="jump-url-text"></span>
<div class="copy-btn-container">
<button class="copy-btn" onclick="copyToClipboard()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path d="M208 0L332.1 0c12.7 0 24.9 5.1 33.9 14.1l67.9 67.9c9 9 14.1 21.2 14.1 33.9L448 336c0 26.5-21.5 48-48 48l-192 0c-26.5 0-48-21.5-48-48l0-288c0-26.5 21.5-48 48-48zM48 128l80 0 0 64-64 0 0 256 192 0 0-32 64 0 0 48c0 26.5-21.5 48-48 48L48 512c-26.5 0-48-21.5-48-48L0 176c0-26.5 21.5-48 48-48z"/>
</svg>
</button>
</div>
</div>
</div>
<div class="countdown-text">⚡请自行确认链接安全性,手动点击跳转</div>
<div class="button-container">
<button class="button cancel-button" onclick="closeWindow()">取消跳转</button>
<button class="button confirm-button" onclick="jump()">立即跳转</button>
</div>
</div>
</div>
</body>
</html>在source/js (没有js文件夹自行创捷) 创捷一个redirect.js和redirect-1.js文件,分别复制一下代码:
(function() {
var siteDomain = window.location.hostname; // 自动获取本站域名,无需手动改
var redirectPage = '/link.html'; // 中间页路径
// 核心函数:判断是否为外部链接
function isExternalLink(url) {
if (!url || !url.startsWith('http')) return false;
// 排除mailto/tel等非网页链接
if (url.startsWith('mailto:') || url.startsWith('tel:')) return false;
// 排除本站链接
var linkHost = new URL(url).hostname.replace(/^www\./, '');
var siteHost = siteDomain.replace(/^www\./, '');
return linkHost !== siteHost;
}
// 核心函数:替换链接跳转
function replaceLinkHref(link) {
var originalHref = link.getAttribute('href');
if (isExternalLink(originalHref)) {
// 保留原链接的target属性(比如_blank)
var target = link.getAttribute('target') || '_self';
// 核心:替换为中间页跳转
link.setAttribute('href', redirectPage + '?url=' + encodeURIComponent(originalHref));
// 修复:如果原链接是新窗口打开,保持行为
link.setAttribute('target', target);
// 移除可能的onclick冲突
link.onclick = null;
}
}
// 核心函数:处理Twikoo评论区链接(解决异步加载问题)
function handleTwikooLinks() {
// Stellar主题下Twikoo的实际选择器(必对!)
var twikooLinkSelectors = [
'.tk-content a', // 评论内容里的链接(Stellar+Twikoo默认类名)
'.tk-nick a' // 留言人昵称链接(Stellar+Twikoo默认类名)
];
// 遍历并替换链接
twikooLinkSelectors.forEach(function(selector) {
var links = document.querySelectorAll(selector);
links.forEach(function(link) {
replaceLinkHref(link);
});
});
}
// 核心:页面加载后执行 + 监听Twikoo异步加载
window.onload = function() {
// 初始加载的评论链接
handleTwikooLinks();
// 监听Twikoo评论区DOM变化(解决异步加载无效问题)
var twikooContainer = document.getElementById('twikoo');
if (twikooContainer) {
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes.length > 0) {
handleTwikooLinks(); // 新评论加载后重新处理链接
}
});
});
observer.observe(twikooContainer, {
childList: true,
subtree: true
});
}
// 兼容Twikoo官方回调(双重保障)
if (window.twikoo) {
twikoo.init({
onCommentLoaded: handleTwikooLinks
});
}
};
})();(function() {
var siteDomain = window.location.hostname;
var redirectPage = '/link.html';
function isExternalLink(url) {
if (!url || !url.startsWith('http')) return false;
if (url.startsWith('mailto:') || url.startsWith('tel:')) return false;
var linkHost = new URL(url).hostname.replace(/^www\./, '');
var siteHost = siteDomain.replace(/^www\./, '');
return linkHost !== siteHost;
}
function replaceLinkHref(link) {
var originalHref = link.getAttribute('href');
if (originalHref.includes(redirectPage + '?url=')) return;
if (isExternalLink(originalHref)) {
var target = link.getAttribute('target') || '_self';
link.setAttribute('href', redirectPage + '?url=' + encodeURIComponent(originalHref));
link.setAttribute('target', target);
link.onclick = null;
}
}
function handleTwikooLinks() {
var nicknameSelector = '.tk-nick';
var twikooLinkSelectors = [
nicknameSelector,
'.tk-content a'
];
twikooLinkSelectors.forEach(function(selector) {
var links = document.querySelectorAll(selector);
links.forEach(function(link) {
replaceLinkHref(link);
});
});
}
function initObserver() {
var twikooContainer = document.getElementById('twikoo');
if (!twikooContainer) return;
var observer = new MutationObserver(function() {
handleTwikooLinks();
});
observer.observe(twikooContainer, {
childList: true,
subtree: true
});
}
window.addEventListener('load', function() {
handleTwikooLinks();
initObserver();
});
})();最后引入JS样式,_config.stellar.yml找到inject引用就行,这里是我主题所以配置文件是这个名字,根据自己的博客主题找到主题配置引用就行:
inject:
head:
- <link rel="stylesheet" href="...">
script:
- ...
- <script src="/js/redirect.js"></script> #twikoo跳转中间页
- <script src="/js/redirect-1.js"></script> #twikoo跳转中间页杂记
后续又调整修复了中间页的BUG,如跳转链接会本站、显示错误…

当然,首先twikoo本身来说不是hexo本身的功能,其实这玩意更多只是给访客一个提示,我能做的就是该提示提示了,有问题把链接留言下来我给他处理了就好!
欢迎交流,写这类的文比较生疏,有什么缺漏问题可以提出。


