- 数据模型: 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事件
234 lines
8.7 KiB
TypeScript
234 lines
8.7 KiB
TypeScript
import { BookOpen, PenTool, ArrowRight, Star } from "lucide-react";
|
||
import { LocaleLink } from "@/components/locale-link";
|
||
import { SectionHeader } from "../shared/section";
|
||
import { getHomeDataSafe } from "../shared/home-data";
|
||
import type { ThemeModule } from "../types";
|
||
|
||
/** Editorial:AI 媒体 + 工具目录(Serif / 内容优先) */
|
||
const 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.",
|
||
};
|
||
|
||
const 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" },
|
||
];
|
||
|
||
const 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" },
|
||
],
|
||
},
|
||
];
|
||
|
||
async function EditorialHome({ locale }: { locale: string }) {
|
||
const data = await getHomeDataSafe(6);
|
||
const featured = data.articles[0];
|
||
const editorPicks = data.editorsChoice.slice(0, 4);
|
||
|
||
return (
|
||
<div className="min-h-screen bg-background">
|
||
{/* Masthead */}
|
||
<section className="border-b border-border">
|
||
<div className="mx-auto max-w-4xl px-4 py-16 text-center sm:px-6 md:py-20">
|
||
<p className="text-xs font-medium uppercase tracking-[0.3em] text-muted-foreground">
|
||
Independent AI Coverage
|
||
</p>
|
||
<h1 className="mt-4 text-4xl font-bold tracking-tight text-foreground md:text-5xl">
|
||
{brand.headline}
|
||
</h1>
|
||
<p className="mx-auto mt-4 max-w-xl text-base leading-relaxed text-muted-foreground">
|
||
{brand.subheadline}
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<div className="mx-auto max-w-4xl space-y-14 px-4 py-12 sm:px-6">
|
||
{/* Featured Story */}
|
||
{featured && (
|
||
<section>
|
||
<div className="mb-4 flex items-center gap-2 text-xs font-semibold uppercase tracking-widest text-primary">
|
||
<BookOpen className="h-3.5 w-3.5" />
|
||
Featured story
|
||
</div>
|
||
<LocaleLink href={`/blog/${featured.slug}`} className="group block">
|
||
<h2 className="text-2xl font-bold leading-snug text-foreground group-hover:underline md:text-3xl">
|
||
{featured.title}
|
||
</h2>
|
||
{featured.description && (
|
||
<p className="mt-3 text-base leading-relaxed text-muted-foreground">
|
||
{featured.description}
|
||
</p>
|
||
)}
|
||
<span className="mt-3 inline-flex items-center gap-1 text-sm font-medium text-primary">
|
||
Read the story
|
||
<ArrowRight className="h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" />
|
||
</span>
|
||
</LocaleLink>
|
||
</section>
|
||
)}
|
||
|
||
{/* Editor's Picks */}
|
||
{editorPicks.length > 0 && (
|
||
<section>
|
||
<div className="mb-4 flex items-center gap-2 text-xs font-semibold uppercase tracking-widest text-primary">
|
||
<PenTool className="h-3.5 w-3.5" />
|
||
Editor's picks
|
||
</div>
|
||
<div className="divide-y divide-border border-y border-border">
|
||
{editorPicks.map((tool, i) => (
|
||
<LocaleLink
|
||
key={tool.id}
|
||
href={`/tools/${tool.slug}`}
|
||
className="group flex items-baseline gap-4 py-4"
|
||
>
|
||
<span className="text-sm font-bold text-muted-foreground tabular-nums">
|
||
{String(i + 1).padStart(2, "0")}
|
||
</span>
|
||
<div className="min-w-0 flex-1">
|
||
<h3 className="text-base font-semibold text-foreground group-hover:underline">
|
||
{locale === "zh" && tool.nameZh ? tool.nameZh : tool.nameEn}
|
||
</h3>
|
||
<p className="mt-0.5 line-clamp-1 text-sm text-muted-foreground">
|
||
{tool.taglineEn || tool.descriptionEn}
|
||
</p>
|
||
</div>
|
||
{(tool.ratingAvg ?? 0) > 0 && (
|
||
<span className="inline-flex shrink-0 items-center gap-1 text-sm font-semibold text-foreground">
|
||
<Star className="h-3.5 w-3.5 fill-amber-400 text-amber-400" />
|
||
{(tool.ratingAvg ?? 0).toFixed(1)}
|
||
</span>
|
||
)}
|
||
</LocaleLink>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{/* Best Lists(编辑内容型榜单) */}
|
||
{data.collections.length > 0 && (
|
||
<section>
|
||
<SectionHeader
|
||
title="Buying guides"
|
||
subtitle="Deep dives that help you choose"
|
||
viewAllHref="/best"
|
||
viewAllLabel="All guides"
|
||
/>
|
||
<div className="grid gap-6 sm:grid-cols-2">
|
||
{data.collections.slice(0, 4).map((col) => (
|
||
<LocaleLink
|
||
key={col.id}
|
||
href={`/best/${col.slug}`}
|
||
className="group border-t-2 border-foreground/80 pt-4"
|
||
>
|
||
<h3 className="text-lg font-bold leading-snug text-foreground group-hover:underline">
|
||
{locale === "zh" && col.nameZh ? col.nameZh : col.nameEn}
|
||
</h3>
|
||
<p className="mt-2 line-clamp-2 text-sm leading-relaxed text-muted-foreground">
|
||
{locale === "zh" && col.introZh ? col.introZh : col.introEn}
|
||
</p>
|
||
<p className="mt-2 text-xs uppercase tracking-wide text-muted-foreground">
|
||
{col.toolCount} tools compared
|
||
</p>
|
||
</LocaleLink>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{/* Latest articles */}
|
||
{data.articles.length > 1 && (
|
||
<section>
|
||
<SectionHeader title="Latest stories" viewAllHref="/blog" />
|
||
<div className="grid gap-6 sm:grid-cols-2">
|
||
{data.articles.slice(1).map((article) => (
|
||
<LocaleLink key={article.slug} href={`/blog/${article.slug}`} className="group">
|
||
<h3 className="text-base font-semibold leading-snug text-foreground group-hover:underline">
|
||
{article.title}
|
||
</h3>
|
||
{article.description && (
|
||
<p className="mt-1.5 line-clamp-2 text-sm text-muted-foreground">
|
||
{article.description}
|
||
</p>
|
||
)}
|
||
</LocaleLink>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{/* Community signal */}
|
||
{data.latestReviews.length > 0 && (
|
||
<section>
|
||
<SectionHeader
|
||
title="From the community"
|
||
viewAllHref="/tools"
|
||
viewAllLabel="Browse directory"
|
||
/>
|
||
<div className="grid gap-6 sm:grid-cols-2">
|
||
{data.latestReviews.slice(0, 4).map((review, i) => (
|
||
<blockquote
|
||
key={i}
|
||
className="border-l-2 border-primary/40 pl-4"
|
||
>
|
||
<p className="text-sm italic leading-relaxed text-foreground">
|
||
"{review.content.slice(0, 140)}
|
||
{review.content.length > 140 ? "..." : ""}"
|
||
</p>
|
||
<footer className="mt-2 text-xs text-muted-foreground">
|
||
{review.authorName} on{" "}
|
||
<LocaleLink
|
||
href={`/tools/${review.toolSlug}`}
|
||
className="font-medium text-foreground hover:underline"
|
||
>
|
||
{review.toolName}
|
||
</LocaleLink>
|
||
{" · "}
|
||
{review.rating}/5
|
||
</footer>
|
||
</blockquote>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export const editorialTheme: ThemeModule = {
|
||
id: "editorial",
|
||
brand,
|
||
HomePage: EditorialHome,
|
||
};
|
||
|
||
export { navItems as editorialNavItems, footerGroups as editorialFooterGroups };
|