前言
博客中不可避免地会引用外部链接。默认情况下,点击外部链接会直接跳转到目标站点,用户没有任何心理准备。这存在两个问题:
- 用户体验 — 用户无法感知即将离开博客,容易产生困惑
- 安全风险 — 不熟悉的外部链接可能带来钓鱼或恶意网站风险
本文将实现一个外部链接安全跳转页面:用户点击任何外部链接后,先跳到一个中间提示页,展示目标地址和安全警告,确认后才跳转。效果类似 V2EX、知乎的外链跳转机制。
实现思路
整体方案采用构建时重写 + 静态跳转页:
- Hexo Filter — 在 HTML 渲染完成后,通过
after_render:html 钩子扫描所有 <a> 标签,将外部链接重写为 /go/?url=<encoded_url>
- 静态跳转页 — 在
source/go/ 生成一个页面,通过 JS 解析 URL 参数,展示目标信息并实现跳转
- 样式隔离 — 通过内联 CSS 隐藏主题的导航栏、侧边栏等元素,使跳转页成为独立的全屏页面
这种方案是纯静态的,兼容 GitHub Pages 和 Vercel 等静态托管平台,无需服务器支持。
一、创建 Hexo Filter 脚本
在 scripts/external-link-redirect.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
| const cheerio = require("cheerio");
hexo.extend.filter.register("after_render:html", function (html) { const siteUrl = hexo.config.url || ""; const siteHost = siteUrl ? new URL(siteUrl).hostname : ""; const excludedHosts = [siteHost, "localhost"].filter(Boolean);
const $ = cheerio.load(html, null, false);
$("a[href]").each(function () { const el = $(this); const href = el.attr("href"); if (!href) return; if (/^\/go\/|^#|^mailto:|^javascript:|^tel:/.test(href)) return;
let linkHost; try { if (/^https?:\/\//i.test(href)) { linkHost = new URL(href).hostname; } else if (/^\/\/[^/]/.test(href)) { linkHost = new URL("https:" + href).hostname; } else { return; } } catch (e) { return; }
if (excludedHosts.some((h) => linkHost === h || linkHost.endsWith("." + h))) return;
el.attr("href", "/go/?url=" + encodeURIComponent(href)); if (!el.attr("target")) el.attr("target", "_blank"); if (!el.attr("rel")) el.attr("rel", "noopener noreferrer"); });
return $.html(); });
|
关键设计:
- 使用
cheerio 而非正则表达式解析 HTML,更可靠地处理各种边界情况
- 通过
hexo.config.url 自动获取站点域名,排除自身链接
- 对
mailto:、javascript:、tel: 等非 HTTP 协议链接不做处理
- 自动为外部链接添加
target="_blank" 和 rel="noopener noreferrer"
Hexo 会自动加载 scripts/ 目录下的 JS 文件,无需额外配置。
二、创建跳转页面
在 source/go/index.md 创建跳转页面。页面由三部分组成:内联样式、HTML 结构、内联脚本。
2.1 内联样式 — 隐藏主题元素
1 2 3 4 5 6 7 8 9 10 11
| <style> html,body,#body-wrap,#page,#content-inner,#article-container{ height:100%!important;margin:0!important;padding:0!important; overflow:hidden!important;background:#F5F5F5!important;max-width:100%!important } #nav,#footer,#sidebar,#rightside,.page-title,#page-header{display:none!important} [data-theme="dark"] #body-wrap,[data-theme="dark"] #page, [data-theme="dark"] #content-inner,[data-theme="dark"] #article-container{ background:#1a1a2e!important } </style>
|
通过内联 <style> 立即隐藏主题的导航栏、侧边栏、页脚等元素,避免页面加载时出现闪烁。同时将所有父容器的背景色统一为 #F5F5F5,消除白边。
2.2 HTML 结构
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
| <div class="go-wrap"> <div class="go-card-glow"> <div class="go-card"> <div class="go-icon"></div> <h1 class="go-title">您即将离开本站,跳转到:</h1> <div class="go-url-box"> <code class="go-url" id="go-url"></code> <button class="go-copy-btn" id="go-copy" title="复制链接"> </button> </div> <div class="go-warn-group"> <p class="go-warn"> 请自行确认链接安全性</p> <p class="go-warn"> 请注意账号和财产安全</p> </div> <div class="go-actions"> <a class="go-btn go-btn-primary" id="go-continue" href="#">继续访问</a> <a class="go-btn go-btn-secondary" id="go-back" href="#">取消跳转</a> </div> <div class="go-error" id="go-error"> <p>链接无效或缺失</p> <a class="go-btn go-btn-secondary" href="/">返回首页</a> </div> </div> </div> </div>
|
2.3 内联脚本
1 2 3 4 5 6 7 8 9 10 11
| (function(){ document.title = "Lay - 安全中心"; var params = new URLSearchParams(window.location.search); var rawUrl = params.get("url");
if (!rawUrl) { return; }
var decodedUrl = decodeURIComponent(rawUrl); })();
|
注意事项: Markdown 渲染器(CommonMark 规范)在 HTML 块内遇到空行时,会将后续缩进内容识别为代码块并转义。因此所有 HTML 内容必须连续书写,不能有空行。
三、页面样式
在 source/css/go.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
| .go-wrap { position: relative; z-index: 9999; display: flex; align-items: center; justify-content: center; min-height: 100vh; min-height: 100dvh; background: #F5F5F5; }
.go-card { background: #FDFDFD; border-radius: 18px; box-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06), 0 20px 44px rgba(0,0,0,0.04); transition: transform 0.35s, box-shadow 0.35s; }
.go-card-glow:hover .go-card { transform: translateY(-4px); box-shadow: 0 2px 4px rgba(0,0,0,0.04), 0 12px 32px rgba(0,0,0,0.08), 0 28px 56px rgba(0,0,0,0.06); }
.go-card-glow { background: linear-gradient(135deg, #cbd5e1, #e2e8f0, #cbd5e1, #e2e8f0); background-size: 300% 300%; animation: borderGlow 8s ease infinite; }
|
完整样式文件还包含暗色模式([data-theme="dark"])和 prefers-reduced-motion 适配。
四、注入样式
在 _config.butterfly.yml 的 inject.head 中添加 CSS 引用:
1 2 3
| inject: head: - <link rel="stylesheet" href="/css/go.css">
|
五、工作流程
1 2 3 4 5 6 7 8 9 10
| 用户点击外部链接 ↓ Hexo 构建时已将 href 重写为 /go/?url=<encoded_url> ↓ 浏览器跳转到 /go/ 中间页 ↓ JS 解析 url 参数,展示目标地址 ↓ 用户点击"继续访问" → 跳转到目标站点 用户点击"取消跳转" → 关闭当前标签页
|
总结
通过 Hexo Filter + 静态跳转页的方案,实现了:
- 构建时重写 — 使用 cheerio 解析 HTML,可靠地识别和重写外部链接
- 中间提示页 — 展示目标地址、安全警告,用户确认后才跳转
- 样式隔离 — 内联 CSS 隐藏主题元素,避免加载闪烁和布局抖动
- 悬浮交互 — 卡片上浮 + 阴影扩散,提供悬停反馈
- 暗色模式 — 完整的深色主题适配
- 纯静态方案 — 无需服务器,兼容所有静态托管平台