This commit is contained in:
1 parent
c1541b80df
commit
b4c7908029
97 files changed
+12403
-154
No files matched your search
@@ -0,0 +1,135 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Link } from "@/i18n/navigation";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Container, Section } from "@/components/ui/layout";
|
||||
import { SectionHeading } from "@/components/ui/section-heading";
|
||||
import { buttonClassName } from "@/components/ui/button-variants";
|
||||
import { PostCard } from "@/components/post/post-card";
|
||||
import { MotionItem } from "@/components/motion/motion-item";
|
||||
import { HomeHero } from "@/components/home/hero";
|
||||
import { getAllPosts, getAllTags } from "@/lib/posts";
|
||||
import { siteConfig } from "@/lib/site-config";
|
||||
|
||||
/**
|
||||
* 首页渲染策略:ISR 缓存 60 秒。
|
||||
*
|
||||
* 内容来自数据库,因此不在构建期固化;访问时生成并缓存,
|
||||
* 更新文章后最多 60 秒自动生效。
|
||||
*
|
||||
* 注意:Next.js 会静态分析路由段配置,这里必须写成**字面量**,
|
||||
* 不能引用从其他模块导入的常量,否则会报
|
||||
* "Invalid segment configuration export detected"。
|
||||
*/
|
||||
export const revalidate = 60;
|
||||
|
||||
type HomePageProps = {
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: HomePageProps): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: "Site" });
|
||||
|
||||
return {
|
||||
title: `${siteConfig.name} · ${t("tagline")}`,
|
||||
description: siteConfig.description,
|
||||
alternates: { canonical: `/${locale}` },
|
||||
};
|
||||
}
|
||||
|
||||
export default async function HomePage({ params }: HomePageProps) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
const t = await getTranslations("Home");
|
||||
|
||||
// Hero 需要的统计数据量很小,直接在此等待;
|
||||
// 文章列表区块用 Suspense 包裹,先出骨架再填充内容。
|
||||
const [posts, tags] = await Promise.all([getAllPosts(), getAllTags()]);
|
||||
const earliestPost = posts.at(-1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HomeHero
|
||||
eyebrow={t("eyebrow")}
|
||||
title={t("heroTitle")}
|
||||
subtitle={t("heroSubtitle")}
|
||||
primaryLabel={t("ctaPrimary")}
|
||||
secondaryLabel={t("ctaSecondary")}
|
||||
stats={{
|
||||
posts: posts.length,
|
||||
tags: tags.length,
|
||||
since: earliestPost ? earliestPost.date.slice(0, 4) : "—",
|
||||
postsLabel: t("statsPosts"),
|
||||
tagsLabel: t("statsTags"),
|
||||
sinceLabel: t("statsSince"),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Section id="posts" spacing="lg" aria-labelledby="latest-posts-heading">
|
||||
<Container>
|
||||
<SectionHeading
|
||||
eyebrow={t("latestEyebrow")}
|
||||
title={<span id="latest-posts-heading">{t("latestTitle")}</span>}
|
||||
description={t("latestDescription")}
|
||||
className="mb-xxl"
|
||||
/>
|
||||
|
||||
{posts.length === 0 ? (
|
||||
<EmptyState />
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-lg sm:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((post, index) => (
|
||||
<MotionItem
|
||||
key={post.slug}
|
||||
delay={Math.min(index, 6) * 0.05}
|
||||
className="h-full"
|
||||
>
|
||||
<PostCard post={post} className="h-full" />
|
||||
</MotionItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-xxl flex justify-center">
|
||||
<Link
|
||||
href="/about"
|
||||
className={buttonClassName({ variant: "secondary" })}
|
||||
>
|
||||
{t("ctaSecondary")}
|
||||
<ArrowRight className="size-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
</Container>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** 无文章时的空状态(skill 要求前端具备加载态 / 空态 / 错误态) */
|
||||
function EmptyState() {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-sm rounded-lg border border-dashed border-hairline bg-surface-soft px-xxl py-section text-center">
|
||||
<p className="text-heading-5 font-semibold text-ink">还没有已发布的文章</p>
|
||||
|
||||
<p className="max-w-[520px] text-body-sm text-steel">
|
||||
调用{" "}
|
||||
<code className="rounded-xs border border-hairline bg-surface px-[6px] py-[2px] font-mono text-code-inline">
|
||||
POST /api/posts
|
||||
</code>{" "}
|
||||
创建文章(或运行{" "}
|
||||
<code className="rounded-xs border border-hairline bg-surface px-[6px] py-[2px] font-mono text-code-inline">
|
||||
pnpm seed
|
||||
</code>{" "}
|
||||
导入示例内容),状态为{" "}
|
||||
<code className="rounded-xs border border-hairline bg-surface px-[6px] py-[2px] font-mono text-code-inline">
|
||||
published
|
||||
</code>{" "}
|
||||
的文章就会出现在这里。
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user