← 返回博客
架构2026-08-24 17:50:475 分钟 · 1,479 13

给博客装上最后几块拼图:路径式分页、匿名点赞、JSON-LD 与搜索收录闭环

博客第三轮迭代复盘:把 ?page=N 改成 /page/N 救回 ISR、设计 IP 去重的匿名点赞、CSS counter 实现代码行号、JSON-LD 富摘要,以及双域证书、裸域 301 归一化和加密异地备份的完整收录基建。

上一篇写了第二轮改造的坑(ISR 的 DYNAMIC_SERVER_USAGE 与自托管统计三连坑),这一轮继续:功能补全 + 收录基建,把博客的最后几块拼图装完。老规矩,只记踩过的坑和最终方案。

一、路径式分页:把 ISR 从 searchParams 手里救回来

上篇说到分类/标签页因为 await searchParams 在 ISR 缓存填充上下文里炸出 500,当时用 force-dynamic 止的血——页面级缓存没了。这轮做根治:

/blog?page=2        →   /blog/page/2
/category/32?page=2 →   /category/32/page/2

每个模块拆成两个路由文件复用一个视图组件,第 1 页恢复 revalidate = 600 的完整 ISR,第 N 页独立 URL 同样可缓存。改造后 build 日志里 /blog 回到了 ○ Static。

这个方案的额外收益是 SEO:每页一个干净的规范 URL,搜索引擎不用处理 query 参数去重。Pagination 组件只需要改一行链接生成逻辑。

一条通用原则:凡是想按请求变化的维度(页码、筛选),优先编码进路径而不是 query——除非你明确需要它不被缓存(比如搜索结果页,那就大方地 force-dynamic)。

二、匿名点赞:IP 去重就够了

个人博客加登录体系纯属自找麻烦,但「无门槛互动」对作者的正反馈是真实的。设计很简单:

CREATE TABLE article_likes (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  article_id INT UNSIGNED NOT NULL,
  client_ip VARCHAR(45) NOT NULL,
  create_time INT NOT NULL,
  UNIQUE KEY uk_article_ip (article_id, client_ip)
);
ALTER TABLE document ADD COLUMN like_count INT UNSIGNED NOT NULL DEFAULT 0;

核心技巧在写入侧:

res := db.Exec(
    "INSERT IGNORE INTO article_likes (article_id, client_ip, create_time) VALUES (?,?,?)",
    req.ID, c.ClientIP(), time.Now().Unix(),
)
if res.RowsAffected > 0 {
    db.Exec("UPDATE document SET like_count = like_count + 1 WHERE id = ?", req.ID)
}

INSERT IGNORE 撞唯一键时 RowsAffected 为 0——天然幂等,不需要先查再插的两步竞态。前端按钮拿到返回的 liked 状态做视觉锁定,localStorage 都不用。

局限也直说:同一 Wi-Fi 下多人共享出口 IP 只能赞一次;手机流量切换会重复计数。对个人博客这个精度完全够。

三、代码块行号:一行 CSS counter 的事

shiki 输出的高亮 HTML 里每行天然是 <span class="line">…</span>,所以根本不需要在 React 层手动拆行插序号,纯 CSS 搞定:

.shiki-pre code { counter-reset: line-no; }
.shiki-pre code .line::before {
  counter-increment: line-no;
  content: counter(line-no);
  display: inline-block;
  width: 1.6rem;
  margin-right: 1.1rem;
  text-align: right;
  user-select: none; /* 关键:复制时不带行号 */
}

user-select: none 保证访客点「复制」拿到的是干净代码。暗色模式换一档灰即可。

四、JSON-LD 结构化数据:白捡的搜索富摘要

Next.js 里给文章页塞一段 BlogPosting schema,十几行:

<script type="application/ld+json" dangerouslySetInnerHTML={{
  __html: JSON.stringify({
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    headline: article.title,
    datePublished: article.create_time,
    dateModified: article.update_time,
    author: { "@type": "Person", name: siteConfig.author },
    image: `${siteConfig.domain}/article/${id}/opengraph-image`,
    keywords: article.keyword,
  }),
}} />

配合已有的 OG 卡片和 sitemap,Google 富摘要需要的信号基本齐了。

五、搜索收录闭环:三个坑排一个雷

收录这件事的配置本身不难,难的是中间挖出一个隐藏很深的基础设施问题。

坑:裸域 HTTPS 证书不存在

GSC 报 sitemap 错误,排查时发现 https://wxbuluo.com(不带 www)TLS 握手直接失败——证书只签了 www.wxbuluo.com!也就是说裸域在 HTTPS 世界里从来就没通过,之前所有浏览器访问都是先走了 http 301 或者用户手输 www 才绕过去的。

修复用 certbot webroot 方式扩签双域名证书:

certbot certonly --webroot -w /acme-webroot \
  -d wxbuluo.com -d www.wxbuluo.com --expand

然后在 nginx 把裸域单独拆一个 server 块,只做一件事:

server {
    listen 443 ssl http2;
    server_name wxbuluo.com;
    ssl_certificate     /etc/letsencrypt/live/www.wxbuluo.com/fullchain.pem;
    location / { return 301 https://www.wxbuluo.com$request_uri; }
}

主域名归一化后,GSC 里只保留 www 一个资源、一份 sitemap,跨域报错自然消失。

坑中坑:shell 转义让 nginx reload 静默失败

改配置过程中踩了个值得记录的细节:通过 ssh 远程执行 heredoc 时,如果分隔符不加引号(<< PYEOF 而不是 << 'PYEOF'),远端 shell 会展开里面的 $request_uri——变成空串,nginx 配置直接语法错误。更糟的是我把 nginx -t 的输出重定向丢弃了,reload 失败毫无感知,排查了半天「为什么配置不生效」。

教训:远程执行务必 << 'EOF',且 nginx -t 的输出永远别丢。

百度验证:文件方式失败就换 meta 标签

百度的文件验证报「无法连接到服务器」,翻遍服务器日志发现它的抓取请求根本没到达过。没有纠结原因(可能是平台瞬时线路问题),直接切 HTML 标签验证——head 里加一行 <meta name="baidu-site-verification">,一次通过。收录通道不必恋战,能过就是赢。

六、异地备份:加密推到私有仓库

数据库备份原来就躺在服务器本地盘——机器挂了副本陪葬,这是拖了很久的风险。最终方案:每日打包(MySQL dump + Fathom 统计库 + nginx/compose 配置)→ AES-256 加密 → 推到私有 GitHub 仓库。

几个实现细节:

  • Fathom 是 SQLite,WAL 模式下直接 cp 可能不一致,用 Python 只读连接 iterdump() 导出 SQL 文本,顺带恢复了兼容性更好
  • 服务器 openssl 太老不支持 -pbkdf2,降级用默认 KDF + 24 位随机密码,个人数据够用
  • 解密密码绝不能只存在服务器上——否则服务器没了密码陪葬,加密备份等于自杀。生成后第一时间进密码管理器
  • git 天然保留全部历史,工作树只留近 30 天防膨胀

cron 全家桶现在是:03:30 本地 dump → 04:10 加密推 GitHub → 周一续证书 → 每天 09:00 向百度 API 轮转推送 10 条 URL(新站配额就这么多,20 天覆盖全站)。

收尾

三轮迭代下来:阅读体验、SEO 全链路、互动、统计、容灾都齐了。回头看最大的心得反而是克制——每一轮都想再加点什么,但真正提升体验的是把已有功能的坑填平(分页、收录、备份),而不是继续堆新玩具。

接下来该还债了:多写文章。

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/148