/** * # 脚本 generate-site * 说明:从底座生成一个独立网站应用(sites//)。 * - src/app/**:全部为薄路由(re-export core/src/pages 的同名模块) * - (marketing)/layout + 首页:站点组合器(品牌 + 站点首页组件) * - messages/:core 基础文案 + 站点品牌 SEO 覆盖 * - 配置:独立 package.json / next.config / tsconfig / tailwind / postcss * * 用法:bun scripts/generate-site.mjs * 站点定义见本文件 SITES 常量。 */ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const CORE_ROOT = path.resolve(__dirname, ".."); const REPO_ROOT = path.resolve(CORE_ROOT, "..", ".."); const SITES_ROOT = path.join(REPO_ROOT, "sites"); // ==================================================================== // 站点定义:一个条目 = 一个独立网站(品牌 / 导航 / 页脚 / 首页 / 端口) // ==================================================================== const SITES = { "ai-stack": { themeId: "ai-stack", authVariant: "glass", themeClass: "site-ai-stack", brandZh: { name: "AI Stack", tagline: "由真实用户排名的专业 AI 工具目录。", headline: "找到最好的 AI 工具,由真实用户排名", subheadline: "14 个分类、1000+ AI 工具。对比投票、评论与定价,选出最适合你的。", nav: ["发现", "用例", "最佳清单", "排行榜", "博客", "提交工具"], footer: ["发现", "社区", "法律"], searchPlaceholder: "搜索 1000+ AI 工具...", }, accent: "222 47% 11%", // 近黑(专业) port: 3001, theme: "classic", brand: { name: "AI Stack", tagline: "The professional AI tools directory — ranked by the community.", headline: "Find the best AI tools, ranked by real users", subheadline: "1,000+ AI tools across 14 categories. Compare votes, reviews and pricing to pick the right one.", }, navItems: [ { label: "Discover", href: "/tools" }, { label: "Use Cases", href: "/ai-tools-for" }, { label: "Best Lists", href: "/best" }, { label: "Rankings", href: "/rankings" }, { label: "Blog", href: "/blog" }, { label: "Submit", href: "/submit" }, ], footerGroups: [ { title: "Discover", links: [ { label: "All Tools", href: "/tools" }, { label: "Use Cases", href: "/ai-tools-for" }, { label: "Best Lists", href: "/best" }, { label: "Rankings", href: "/rankings" }, ], }, { title: "Community", links: [ { label: "Submit a Tool", href: "/submit" }, { label: "Blog", href: "/blog" }, { label: "Dashboard", href: "/dashboard" }, ], }, { title: "Legal", links: [ { label: "Terms", href: "/terms" }, { label: "Privacy", href: "/privacy" }, { label: "Contact", href: "/contact" }, ], }, ], searchPlaceholder: "Search AI tools...", seo: { en: { title: "AI Stack — Find the Best AI Tools, Ranked by Real Users", description: "The professional AI tools directory. 1,000+ AI tools across 14 categories ranked by community votes, reviews and engagement.", }, zh: { title: "AI Stack — 由真实用户排名的 AI 工具目录", description: "专业 AI 工具导航:14 个分类、1000+ 工具,由社区投票、评论与互动数据实时排名。", }, }, }, launchai: { themeId: "launchai", authVariant: "gradient", themeClass: "site-launchai", brandZh: { name: "LaunchAI", tagline: "发现并投票今日的 AI 新品。", headline: "今日 AI 产品发布", subheadline: "每天上新。为最好的产品投票,冲上排行榜。", nav: ["今日", "排行榜", "分类", "合集", "博客", "提交产品"], footer: ["社区", "探索", "关于"], searchPlaceholder: "搜索 AI 产品...", }, accent: "16 85% 55%", // 橙红(产品社区) port: 3002, theme: "product", brand: { name: "LaunchAI", tagline: "Discover and upvote today's AI products.", headline: "Today's AI product launches", subheadline: "Fresh AI products, every day. Upvote the best ones to the top.", }, navItems: [ { label: "Today", href: "/" }, { label: "Leaderboard", href: "/rankings" }, { label: "Categories", href: "/tools" }, { label: "Collections", href: "/best" }, { label: "Blog", href: "/blog" }, { label: "Submit", href: "/submit" }, ], footerGroups: [ { title: "Community", links: [ { label: "Submit a Product", href: "/submit" }, { label: "Leaderboard", href: "/rankings" }, { label: "Dashboard", href: "/dashboard" }, ], }, { title: "Explore", links: [ { label: "All Products", href: "/tools" }, { label: "Use Cases", href: "/ai-tools-for" }, { label: "Collections", href: "/best" }, ], }, { title: "About", links: [ { label: "Blog", href: "/blog" }, { label: "Terms", href: "/terms" }, { label: "Privacy", href: "/privacy" }, ], }, ], searchPlaceholder: "Search AI products...", seo: { en: { title: "LaunchAI — Discover and Upvote Today's AI Products", description: "A daily launch feed for AI products. Upvote the best AI tools, follow makers and climb the leaderboard.", }, zh: { title: "LaunchAI — 发现并投票今日 AI 新品", description: "AI 产品每日发布社区:为最好的 AI 工具投票,登上社区排行榜。", }, }, }, "the-ai-ledger": { themeId: "the-ai-ledger", authVariant: "editorial", themeClass: "site-the-ai-ledger", brandZh: { name: "The AI Ledger", tagline: "独立 AI 报道,附社区排名目录。", headline: "AI 工具,评测与排名", subheadline: "指南、对比,以及由社区驱动的最佳 AI 工具目录。", nav: ["文章", "指南", "最佳推荐", "目录", "排行榜", "提交"], footer: ["阅读", "目录", "公司"], searchPlaceholder: "搜索目录...", }, accent: "36 70% 40%", // 纸墨棕(编辑部) port: 3003, theme: "editorial", brand: { name: "The AI Ledger", tagline: "Independent journalism on AI tools, with a community-ranked directory.", headline: "AI tools, reviewed and ranked", subheadline: "Guides, comparisons and a community-powered directory of the best AI tools.", }, navItems: [ { label: "Stories", href: "/blog" }, { label: "Guides", href: "/ai-tools-for" }, { label: "Best Picks", href: "/best" }, { label: "Directory", href: "/tools" }, { label: "Rankings", href: "/rankings" }, { label: "Submit", href: "/submit" }, ], footerGroups: [ { title: "Read", links: [ { label: "Latest Stories", href: "/blog" }, { label: "Buying Guides", href: "/best" }, { label: "Use Case Guides", href: "/ai-tools-for" }, ], }, { title: "Directory", links: [ { label: "All Tools", href: "/tools" }, { label: "Community Rankings", href: "/rankings" }, { label: "Submit a Tool", href: "/submit" }, ], }, { title: "Company", links: [ { label: "Contact", href: "/contact" }, { label: "Terms", href: "/terms" }, { label: "Privacy", href: "/privacy" }, ], }, ], searchPlaceholder: "Search the directory...", seo: { en: { title: "The AI Ledger — AI Tools, Reviewed and Ranked", description: "Independent journalism on AI: buying guides, comparisons and a community-ranked directory of the best AI tools.", }, zh: { title: "The AI Ledger — AI 工具评测与排名", description: "独立 AI 媒体:深度指南、横向对比,以及由社区排名的 AI 工具目录。", }, }, }, "devai-tools": { themeId: "devai-tools", authVariant: "terminal", themeClass: "site-devai-tools", brandZh: { name: "DevAI.tools", tagline: "面向开发者的 AI 工具——精选、实测、社区排名。", headline: "开发者的 AI 工具", subheadline: "代码助手、运维自动化、测试副驾驶,你上手的一切。", nav: ["工具", "工作流", "用例", "排行榜", "提交"], footer: ["目录", "构建", "法律"], searchPlaceholder: "搜索开发者工具...", }, accent: "150 60% 30%", // 终端绿(开发者) port: 3004, theme: "niche", brand: { name: "DevAI.tools", tagline: "AI tools for developers — curated, tested, ranked by the community.", headline: "AI tools for developers", subheadline: "Code assistants, DevOps automation, testing copilots and everything else you ship with.", }, navItems: [ { label: "Tools", href: "/tools" }, { label: "Workflows", href: "/best" }, { label: "Use Cases", href: "/ai-tools-for" }, { label: "Rankings", href: "/rankings" }, { label: "Submit", href: "/submit" }, ], footerGroups: [ { title: "Directory", links: [ { label: "All Tools", href: "/tools" }, { label: "Rankings", href: "/rankings" }, { label: "Use Cases", href: "/ai-tools-for" }, ], }, { title: "Build", links: [ { label: "Workflows", href: "/best" }, { label: "Submit a Tool", href: "/submit" }, { label: "Blog", href: "/blog" }, ], }, { title: "Legal", links: [ { label: "Terms", href: "/terms" }, { label: "Privacy", href: "/privacy" }, ], }, ], searchPlaceholder: "Search dev tools...", seo: { en: { title: "DevAI.tools — AI Tools for Developers", description: "Code assistants, DevOps automation and testing copilots — the AI tools developers actually upvote, ranked daily.", }, zh: { title: "DevAI.tools — 开发者 AI 工具", description: "面向开发者的 AI 工具目录:代码助手、运维自动化、测试副驾驶,由社区每日排名。", }, }, }, findai: { themeId: "findai", authVariant: "minimal", themeClass: "site-findai", brandZh: { name: "findai", tagline: "极简 AI 工具搜索引擎。", headline: "你想完成什么?", subheadline: "", nav: ["搜索", "用例", "排行榜", "提交"], footer: ["搜索", "关于"], searchPlaceholder: "你需要什么?", }, accent: "260 70% 52%", // 紫(极简搜索) port: 3005, theme: "minimal", brand: { name: "findai", tagline: "The minimal AI tool search engine.", headline: "What are you trying to do?", subheadline: "", }, navItems: [ { label: "Search", href: "/tools" }, { label: "Use Cases", href: "/ai-tools-for" }, { label: "Rankings", href: "/rankings" }, { label: "Submit", href: "/submit" }, ], footerGroups: [ { title: "Search", links: [ { label: "All Tools", href: "/tools" }, { label: "Use Cases", href: "/ai-tools-for" }, { label: "Rankings", href: "/rankings" }, ], }, { title: "About", links: [ { label: "Submit a Tool", href: "/submit" }, { label: "Privacy", href: "/privacy" }, { label: "Terms", href: "/terms" }, ], }, ], searchPlaceholder: "What do you need?", seo: { en: { title: "findai — The Minimal AI Tool Search Engine", description: "One search box, every AI tool. Describe what you're trying to do and find the right AI tool, ranked by the community.", }, zh: { title: "findai — 极简 AI 工具搜索引擎", description: "一个搜索框,找遍全部 AI 工具。说出你想做的事,社区排名替你选。", }, }, }, }; // ==================================================================== // 生成逻辑 // ==================================================================== /** 遍历 core/src/pages 下所有 page.tsx / route.ts,返回相对路由信息 */ function collectRoutes() { const pagesRoot = path.join(CORE_ROOT, "src", "pages"); const routes = []; function walk(dir) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { walk(full); } else if ( entry.name === "page.tsx" || entry.name === "route.ts" || entry.name === "layout.tsx" || entry.name === "loading.tsx" || entry.name === "not-found.tsx" || entry.name === "manifest.ts" || entry.name === "sitemap.ts" ) { const rel = path.relative(pagesRoot, full); const kind = entry.name === "route.ts" ? "route" : entry.name === "layout.tsx" ? "layout" : "page"; routes.push({ rel, kind, full }); } } } walk(pagesRoot); return routes; } /** 从 core 页面模块源码中提取可 re-export 的导出名 */ /** * 从 core 页面模块源码中提取导出: * - 函数导出(default/generateMetadata/generateStaticParams...)→ re-export * - 路由段配置常量(revalidate/dynamic...)→ 内联字面量(Next 要求静态可解析,不能 re-export) */ function extractExports(file, kind) { const srcText = fs.readFileSync(file, "utf8"); const names = new Set(); const inlineConfigs = []; if (kind !== "route" && /export\s+default/.test(srcText)) { names.add("default"); } const fnRe = /export\s+(?:async\s+)?function\s+([A-Za-z0-9_]+)/g; for (const m of srcText.matchAll(fnRe)) names.add(m[1]); const configNames = ["revalidate", "dynamic", "runtime", "fetchCache", "maxDuration", "dynamicParams"]; const constRe = /export\s+const\s+([A-Za-z0-9_]+)\s*=\s*([^;\n]+);/g; for (const m of srcText.matchAll(constRe)) { const full = m[0]; const name = m[1]; if (configNames.includes(name)) { inlineConfigs.push(full); } } // route 的解构导出(如 export const { POST, GET } = ...)→ 整句 re-export const destructureRe = /export\s+const\s*\{([^}]+)\}\s*=[^;\n]+;/g; for (const m of srcText.matchAll(destructureRe)) { for (const raw of m[1].split(",")) { const name = raw.trim().split(":")[0].trim(); if (/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/.test(name)) names.add(name); } } return { names: [...names].filter((n) => !n.startsWith("type")), inlineConfigs, }; } function writeFileSafe(file, content) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, content); } function generateSite(siteKey) { const site = SITES[siteKey]; if (!site) throw new Error(`Unknown site: ${siteKey}. 可选: ${Object.keys(SITES).join(", ")}`); const siteRoot = path.join(SITES_ROOT, siteKey); console.log(`\n=== 生成站点 ${siteKey} (port ${site.port}, theme ${site.theme}) ===`); fs.mkdirSync(siteRoot, { recursive: true }); // 清空生成物,但保留本地环境与依赖 for (const entry of fs.readdirSync(siteRoot)) { if (entry === ".env.local" || entry === "node_modules") continue; fs.rmSync(path.join(siteRoot, entry), { recursive: true, force: true }); } const corePkg = "@aidir/core"; // ---------- 1) 薄路由 ---------- const skipRoutes = new Set([ // 站点组合器文件由下方单独生成(营销布局 + 首页) "[locale]/(marketing)/layout.tsx", "[locale]/(marketing)/page.tsx", // 根布局单独生成(需引入站点样式) "layout.tsx", ]); let routeCount = 0; for (const route of collectRoutes()) { if (skipRoutes.has(route.rel)) continue; const rel = route.rel.replace(/\\/g, "/"); const target = path.join(siteRoot, "src", "app", rel); const importPath = `${corePkg}/pages/${rel.replace(/\.tsx?$/, "")}`; const { names, inlineConfigs } = extractExports(route.full, route.kind); if (names.length === 0 && inlineConfigs.length === 0) continue; let content = `// 自动生成:${rel} 的薄路由\n`; for (const cfg of inlineConfigs) content += `${cfg}\n`; if (names.length > 0) { content += `export { ${names.join(", ")} } from "${importPath}";\n`; } writeFileSafe(target, content); routeCount++; } // ---------- 2) 根布局(css + 透传) ---------- writeFileSafe( path.join(siteRoot, "src/app/layout.tsx"), `import "${corePkg}/styles/globals.css";\n\nexport { default } from "${corePkg}/pages/app-layout";\n` ); // ---------- 3) 营销布局 + 首页(站点组合器) ---------- writeFileSafe( path.join(siteRoot, "src/app/[locale]/(marketing)/layout.tsx"), `import { createMarketingLayout } from "${corePkg}/kit/create-marketing-layout";\nimport { siteConfig } from "@/site/brand";\n\nexport default createMarketingLayout(siteConfig);\n` ); writeFileSafe( path.join(siteRoot, "src/app/[locale]/(marketing)/page.tsx"), `import { createHomePage, createHomeMetadata } from "${corePkg}/kit/create-home";\nimport { siteConfig } from "@/site/brand";\nimport { SiteHome } from "@/site/home";\n\n// 首页内容变化不频繁,ISR 每小时再生\nexport const revalidate = 3600;\n\nexport const generateMetadata = createHomeMetadata(siteConfig);\nexport default createHomePage(siteConfig, SiteHome);\n` ); // ---------- 4) 站点品牌 + 首页组件 ---------- const themeSrc = fs.readFileSync( path.join(CORE_ROOT, "src/kit/site-themes", site.theme, "index.tsx"), "utf8" ); // 改写 import:kit 路径 + 站点品牌 let homeSrc = themeSrc .replace(/from "[.\/]*shared\/(section|home-data)"/g, 'from "@/kit/$1"') .replace(/from "[.\/]*shared\/theme-header"/g, 'from "@/kit/theme-header"') .replace(/from "[.\/]*shared\/theme-footer"/g, 'from "@/kit/theme-footer"') .replace(/import type \{ ThemeModule \} from "[.\/]*types";\n/g, ""); // 去掉主题文件里的 brand 常量与尾部主题注册导出(品牌来自 brand.tsx) homeSrc = homeSrc.replace(/const brand = \{[\s\S]*?\};/, ""); const tailIdx = homeSrc.indexOf("\nexport const "); if (tailIdx !== -1) homeSrc = homeSrc.slice(0, tailIdx); // 统一导出名为 SiteHome(原函数如 ClassicHome / ProductHome...) homeSrc = homeSrc.replace( /(async\s+function\s+)(\w+Home)(\s*\()/, "$1SiteHome$3" ); homeSrc += "\nexport { SiteHome };\n"; // 未使用的 brand 引用替换 homeSrc = homeSrc.replace(/\bbrand\.name\b/g, "siteConfig.brand.name") .replace(/\bbrand\.headline\b/g, "siteConfig.brand.headline") .replace(/\bbrand\.subheadline\b/g, "siteConfig.brand.subheadline") .replace(/\bbrand\.tagline\b/g, "siteConfig.brand.tagline"); // 追加 import siteConfig const firstImport = homeSrc.indexOf('import '); homeSrc = homeSrc.slice(0, firstImport) + 'import { siteConfig } from "@/site/brand";\n' + homeSrc.slice(firstImport); homeSrc = `// 自动生成:站点首页组件(源自底座 ${site.theme} 形态,品牌来自 @/site/brand)\n` + homeSrc; writeFileSafe(path.join(siteRoot, "src/site/home.tsx"), homeSrc); // brand.tsx(SiteConfig,不含 theme 生成元数据) const { theme: _theme, ...siteConfigData } = site; siteConfigData.id = siteKey; writeFileSafe( path.join(siteRoot, "src/site/brand.tsx"), `import type { SiteConfig } from "@/kit/types";\n\n// ${site.brand.name} —— 独立站点的全部品牌/导航/页脚配置\nexport const siteConfig: SiteConfig = ${JSON.stringify(siteConfigData, null, 2)};\n` ); // ---------- 5) i18n request(站点文案覆盖) ---------- writeFileSafe( path.join(siteRoot, "src/i18n/request.ts"), `import { getRequestConfig } from "next-intl/server"; import { notFound } from "next/navigation"; import { locales, defaultLocale, type Locale } from "${corePkg}/i18n.config"; /** 站点品牌 SEO 文案覆盖(每个网站不同) */ const brandMessages = { en: { seo: { home: ${JSON.stringify({ title: site.seo.en.title, description: site.seo.en.description, ogImage: "/og-image.png" })} }, hero: { title: ${JSON.stringify(site.brand.headline)}, description: ${JSON.stringify(site.brand.subheadline || site.seo.en.description)} }, common: { brand: { name: ${JSON.stringify(site.brand.name)}, tagline: ${JSON.stringify(site.brand.tagline)} } }, navigation: { main: { tools: ${JSON.stringify(site.navItems[0]?.label ?? "Discover")}, blog: ${JSON.stringify(site.navItems.find((n) => n.href === "/blog")?.label ?? "Blog")} }, site: { items: ${JSON.stringify(site.navItems.map((n) => n.label))}, hrefs: ${JSON.stringify(site.navItems.map((n) => n.href))} }, }, siteFooter: { groups: ${JSON.stringify(site.footerGroups)} }, siteHeader: { searchPlaceholder: ${JSON.stringify(site.searchPlaceholder)} }, }, zh: { seo: { home: ${JSON.stringify({ title: site.seo.zh.title, description: site.seo.zh.description, ogImage: "/og-image.png" })} }, hero: { title: ${JSON.stringify(site.brandZh?.headline ?? site.seo.zh.title)}, description: ${JSON.stringify(site.brandZh?.subheadline || site.seo.zh.description)} }, common: { brand: { name: ${JSON.stringify(site.brandZh?.name ?? site.brand.name)}, tagline: ${JSON.stringify(site.brandZh?.tagline ?? site.brand.tagline)} } }, navigation: { main: { tools: ${JSON.stringify(site.brandZh?.nav?.[0] ?? site.navItems[0]?.label ?? "发现")}, blog: ${JSON.stringify(site.brandZh?.nav?.[site.navItems.findIndex((n) => n.href === "/blog")] ?? "博客")} }, site: { items: ${JSON.stringify(site.brandZh?.nav ?? site.navItems.map((n) => n.label))}, hrefs: ${JSON.stringify(site.navItems.map((n) => n.href))} }, }, siteFooter: { groups: ${JSON.stringify(site.footerGroups.map((g, gi) => ({ ...g, title: (site.brandZh?.footer ?? [])[gi] ?? g.title })))} }, siteHeader: { searchPlaceholder: ${JSON.stringify(site.brandZh?.searchPlaceholder ?? site.searchPlaceholder)} }, }, } as const; export default getRequestConfig(async ({ requestLocale }) => { let locale = await requestLocale; if (!locale || !locales.includes(locale as Locale)) { locale = defaultLocale; } try { // 底座基础文案 + 站点品牌覆盖(浅合并到顶层命名空间) const base = (await import(\`${corePkg}/messages/\${locale}.json\`)).default; const override = brandMessages[locale as keyof typeof brandMessages]; const messages = { ...base }; for (const [ns, value] of Object.entries(override)) { messages[ns] = { ...(messages[ns] ?? {}), ...value }; } return { locale, messages }; } catch (error) { console.error("Error loading messages for locale:", locale, error); notFound(); } }); ` ); // ---------- 6) proxy / mdx-components ---------- writeFileSafe( path.join(siteRoot, "src/proxy.ts"), `import { proxy } from "${corePkg}/proxy";\n\nexport default proxy;\n\nexport const config = {\n matcher: [\n "/",\n "/((?!api|_next|_vercel|.*\\\\..*).*)"\n ]\n};\n` ); writeFileSafe( path.join(siteRoot, "mdx-components.tsx"), `export { useMDXComponents } from "${corePkg}/src/mdx-components";\n` ); // ---------- 7) 配置文件 ---------- writeFileSafe( path.join(siteRoot, "package.json"), JSON.stringify( { name: `site-${siteKey}`, version: "1.0.0", description: `${site.brand.name} — AI directory site powered by @aidir/core`, private: true, scripts: { dev: `next dev -p ${site.port}`, build: "next build", start: `next start -p ${site.port}`, typecheck: "tsc --noEmit", }, dependencies: { "@aidir/core": "workspace:*", next: "16.1.1", "react": "^19.2.3", "react-dom": "^19.2.3", "next-intl": "^4.7.0", "next-themes": "^0.4.6", "next-view-transitions": "^0.3.5", "sonner": "^2.0.7", "geist": "^1.3.0", "lucide-react": "^0.562.0", "framer-motion": "^12.43.0", }, devDependencies: { "@types/node": "^20", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@types/mdx": "^2.0.13", "typescript": "^5", "tailwindcss": "^3.4.1", "postcss": "^8", }, }, null, 2 ) + "\n" ); writeFileSafe( path.join(siteRoot, "next.config.mjs"), `import createSiteConfig from "@aidir/core/next-config";\n\nexport default createSiteConfig({\n requestPath: "./src/i18n/request.ts",\n});\n` ); writeFileSafe( path.join(siteRoot, "tsconfig.json"), JSON.stringify( { compilerOptions: { target: "ES2017", lib: ["dom", "dom.iterable", "esnext"], allowJs: true, skipLibCheck: true, strict: true, noEmit: true, esModuleInterop: true, module: "esnext", moduleResolution: "bundler", resolveJsonModule: true, isolatedModules: true, jsx: "preserve", incremental: true, plugins: [{ name: "next" }], paths: { "@/*": ["./src/*", "../../packages/core/src/*"], "@aidir/core/pages/*": ["../../packages/core/src/pages/*"], "@aidir/core/kit/*": ["../../packages/core/src/kit/*"], "@aidir/core/src/*": ["../../packages/core/src/*"], "@aidir/core/proxy": ["../../packages/core/src/proxy.ts"], "@aidir/core/i18n.config": ["../../packages/core/src/i18n.config.ts"], "@aidir/core/styles/globals.css": ["../../packages/core/src/styles/globals.css"], "@aidir/core/*": ["../../packages/core/src/*", "../../packages/core/*"], }, }, include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], exclude: ["node_modules"], }, null, 2 ) + "\n" ); writeFileSafe( path.join(siteRoot, "postcss.config.mjs"), `const config = {\n plugins: {\n tailwindcss: {},\n },\n};\n\nexport default config;\n` ); // tailwind:content 扫描站点 + 底座;主题/插件来自底座 preset writeFileSafe( path.join(siteRoot, "tailwind.config.ts"), `import type { Config } from "tailwindcss";\n\nconst config: Config = {\n content: [\n "./src/**/*.{js,ts,jsx,tsx,mdx}",\n "../../packages/core/src/**/*.{js,ts,jsx,tsx,mdx}",\n ],\n presets: [require("../../packages/core/tailwind.preset.cjs")],\n};\n\nexport default config;\n` ); // ---------- 8) messages(base 由 i18n request 运行时合并;站点仍保留一份覆盖说明) ---------- // 静态拷贝 core messages 便于独立部署检查 for (const locale of ["en", "zh"]) { const base = JSON.parse(fs.readFileSync(path.join(CORE_ROOT, "messages", `${locale}.json`), "utf8")); // 品牌覆盖(静态合并进 messages,运行时 request.ts 仍会覆盖以双保险) base.common = base.common ?? {}; base.common.brand = { ...(base.common.brand ?? {}), name: site.brand.name, tagline: site.brand.tagline }; base.seo = base.seo ?? {}; base.seo.home = { ...(base.seo.home ?? {}), ...site.seo[locale] }; base.hero = { ...(base.hero ?? {}), title: locale === "zh" ? (site.brandZh?.headline ?? site.seo.zh.title) : site.brand.headline, description: locale === "zh" ? (site.brandZh?.subheadline || site.seo.zh.description) : (site.brand.subheadline || site.seo.en.description), badge: locale === "zh" ? "" : base.hero?.badge ?? "", }; if (locale === "zh") delete base.hero.badge; // 站点导航/页脚/搜索文案(供 ThemeChrome 消费) base.navigation = { ...(base.navigation ?? {}), site: { items: locale === "zh" ? (site.brandZh?.nav ?? site.navItems.map((n) => n.label)) : site.navItems.map((n) => n.label), hrefs: site.navItems.map((n) => n.href), }, }; base.siteFooter = { groups: site.footerGroups.map((g, gi) => ({ ...g, title: locale === "zh" ? ((site.brandZh?.footer ?? [])[gi] ?? g.title) : g.title, })), }; base.siteHeader = { searchPlaceholder: locale === "zh" ? (site.brandZh?.searchPlaceholder ?? site.searchPlaceholder) : site.searchPlaceholder, }; fs.mkdirSync(path.join(siteRoot, "messages"), { recursive: true }); fs.writeFileSync( path.join(siteRoot, "messages", `${locale}.json`), JSON.stringify(base, null, 2) + "\n" ); } // 但 request.ts 目前从 core 读基础文案 —— 改为读本站 messages(保证站点内容独立可改) let requestSrc = fs.readFileSync(path.join(siteRoot, "src/i18n/request.ts"), "utf8"); requestSrc = requestSrc.replace( 'const base = (await import(`@aidir/core/messages/${locale}.json`)).default;', 'const base = (await import(`../../messages/${locale}.json`)).default;' ); writeFileSafe(path.join(siteRoot, "src/i18n/request.ts"), requestSrc); // ---------- 9) public + env ---------- fs.cpSync(path.join(CORE_ROOT, "public"), path.join(siteRoot, "public"), { recursive: true }); // 品牌首字母 favicon(SVG,无 Emoji) const initial = site.brand.name.slice(0, 1).toUpperCase(); writeFileSafe( path.join(siteRoot, "public", "icon.svg"), `${initial}\n` ); writeFileSafe( path.join(siteRoot, ".env.example"), `# ${site.brand.name} 环境变量(与底座共享同一套 PostgreSQL / Better Auth / Admin) DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:15433/ai_directory" BETTER_AUTH_SECRET="dev-secret-ai-directory-platform-0123456789abcdef" BETTER_AUTH_URL="http://localhost:${site.port}" AUTH_GOOGLE_ID="" AUTH_GOOGLE_SECRET="" NEXT_PUBLIC_AUTH_GOOGLE_ENABLED="false" AUTH_GITHUB_ID="" AUTH_GITHUB_SECRET="" NEXT_PUBLIC_AUTH_GITHUB_ENABLED="false" RESEND_API_KEY="" RESEND_FROM_EMAIL="${site.brand.name} " NEXT_PUBLIC_APP_URL="http://localhost:${site.port}" NEXT_PUBLIC_APP_NAME="${site.brand.name}" # 登录页品牌模板(分栏侧栏颜色与标语) NEXT_PUBLIC_AUTH_ACCENT="${site.accent}" NEXT_PUBLIC_AUTH_VARIANT="${site.authVariant}" # 站点主题标识()与默认深浅(light | dark,用户可切换) NEXT_PUBLIC_THEME_ID="${site.themeId}" NEXT_PUBLIC_DEFAULT_THEME="${site.defaultTheme}" NEXT_PUBLIC_AUTH_BRAND_TAGLINE="${site.brand.tagline}" # 开发阶段免邮箱验证(生产置 false 发送真实验证邮件) DEV_SKIP_EMAIL_VERIFY="true" NEXT_PUBLIC_DEV_SKIP_EMAIL_VERIFY="true" ` ); // ---------- 10) .gitignore ---------- writeFileSafe( path.join(siteRoot, ".gitignore"), "/node_modules\n/.next\n/.env*.local\nnext-env.d.ts\n*.tsbuildinfo\n" ); console.log(`路由薄文件: ${routeCount + 2}(含根布局/营销布局/首页组合器)`); console.log(`输出: sites/${siteKey}`); } // main const key = process.argv[2]; if (!key || key === "all") { for (const k of Object.keys(SITES)) generateSite(k); } else { generateSite(key); }