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
+107
View File
@@ -0,0 +1,107 @@
import type { SiteConfig } from "@/kit/types";
// AI Stack —— 独立站点的全部品牌/导航/页脚配置
export const siteConfig: SiteConfig = {
"port": 3001,
"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+ 工具,由社区投票、评论与互动数据实时排名。"
}
},
"id": "ai-stack"
};
+277
View File
@@ -0,0 +1,277 @@
// 自动生成:站点首页组件(源自底座 classic 形态,品牌来自 @/site/brand)
import { siteConfig } from "@/site/brand";
import { Search, TrendingUp, Sparkles, Clock, Award, LayoutGrid, MessageSquareQuote } from "lucide-react";
import { ToolCard, ToolCardGrid } from "@/components/tool-card";
import { LocaleLink } from "@/components/locale-link";
import { SectionHeader } from "@/kit/section";
import { ThemeHeader } from "@/kit/theme-header";
import { ThemeFooter } from "@/kit/theme-footer";
import { getHomeDataSafe } from "@/kit/home-data";
/** Classic:专业 AI Directory(Clean / Dense / Professional) */
const navItems = [
{ label: "Discover", href: "/tools" },
{ label: "Categories", 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" },
];
const 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" },
],
},
];
async function SiteHome({ locale }: { locale: string }) {
const data = await getHomeDataSafe(8);
return (
<div className="bg-background">
{/* Hero */}
<section className="border-b border-border bg-gradient-to-b from-secondary/40 to-background">
<div className="mx-auto max-w-7xl px-4 py-16 text-center sm:px-6 md:py-24">
<h1 className="mx-auto max-w-3xl text-3xl font-bold tracking-tight text-foreground sm:text-4xl md:text-5xl">
{siteConfig.brand.headline}
</h1>
<p className="mx-auto mt-4 max-w-2xl text-sm text-muted-foreground sm:text-base">
{siteConfig.brand.subheadline}
</p>
<form action={`/${locale}/tools`} className="mx-auto mt-8 max-w-xl">
<div className="relative">
<Search className="absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<input
name="search"
placeholder="Search 1,000+ AI tools..."
className="h-12 w-full rounded-full border border-border bg-background pl-11 pr-28 text-sm text-foreground shadow-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
aria-label="Search AI tools"
/>
<button
type="submit"
className="absolute right-1.5 top-1.5 h-9 rounded-full bg-primary px-5 text-xs font-semibold text-primary-foreground transition-opacity hover:opacity-90"
>
Search
</button>
</div>
</form>
<div className="mt-4 flex flex-wrap items-center justify-center gap-2 text-xs text-muted-foreground">
<span>Popular:</span>
{["writing", "video", "coding", "image"].map((q) => (
<LocaleLink
key={q}
href={`/tools?search=${q}`}
className="rounded-full bg-secondary px-2.5 py-1 transition-colors hover:bg-hover"
>
{q}
</LocaleLink>
))}
</div>
</div>
</section>
<div className="mx-auto max-w-7xl space-y-12 px-4 py-12 sm:px-6">
{/* Trending */}
{data.trending.length > 0 && (
<section>
<SectionHeader
title="Trending this week"
subtitle="Ranked by votes, reviews and engagement over the last 7 days"
viewAllHref="/rankings?board=trending"
viewAllLabel="All rankings"
/>
<ToolCardGrid tools={data.trending} locale={locale} ranked />
</section>
)}
{/* Categories */}
{data.categories.length > 0 && (
<section>
<SectionHeader
title="Browse by category"
viewAllHref="/tools"
viewAllLabel="All tools"
/>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
{data.categories.slice(0, 10).map((category) => (
<LocaleLink
key={category.id}
href={`/category/${category.slug}`}
className="group rounded-xl border border-border p-4 transition-all hover:border-primary/30 hover:shadow-sm"
>
<LayoutGrid className="h-4 w-4 text-primary" />
<h3 className="mt-2 truncate text-sm font-medium text-foreground group-hover:text-primary">
{locale === "zh" && category.nameZh
? category.nameZh
: category.nameEn}
</h3>
<p className="mt-0.5 text-xs text-muted-foreground tabular-nums">
{category.toolCount} tools
</p>
</LocaleLink>
))}
</div>
</section>
)}
{/* New + Editor's Choice 两列 */}
<div className="grid gap-12 lg:grid-cols-2">
{data.newest.length > 0 && (
<section>
<SectionHeader
title="New AI tools"
viewAllHref="/rankings?board=new"
/>
<ToolCardGrid tools={data.newest.slice(0, 4)} locale={locale} />
</section>
)}
{data.editorsChoice.length > 0 && (
<section>
<SectionHeader
title="Editor's Choice"
viewAllHref="/rankings?board=editors-choice"
/>
<ToolCardGrid tools={data.editorsChoice.slice(0, 4)} locale={locale} />
</section>
)}
</div>
{/* Use Cases */}
{data.useCases.length > 0 && (
<section>
<SectionHeader
title="Tools by use case"
subtitle="What do you want to get done?"
viewAllHref="/ai-tools-for"
viewAllLabel="All use cases"
/>
<div className="flex flex-wrap gap-2">
{data.useCases.slice(0, 12).map((uc) => (
<LocaleLink
key={uc.id}
href={`/ai-tools-for/${uc.slug}`}
className="rounded-full border border-border px-3.5 py-1.5 text-xs text-foreground transition-colors hover:border-primary/40 hover:bg-primary/5"
>
{locale === "zh" && uc.nameZh ? uc.nameZh : uc.nameEn}
<span className="ml-1.5 text-muted-foreground tabular-nums">
{uc.toolCount}
</span>
</LocaleLink>
))}
</div>
</section>
)}
{/* Best Lists */}
{data.collections.length > 0 && (
<section>
<SectionHeader
title="Best AI tool lists"
subtitle="Curated rankings by our editors"
viewAllHref="/best"
viewAllLabel="All lists"
/>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{data.collections.slice(0, 3).map((col) => (
<LocaleLink
key={col.id}
href={`/best/${col.slug}`}
className="group rounded-xl border border-border p-5 transition-all hover:border-primary/30 hover:shadow-sm"
>
<Award className="h-4 w-4 text-primary" />
<h3 className="mt-2 text-sm font-semibold text-foreground group-hover:text-primary">
{locale === "zh" && col.nameZh ? col.nameZh : col.nameEn}
</h3>
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
{locale === "zh" && col.introZh ? col.introZh : col.introEn}
</p>
<p className="mt-2 text-xs text-muted-foreground tabular-nums">
{col.toolCount} tools
</p>
</LocaleLink>
))}
</div>
</section>
)}
{/* Latest Reviews */}
{data.latestReviews.length > 0 && (
<section>
<SectionHeader title="Latest community reviews" />
<div className="grid gap-3 md:grid-cols-2">
{data.latestReviews.map((review, i) => (
<LocaleLink
key={i}
href={`/tools/${review.toolSlug}#reviews`}
className="rounded-xl border border-border p-4 transition-colors hover:border-primary/30"
>
<div className="flex items-center gap-2">
<MessageSquareQuote className="h-3.5 w-3.5 text-primary" />
<span className="text-xs font-medium text-foreground">
{review.authorName}
</span>
<span className="text-xs text-muted-foreground">
on {review.toolName}
</span>
<span className="ml-auto text-xs font-semibold text-amber-500 tabular-nums">
{review.rating}/5
</span>
</div>
<p className="mt-2 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
{review.content}
</p>
</LocaleLink>
))}
</div>
</section>
)}
{/* Submit CTA */}
<section className="rounded-2xl border border-border bg-secondary/30 p-8 text-center">
<Sparkles className="mx-auto h-5 w-5 text-primary" />
<h2 className="mt-3 text-lg font-semibold text-foreground">
Built an AI tool?
</h2>
<p className="mx-auto mt-1 max-w-md text-sm text-muted-foreground">
Submit it to {siteConfig.brand.name} and get discovered by thousands of users
looking for tools like yours.
</p>
<LocaleLink
href="/submit"
className="mt-4 inline-flex items-center gap-1.5 rounded-full bg-primary px-5 py-2 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90"
>
<TrendingUp className="h-4 w-4" />
Submit your tool
</LocaleLink>
</section>
</div>
</div>
);
}
export { SiteHome };