feat: AI Directory Platform 底座升级 + 五套主题站点

- 数据模型: vote/review/submission/claim/use_case/audience/collection/event (+tool 冗余统计列)
- 社区: 投票(限流+原子toggle+API) / 评论(评分+优缺点+Helpful+聚合回写) / 提交(重复检测+审批发布) / 认领(Owner白名单编辑)
- Ranking Engine: trending/popular/most-voted/most-saved/highest-rated/new/editors-choice (纯函数可测)
- 发现体系: Use Case + Audience + Collection 三维发现 / /ai-tools-for / /best SEO 页
- Programmatic SEO: 页面生成器 / sitemap 270条 / AggregateRating/FAQPage/Breadcrumb/ItemList schema / alternatives + vs 对比页
- Analytics: event 事件流 + 外链跳转埋点 + /admin/analytics CTR 与搜索词分析
- Admin: submissions/claims/reviews/discovery/vote-audit/analytics 六个新后台 + 仪表盘扩展
- 主题系统: themes registry + 五套站点 (classic/product/editorial/niche/minimal) 构建期注入
- 认证: GitHub OAuth (env门控) + 登录回跳 redirect
- i18n: 新增 7 个命名空间中英文案 (546 键一致)
- 测试: bun test 32 用例 (ranking/slug/去重/SEO生成器) + Playwright e2e 脚本
- 种子: 14分类/16用例/9受众/40工具/6用户/投票/评论/6合集/7718事件
This commit is contained in:
AI Directory Platform
2026-09-08 17:38:06 +00:00
parent 41d9fcac06
commit e46b4b52a3
90 changed files with 13642 additions and 7940 deletions
+82
View File
@@ -0,0 +1,82 @@
import { describe, test, expect } from "bun:test";
import { generateSeoPage, generateToolFaqs } from "../src/lib/seo/page-generator";
describe("generateSeoPage", () => {
const base = {
name: "Blogging",
kind: "use-case" as const,
brandName: "AI Stack",
toolCount: 12,
freeCount: 4,
topToolNames: ["ChatGPT", "Claude", "Grammarly"],
year: 2026,
};
test("生成 title / h1 / intro / faqs", () => {
const page = generateSeoPage(base);
expect(page.title).toContain("Best AI Tools for Blogging in 2026");
expect(page.h1).toBe("Best AI Tools for Blogging");
expect(page.intro).toContain("12");
expect(page.faqs.length).toBeGreaterThanOrEqual(3);
});
test("description 含工具数与排序说明", () => {
const page = generateSeoPage(base);
expect(page.description).toContain("12");
expect(page.description).toContain("votes");
});
test("FAQ 具体化:引用真实工具名", () => {
const page = generateSeoPage(base);
const firstAnswer = page.faqs[0].answer;
expect(firstAnswer).toContain("ChatGPT");
});
test("free 数量反映到 FAQ", () => {
const page = generateSeoPage(base);
const freeFaq = page.faqs.find((f) => f.question.startsWith("Are there free"));
expect(freeFaq?.answer).toContain("4");
});
test("use case + audience 生成交叉引用 FAQ", () => {
const page = generateSeoPage({ ...base, audienceName: "Marketers" });
const crossFaq = page.faqs.find((f) => f.question.toLowerCase().includes("marketer"));
expect(crossFaq).toBeDefined();
});
test("零数据实体也能安全生成(无 NaN/undefined)", () => {
const page = generateSeoPage({
name: "Research",
kind: "audience",
brandName: "X",
toolCount: 0,
});
expect(page.intro).not.toContain("undefined");
expect(page.title).not.toContain("NaN");
});
});
describe("generateToolFaqs", () => {
test("由定价生成免费问答", () => {
const faqs = generateToolFaqs({ name: "ChatGPT", pricing: "freemium" });
const freeFaq = faqs.find((f) => f.question.includes("free"));
expect(freeFaq?.answer).toContain("freemium");
});
test("含替代品问答", () => {
const faqs = generateToolFaqs(
{ name: "Cursor", pricing: "freemium" },
["Windsurf", "Copilot"]
);
const altFaq = faqs.find((f) => f.question.includes("alternatives"));
expect(altFaq?.answer).toContain("Windsurf");
});
test("描述自动补句号", () => {
const faqs = generateToolFaqs({
name: "X",
description: "It does things",
});
expect(faqs[0].answer).toBe("It does things.");
});
});