v0.10.5 首页 H2 产品线入口 ×6:ImageBoxWidget 重构为 Elementor 忠实 DOM + slim 白名单补齐 + H2 scoped CSS(post-18 对齐,桌面/移动/hover 实测一致)

This commit is contained in:
BigCode
2026-11-01 00:15:29 +08:00
commit 5b83e71ad9
128 changed files with 20008 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
/**
* # 脚本 文章分类归属修复(一次性,幂等,需外网可达 www.dusuniot.com)
* 背景:WP 导入只带文章不带 category 归属,news/case-study/gateway/technical-brief
* 四个文章分类在本库文章数均为 0,归档页是空的。
* 做法:抓取原站这四个分类的归档页(含 /page/N/ 翻页),提取 /<cat>/<slug>/ 文章链接,
* 与本地已发布文章按 slug 匹配,缺失的 content_terms 归属补插。
* 不在本地库的原站文章只计数不插入(避免归档里再出死链)。
* 重复执行安全:插入前先查存在。
*/
import { SQL } from "bun";
const sql = new SQL(process.env.DATABASE_URL || "postgres://dusuniot:dusuniot@127.0.0.1:25432/dusuniot");
/** # 成员变量 抓取 UA(原站对无 UA 的 curl 返回空体) **/
const UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36";
/** # 类型 归档抓取目标 **/
const TARGETS = [
{ catId: 98, catSlug: "case-study", start: "https://www.dusuniot.com/case-study/success-stories/" }, // 原站 /case-study/ 301 到 success-stories
{ catId: 14, catSlug: "news", start: "https://www.dusuniot.com/news/" },
{ catId: 119, catSlug: "gateway", start: "https://www.dusuniot.com/gateway/", alsoBlogH3: true }, // 该分类文章 permalink 是 /blog/<slug>/(主分类 blog),卡片在 h3>a
{ catId: 120, catSlug: "technical-brief", start: "https://www.dusuniot.com/resources/technical-brief/" },
];
/** # 函数 抓一个页面 HTML(失败返回空串) */
async function get(url: string): Promise<string> {
try {
const res = await fetch(url, { headers: { "user-agent": UA } });
if (res.status !== 200) return "";
return await res.text();
} catch {
return "";
}
}
/** # 函数 从归档 HTML 提取该分类下的文章 slug(去重、排除分页页码/feed 与归档自身)
* 分类路径允许一段可选前缀(如 /resources/technical-brief/<slug>/、/blog/gateway/<slug>/)
*/
function extractSlugs(html: string, catSlug: string): string[] {
const re = new RegExp(`https://www\\.dusuniot\\.com/(?:[a-z0-9-]+/)?${catSlug}/([a-z0-9-]+)/`, "gi");
const out = new Set<string>();
let m: RegExpExecArray | null;
while ((m = re.exec(html))) {
const slug = m[1]!;
if (slug === "page" || slug === "feed" || slug === "amp" || /^\d+$/.test(slug)) continue;
out.add(slug);
}
return [...out];
}
/** # 函数 从归档 HTML 提取 h3 文章卡片的 /blog/<slug>/ 链接(gateway 归档专用:文章 permalink 无分类段) */
function extractBlogH3Slugs(html: string): string[] {
const re = /<h3[^>]*>\s*<a[^>]*href="https:\/\/www\.dusuniot\.com\/blog\/([a-z0-9-]+)\/"/gi;
const out = new Set<string>();
let m: RegExpExecArray | null;
while ((m = re.exec(html))) out.add(m[1]!);
return [...out];
}
for (const t of TARGETS) {
// 首页 + 从首页发现的 /page/N/ 翻页(上限 40 页防失控)
let html = await get(t.start);
const pages = new Set<string>();
for (let i = 2; i <= 40; i++) {
const u = t.start.replace(/\/$/, "") + `/page/${i}/`;
// 只有首页 HTML 里真实出现翻页链接才抓
if (html.includes(`/${t.catSlug}/page/${i}/`) || html.includes(`/page/${i}/`)) pages.add(u);
}
const slugs = new Set<string>(extractSlugs(html, t.catSlug));
if ("alsoBlogH3" in t && t.alsoBlogH3) for (const s of extractBlogH3Slugs(html)) slugs.add(s);
for (const u of pages) {
html = await get(u);
for (const s of extractSlugs(html, t.catSlug)) slugs.add(s);
if ("alsoBlogH3" in t && t.alsoBlogH3) for (const s of extractBlogH3Slugs(html)) slugs.add(s);
await Bun.sleep(300);
}
// 与本地已发布文章匹配并补归属
let inserted = 0;
let missing = 0;
for (const slug of slugs) {
const [local] = await sql`SELECT id FROM contents WHERE slug = ${slug} AND type = 'post' AND status = 'published' LIMIT 1`;
if (!local) {
missing++;
continue;
}
const [dup] = await sql`SELECT 1 FROM content_terms WHERE content_id = ${local.id} AND category_id = ${t.catId}`;
if (dup) continue;
await sql`INSERT INTO content_terms (content_id, category_id) VALUES (${local.id}, ${t.catId})`;
inserted++;
}
console.log(`[${t.catSlug}] 原站文章 ${slugs.size}(翻页 ${pages.size + 1} 页);本地命中并补归属 ${inserted},本地缺失 ${missing}`);
}
console.log("完成");
process.exit(0);