v0.10.36: 文章分类成员同步(import-live-terms,+16/-308 对齐原站 REST)+ /blog/ 越界分页真 404(对齐原站)+ docs 数据滞后记录核实更新

This commit is contained in:
bigcode
2026-11-04 00:44:14 +08:00
parent 57cf5956e8
commit 022a3cd76d
4 changed files with 74 additions and 5 deletions
+5 -2
View File
@@ -124,8 +124,11 @@
侧栏 = 社交图标 + CONTACT US 表单 + IoT Gateways 折叠组;正文 Elementor 直出 +
pageCss + 图片 WP 尺寸变体解析(medium→-300x272 类文件,服务端目录扫描缓存)。
- page_css 导入扩展至全部文章:254 篇有 post-<id>.css 已导入,355 篇原站 404(无自定义 CSS)。
- 已知数据差异:/blog/ 本地 category-blog 302 篇(26 页)vs 原站当前 7 页(dump 后原站
调整了分类成员);文章内文逐节高度仍有数据快照/lazy 假象差。
- 已知数据差异:/blog/ ~~本地 302 篇 vs 原站 7 页~~ ✅ v0.10.36 核实:原站分类成员实为
318 篇(REST categories=53 = 本地同步后一致),"7 页"系取证只抓到分页窗口尾页;
两侧均为 27 页(page/28 原站 404 → 本站分页路由越界已改真 404);
文章内文逐节高度仍有数据快照/lazy 假象差。分类成员同步脚本 `scripts/import-live-terms.ts`
(幂等,dry 支持)。
- P1 产品详情 `/product/$slug` v0.10.27 ✅ 产品单模板一比一(elementor_library 2741,从
legacy dump 补录入库 slug=product-template;内嵌模板 29972「IoT Gateways for
Recommendation」同步补录):hero 图廊 484×617/标题/meta(Model/Category)/短描述精确、
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "dusuniot-rebuild",
"version": "0.10.35",
"version": "0.10.36",
"private": true,
"type": "module",
"scripts": {
+62
View File
@@ -0,0 +1,62 @@
/**
* # 脚本 同步文章分类成员(dump 快照 → 原站当前状态)
*
* 背景:/blog/ 归档本地 302 篇(26 页)vs 原站当前 7 页——dump 之后原站调整了
* Blog/Gateway 等分类的成员。本脚本按原站 REST API 的 categories 重建本地
* content_terms 分类关联(仅调整 REST 返回的文章;本地有而原站已删除的不动)。
*
* 用法:bun scripts/import-live-terms.ts [--dry]
*/
import { SQL } from "bun";
const LIVE = "https://www.dusuniot.com";
const sql = new SQL(process.env.DATABASE_URL || "postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot", { max: 1 });
const dry = process.argv.includes("--dry");
/** 本地分类 legacy_id → id(WP term id 与 legacy_id 一致) */
const catRows = await sql`select id, legacy_id, slug from categories where taxonomy = 'category' and legacy_id is not null`;
const catByLegacy = new Map<number, number>(catRows.map((r: any) => [Number(r.legacy_id), r.id as number]));
/** 本地已发布文章 legacy_id → content id */
const posts = await sql`select id, legacy_id from contents where type = 'post' and status = 'published' and legacy_id is not null`;
const contentByLegacy = new Map<number, number>(posts.map((r: any) => [Number(r.legacy_id), r.id as number]));
console.log("本地已发布文章:", posts.length);
/** 逐页拉原站文章的分类(_fields 精简返回) */
const liveCats = new Map<number, number[]>(); // wp post id → term ids
let page = 1;
while (true) {
const res = await fetch(`${LIVE}/wp-json/wp/v2/posts?per_page=100&page=${page}&_fields=id,categories`, { headers: { "User-Agent": "Mozilla/5.0" } });
if (!res.ok) {
if (res.status === 400) break; // 翻页越界
console.log("REST", res.status, "停止");
break;
}
const items = (await res.json()) as { id: number; categories: number[] }[];
for (const it of items) liveCats.set(it.id, it.categories || []);
if (items.length < 100) break;
page++;
await new Promise((r) => setTimeout(r, 400)); // 限速(429 防护)
}
console.log("原站文章:", liveCats.size);
let added = 0, removed = 0;
for (const [wpId, cats] of liveCats) {
const cid = contentByLegacy.get(wpId);
if (!cid) continue;
const localRows = await sql`select category_id from content_terms where content_id = ${cid}`;
const localSet = new Set(localRows.map((r: any) => r.category_id as number));
const liveSet = new Set(cats.map((t) => catByLegacy.get(t)).filter((x): x is number => x != null));
for (const add of liveSet) {
if (localSet.has(add)) continue;
added++;
if (!dry) await sql`insert into content_terms (content_id, category_id) values (${cid}, ${add}) on conflict do nothing`;
}
for (const rm of localSet) {
if (liveSet.has(rm)) continue;
removed++;
if (!dry) await sql`delete from content_terms where content_id = ${cid} and category_id = ${rm}`;
}
}
console.log(`分类成员同步: 新增 ${added} 移除 ${removed}${dry ? "(dry 未写库)" : ""}`);
await sql.end();
+6 -2
View File
@@ -2,13 +2,17 @@
* # 路由 博客归档分页 /blog/page/$page/
* 与原站一致的第 2+ 页归档(每页 12 篇,日期倒序)。
*/
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, notFound } from "@tanstack/react-router";
import { BlogArchive, loadBlogArchive } from "~/components/website/BlogArchive";
export const Route = createFileRoute("/blog/page/$page")({
loader: async ({ params }) => {
const n = Number(params.page);
return loadBlogArchive(Number.isFinite(n) && n > 1 ? Math.floor(n) : 1);
const data = await loadBlogArchive(Number.isFinite(n) && n > 1 ? Math.floor(n) : 1);
// 越界页真 404(原站 /blog/page/28/ 404;软 404 影响 SEO)
if (!Number.isFinite(n) || n < 1 || (data.page > 1 && data.page > Math.ceil(data.list.total / data.list.pageSize)))
throw notFound();
return data;
},
head: ({ loaderData }) => ({
meta: [