refactor: 主题系统 → monorepo 五个独立网站

- packages/core: @aidir/core 底座库(src/pages 页面组件体 1:1 镜像路由树 / kit 站点外壳工厂 / content/blog / scripts)
- sites/: 5 个独立 Next.js 网站(ai-stack / launchai / the-ai-ledger / devai-tools / findai)
  - 每站独立 package.json / next.config / tailwind / messages / .env / 构建
  - src/app 约 56 个薄路由文件(re-export 底座页面 + 内联路由段配置 revalidate/dynamic)
  - src/site/brand.tsx 站点配置 + src/site/home.tsx 站点首页(createMarketingLayout / createHomePage 工厂)
- generate-site.mjs: 站点生成器(新增网站 = SITES 加条目,零 Core 修改)
- start-all-sites.sh: 一键启动 3001-3005
- next-config.mjs: 底座统一 Next 配置工厂(next-intl + MDX + standalone)
- 五站共享同一套 PostgreSQL / Better Auth / Admin(设计文档 §42)
- 验证: 5 站 build OK / typecheck OK / 55 路由冒烟 200 / 品牌-内容-截图 差异确认 / 登录-投票-重复检测 e2e PASS
This commit is contained in:
AI Directory Platform
2026-09-08 19:19:33 +00:00
parent e46b4b52a3
commit 4a1a034386
699 changed files with 19829 additions and 2029 deletions
+61
View File
@@ -0,0 +1,61 @@
/**
* # 脚本 screenshot-pages
* 说明:用 Playwright 对五个主题站点与关键页面截图 + 控制台错误采集。
* 用于交付前的视觉验收。
*/
import { chromium } from "playwright";
import { mkdirSync } from "node:fs";
const OUT = "/tmp/aidir-shots";
mkdirSync(OUT, { recursive: true });
const targets = [
{ name: "classic-home", url: "http://localhost:3001/" },
{ name: "classic-rankings", url: "http://localhost:3001/rankings" },
{ name: "classic-tool-detail", url: "http://localhost:3001/tools/claude" },
{ name: "classic-tools-list", url: "http://localhost:3001/tools" },
{ name: "classic-best", url: "http://localhost:3001/best/best-ai-writing-tools" },
{ name: "classic-usecase", url: "http://localhost:3001/ai-tools-for/blogging" },
{ name: "classic-compare", url: "http://localhost:3001/tools/cursor-vs-windsurf" },
{ name: "classic-alt", url: "http://localhost:3001/tools/midjourney-alternatives" },
{ name: "classic-zh", url: "http://localhost:3001/zh" },
{ name: "classic-submit", url: "http://localhost:3001/submit" },
{ name: "product-home", url: "http://localhost:3002/" },
{ name: "editorial-home", url: "http://localhost:3003/" },
{ name: "niche-home", url: "http://localhost:3004/" },
{ name: "minimal-home", url: "http://localhost:3005/" },
{ name: "minimal-mobile", url: "http://localhost:3005/", mobile: true },
{ name: "product-mobile", url: "http://localhost:3002/", mobile: true },
];
const browser = await chromium.launch();
const errors: string[] = [];
for (const target of targets) {
const context = await browser.newContext(
target.mobile
? { viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true }
: { viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 }
);
const page = await context.newPage();
page.on("console", (msg) => {
if (msg.type() === "error") errors.push(`${target.name}: ${msg.text().slice(0, 200)}`);
});
page.on("pageerror", (err) => errors.push(`${target.name}: PAGEERROR ${String(err).slice(0, 200)}`));
try {
await page.goto(target.url, { waitUntil: "domcontentloaded", timeout: 30000 });
await page.waitForTimeout(1200);
await page.screenshot({ path: `${OUT}/${target.name}.png`, fullPage: false });
// 顺便记录 H1
const h1 = await page.locator("h1").first().textContent().catch(() => "(no h1)");
console.log(`OK ${target.name} | h1: ${h1?.trim().slice(0, 60)}`);
} catch (error) {
console.log(`FAIL ${target.name}: ${String(error).slice(0, 120)}`);
}
await context.close();
}
await browser.close();
console.log("\n=== 控制台错误 ===");
console.log(errors.length === 0 ? "(none)" : errors.slice(0, 12).join("\n"));