为静态站点配置 Telegram Instant View:从页面标记到部署等待

Henri · 2026-03-14

Telegram 的 Instant View(IV)可以让用户在 Telegram 客户端里直接阅读网页正文,不用跳转浏览器。对内容站来说,文章链接带上 INSTANT VIEW 按钮,阅读体验会好不少。

但要让 IV 稳定工作,特别是对部署在 GitHub Pages 上的静态站点来说,需要把页面标记、IV 规则、Bot 消息和部署时序几个环节串起来。这篇文章按操作顺序整理了关键步骤。

1. 页面端:加语义标记

IV 模板需要从页面 HTML 中提取标题、正文、作者、发布时间等信息。最直觉的做法是靠 CSS 类名来定位——但这非常脆弱。你用 Tailwind 生成的 text-xlmt-16 之类的类名随时可能因为样式调整而变化,IV 规则会悄悄失效,而且没有任何报错。

更稳妥的做法是在页面模板中加一组专用的 data-iv-* 属性:

<article data-iv-page="post">
  <header data-iv-header="true">
    <h1 data-iv-title="true">{post.title}</h1>
    {post.subtitle && (
      <p data-iv-subtitle="true">{post.subtitle}</p>
    )}
    <div>
      <time dateTime={post.date} data-iv-published={String(publishedUnix)}>
        {publishedLabel}
      </time>
      <span data-iv-author="true">{post.author}</span>
    </div>
  </header>

  <div data-iv-body="true"
       dangerouslySetInnerHTML={{ __html: post.contentHtml }}
  />

  {/* 不希望 IV 抓取的区域 */}
  <nav data-iv-ignore="true">...</nav>
</article>

几个要点:

  • data-iv-titledata-iv-bodydata-iv-author 分别标记标题、正文和作者
  • data-iv-published 放在 <time> 元素上,值为 Unix 时间戳(IV 引擎可以直接解析)
  • data-iv-ignore 标记导航栏、侧边栏、评论区等不应出现在阅读视图中的元素

同时检查一下页面的 <link rel="canonical"> 是否包含了正确的部署子路径。如果你的站点部署在 /alpacanotes 下,canonical URL 也要带上这个前缀。这个问题浏览器不在乎,但 Telegram 在抓取和缓存时会参考 canonical。

2. IV 规则:编写与同域名兼容

Telegram Instant View Editor 编写规则。规则用 XPath 语法告诉 IV 引擎从页面中提取哪些内容。

如果你的域名下只有一套页面结构,规则很简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
~version: "2.1"

body:           //*[@data-iv-body]
title:          //*[@data-iv-title]
subtitle:       //*[@data-iv-subtitle]
author:         //*[@data-iv-author]
published_date: //*[@data-iv-published]/@data-iv-published

@split_parent:  //p/img
@remove:        //*[@data-iv-ignore]

同域名多模板的情况

如果同一个域名下还跑着其他站点(比如主站的文档页),HTML 结构可能完全不同。IV 规则是按域名绑定的,一个域名只有一份规则。这时需要用 XPath 的 |(或)运算符做双路匹配:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
~version: "2.1"

body: //*[@data-iv-body] | //article[contains(@class, "docs-content")]//div[contains(@class, "md-theme-amp")]
title: //*[@data-iv-title] | //article[contains(@class, "docs-content")]//div[contains(@class, "md-theme-amp")]//h1[1]
subtitle: //*[@data-iv-subtitle] | //article[contains(@class, "docs-content")]//div[contains(@class, "md-theme-amp")]//h1[1]/following-sibling::p[1]
author: //*[@data-iv-author] | //article[contains(@class, "docs-content")]//div[contains(@class, "text-muted") and contains(@class, "text-sm")][1]/span[1]
published_date: //*[@data-iv-published]/@data-iv-published | //article[contains(@class, "docs-content")]//div[contains(@class, "text-muted") and contains(@class, "text-sm")][1]/span[last()]

@split_parent: //p/img
@remove: //*[@data-iv-ignore] | //article[contains(@class, "docs-content")]//div[contains(@class, "mb-4")]

@set_attr(width, "auto"): //img

规则会从左到右尝试匹配,第一个命中的生效。把 data-iv-* 放在前面,回退规则放在 | 后面。

写好之后在编辑器里用几个不同页面测试,确认都能正确提取,然后点 Mark as Published,会得到一个 rhash。把它存到 GitHub Secrets 的 TELEGRAM_IV_RHASH 里。

3. Bot 消息构造

有了 rhash,就可以构造 IV 链接了:

https://t.me/iv?url=<encoded_article_url>&rhash=<your_rhash>

通过 Bot API 发消息时,可以用 link_preview_options 参数指定预览 URL:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const ivUrl = `https://t.me/iv?url=${encodeURIComponent(articleUrl)}&rhash=${rhash}`;

const body = {
  chat_id: channelId,
  text: messageText,
  parse_mode: 'HTML',
  link_preview_options: {
    url: ivUrl,
    prefer_large_media: true,
    show_above_text: true
  }
};

await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(body)
});

注意事项

link_preview_options 的行为和用户手动粘贴链接不完全一样。几个实际经验:

  • 消息文本中最好也包含原始文章链接(作为阅读兜底),不要只依赖 IV 链接
  • 同一条消息里不要塞太多链接,否则 Telegram 可能会选错一条来生成预览
  • 新发布的链接效果最好;旧链接可能因为 Telegram 缓存而无法及时更新 IV 状态

4. GitHub Pages 部署:两个容易踩的坑

4.1 .nojekyll 文件

如果你用 Next.js(或任何会生成 _next 目录的框架)部署到 GitHub Pages,必须在发布根目录放一个空的 .nojekyll 文件。

原因:GitHub Pages 默认启用 Jekyll 处理,而 Jekyll 会忽略所有下划线开头的目录。_next 里放着你全部的 CSS 和 JS 资源——被忽略之后,页面就变成光秃秃的纯文本,CSS/JS 全 404。

如果你用手写 shell 脚本部署,不要忘了这一步:

1
2
cp -R ./out/. "${deploy_dir}/"
touch "${deploy_dir}/.nojekyll"   # 关键

这个问题的症状很容易被误判为前端代码错误,因为 HTML 本身是正常的,只是样式全丢了。

4.2 发布时序:先确保页面在线,再发通知

这是整个流程中最重要的一条。

代码推送到 gh-pages 分支之后,GitHub Pages 的 CDN 并不是立刻就能返回新内容。可能要几十秒,甚至一两分钟。如果 Bot 在部署步骤完成后立刻发通知,Telegram 去抓取页面时拿到的可能是旧版本、404 或者不完整的页面。IV 引擎匹配失败,卡片不会挂上 INSTANT VIEW 按钮。更糟的是 Telegram 会缓存这个失败的结果。

解决方案是在部署和通知之间加一个等待步骤,轮询目标 URL 直到确认新页面已经可以正常访问:

 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
async function waitForTarget(target) {
  const MAX_ATTEMPTS = 18;  // 最多 18 次
  const DELAY_MS = 10000;   // 每次间隔 10 秒

  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
    // 加随机参数绕过 CDN 缓存
    const probeUrl = `${target.url}?deploy_check=${Date.now()}`;

    try {
      const response = await fetch(probeUrl, {
        headers: { 'cache-control': 'no-cache' }
      });
      const html = await response.text();

      // 检查 HTTP 200 且页面包含预期内容
      const ready = response.ok && (
        html.includes('data-iv-body') ||
        html.includes(target.expectedTitle)
      );

      if (ready) return;
    } catch (error) {
      // 网络错误,继续重试
    }

    await new Promise(r => setTimeout(r, DELAY_MS));
  }

  throw new Error(`Timed out waiting for: ${target.url}`);
}

核心思路:

  • 查询参数 deploy_check=${Date.now()} 让每次请求的 URL 都不同,避免 CDN 返回缓存
  • 检查标准不只是 HTTP 200,还要确认页面内容包含 data-iv-body 或文章标题——防止 CDN 返回旧版页面
  • 最多等 3 分钟(18 次 × 10 秒),超时则报错中断部署流程

在 GitHub Actions 的 deploy workflow 中,把这个等待步骤放在部署和通知之间:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
- name: Deploy to GitHub Pages
  run: # ...部署脚本...

- name: Wait for deployment propagation
  run: node scripts/wait-for-publication.js

- name: Cool down before notify
  run: sleep 20

- name: Notify Telegram
  run: node scripts/telegram-notify.js

sleep 20 是轮询通过之后的额外冷却时间。CDN 传播不是瞬时完成的,多等 20 秒可以让更多边缘节点同步完毕。

5. Telegram 客户端缓存

即使以上全部配置正确,你可能仍然会遇到一个现象:同一条 IV 链接第一次打开正常,再次打开偶尔白屏。

这是 Telegram 客户端自身的缓存/渲染行为,不在站点侧能控制的范围内。判断方法很简单:如果第一次能打开,说明规则和页面都没问题;如果是重复打开才出问题,那就是客户端侧的事。

务实的做法是:消息里始终保留原始文章链接作为兜底,验证 IV 效果时用新链接而不是反复测试同一条旧消息。

总结一下关键步骤

  1. 页面模板加 data-iv-* 语义标记
  2. 在 IV 编辑器中编写基于这些标记的规则,发布拿到 rhash
  3. Bot 消息用 link_preview_options 挂 IV 链接,同时保留原始链接
  4. 部署目录放 .nojekyll
  5. 部署后轮询确认页面在线,再发通知——这一条最容易漏,也最关键

如果你想看我实际排查这些问题时的弯路和心路历程,可以读读 《Instant View 踩坑记:折腾了一整天,答案是「等一下」》