~cytrogen/blog-public

ref: 88eebf3dfdd8ab819fa1a84e1976a8a75d5af2b6 blog-public/posts/1de.html -rw-r--r-- 89.5 KiB
88eebf3dCytrogen Deploy 2026-02-19 08:34:27 4 days ago
                                                                                
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
<!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 插件 &amp; 主题开发者!如果你不是、又或者说只是想要即拿即用的方案,见 <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">&lt;<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> /&gt;</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 =&gt; </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>&lt;link&gt;</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">&lt;link rel="webmention" href="https://webmention.io/你的域名/webmention" /&gt;</span><br></pre></td></tr></tbody></table></figure>
<p>再将其添加网站的 <code>&lt;head&gt;</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 &amp;&amp; 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=令牌&amp;limit=限制发送多少条&amp;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">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"h-entry"</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span>&gt;</span>世界你好~<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</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">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"h-entry"</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span> <span class="attr">class</span>=<span class="string">"p-name"</span>&gt;</span>我是标题<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span>&gt;</span>世界你好~<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</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">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"h-entry"</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span> <span class="attr">class</span>=<span class="string">"p-name"</span>&gt;</span>我是标题<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span> <span class="attr">class</span>=<span class="string">"p-summary"</span>&gt;</span>我是摘要<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span>&gt;</span>世界你好~<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</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">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"h-entry"</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span> <span class="attr">class</span>=<span class="string">"p-name"</span>&gt;</span>我是标题<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span> <span class="attr">class</span>=<span class="string">"p-summary"</span>&gt;</span>我是摘要<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span> <span class="attr">class</span>=<span class="string">"e-content"</span>&gt;</span>世界你好~<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p><code>dt-published</code>:发布时间(建议使用 <code>&lt;time datetime&gt;</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>&lt;article&gt;</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 &amp;&amp; (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">'&lt;div class="reply-error"&gt;回复标签缺少目标URL&lt;/div&gt;'</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">'&lt;div class="reply-error"&gt;回复标签包含无效URL&lt;/div&gt;'</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">  &lt;div class="reply-block h-entry"&gt;</span></span><br><span class="line"><span class="string">    &lt;!-- 作者信息(h-card) --&gt;</span></span><br><span class="line"><span class="string">    &lt;div class="post-meta p-author h-card" style="display: none"&gt;</span></span><br><span class="line"><span class="string">      <span class="subst">${authorAvatar ? <span class="string">`&lt;img class="author-avatar u-photo" src="<span class="subst">${authorAvatar}</span>" alt="<span class="subst">${authorName}</span>"&gt;`</span> : <span class="string">''</span>}</span></span></span><br><span class="line"><span class="string">      &lt;a class="author-name p-name u-url" href="<span class="subst">${siteUrl}</span>"&gt;<span class="subst">${authorName}</span>&lt;/a&gt;</span></span><br><span class="line"><span class="string">    &lt;/div&gt;</span></span><br><span class="line"><span class="string">    </span></span><br><span class="line"><span class="string">    &lt;!-- 回复内容 --&gt;</span></span><br><span class="line"><span class="string">    &lt;div class="reply-content e-content"&gt;</span></span><br><span class="line"><span class="string">      <span class="subst">${renderedContent}</span></span></span><br><span class="line"><span class="string">    &lt;/div&gt;</span></span><br><span class="line"><span class="string">    </span></span><br><span class="line"><span class="string">    &lt;!-- 回复元信息 - 通过 CSS 在列表页隐藏 --&gt;</span></span><br><span class="line"><span class="string">    &lt;div class="reply-meta"&gt;</span></span><br><span class="line"><span class="string">      &lt;span class="reply-label"&gt;<span class="subst">${replyText}</span>:&lt;/span&gt;</span></span><br><span class="line"><span class="string">      &lt;a class="reply-target u-in-reply-to" href="<span class="subst">${absoluteUrl}</span>"&gt;<span class="subst">${absoluteUrl}</span>&lt;/a&gt;</span></span><br><span class="line"><span class="string">    &lt;/div&gt;</span></span><br><span class="line"><span class="string">  &lt;/div&gt;</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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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>) =&gt; {</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">/&lt;[^&gt;]*&gt;/g</span>, <span class="string">''</span>);</span><br><span class="line">    <span class="keyword">if</span> (textContent.<span class="property">length</span> &lt;= 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">'&lt;'</span>) ? <span class="string">`&lt;p&gt;<span class="subst">${truncatedText}</span>&lt;/p&gt;`</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">'&lt;div class="webmention-section webmention-empty"&gt;&lt;span&gt;无法获取页面URL&lt;/span&gt;&lt;/div&gt;'</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> =&gt;</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">`&lt;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>"&gt;</span></span><br><span class="line"><span class="string">                &lt;h3 class="webmention-title"&gt;Webmentions (&lt;span class="webmention-count"&gt;<span class="subst">${mentionsForThisPage.length}</span>&lt;/span&gt;)&lt;/h3&gt;</span></span><br><span class="line"><span class="string">                &lt;div class="webmention-list"&gt;`</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> =&gt;</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">`&lt;a class="webmention-author-name" href="<span class="subst">${mention.author.url}</span>" target="_blank" rel="noopener ugc"&gt;<span class="subst">${mention.author.name}</span>&lt;/a&gt;`</span></span><br><span class="line">      : <span class="string">`&lt;span class="webmention-author-name"&gt;<span class="subst">${mention.author.name}</span>&lt;/span&gt;`</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">`&lt;img class="webmention-author-photo" src="<span class="subst">${mention.author.photo}</span>" alt="<span class="subst">${mention.author.name}</span>" loading="lazy"&gt;`</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">      &lt;div class="webmention-item" id="webmention-<span class="subst">${mention.id}</span>" data-webmention-id="<span class="subst">${mention.id}</span>"&gt;</span></span><br><span class="line"><span class="string">        &lt;div class="webmention-author"&gt;</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">          &lt;span class="webmention-date"&gt;<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>&lt;/span&gt;</span></span><br><span class="line"><span class="string">        &lt;/div&gt;</span></span><br><span class="line"><span class="string">        &lt;div class="webmention-content"&gt;</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">        &lt;/div&gt;</span></span><br><span class="line"><span class="string">        &lt;div class="webmention-meta"&gt;</span></span><br><span class="line"><span class="string">          &lt;a class="webmention-source" href="<span class="subst">${mention.source}</span>" target="_blank" rel="noopener ugc"&gt;查看原文&lt;/a&gt;</span></span><br><span class="line"><span class="string">        &lt;/div&gt;</span></span><br><span class="line"><span class="string">      &lt;/div&gt;</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">'&lt;/div&gt;&lt;/div&gt;'</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 &amp;&amp; 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>