<!DOCTYPE html><html lang="zh" data-theme="dark"><head><meta charset="utf-8"><meta name="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><title>在 Hexo 项目中添加 Webmention · Cytrogen 的个人博客</title><meta name="description" content="Hexo 缺少开箱即用的 Webmention 支持?本篇终极指南将带你从零开始,通过编写自定义 Generator、Helper 和标签插件,深度集成 Microformats,为你的博客实现完整的 Webmention 接收、发送(包括独特的“引用式”提及)与静态渲染功能。文章最后还提供了两个即插即用的 npm 插件,让你轻松为博客赋能。"><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/1de.html"><link rel="webmention" href="https://webmention.io/cytrogen.icu/webmention"><link rel="me" href="https://m.otter.homes/@Cytrogen"><link rel="me" href="https://github.com/cytrogen"><meta name="fediverse:creator" content="@Cytrogen@m.otter.homes"><link rel="preload" href="../fonts/opensans-regular-latin.woff2" as="font" type="font/woff2" crossorigin="anonymous"><style>@font-face {
font-family: 'Open Sans';
src: url('../fonts/opensans-regular-latin.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
size-adjust: 107%;
ascent-override: 97%;
descent-override: 25%;
line-gap-override: 0%;
}
</style><script>(function() {
try {
// 优先级:用户选择 > 系统偏好 > 默认浅色
const saved = localStorage.getItem('theme');
const theme = saved ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
} catch (error) {
// 失败时使用默认主题,不阻塞渲染
document.documentElement.setAttribute('data-theme', 'light');
}
})();
</script><link rel="stylesheet" href="../css/ares.css"><script data-netlify-skip-bundle="true">(function() {
document.addEventListener('DOMContentLoaded', function() {
const theme = document.documentElement.getAttribute('data-theme');
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper && theme) {
pageWrapper.setAttribute('data-theme', theme);
}
});
})();
</script><!-- hexo injector head_end start -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hexo-math@4.0.0/dist/style.css">
<!-- hexo injector head_end end --><meta name="generator" content="Hexo 8.1.1"><link rel="alternate" href="atom.xml" title="Cytrogen 的个人博客" type="application/atom+xml">
</head><body><div id="page-wrapper"><a class="skip-link" href="#main-content">跳到主要内容</a><div class="wrap"><header><a class="logo-link" href="../index.html"><img src="../favicon.png" alt="logo"></a><div class="h-card visually-hidden"><img class="u-photo" src="https://cytrogen.icu/favicon.png" alt="Cytrogen"><a class="p-name u-url u-uid" href="https://cytrogen.icu">Cytrogen</a><p class="p-note">Cytrogen 的个人博客,Cytrogen's Blog</p><a class="u-url" rel="me noopener" target="_blank" href="https://m.otter.homes/@Cytrogen">Mastodon</a><a class="u-url" rel="me noopener" target="_blank" href="https://github.com/cytrogen">GitHub</a></div><nav class="site-nav"><div class="nav-main"><div class="nav-primary"><ul class="nav-list hidden-mobile"><li class="nav-item"><a class="nav-link" href="../index.html">首页</a></li></ul><div class="nav-tools"><div class="language-menu"><button class="language-toggle" type="button"><svg class="icon icon-globe" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5zM5.145 12c.138.386.295.744.468 1.068.552 1.035 1.218 1.65 1.887 1.855V12H5.145zm.182 2.472a6.696 6.696 0 0 1-.597-.933A9.268 9.268 0 0 1 4.09 12H2.255a7.024 7.024 0 0 0 3.072 2.472zM3.82 11a13.652 13.652 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5H3.82zm6.853 3.472A7.024 7.024 0 0 0 13.745 12H11.91a9.27 9.27 0 0 1-.64 1.539 6.688 6.688 0 0 1-.597.933zM8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855A7.97 7.97 0 0 0 10.855 12H8.5zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.65 13.65 0 0 1-.312 2.5zm2.802-3.5a6.959 6.959 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5h2.49zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7.024 7.024 0 0 0-3.072-2.472c.218.284.418.598.597.933zM10.855 4a7.966 7.966 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4h2.355z"></path></svg><span>中文</span></button><div class="language-dropdown"></div></div></div><div class="nav-controls"><div class="more-menu hidden-mobile"><button class="more-toggle" type="button"><span>更多</span><svg class="icon icon-chevron-down" width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 8.825c-.2 0-.4-.1-.5-.2l-3.3-3.3c-.3-.3-.3-.8 0-1.1s.8-.3 1.1 0l2.7 2.7 2.7-2.7c.3-.3.8-.3 1.1 0s.3.8 0 1.1l-3.3 3.3c-.1.1-.3.2-.5.2z"></path></svg></button><div class="more-dropdown"><ul class="dropdown-list"><li class="dropdown-item"><a class="nav-link" href="../archives/index.html">归档</a></li><li class="dropdown-item"><a class="nav-link" href="../categories/index.html">分类</a></li><li class="dropdown-item"><a class="nav-link" href="../tags/index.html">标签</a></li><li class="dropdown-item"><a class="nav-link" href="../about/index.html">关于</a></li><li class="dropdown-item"><a class="nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></div><div class="theme-switcher"><button class="theme-toggle" type="button" role="switch" aria-pressed="false" aria-label="切换主题"><div class="theme-icon moon-icon"><svg class="icon icon-moon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"></path></svg></div><div class="theme-icon sun-icon"><svg class="icon icon-sun" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"></path></svg></div></button></div><details class="mobile-menu-details hidden-desktop"><summary class="hamburger-menu" aria-label="nav.menu"><svg class="icon icon-bars" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"></path></svg><span class="menu-text">nav.menu</span></summary><div class="mobile-menu-dropdown"><ul class="mobile-nav-list"><li class="mobile-nav-item"><a class="mobile-nav-link" href="../index.html">首页</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../archives/index.html">归档</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../categories/index.html">分类</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../tags/index.html">标签</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../about/index.html">关于</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></details></div></div></div></nav></header><main class="container" id="main-content" tabindex="-1"><div class="post"><article class="post-block h-entry"><div class="post-meta p-author h-card visually-hidden"><img class="author-avatar u-photo" src="../favicon.png" alt="Cytrogen"><span class="p-name">Cytrogen</span><a class="u-url" href="https://cytrogen.icu">https://cytrogen.icu</a></div><a class="post-permalink u-url u-uid visually-hidden" href="https://cytrogen.icu/posts/1de.html">永久链接</a><div class="p-summary visually-hidden"><p>Webmention 是 IndieWeb 的一个核心的开放标准,允许网站之间像社交平台一样互动:评论、点赞、转发等。重点是它去中心化、跨站点实现这些功能。</p>
<p>要知道,我的博客是用「老掉牙」的 Hexo 构建的。Hexo 的插件列表里,可是没有任何一款插件是和 Webmention 相关的。假设我想要在自己的网站里添加 Webmention 的发送和接收功能,只能自己写。更悲伤的是,我的「强迫症」要求我在自己开发的主题里也添加上即拿即用的 Webmention 功能。</p></div><div class="visually-hidden"><a class="p-category" href="../categories/%E7%BC%96%E7%A8%8B%E7%AC%94%E8%AE%B0/">编程笔记</a><a class="p-category" href="../tags/Hexo/">Hexo</a><a class="p-category" href="../tags/JavaScript/">JavaScript</a><a class="p-category" href="../tags/Pug/">Pug</a></div><h1 class="post-title p-name">在 Hexo 项目中添加 Webmention</h1><div class="post-info"><time class="post-date dt-published" datetime="2025-10-14T16:16:17.000Z">10/14/2025</time><time class="dt-updated visually-hidden" datetime="2026-02-09T17:16:55.213Z"></time></div><div class="post-content e-content"><html><head></head><body><p>Webmention 是 IndieWeb 的一个核心的开放标准,允许网站之间像社交平台一样互动:评论、点赞、转发等。重点是它去中心化、跨站点实现这些功能。</p>
<p>要知道,我的博客是用「老掉牙」的 Hexo 构建的。Hexo 的插件列表里,可是没有任何一款插件是和 Webmention 相关的。假设我想要在自己的网站里添加 Webmention 的发送和接收功能,只能自己写。更悲伤的是,我的「强迫症」要求我在自己开发的主题里也添加上即拿即用的 Webmention 功能。</p>
<span id="more"></span>
<div class="danger">
<p>和过去一样,我会将 Hexo 项目根目录的 <code>_config.yml</code> 称呼为 Hexo 配置项、将 <code>themes/主题名/</code> 目录下的 <code>_config.yml</code> 称呼为主题配置项。</p>
<p>我用的是 Markdown + Pug。如果你用的是其他引擎,请自行改动代码,逻辑应当都是相同的。</p>
<p>该文章默认你是 Hexo 插件 & 主题开发者!如果你不是、又或者说只是想要即拿即用的方案,见 <a href="#%E6%8F%92%E4%BB%B6">这里</a>。</p>
</div>
<h2 id="接收"><a class="markdownIt-Anchor" href="#接收"></a> 接收</h2>
<p>要想接收 Webmention,非常简单。你只需要在网站里添加这样的标签:</p>
<figure class="highlight html"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">link</span> <span class="attr">rel</span>=<span class="string">"me"</span> <span class="attr">href</span>=<span class="string">"https://github.com/yourusername"</span> /></span></span><br></pre></td></tr></tbody></table></figure>
<p>这不需要是 GitHub 链接。实际上大多数社交平台的链接都可以,像 GitLab 啦、推特啦、长毛象啦…… 我写了一个辅助函数来检测一个链接是否是社交平台的链接:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">-</span><br><span class="line"> function isSocialMediaUrl(url) {</span><br><span class="line"> if (!url) return false;</span><br><span class="line"> const socialDomains = [</span><br><span class="line"> 'github.com', 'gitlab.com', 'bitbucket.org',</span><br><span class="line"> 'twitter.com', 'x.com', 'mastodon.social', 'mastodon.online',</span><br><span class="line"> 'linkedin.com', 'facebook.com', 'instagram.com',</span><br><span class="line"> 'weibo.com', 'zhihu.com', 'bilibili.com',</span><br><span class="line"> 'youtube.com', 'youtu.be', 'tiktok.com',</span><br><span class="line"> 'discord.gg', 'telegram.me', 't.me'</span><br><span class="line"> ];</span><br><span class="line"> try {</span><br><span class="line"> const domain = new URL(url).hostname.toLowerCase();</span><br><span class="line"> return socialDomains.some(socialDomain => </span><br><span class="line"> domain === socialDomain || domain.endsWith('.' + socialDomain)</span><br><span class="line"> );</span><br><span class="line"> } catch {</span><br><span class="line"> return false;</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></tbody></table></figure>
<p>这个 <code><link></code> 呢,放在哪里都可以。像我就偷懒了,直接在我的二级菜单里添加了这个判断条件:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">mixin nav-link(item)</span><br><span class="line"> if item.type === 'internal'</span><br><span class="line"> // 省略</span><br><span class="line"> else if item.type === 'external'</span><br><span class="line"> - var relAttr = "noopener noreferrer" + (isSocialMediaUrl(item.path) ? " me" : "")</span><br><span class="line"> a.nav-link(href=item.path target="_blank" rel=relAttr)</span><br><span class="line"> != __(`menu.${item.key}`)</span><br><span class="line"> +icon-external-link()</span><br></pre></td></tr></tbody></table></figure>
<p>我的主题可以手动标记二级菜单里的链接是否是内部还是外部的(其实自动识别更好,但我偷懒了)。假设是外部链接,就让主题判断它是否是社交平台链接。如果是的话,就添加 <code>rel="me"</code> 这个属性。因为二级菜单里的链接大概率都是自己的社交平台链接,所以我认为这么写完全没问题~</p>
<p>这样做的目的是身份验证。现在来到 <a target="_blank" rel="noopener" href="https://webmention.io/">Webmention.io</a> 里便可以注册该网站、获得你的接收网址:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><link rel="webmention" href="https://webmention.io/你的域名/webmention" /></span><br></pre></td></tr></tbody></table></figure>
<p>再将其添加网站的 <code><head></code> 里:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">if theme.webmention && theme.webmention.domain</span><br><span class="line"> link(rel="webmention", href=`https://webmention.io/${theme.webmention.domain}/webmention`)</span><br></pre></td></tr></tbody></table></figure>
<figure class="highlight yaml"><figcaption><span>themes/主题/_config.yml</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">webmention:</span></span><br><span class="line"> <span class="attr">domain:</span> <span class="string">你的域名</span></span><br></pre></td></tr></tbody></table></figure>
<p>为了让使用到 Hexo-Theme-Ares 的用户也可以立即使用上 Webmention,我写成了需要在主题配置项里配置、才能自动添加接收链接的逻辑。</p>
<p>收到的所有 Webmention 都可以在 <a target="_blank" rel="noopener" href="https://webmention.io/settings/sites">Webmention.io 的仪表盘</a> 里查看。</p>
<h2 id="发送"><a class="markdownIt-Anchor" href="#发送"></a> 发送</h2>
<p>我参考的是 <a target="_blank" rel="noopener" href="https://webmention.app/docs">Webmention.app 文档</a> 里的做法。</p>
<div class="danger">
<p>我建议还在开发该功能的朋友们,先使用手动发送。待功能完善后,再进行自动化。</p>
<p>问的话,只能说都是泪。我在功能还未完善时就采用了 Netlify 自动请求 Webhook 的方案。结果向接收方发送了数个不完善的 Webmention。这些 Webmention 还无法撤回,只能等待对方自行移除。不是什么大问题,但心里会想着给对方添了麻烦、实在是抱歉。</p>
<p>再就是手动发送时请一定要注意自己网页的 URL 规范。例如我现在这个文章,你可以用 <code>/posts/1de</code> 访问,也可以用 <code>/posts/1de.html</code> 访问。看似是一样的,但是对于 Webmention.io 而言它们是完全不同的源地址。</p>
<p>假设你先用了不带 <code>.html</code> 的 URL 作为源地址发送,之后又用了带 <code>.html</code> 的 URL 作为源地址发送。即时它们内容一样、目标一样,服务器也会创建两条独立的记录。</p>
<p>想知道我是如何知道的吗?就算你问我这个问题,我也只能看着接收方数个重复的 Webmention 记录、笑笑不说话了。</p>
<p>我个人建议用后者发送,因为是 Permalink。</p>
</div>
<p>Webmention.app 提供了数个发送的方法,其核心都是通过检查指定 URL 的内容并自动发现其中的链接目标,然后向这些目标发送 Webmention 通知:</p>
<ul>
<li>基础发送方式
<ol>
<li>命令行工具
<ul>
<li>使用 <code>curl -X POST</code> 向 <code>https://webmention.app/check?url=你的网页地址</code> 发起请求</li>
<li>支持一次性发送单条或多条条目,会自动发现 <code>.h-entry</code> 等 Microformats 标记(见 <a href="#microformats">Microformats</a>)</li>
<li>可通过 GET 请求进行 dry-run 预览,也就是检查将要发送的 Webmention 但不会实际发送</li>
<li>建议在 Webmention.app 内领取令牌并配合使用,避免速率限制。如果不添加令牌的话,每个独立的 URL 每四个小时只能请求一次。用法很简单,只需要在 API 里添加 <code>token</code> 参数</li>
</ul>
</li>
<li>Node.JS 本地工具
<ul>
<li>安装 <code>@remy/webmention</code> 包后,可以使用 <code>webmention</code> 或者简写命令 <code>wm</code></li>
<li>支持读取多种内容格式,包括 HTML、RSS、Atom</li>
<li>使用方法:<code>npx webmention 你的网站的feed地址 --limit 数字 --send</code>
<ul>
<li><code>--limit</code> 默认为 10 条。假设设置成 1,就是发送 feed 里最新的文章的 Webmention</li>
<li>默认执行 dry-run,加上 <code>--send</code> 参数才会实际发送 Webmention</li>
</ul>
</li>
</ul>
</li>
<li>Webmention.app 网页
<ul>
<li>该方案没有在文档中提及。简单来说便是在 <a target="_blank" rel="noopener" href="https://webmention.app/check">Webmention.app 的 Test a URL 页</a> 手动填写网页地址、手动点击 <strong>Send All Webmentions</strong> 按钮发送</li>
<li>其实就是「命令行工具」方案的网页版,更好看些</li>
</ul>
</li>
</ol>
</li>
<li>自动化发送方案
<ol>
<li>Netlify 部署通知
<ul>
<li>步骤
<ol>
<li>在 Netlify 项目中访问 <strong>Deploys</strong> 页</li>
<li>点击 <strong>Notifications</strong> 按钮</li>
<li>在 <strong>Deploy notifications</strong> 块旁找到 <strong>Add notification</strong> 选项</li>
<li>点击后选择 <strong>HTTP POST request</strong></li>
<li><strong>Event to listen for</strong> 选择 <strong>Deploy succeeded</strong></li>
<li><strong>URL to notify</strong> 输入该 URL:<code>https://webmention.app/check?token=令牌&limit=限制发送多少条&url=你的网站feed地址</code></li>
</ol>
</li>
<li>每次部署成功后,Netlify 都会自动向 Webmention.app 发送 POST 请求</li>
<li>其实该方法就是最先说的「命令行工具」方案的进化版,并不意味着必须要使用 Netlify 部署网站才能自动化。你只要能确保每次部署成功后都可以发送一次该 POST 请求即可</li>
</ul>
</li>
<li>IFTTT + RSS Feed
<ul>
<li>该方案需要付费。我个人不推荐该方案,你可以找找 IFTTT 的免费代替品,或者使用其他方案。和我上面说的一样,这些方案的本质都是向 Webmention.app 发送 POST 请求</li>
</ul>
</li>
<li>IFTTT + 定时触发
<ul>
<li>同上,不推荐</li>
</ul>
</li>
</ol>
</li>
</ul>
<h4 id="microformats"><a class="markdownIt-Anchor" href="#microformats"></a> Microformats</h4>
<p>Microformats 是一组基于 HTML 的轻量级语义标记规范,用于在网页中嵌入结构化数据,使人类和机器都能更好地理解和使用网页内容。</p>
<p>它的核心概念是:</p>
<ul>
<li>不需要额外的语法或嵌套语言,直接在网页中使用标准 HTML 元素和 <code>class</code> 属性</li>
<li>通过添加特定 <code>class</code> 名称来标记文章、人物、事件等信息</li>
<li>搜索引擎、浏览器、社交平台等可以自动识别这些标记并提取结构化数据</li>
<li>标记内容仍然是普通网页的一部分,不影响用户阅读体验</li>
</ul>
<p>为了让 Webmention 功能更完整地显示我们的内容,Microformats 是十分建议添加的。重点在于第二条:<em>标记文章、人物、事件等信息</em>。</p>
<ul>
<li>
<p>文章:<code>h-entry</code> 用于标记网页内容条目的语义类名</p>
<ul>
<li>
<p>用纯 HTML 作为例子,用法是这样的:</p>
<figure class="highlight html"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">div</span> <span class="attr">class</span>=<span class="string">"h-entry"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">p</span>></span>世界你好~<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"><span class="tag"></<span class="name">div</span>></span></span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p>要想添加标题、摘要、正文等信息,需要在 <code>h-entry</code> 所属的标签内部添加子标签。这些是广泛使用的属性:</p>
<ul>
<li>
<p><code>p-name</code>:条目标题或名称</p>
<figure class="highlight html"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">div</span> <span class="attr">class</span>=<span class="string">"h-entry"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">p</span> <span class="attr">class</span>=<span class="string">"p-name"</span>></span>我是标题<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">p</span>></span>世界你好~<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"><span class="tag"></<span class="name">div</span>></span></span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p><code>p-summary</code>:简短摘要</p>
<figure class="highlight html"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">div</span> <span class="attr">class</span>=<span class="string">"h-entry"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">p</span> <span class="attr">class</span>=<span class="string">"p-name"</span>></span>我是标题<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">p</span> <span class="attr">class</span>=<span class="string">"p-summary"</span>></span>我是摘要<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">p</span>></span>世界你好~<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"><span class="tag"></<span class="name">div</span>></span></span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p><code>e-content</code>:正文内容</p>
<figure class="highlight html"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">div</span> <span class="attr">class</span>=<span class="string">"h-entry"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">p</span> <span class="attr">class</span>=<span class="string">"p-name"</span>></span>我是标题<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">p</span> <span class="attr">class</span>=<span class="string">"p-summary"</span>></span>我是摘要<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">p</span> <span class="attr">class</span>=<span class="string">"e-content"</span>></span>世界你好~<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"><span class="tag"></<span class="name">div</span>></span></span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p><code>dt-published</code>:发布时间(建议使用 <code><time datetime></code>)</p>
<p>就不举例了,总之和上面的例子一样,都是标签里加类名。</p>
</li>
<li>
<p><code>dt-updated</code>:更新时间</p>
</li>
<li>
<p><code>p-author</code>:分类或标签</p>
</li>
<li>
<p><code>u-url</code>:条目的永久链接</p>
</li>
<li>
<p><code>u-uid</code>:唯一标识符,通常与 <code>u-url</code> 相同</p>
</li>
<li>
<p><code>p-location</code>:发布地点(可嵌套 <code>h-card</code>、<code>h-adr</code>、<code>h-geo</code>)</p>
</li>
<li>
<p><code>u-syndication</code>:条目的分发副本链接</p>
</li>
<li>
<p><code>u-in-reply-to</code>:所回应的原始条目链接(可嵌套 <code>h-cite</code>)</p>
</li>
<li>
<p><code>p-rsvp</code>:RSVP 状态</p>
<ul>
<li>
<p>RSVP 指的是法语短语「<em>Répondez s'il vous plaît</em>」的缩写,意思是「请回复」。在活动邀请中,它用于请求受邀者告知是否会出席。虽然来源是法语,但这个缩写已经被英语和其他语言广泛采用,尤其在正式或半正式的场合中</p>
</li>
<li>
<p>在 Microformats2 的语义标记中,<code>p-rsvp</code> 用来表示你对某个活动(<code>h-event</code>)的回应状态。支持的值包括:</p>
<ul>
<li><code>yes</code>:会参加</li>
<li><code>no</code>:不会参加</li>
<li><code>maybe</code>:尚未决定</li>
<li><code>interested</code>:感兴趣但未承诺参加</li>
</ul>
</li>
</ul>
</li>
<li>
<p><code>u-like-of</code>:所点赞的条目链接</p>
</li>
<li>
<p><code>u-repost-of</code>:所转发的条目链接</p>
</li>
</ul>
</li>
<li>
<p>虽然没有强制要求,但一个典型的 <code>h-entry</code> 应至少包含:</p>
<ul>
<li><code>u-url</code></li>
<li><code>dt-published</code></li>
<li><code>e-content</code> 或 <code>p-summary</code></li>
<li><code>p-name</code></li>
<li><code>p-author</code>:稍后在 <code>h-card</code> 提到</li>
</ul>
</li>
</ul>
</li>
<li>
<p>人物:<code>h-card</code> 用于在网页中嵌入人物或组织的结构化信息</p>
<ul>
<li>和 <code>h-entry</code> 一样的用法。核心属性是这些,它们都是可选的,并且支持多值:
<ul>
<li><code>p-name</code>:姓名或组织名称</li>
<li><code>u-url</code>:主页或代表链接</li>
<li><code>u-photo</code>:头像或代表图像</li>
<li><code>u-email</code>:邮箱地址</li>
<li><code>p-org</code>:所属组织</li>
<li><code>p-job-title</code>:职位名称</li>
<li><code>p-note</code>:附加说明</li>
<li><code>p-category</code>:标签或分类</li>
<li><code>dt-bday</code>:生日日期</li>
<li><code>p-tel</code>:电话号码</li>
<li><code>p-adr</code>:地址信息,可嵌套 <code>h-adr</code></li>
<li><code>p-geo</code> / <code>u-geo</code>:地理位置,可嵌套 <code>h-adr</code></li>
</ul>
</li>
<li><code>h-card</code> 不强制放入 <code>h-entry</code> 内,但我 <strong>推荐</strong> 嵌套使用。这样有助于 Webmention 接收方或 Microformats 解析器识别作者是谁,并展示头像、名称、主页等信息</li>
</ul>
</li>
</ul>
<p>现在我们回到 Hexo。我建议在渲染文章的 <code><article></code> 标签里添加 <code>h-entry</code> 类。</p>
<p>我个人的写法是这样的:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">mixin post(item)</span><br><span class="line"> .post</span><br><span class="line"> article.post-block.h-entry</span><br><span class="line"> .post-meta.p-author.h-card</span><br><span class="line"> img.author-avatar.u-photo(src=url_for(theme.logo), alt=config.author)</span><br><span class="line"> a.author-name.p-name.u-url(href=config.url)= config.author</span><br><span class="line"></span><br><span class="line"> a.post-permalink.u-url(href=full_url_for(item.path)) 永久链接</span><br><span class="line"></span><br><span class="line"> if item.excerpt</span><br><span class="line"> .p-summary</span><br><span class="line"> != item.excerpt</span><br><span class="line"></span><br><span class="line"> h1.post-title.p-name</span><br><span class="line"> != item.title</span><br><span class="line"> +postInfo(item)</span><br><span class="line"></span><br><span class="line"> if item.in_reply_to</span><br><span class="line"> .post-reply</span><br><span class="line"> != __('reply_to') + ': '</span><br><span class="line"> a.u-in-reply-to(href=item.in_reply_to)= item.in_reply_to</span><br><span class="line"></span><br><span class="line"> .post-content.e-content</span><br><span class="line"> != item.content</span><br></pre></td></tr></tbody></table></figure>
<div class="danger">
<p>此处我直接照搬的主题源码。实际传参用的是 <code>page</code>。</p>
<p>需要注意的是,我没有使用到全部的属性。未来我会慢慢扩展这些内容。</p>
</div>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">mixin postInfo(item)</span><br><span class="line"> .post-info</span><br><span class="line"> time.post-date.dt-published(datetime=date_xml(item.date))</span><br><span class="line"> != full_date(item.date, 'l')</span><br><span class="line"> if item.from && (is_home() || is_post())</span><br><span class="line"> a.post-from(href=item.from target="_blank" title=item.from)!= __('translated')</span><br></pre></td></tr></tbody></table></figure>
<p>在文章中的 Front Matter 里添加该属性来指定目标地址:</p>
<figure class="highlight markdown"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line"><span class="section">in<span class="emphasis">_reply_</span>to: 目标地址</span></span><br><span class="line"><span class="section">---</span></span><br></pre></td></tr></tbody></table></figure>
<p>不过我的主题有些特殊:</p>
<ol>
<li>导航栏会渲染我的头像作为 logo,我不想要文章又一次显示该头像</li>
<li>部分内容显示出来会导致整体页面看上去比较臃肿</li>
<li><code>item.content</code> 包含了 <code>item.excerpt</code> 在内。直接从 <code>item.content</code> 里取摘要会很麻烦,再显示一次 <code>item.excerpt</code> 又很奇怪</li>
</ol>
<p>解决方案也很简单。既然这部分内容只是给机器看的,我们将其隐藏就好了。直接全部加上 <code>display: none</code> 样式。</p>
<h2 id="进阶玩法"><a class="markdownIt-Anchor" href="#进阶玩法"></a> 进阶玩法</h2>
<h4 id="仅发送一部分内容"><a class="markdownIt-Anchor" href="#仅发送一部分内容"></a> 仅发送一部分内容</h4>
<p>有时候,你一个文章里只有小部分内容是你想要发送的;有时候,接收方的 Webmention 显示处并没有写字数限制,你的文章会原封不动地全部显示在接收方的网页里。至少我是想要一个更灵活的发送方法,因此我写了个脚本。</p>
<p>效果如下:</p>
<figure class="highlight markdown"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">{% reply 目标地址 %}</span><br><span class="line"></span><br><span class="line">这是会被发送给目标地址的内容。目标地址是 xxx。</span><br><span class="line"></span><br><span class="line">{% endreply %}</span><br><span class="line"></span><br><span class="line">这是不会被发送给目标地址的内容。</span><br></pre></td></tr></tbody></table></figure>
<p>首先我们需要注册标签 <code>reply</code>:</p>
<figure class="highlight javascript"><figcaption><span>reply-tag.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">hexo.<span class="property">extend</span>.<span class="property">tag</span>.<span class="title function_">register</span>(<span class="string">'reply'</span>, <span class="keyword">function</span>(<span class="params">args, content</span>) {</span><br><span class="line"> <span class="keyword">const</span> targetUrl = args[<span class="number">0</span>];</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}, {<span class="attr">ends</span>: <span class="literal">true</span>});</span><br></pre></td></tr></tbody></table></figure>
<p>我们先前的主题模板写法,会默认给文章最外部包上 <code>h-entry</code>、发送整个文章。既然有些文章我们只想在内部包含多个小的 <code>h-entry</code> 回复块,我们可以在内存中添加一个属性 <code>entry</code>。</p>
<p>如果该文章我们希望整个打包送走,那就在文章的 Front Matter 里添加:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">entry: true</span><br><span class="line">---</span><br></pre></td></tr></tbody></table></figure>
<p>并在主题模板里添加条件判断:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line">mixin post(item)</span><br><span class="line"> .post</span><br><span class="line"> //- 如果 entry 为 true,包裹整个 article</span><br><span class="line"> if item.entry === true</span><br><span class="line"> article.post-block.h-entry</span><br><span class="line"> .post-meta.p-author.h-card(style="display: none;")</span><br><span class="line"> img.author-avatar.u-photo(src=url_for(theme.logo), alt=config.author)</span><br><span class="line"> a.author-name.p-name.u-url(href=config.url)= config.author</span><br><span class="line"> </span><br><span class="line"> a.post-permalink.u-url(href=full_url_for(item.path), style="display: none;") 永久链接</span><br><span class="line"> </span><br><span class="line"> if item.excerpt</span><br><span class="line"> .p-summary(style="display: none;")</span><br><span class="line"> != item.excerpt</span><br><span class="line"> </span><br><span class="line"> h1.post-title.p-name</span><br><span class="line"> != item.title</span><br><span class="line"> +postInfo(item)</span><br><span class="line"> </span><br><span class="line"> if item.in_reply_to</span><br><span class="line"> .post-reply</span><br><span class="line"> != __('reply_to') + ': '</span><br><span class="line"> a.u-in-reply-to(href=item.in_reply_to)= item.in_reply_to</span><br><span class="line"> </span><br><span class="line"> .post-content.e-content</span><br><span class="line"> != item.content</span><br><span class="line"> else</span><br><span class="line"> article.post-block</span><br><span class="line"> h1.post-title</span><br><span class="line"> != item.title</span><br><span class="line"> +postInfo(item)</span><br><span class="line"> </span><br><span class="line"> if item.in_reply_to</span><br><span class="line"> .post-reply</span><br><span class="line"> != __('reply_to') + ': '</span><br><span class="line"> a.u-in-reply-to(href=item.in_reply_to)= item.in_reply_to</span><br><span class="line"> </span><br><span class="line"> .post-content</span><br><span class="line"> != item.content</span><br></pre></td></tr></tbody></table></figure>
<p>由于会使用到 <code>reply</code> 标签的文章都不希望发送整个文章,所以在脚本内我们要将 <code>entry</code> 设置为 <code>false</code>:</p>
<figure class="highlight javascript"><figcaption><span>reply-tag.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">page</span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">page</span>.<span class="property">entry</span> = <span class="literal">false</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>接着验证 URL:</p>
<figure class="highlight javascript"><figcaption><span>reply-tag.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 1. 检查用户是否忘记提供 URL</span></span><br><span class="line"><span class="keyword">if</span> (!targetUrl) {</span><br><span class="line"> hexo.<span class="property">log</span>.<span class="title function_">warn</span>(<span class="string">'Reply tag: Missing target URL'</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="string">'<div class="reply-error">回复标签缺少目标URL</div>'</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2. 确保是绝对URL</span></span><br><span class="line"><span class="keyword">let</span> absoluteUrl;</span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">new</span> <span class="title function_">URL</span>(targetUrl);</span><br><span class="line"> absoluteUrl = targetUrl;</span><br><span class="line">} <span class="keyword">catch</span> (e) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// 3. 如果失败,尝试作为相对路径处理</span></span><br><span class="line"> absoluteUrl = hexo.<span class="property">extend</span>.<span class="property">helper</span>.<span class="title function_">get</span>(<span class="string">'full_url_for'</span>).<span class="title function_">call</span>({<span class="attr">config</span>: hexo.<span class="property">config</span>}, targetUrl);</span><br><span class="line"> } <span class="keyword">catch</span> (e2) {</span><br><span class="line"> <span class="comment">// 4. 如果再次失败,则判定为无效 URL 并报错</span></span><br><span class="line"> hexo.<span class="property">log</span>.<span class="title function_">warn</span>(<span class="string">`Reply tag: Invalid URL - <span class="subst">${targetUrl}</span>`</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="string">'<div class="reply-error">回复标签包含无效URL</div>'</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>渲染 Markdown 内容:</p>
<figure class="highlight javascript"><figcaption><span>reply-tag.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> renderedContent;</span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line"> renderedContent = hexo.<span class="property">render</span>.<span class="title function_">renderSync</span>({ </span><br><span class="line"> <span class="attr">text</span>: content.<span class="title function_">trim</span>(), </span><br><span class="line"> <span class="attr">engine</span>: <span class="string">'markdown'</span> </span><br><span class="line"> });</span><br><span class="line">} <span class="keyword">catch</span> (e) {</span><br><span class="line"> hexo.<span class="property">log</span>.<span class="title function_">warn</span>(<span class="string">'Reply tag: Failed to render markdown content'</span>);</span><br><span class="line"> renderedContent = content; <span class="comment">// 降级到纯文本</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>国际化支持:</p>
<figure class="highlight javascript"><figcaption><span>reply-tag.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 获取当前语言的“回复”翻译</span></span><br><span class="line"><span class="keyword">const</span> currentLang = <span class="variable language_">this</span>.<span class="property">page</span> ? <span class="variable language_">this</span>.<span class="property">page</span>.<span class="property">lang</span> : hexo.<span class="property">config</span>.<span class="property">language</span>;</span><br><span class="line"><span class="keyword">let</span> replyText = <span class="string">'回复'</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 尝试从主题语言文件获取翻译</span></span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">const</span> langHelper = hexo.<span class="property">extend</span>.<span class="property">helper</span>.<span class="title function_">get</span>(<span class="string">'__'</span>);</span><br><span class="line"> <span class="keyword">if</span> (langHelper) {</span><br><span class="line"> replyText = langHelper.<span class="title function_">call</span>({<span class="attr">page</span>: {<span class="attr">lang</span>: currentLang}}, <span class="string">'reply_to'</span>) || replyText;</span><br><span class="line"> }</span><br><span class="line">} <span class="keyword">catch</span> (e) {</span><br><span class="line"> <span class="comment">// 使用默认文本</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>获取并处理作者信息:</p>
<figure class="highlight javascript"><figcaption><span>reply-tag.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 获取作者信息和网站配置</span></span><br><span class="line"><span class="keyword">const</span> config = hexo.<span class="property">config</span>;</span><br><span class="line"><span class="keyword">const</span> theme = hexo.<span class="property">theme</span>.<span class="property">config</span>;</span><br><span class="line"><span class="keyword">const</span> authorName = config.<span class="property">author</span> || <span class="string">'Anonymous'</span>;</span><br><span class="line"><span class="keyword">const</span> siteUrl = config.<span class="property">url</span> || <span class="string">''</span>;</span><br><span class="line"><span class="comment">// 简单构造绝对 URL,确保 logo 路径正确</span></span><br><span class="line"><span class="keyword">let</span> authorAvatar = <span class="string">''</span>;</span><br><span class="line"><span class="keyword">if</span> (theme.<span class="property">logo</span>) {</span><br><span class="line"> <span class="comment">// 如果 logo 已经是绝对 URL,直接使用</span></span><br><span class="line"> <span class="keyword">if</span> (theme.<span class="property">logo</span>.<span class="title function_">startsWith</span>(<span class="string">'http'</span>)) {</span><br><span class="line"> authorAvatar = theme.<span class="property">logo</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 确保路径以/开头,构造网站根路径</span></span><br><span class="line"> <span class="keyword">const</span> logoPath = theme.<span class="property">logo</span>.<span class="title function_">startsWith</span>(<span class="string">'/'</span>) ? theme.<span class="property">logo</span> : <span class="string">'/'</span> + theme.<span class="property">logo</span>;</span><br><span class="line"> authorAvatar = (config.<span class="property">url</span> || <span class="string">''</span>).<span class="title function_">replace</span>(<span class="regexp">/\/$/</span>, <span class="string">''</span>) + logoPath;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>最后生成 HTML 结构:</p>
<figure class="highlight javascript"><figcaption><span>reply-tag.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> <span class="string">`</span></span><br><span class="line"><span class="string"> <div class="reply-block h-entry"></span></span><br><span class="line"><span class="string"> <!-- 作者信息(h-card) --></span></span><br><span class="line"><span class="string"> <div class="post-meta p-author h-card" style="display: none"></span></span><br><span class="line"><span class="string"> <span class="subst">${authorAvatar ? <span class="string">`<img class="author-avatar u-photo" src="<span class="subst">${authorAvatar}</span>" alt="<span class="subst">${authorName}</span>">`</span> : <span class="string">''</span>}</span></span></span><br><span class="line"><span class="string"> <a class="author-name p-name u-url" href="<span class="subst">${siteUrl}</span>"><span class="subst">${authorName}</span></a></span></span><br><span class="line"><span class="string"> </div></span></span><br><span class="line"><span class="string"> </span></span><br><span class="line"><span class="string"> <!-- 回复内容 --></span></span><br><span class="line"><span class="string"> <div class="reply-content e-content"></span></span><br><span class="line"><span class="string"> <span class="subst">${renderedContent}</span></span></span><br><span class="line"><span class="string"> </div></span></span><br><span class="line"><span class="string"> </span></span><br><span class="line"><span class="string"> <!-- 回复元信息 - 通过 CSS 在列表页隐藏 --></span></span><br><span class="line"><span class="string"> <div class="reply-meta"></span></span><br><span class="line"><span class="string"> <span class="reply-label"><span class="subst">${replyText}</span>:</span></span></span><br><span class="line"><span class="string"> <a class="reply-target u-in-reply-to" href="<span class="subst">${absoluteUrl}</span>"><span class="subst">${absoluteUrl}</span></a></span></span><br><span class="line"><span class="string"> </div></span></span><br><span class="line"><span class="string"> </div></span></span><br><span class="line"><span class="string">`</span>;</span><br></pre></td></tr></tbody></table></figure>
<div class="danger">
<p>这里我声明了一些类,需要自定义样式:</p>
<ul>
<li><code>reply-block</code></li>
<li><code>post-meta</code></li>
<li><code>author-avatar</code></li>
<li><code>author-name</code></li>
<li><code>reply-content</code></li>
<li><code>reply-meta</code></li>
<li><code>reply-label</code></li>
<li><code>reply-target</code></li>
</ul>
<p>不用这些样式也可以。我个人更希望回复块和普通文本们可以区分开来。</p>
<p>和先前做的一样,我也为 <code>h-card</code> 标签添加了 <code>display: none</code> 样式。这个样式可以根据你们的需求、选择保留或者删除。</p>
</div>
<h4 id="显示"><a class="markdownIt-Anchor" href="#显示"></a> 显示</h4>
<p>要想在博客内显示其他人向我们发送的 Webmention,就需要用到 Webmention.io 提供的 <a target="_blank" rel="noopener" href="https://webmention.io/settings">Mentions Feed API</a>。Webmention.io 会提供给我们一个 API 密钥,只需要将其添加在以下任意一个 URL 中即可:</p>
<ul>
<li><code>https://webmention.io/api/mentions.html?token=API密钥</code></li>
<li><code>https://webmention.io/api/mentions.atom?token=API密钥</code></li>
<li><code>https://webmention.io/api/mentions.jf2?token=API密钥</code></li>
</ul>
<p>这些链接会根据自己的格式,显示网站所有的 Webmention。但要是用在我们的博客网站上的话,就遭殃了:读者只想阅读你的一篇文章,但你的网站已经获取了全部一千篇文章的 Webmention。<em><strong>请不要这么做。</strong></em></p>
<p>正确做法是使用 <code>url</code> 参数,指定获取哪个文章的 Webmention。我建议使用 <code>jf2</code> 来获取数据,它是纯粹的、为机器而生的数据格式:</p>
<ul>
<li><code>https://webmention.io/api/mentions.jf2?url=文章URL</code></li>
</ul>
<p>如果你认为数据获取可以在客户端上进行、希望 Webmention 可以实时显示,你完全可以在主题的 <code>/source/js/</code> 目录下添加一个 JavaScript 文件执行网络请求和数据处理。不过纯动态有些劣势:</p>
<ol>
<li>对 SEO 不友好。其他人发送的 Webmention 不会被搜索引擎索引</li>
<li>可能在内容出现时导致页面布局发生跳动</li>
<li>浏览器需要发起一次额外的网络请求</li>
<li>如果 Webmention.io 的 API 出现故障或者相应缓慢,就什么都加载不出来了</li>
</ol>
<p>如果你认为这四点很重要,你可以考虑采用纯静态构建。这需要你在项目根目录的 <code>/scripts/</code> 目录下创建 Hexo 脚本:</p>
<figure class="highlight javascript"><figcaption><span>webmention-receiver.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">hexo.<span class="property">extend</span>.<span class="property">generator</span>.<span class="title function_">register</span>(<span class="string">'webmentions'</span>, <span class="keyword">async</span> <span class="keyword">function</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">const</span> hexo = <span class="variable language_">this</span>;</span><br><span class="line"> <span class="keyword">const</span> webmentionConfig = hexo.<span class="property">config</span>.<span class="property">webmention</span>;</span><br><span class="line"> <span class="keyword">let</span> allMentions = [];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (webmentionConfig && webmentionConfig.<span class="property">enable</span>) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="comment">// 拼接 Webmention.io API URL 并且发起请求</span></span><br><span class="line"> <span class="keyword">const</span> newWebmentions = <span class="keyword">await</span> <span class="title function_">fetchWebmentions</span>(hexo.<span class="property">config</span>);</span><br><span class="line"> <span class="comment">// 更新本地的缓存文件</span></span><br><span class="line"> <span class="comment">// 缓存功能非必需,可以无视掉!</span></span><br><span class="line"> allMentions = <span class="title function_">updateWebmentionsCache</span>(hexo, newWebmentions);</span><br><span class="line"> } <span class="keyword">catch</span> (error) {</span><br><span class="line"> hexo.<span class="property">log</span>.<span class="title function_">error</span>(error.<span class="property">message</span>);</span><br><span class="line"> hexo.<span class="property">log</span>.<span class="title function_">warn</span>(<span class="string">'[Webmention] Falling back to local cache due to fetch error.'</span>);</span><br><span class="line"> allMentions = <span class="title function_">loadWebmentionsCache</span>(hexo);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> allMentions = <span class="title function_">loadWebmentionsCache</span>(hexo);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 将获取到的数据注入到 Hexo 的本地变量</span></span><br><span class="line"> hexo.<span class="property">locals</span>.<span class="title function_">set</span>(<span class="string">'all_webmentions'</span>, allMentions);</span><br><span class="line"> <span class="keyword">if</span> (webmentionConfig && webmentionConfig.<span class="property">debug</span>) {</span><br><span class="line"> hexo.<span class="property">log</span>.<span class="title function_">info</span>(<span class="string">`[Webmention] Injected <span class="subst">${allMentions.length}</span> total mentions into hexo.locals.`</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line">});</span><br></pre></td></tr></tbody></table></figure>
<p>我假设了项目配置项里有这些内容:</p>
<figure class="highlight yaml"><figcaption><span>__config.yml</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">webmention:</span></span><br><span class="line"> <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line"> <span class="attr">domain:</span> <span class="string">域名</span></span><br><span class="line"> <span class="attr">api_endpoint:</span> <span class="string">https://webmention.io/api/mentions.jf2</span></span><br><span class="line"> <span class="attr">token:</span> <span class="string">令牌</span></span><br></pre></td></tr></tbody></table></figure>
<p>要想要在主题模板里渲染这些变量,有两种方法:</p>
<ol>
<li>
<p>直接在主题模板里用 <code>site</code> 变量渲染</p>
<p>关于 Hexo 的局部变量,建议看以下文档:</p>
<ul>
<li><a target="_blank" rel="noopener" href="https://www.bookstack.cn/read/hexo/b61d0b214ee2fdc6.md">自定义 - 变量 - 《Hexo 博客框架文档》 - 书栈网・BookStack</a>:列出了模板中所有可用的变量</li>
<li><a target="_blank" rel="noopener" href="https://hexo.io/zh-cn/api/locals#logo-wrap">局部变量 | Hexo</a>:官方文档。主要是说明如何在 JavaScript 里操作 <code>hexo.locals</code></li>
</ul>
<p>总之,脚本里 <code>hexo.locals.get("posts")</code> 等同于模板里用 <code>site.posts</code>。</p>
</li>
<li>
<p>写一个辅助函数,在模板中调用、渲染返回的 HTML:</p>
<figure class="highlight javascript"><figcaption><span>webmention-receiver.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br></pre></td><td class="code"><pre><span class="line">hexo.<span class="property">extend</span>.<span class="property">helper</span>.<span class="title function_">register</span>(<span class="string">'render_webmentions'</span>, <span class="keyword">function</span>(<span class="params">pageUrl</span>) {</span><br><span class="line"> <span class="keyword">const</span> webmentionConfig = <span class="variable language_">this</span>.<span class="property">config</span>.<span class="property">webmention</span>;</span><br><span class="line"> <span class="keyword">const</span> mode = webmentionConfig && webmentionConfig.<span class="property">mode</span> ? webmentionConfig.<span class="property">mode</span> : <span class="string">'static'</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 从 hexo.locals 获取 webmention 数据</span></span><br><span class="line"> <span class="keyword">const</span> allMentions = hexo.<span class="property">locals</span>.<span class="title function_">get</span>(<span class="string">'all_webmentions'</span>) || [];</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 文字截断函数</span></span><br><span class="line"> <span class="keyword">const</span> <span class="title function_">truncateContent</span> = (<span class="params">content, maxLength = <span class="number">200</span></span>) => {</span><br><span class="line"> <span class="keyword">if</span> (!content) <span class="keyword">return</span> <span class="string">''</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果是HTML内容,先提取纯文本</span></span><br><span class="line"> <span class="keyword">const</span> textContent = content.<span class="title function_">replace</span>(<span class="regexp">/<[^>]*>/g</span>, <span class="string">''</span>);</span><br><span class="line"> <span class="keyword">if</span> (textContent.<span class="property">length</span> <= maxLength) {</span><br><span class="line"> <span class="keyword">return</span> content;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 截断文本并添加省略号</span></span><br><span class="line"> <span class="keyword">const</span> truncatedText = textContent.<span class="title function_">substring</span>(<span class="number">0</span>, maxLength).<span class="title function_">trim</span>();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果原内容是HTML,包装成段落;否则直接返回</span></span><br><span class="line"> <span class="keyword">return</span> content.<span class="title function_">includes</span>(<span class="string">'<'</span>) ? <span class="string">`<p><span class="subst">${truncatedText}</span>…</p>`</span> : <span class="string">`<span class="subst">${truncatedText}</span>…`</span>;</span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!pageUrl) {</span><br><span class="line"> <span class="comment">// 尝试多种方式获取当前页面 URL</span></span><br><span class="line"> pageUrl = <span class="variable language_">this</span>.<span class="property">page</span>.<span class="property">path</span> || <span class="variable language_">this</span>.<span class="property">page</span>.<span class="property">url</span> || <span class="variable language_">this</span>.<span class="property">url</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 确保 pageUrl 有值</span></span><br><span class="line"> <span class="keyword">if</span> (!pageUrl) {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">'<div class="webmention-section webmention-empty"><span>无法获取页面URL</span></div>'</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> fullUrl = <span class="keyword">new</span> <span class="title function_">URL</span>(pageUrl, <span class="variable language_">this</span>.<span class="property">config</span>.<span class="property">url</span>).<span class="property">href</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> mentionsForThisPage = allMentions.<span class="title function_">filter</span>(<span class="function"><span class="params">mention</span> =></span> {</span><br><span class="line"> <span class="comment">// 简化 URL 匹配逻辑,使用字符串比较而非 URL 构造</span></span><br><span class="line"> <span class="keyword">const</span> targetUrl = mention.<span class="property">target</span>;</span><br><span class="line"> <span class="comment">// 同时尝试完整 URL 和相对路径匹配</span></span><br><span class="line"> <span class="keyword">return</span> targetUrl === fullUrl || </span><br><span class="line"> targetUrl.<span class="title function_">endsWith</span>(pageUrl) || </span><br><span class="line"> fullUrl.<span class="title function_">endsWith</span>(mention.<span class="property">target</span>.<span class="title function_">split</span>(<span class="string">'/'</span>).<span class="title function_">pop</span>()) ||</span><br><span class="line"> mention.<span class="property">target</span>.<span class="title function_">includes</span>(pageUrl.<span class="title function_">replace</span>(<span class="regexp">/^\//</span>, <span class="string">''</span>));</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> <span class="keyword">let</span> html = <span class="string">`<div class="webmention-section" data-page-url="<span class="subst">${pageUrl}</span>" data-full-url="<span class="subst">${fullUrl}</span>" data-mode="<span class="subst">${mode}</span>"></span></span><br><span class="line"><span class="string"> <h3 class="webmention-title">Webmentions (<span class="webmention-count"><span class="subst">${mentionsForThisPage.length}</span></span>)</h3></span></span><br><span class="line"><span class="string"> <div class="webmention-list">`</span>;</span><br><span class="line"></span><br><span class="line"> mentionsForThisPage.<span class="title function_">forEach</span>(<span class="function"><span class="params">mention</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> authorHtml = mention.<span class="property">author</span>.<span class="property">url</span> </span><br><span class="line"> ? <span class="string">`<a class="webmention-author-name" href="<span class="subst">${mention.author.url}</span>" target="_blank" rel="noopener ugc"><span class="subst">${mention.author.name}</span></a>`</span></span><br><span class="line"> : <span class="string">`<span class="webmention-author-name"><span class="subst">${mention.author.name}</span></span>`</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> photoHtml = mention.<span class="property">author</span>.<span class="property">photo</span></span><br><span class="line"> ? <span class="string">`<img class="webmention-author-photo" src="<span class="subst">${mention.author.photo}</span>" alt="<span class="subst">${mention.author.name}</span>" loading="lazy">`</span></span><br><span class="line"> : <span class="string">''</span>;</span><br><span class="line"></span><br><span class="line"> html += <span class="string">`</span></span><br><span class="line"><span class="string"> <div class="webmention-item" id="webmention-<span class="subst">${mention.id}</span>" data-webmention-id="<span class="subst">${mention.id}</span>"></span></span><br><span class="line"><span class="string"> <div class="webmention-author"></span></span><br><span class="line"><span class="string"> <span class="subst">${photoHtml}</span></span></span><br><span class="line"><span class="string"> <span class="subst">${authorHtml}</span></span></span><br><span class="line"><span class="string"> <span class="webmention-date"><span class="subst">${<span class="keyword">new</span> <span class="built_in">Date</span>(mention.published || mention.received).toLocaleDateString(<span class="string">'zh-CN'</span>)}</span></span></span></span><br><span class="line"><span class="string"> </div></span></span><br><span class="line"><span class="string"> <div class="webmention-content"></span></span><br><span class="line"><span class="string"> <span class="subst">${truncateContent(mention.content.html || mention.content.text)}</span></span></span><br><span class="line"><span class="string"> </div></span></span><br><span class="line"><span class="string"> <div class="webmention-meta"></span></span><br><span class="line"><span class="string"> <a class="webmention-source" href="<span class="subst">${mention.source}</span>" target="_blank" rel="noopener ugc">查看原文</a></span></span><br><span class="line"><span class="string"> </div></span></span><br><span class="line"><span class="string"> </div></span></span><br><span class="line"><span class="string"> `</span>;</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> html += <span class="string">'</div></div>'</span>;</span><br><span class="line"> <span class="keyword">return</span> html;</span><br><span class="line">});</span><br></pre></td></tr></tbody></table></figure>
<p>实际返回的 HTML 可以自行修改。如果想要直接使用该脚本文件,不要忘了为里面提到的类写样式。</p>
<p>接下来你要做的就是在模板里找到个顺眼的地方调用该方法:</p>
<figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">if config.webmention && config.webmention.enable</span><br><span class="line"> != render_webmentions()</span><br></pre></td></tr></tbody></table></figure>
</li>
</ol>
<h2 id="插件"><a class="markdownIt-Anchor" href="#插件"></a> 插件</h2>
<p>先前说过了,我有「强迫症」。我将上述的所有功能整合成了多个 Hexo 插件,你只需要安装和配置。详情以仓库里的 README 文件为标准。</p>
<ul>
<li>
<p>Hexo-Tag-Quotemention 涵盖了 <a href="#%E4%BB%85%E5%8F%91%E9%80%81%E4%B8%80%E9%83%A8%E5%88%86%E5%86%85%E5%AE%B9">发送部分文字</a> 功能</p>
<p><a target="_blank" rel="noopener" href="https://github.com/Cytrogen/hexo-tag-quotemention"><img src="https://gh-card.dev/repos/Cytrogen/hexo-tag-quotemention.svg" alt="Cytrogen/hexo-tag-quotemention - GitHub"></a></p>
</li>
<li>
<p>Hexo-Generator-Webmention 涵盖了 <a href="#%E6%98%BE%E7%A4%BA">显示</a> 功能</p>
<p><a target="_blank" rel="noopener" href="https://github.com/Cytrogen/hexo-generator-webmention"><img src="https://gh-card.dev/repos/Cytrogen/hexo-generator-webmention.svg" alt="Cytrogen/hexo-generator-webmention - GitHub"></a></p>
</li>
</ul>
<h2 id="测试"><a class="markdownIt-Anchor" href="#测试"></a> 测试</h2>
<div class="reply-block h-entry"><div class="post-meta p-author h-card visually-hidden"><img class="u-photo" src="https://cytrogen.icu/favicon.png" alt="Cytrogen"><span class="p-name">Cytrogen</span><a class="u-url" href="https://cytrogen.icu">https://cytrogen.icu</a></div><time class="dt-published visually-hidden" datetime="2026-02-19T08:33:14.192Z">2026-02-19T08:33:14.192Z</time><a class="u-url visually-hidden" href="https://cytrogen.icu">Post Link</a><div class="reply-content e-content"><p>如果你想要测试,又不想要打扰到其他人,你可以向 <a target="_blank" rel="noopener" href="https://hexo-theme-ares-demo.netlify.app/2025/10/14/webmention-test-zh/">这个地址</a> 发送 Webmention。这是我为 Hexo-Theme-Ares 搭建的 Demo 页面。</p>
</div><div class="reply-meta p-in-reply-to h-cite"><span class="reply-label">回复:</span><a class="reply-target u-url" target="_blank" rel="noopener" href="https://hexo-theme-ares-demo.netlify.app/2025/10/14/webmention-test-zh/">https://hexo-theme-ares-demo.netlify.app/2025/10/14/webmention-test-zh/</a></div></div>
</body></html></div></article></div></main><footer><div class="paginator"><a class="prev" href="a2a2.html">上一篇</a><a class="next" href="dc84.html">下一篇</a></div><!-- Webmention 显示区域--><div class="webmention-section" data-page-url="posts/1de.html" data-full-url="https://cytrogen.icu/posts/1de.html" data-mode="static">
<h3 class="webmention-title">Webmentions (<span class="webmention-count">2</span>)</h3><div class="webmention-group webmention-group-replies"><h4 class="webmention-group-title">回复 (2)</h4><div class="webmention-list">
<div class="webmention-item" id="webmention-1947274" data-webmention-id="1947274">
<div class="webmention-author">
<img class="webmention-author-photo" src="https://avatars.webmention.io/hexo-theme-ares-demo.netlify.app/7cea72f6b7c6b9804b41234f9dbaf35f6dc09135d141765c9ab149328277dea8.png" alt="John Doe" loading="lazy">
<a class="webmention-author-name" href="https://hexo-theme-ares-demo.netlify.app" target="_blank" rel="noopener ugc">John Doe</a>
<span class="webmention-date">2025/10/14</span>
</div>
<div class="webmention-content">
…旨在实现跨网站的通知与对话。其核心功能是:当一个 URL(源,Source)链接到另一个 URL(目标,Target)时,前者可以通知后者这一行为。作为对传统 Pingback 和 Trackback 协议的现代化替代方案,Webmention 在 IndieWeb 生态中扮演着基础通信层的角色,旨在将互动数据的所有权归还给独立的网站。 测试发送: <a href="https://cytrogen.icu/posts/1de.html">在 Hexo 项目中添加 Webmention</a> 。
</div>
<div class="webmention-meta">
<a class="webmention-source" href="https://hexo-theme-ares-demo.netlify.app/2025/10/14/Webmention-test-zh/" target="_blank" rel="noopener ugc">查看原文</a>
</div>
</div>
<div class="webmention-item" id="webmention-1947557" data-webmention-id="1947557">
<div class="webmention-author">
<img class="webmention-author-photo" src="https://avatars.webmention.io/taxodium.ink/2b954e9d7ff8da1cb55e49fc098ea06f464c9ffd1872e1f92cc2ac1bb10ad7c4.png" alt="Spike Leung" loading="lazy">
<a class="webmention-author-name" href="https://taxodium.ink" target="_blank" rel="noopener ugc">Spike Leung</a>
<span class="webmention-date">2025/9/25</span>
</div>
<div class="webmention-content">
… blog 的社群迴響 | Jason Chen Webmention 实现参考 | Runye Now, I'm in IndieWeb? | Owen Young <a href="https://cytrogen.icu/posts/1de">在 Hexo 项目中添加 Webmention | Cytrogen</a> 脚注: 1 或者在社交媒体如 BlueSky、Mastodon 上发一个帖子,引用文章的链接。 2 你可以从 Webmention | IndieWeb 里找到很多工…
</div>
<div class="webmention-meta">
<a class="webmention-source" href="https://taxodium.ink/add-webmention-to-blog.html" target="_blank" rel="noopener ugc">查看原文</a>
</div>
</div></div></div></div><div class="copyright"><p class="footer-links"><a href="../friends/index.html">友链</a><span class="footer-separator"> ·</span><a href="../links/index.html">邻邦</a><span class="footer-separator"> ·</span><a href="../contact/index.html">联络</a><span class="footer-separator"> ·</span><a href="../colophon/index.html">营造记</a><span class="footer-separator"> ·</span><a href="../atom.xml">RSS订阅</a></p><p>© 2025 - 2026 <a href="https://cytrogen.icu">Cytrogen</a>, powered by <a href="https://hexo.io/" target="_blank">Hexo</a> and <a href="https://github.com/cytrogen/hexo-theme-ares" target="_blank">hexo-theme-ares</a>.</p><p><a href="https://blogscn.fun" target="_blank" rel="noopener">BLOGS·CN</a></p></div></footer></div></div><a class="back-to-top" href="#top" aria-label="返回顶部"><svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M3.293 9.707a1 1 0 010-1.414L9.586 2a2 2 0 012.828 0l6.293 6.293a1 1 0 01-1.414 1.414L11 3.414V17a1 1 0 11-2 0V3.414L2.707 9.707a1 1 0 01-1.414 0z"></path></svg></a><script>document.addEventListener('DOMContentLoaded', function() {
const codeBlocks = document.querySelectorAll('figure.highlight');
codeBlocks.forEach(block => {
let caption = block.querySelector('figcaption');
if (!caption) {
caption = document.createElement('figcaption');
block.insertBefore(caption, block.firstChild);
}
const info = document.createElement('div');
info.className = 'info';
const filename = caption.querySelector('span');
if (filename) {
filename.className = 'filename';
info.appendChild(filename);
}
const lang = block.className.split(' ')[1];
if (lang) {
const langSpan = document.createElement('span');
langSpan.className = 'lang-name';
langSpan.textContent = lang;
info.appendChild(langSpan);
}
const sourceLink = caption.querySelector('a');
if (sourceLink) {
sourceLink.className = 'source-link';
info.appendChild(sourceLink);
}
const actions = document.createElement('div');
actions.className = 'actions';
const codeHeight = block.scrollHeight;
const threshold = 300;
if (codeHeight > threshold) {
block.classList.add('folded');
const toggleBtn = document.createElement('button');
toggleBtn.textContent = '展开';
toggleBtn.addEventListener('click', () => {
block.classList.toggle('folded');
toggleBtn.textContent = block.classList.contains('folded') ? '展开' : '折叠';
});
actions.appendChild(toggleBtn);
}
const copyBtn = document.createElement('button');
copyBtn.textContent = '复制';
copyBtn.addEventListener('click', async () => {
const codeLines = block.querySelectorAll('.code .line');
const code = Array.from(codeLines)
.map(line => line.textContent)
.join('\n')
.replace(/\n\n/g, '\n');
try {
await navigator.clipboard.writeText(code);
copyBtn.textContent = '已复制';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = '复制';
copyBtn.classList.remove('copied');
}, 3000);
} catch (err) {
console.error('复制失败:', err);
copyBtn.textContent = '复制失败';
setTimeout(() => {
copyBtn.textContent = '复制';
}, 3000);
}
});
actions.appendChild(copyBtn);
caption.innerHTML = '';
caption.appendChild(info);
caption.appendChild(actions);
const markedLines = block.getAttribute('data-marked-lines');
if (markedLines) {
const lines = markedLines.split(',');
lines.forEach(range => {
if (range.includes('-')) {
const [start, end] = range.split('-').map(Number);
for (let i = start; i <= end; i++) {
const line = block.querySelector(`.line-${i}`);
if (line) line.classList.add('marked');
}
} else {
const line = block.querySelector(`.line-${range}`);
if (line) line.classList.add('marked');
}
});
}
});
});</script><script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script"></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.querySelector('.theme-toggle');
if (!themeToggle) return;
const getCurrentTheme = () => {
return document.documentElement.getAttribute('data-theme') || 'light';
};
const updateUI = (theme) => {
const isDark = theme === 'dark';
themeToggle.setAttribute('aria-pressed', isDark.toString());
};
const setTheme = (theme) => {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper) {
pageWrapper.setAttribute('data-theme', theme);
}
// Find and remove the temporary anti-flicker style tag if it exists.
// This ensures the main stylesheet takes full control after the initial load.
const antiFlickerStyle = document.getElementById('anti-flicker-style');
if (antiFlickerStyle) {
antiFlickerStyle.remove();
}
localStorage.setItem('theme', theme);
updateUI(theme);
};
const toggleTheme = () => {
const current = getCurrentTheme();
const newTheme = current === 'light' ? 'dark' : 'light';
setTheme(newTheme);
};
updateUI(getCurrentTheme());
themeToggle.addEventListener('click', toggleTheme);
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', function(e) {
if (!localStorage.getItem('theme')) {
const theme = e.matches ? 'dark' : 'light';
setTheme(theme);
}
});
}
});
})();
</script><script src="../js/details-toggle.js" defer></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const backToTopBtn = document.querySelector('.back-to-top');
if (!backToTopBtn) return;
const toggleButtonVisibility = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const shouldShow = scrollTop > 200;
if (shouldShow) {
backToTopBtn.classList.add('is-visible');
} else {
backToTopBtn.classList.remove('is-visible');
}
};
let ticking = false;
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
toggleButtonVisibility();
ticking = false;
});
ticking = true;
}
};
const scrollToTop = (event) => {
event.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
window.addEventListener('scroll', handleScroll);
backToTopBtn.addEventListener('click', scrollToTop);
toggleButtonVisibility();
});
})();</script></body></html>