为 Hugo 站点实现外部链接跳转访问

最近,我为博客增加了一个新功能:外部链接跳转页。现在,点击站外链接会先进入一个中转页,经用户确认点击 继续访问 后才前往目标网站。当然这功能并不少见,多数网站都有此功能。

为何需要跳转页?

  1. 明确告知:清晰告知访客“您即将离开本站”,避免意外跳转。
  2. 提升安全:为访客提供一道心理屏障,降低链接被篡改的风险。
  3. 统一体验:统一站外跳转的设计规范,并同步了主站的深浅色模式。
日间模式
日间模式
夜间模式
夜间模式

实现方法

该功能主要依赖 Hugo 的 Render HookJavaScript 协同工作:

  1. 服务端预处理 (Hugo):网站构建时,Hugo 通过 Link Render Hook 为所有外部链接添加 target="_blank" 等属性,确保其在新标签页打开,完成基础优化。此时链接的 href 保持不变。

  2. 客户端动态改写 (JavaScript):页面加载后,external-links.js 脚本开始运行。它会遍历所有 <a> 标签,识别出未被排除的外部链接,并将其 href 属性动态地重写,指向我们自定义的跳转页 (/pages/redirect?target=[原始链接])。脚本还能通过 MutationObserver 监听动态内容(如评论区),确保所有外部链接都被处理。

  3. 中转页等待用户确认:用户点击链接后会访问 redirect.html 页面。该页面的脚本会解析 URL 参数,并等待用户手动点击“继续访问”按钮后,才会将用户重定向至目标网站。

总之,通过服务端预处理、客户端动态修改方式,将链接重写的逻辑完全放在浏览器端,避免了在 Hugo 构建时对 Markdown 内容的修改,实现方式更为灵活。

如何使用?

若想让特定链接“豁免”跳转,操作非常简单,有两种方式可选:

方法一:URL 标记法 (推荐)

在 Markdown 中,只需在链接 URL 的末尾加上 #no-redirect 即可。

示例

这是一个普通的外部链接:[GitHub](https://github.com)

这是一个不会跳转的链接:[Gitee](https://gitee.com#no-redirect)

Hugo 的 render-link.html 模板会自动识别这个标记,为链接添加 class="no-redirect",同时在最终生成的 href 属性中将这个标记移除,确保链接地址的整洁与正确。

方法二:HTML class

如果你需要在文章中直接编写 HTML,也可以手动为 <a> 标签添加 no-redirect 类。

示例

<a href="https://example.com" class="no-redirect">这个链接也不会跳转</a>

核心代码

1. Link Render Hook (render-link.html)

这个模板是实现新方案的核心。它会检查 URL 是否包含 #no-redirect,然后动态地添加 class 并清理 URL。

{{ $url_str := .Destination }}
{{ $class := "" }}
{{ $is_external := strings.HasPrefix $url_str "http" }}

{{ if strings.HasSuffix $url_str "#no-redirect" }}
  {{ $url_str = strings.TrimSuffix "#no-redirect" $url_str }}
  {{ $class = "no-redirect" }}
{{ end }}

<a href="{{ $url_str | safeURL }}"
   {{ if $class }}class="{{ $class }}"{{ end }}
   {{ if $is_external }}target="_blank" rel="noopener noreferrer"{{ end }}
   {{ with .Title }}title="{{ . }}"{{ end }}>
   {{ .Text | safeHTML }}
</a>

2. 核心 JS 逻辑 (external-links.js)

function processExternalLinks() {
    var links = document.querySelectorAll('a');
    var host = window.location.hostname;
    // ...

    for (var i = 0; i < links.length; i++) {
        var link = links[i];
        // 如果是外部链接,且不含 'no-redirect' class
        if (link.hostname !== host && link.protocol.startsWith('http') && !link.classList.contains('no-redirect')) {
            var href = link.getAttribute('href');
            // ...
            // 重写 href 指向跳转页
            link.href = '/pages/redirect?target=' + encodeURIComponent(href) + '&theme=' + currentTheme;
        }
    }
}

3. 跳转页逻辑 (redirect.html)

(function() {
    const params = new URLSearchParams(window.location.search);
    const target = params.get('target');
    const continueBtn = document.getElementById('continue-btn');

    if (target && continueBtn) {
        const decodedUrl = decodeURIComponent(target);
        document.getElementById('target-url').innerText = decodedUrl;
        
        // 为按钮设置点击事件,手动跳转
        continueBtn.onclick = function() {
            window.location.href = decodedUrl;
        };
    }
})();
评论