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
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(admin)/admin/analytics/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(admin)/admin/analytics/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(admin)/admin/categories/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(admin)/admin/categories/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(admin)/admin/claims/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(admin)/admin/claims/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(admin)/admin/discovery/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(admin)/admin/discovery/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(admin)/admin/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(admin)/admin/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(admin)/admin/reviews/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(admin)/admin/reviews/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(admin)/admin/submissions/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(admin)/admin/submissions/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(admin)/admin/tags/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(admin)/admin/tags/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(admin)/admin/tools/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(admin)/admin/tools/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(admin)/admin/users/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(admin)/admin/users/page";
@@ -0,0 +1,3 @@
// 自动生成:[locale]/(admin)/admin/votes/page.tsx 的薄路由
export const revalidate = 0;
export { default } from "@aidir/core/pages/[locale]/(admin)/admin/votes/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(admin)/layout.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(admin)/layout";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(auth)/forgot-password/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(auth)/forgot-password/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(auth)/layout.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(auth)/layout";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(auth)/login/page.tsx 的薄路由
export { default, generateMetadata } from "@aidir/core/pages/[locale]/(auth)/login/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(auth)/reset-password/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(auth)/reset-password/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(auth)/signup/page.tsx 的薄路由
export { default, generateMetadata } from "@aidir/core/pages/[locale]/(auth)/signup/page";
@@ -0,0 +1,3 @@
// 自动生成:[locale]/(marketing)/ai-tools-for/[slug]/page.tsx 的薄路由
export const revalidate = 3600;
export { default, generateStaticParams, generateMetadata } from "@aidir/core/pages/[locale]/(marketing)/ai-tools-for/[slug]/page";
@@ -0,0 +1,3 @@
// 自动生成:[locale]/(marketing)/ai-tools-for/page.tsx 的薄路由
export const revalidate = 3600;
export { default, generateMetadata } from "@aidir/core/pages/[locale]/(marketing)/ai-tools-for/page";
@@ -0,0 +1,3 @@
// 自动生成:[locale]/(marketing)/best/[slug]/page.tsx 的薄路由
export const revalidate = 3600;
export { default, generateStaticParams, generateMetadata } from "@aidir/core/pages/[locale]/(marketing)/best/[slug]/page";
@@ -0,0 +1,3 @@
// 自动生成:[locale]/(marketing)/best/page.tsx 的薄路由
export const revalidate = 3600;
export { default, generateMetadata } from "@aidir/core/pages/[locale]/(marketing)/best/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(marketing)/blog/[slug]/page.tsx 的薄路由
export { default, generateStaticParams, generateMetadata } from "@aidir/core/pages/[locale]/(marketing)/blog/[slug]/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(marketing)/blog/page.tsx 的薄路由
export { default, generateMetadata } from "@aidir/core/pages/[locale]/(marketing)/blog/page";
@@ -0,0 +1,3 @@
// 自动生成:[locale]/(marketing)/category/[slug]/page.tsx 的薄路由
export const revalidate = 3600;
export { default, generateStaticParams, generateMetadata } from "@aidir/core/pages/[locale]/(marketing)/category/[slug]/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(marketing)/contact/page.tsx 的薄路由
export { default, generateMetadata } from "@aidir/core/pages/[locale]/(marketing)/contact/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(marketing)/cookies/page.tsx 的薄路由
export { default, generateMetadata } from "@aidir/core/pages/[locale]/(marketing)/cookies/page";
@@ -0,0 +1,4 @@
import { createMarketingLayout } from "@aidir/core/kit/create-marketing-layout";
import { siteConfig } from "@/site/brand";
export default createMarketingLayout(siteConfig);
@@ -0,0 +1,9 @@
import { createHomePage, createHomeMetadata } from "@aidir/core/kit/create-home";
import { siteConfig } from "@/site/brand";
import { SiteHome } from "@/site/home";
// 首页内容变化不频繁,ISR 每小时再生
export const revalidate = 3600;
export const generateMetadata = createHomeMetadata(siteConfig);
export default createHomePage(siteConfig, SiteHome);
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(marketing)/privacy/page.tsx 的薄路由
export { default, generateMetadata } from "@aidir/core/pages/[locale]/(marketing)/privacy/page";
@@ -0,0 +1,3 @@
// 自动生成:[locale]/(marketing)/rankings/page.tsx 的薄路由
export const revalidate = 1800;
export { default, generateMetadata } from "@aidir/core/pages/[locale]/(marketing)/rankings/page";
@@ -0,0 +1,3 @@
// 自动生成:[locale]/(marketing)/submit/page.tsx 的薄路由
export const revalidate = 3600;
export { default, generateMetadata } from "@aidir/core/pages/[locale]/(marketing)/submit/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(marketing)/terms/page.tsx 的薄路由
export { default, generateMetadata } from "@aidir/core/pages/[locale]/(marketing)/terms/page";
@@ -0,0 +1,3 @@
// 自动生成:[locale]/(marketing)/tools/[slug]/page.tsx 的薄路由
export const revalidate = 3600;
export { default, generateStaticParams, generateMetadata } from "@aidir/core/pages/[locale]/(marketing)/tools/[slug]/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(marketing)/tools/page.tsx 的薄路由
export { default, generateMetadata } from "@aidir/core/pages/[locale]/(marketing)/tools/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(protected)/dashboard/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(protected)/dashboard/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(protected)/layout.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(protected)/layout";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/(protected)/profile/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/(protected)/profile/page";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/check-email/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/check-email/page";
+2
View File
@@ -0,0 +1,2 @@
// 自动生成:[locale]/layout.tsx 的薄路由
export { default, generateStaticParams } from "@aidir/core/pages/[locale]/layout";
@@ -0,0 +1,2 @@
// 自动生成:[locale]/verify-email/page.tsx 的薄路由
export { default } from "@aidir/core/pages/[locale]/verify-email/page";
@@ -0,0 +1,2 @@
// 自动生成:api/auth/[...all]/route.ts 的薄路由
export { POST, GET } from "@aidir/core/pages/api/auth/[...all]/route";
@@ -0,0 +1,2 @@
// 自动生成:api/auth/forgot-password/route.ts 的薄路由
export { POST } from "@aidir/core/pages/api/auth/forgot-password/route";
@@ -0,0 +1,2 @@
// 自动生成:api/auth/resend-verification/route.ts 的薄路由
export { POST } from "@aidir/core/pages/api/auth/resend-verification/route";
@@ -0,0 +1,2 @@
// 自动生成:api/auth/reset-password/route.ts 的薄路由
export { POST } from "@aidir/core/pages/api/auth/reset-password/route";
@@ -0,0 +1,2 @@
// 自动生成:api/auth/verify-email/route.ts 的薄路由
export { GET } from "@aidir/core/pages/api/auth/verify-email/route";
@@ -0,0 +1,2 @@
// 自动生成:api/auth/verify-reset-token/route.ts 的薄路由
export { GET } from "@aidir/core/pages/api/auth/verify-reset-token/route";
@@ -0,0 +1,2 @@
// 自动生成:api/newsletter/unsubscribe/route.ts 的薄路由
export { GET } from "@aidir/core/pages/api/newsletter/unsubscribe/route";
@@ -0,0 +1,2 @@
// 自动生成:api/outbound/route.ts 的薄路由
export { GET } from "@aidir/core/pages/api/outbound/route";
+2
View File
@@ -0,0 +1,2 @@
// 自动生成:api/search/route.ts 的薄路由
export { GET } from "@aidir/core/pages/api/search/route";
@@ -0,0 +1,2 @@
// 自动生成:api/tools/[id]/vote/route.ts 的薄路由
export { POST, DELETE } from "@aidir/core/pages/api/tools/[id]/vote/route";
@@ -0,0 +1,2 @@
// 自动生成:api/upload/image/route.ts 的薄路由
export { POST } from "@aidir/core/pages/api/upload/image/route";
@@ -0,0 +1,2 @@
// 自动生成:api/upload/simple/route.ts 的薄路由
export { POST } from "@aidir/core/pages/api/upload/simple/route";
@@ -0,0 +1,2 @@
// 自动生成:api/user/admin-status/route.ts 的薄路由
export { GET } from "@aidir/core/pages/api/user/admin-status/route";
@@ -0,0 +1,2 @@
// 自动生成:api/user/profile/route.ts 的薄路由
export { GET } from "@aidir/core/pages/api/user/profile/route";
+3
View File
@@ -0,0 +1,3 @@
import "@aidir/core/styles/globals.css";
export { default } from "@aidir/core/pages/app-layout";
+2
View File
@@ -0,0 +1,2 @@
// 自动生成:manifest.ts 的薄路由
export { default } from "@aidir/core/pages/manifest";
+2
View File
@@ -0,0 +1,2 @@
// 自动生成:not-found.tsx 的薄路由
export { default } from "@aidir/core/pages/not-found";
+2
View File
@@ -0,0 +1,2 @@
// 自动生成:sitemap.ts 的薄路由
export { default } from "@aidir/core/pages/sitemap";
+37
View File
@@ -0,0 +1,37 @@
import { getRequestConfig } from "next-intl/server";
import { notFound } from "next/navigation";
import { locales, defaultLocale, type Locale } from "@aidir/core/i18n.config";
/** 站点品牌 SEO 文案覆盖(每个网站不同) */
const brandMessages = {
en: {
seo: { home: {"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.","ogImage":"/og-image.png"} },
hero: { 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." },
common: { brand: { name: "findai", tagline: "The minimal AI tool search engine." } },
},
zh: {
seo: { home: {"title":"findai — 极简 AI 工具搜索引擎","description":"一个搜索框,找遍全部 AI 工具。说出你想做的事,社区排名替你选。","ogImage":"/og-image.png"} },
hero: { title: "findai — 极简 AI 工具搜索引擎", description: "一个搜索框,找遍全部 AI 工具。说出你想做的事,社区排名替你选。" },
common: { brand: { name: "findai", tagline: "The minimal AI tool search engine." } },
},
} 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(`../../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();
}
});
+10
View File
@@ -0,0 +1,10 @@
import { proxy } from "@aidir/core/proxy";
export default proxy;
export const config = {
matcher: [
"/",
"/((?!api|_next|_vercel|.*\\..*).*)"
]
};
+78
View File
@@ -0,0 +1,78 @@
import type { SiteConfig } from "@/kit/types";
// findai —— 独立站点的全部品牌/导航/页脚配置
export const siteConfig: SiteConfig = {
"port": 3005,
"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 工具。说出你想做的事,社区排名替你选。"
}
},
"id": "findai"
};
+107
View File
@@ -0,0 +1,107 @@
// 自动生成:站点首页组件(源自底座 minimal 形态,品牌来自 @/site/brand)
import { siteConfig } from "@/site/brand";
import { Search } from "lucide-react";
import { ToolCardGrid } from "@/components/tool-card";
import { LocaleLink } from "@/components/locale-link";
import { getHomeDataSafe } from "@/kit/home-data";
/** Minimal:极简 AI 搜索引擎(一个框,一个任务) */
const navItems = [
{ label: "Search", href: "/tools" },
{ label: "Use Cases", href: "/ai-tools-for" },
{ label: "Rankings", href: "/rankings" },
{ label: "Submit", href: "/submit" },
];
const 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" },
],
},
];
const popularSearches = [
"write a blog post",
"generate video",
"code assistant",
"make a logo",
"edit photos",
"transcribe meetings",
];
async function SiteHome({ locale }: { locale: string }) {
const data = await getHomeDataSafe(6);
return (
<div className="flex min-h-screen flex-col bg-background">
{/* 极简搜索首屏 */}
<section className="flex flex-1 flex-col items-center justify-center px-4 pb-16 pt-24 text-center">
<LocaleLink href="/" className="text-3xl font-bold tracking-tight text-foreground">
{siteConfig.brand.name}
</LocaleLink>
<p className="mt-1 text-xs uppercase tracking-[0.35em] text-muted-foreground">
AI tool search
</p>
<form action={`/${locale}/tools`} className="mt-10 w-full max-w-xl">
<div className="relative">
<Search className="absolute left-5 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
<input
name="search"
autoFocus
placeholder="I need to create marketing videos..."
className="h-14 w-full rounded-full border border-border bg-background pl-14 pr-6 text-base text-foreground shadow-lg shadow-black/[0.03] placeholder:text-muted-foreground focus:border-primary/50 focus:outline-none focus:ring-4 focus:ring-primary/10"
aria-label="Search AI tools"
/>
</div>
</form>
<div className="mt-6 flex flex-wrap items-center justify-center gap-2">
{popularSearches.map((q) => (
<LocaleLink
key={q}
href={`/tools?search=${encodeURIComponent(q)}`}
className="rounded-full px-3 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
>
{q}
</LocaleLink>
))}
</div>
</section>
{/* 搜索之下:Trending */}
{data.trending.length > 0 && (
<section className="mx-auto w-full max-w-3xl px-4 pb-20 sm:px-6">
<h2 className="mb-4 text-center text-xs font-medium uppercase tracking-[0.25em] text-muted-foreground">
Trending now
</h2>
<ToolCardGrid tools={data.trending.slice(0, 4)} locale={locale} />
<div className="mt-6 text-center">
<LocaleLink
href="/rankings"
className="text-xs font-medium text-muted-foreground hover:text-primary"
>
See full rankings
</LocaleLink>
</div>
</section>
)}
</div>
);
}
export { SiteHome };