feat(home): show latest 7 blog titles in shortcuts

This commit is contained in:
Haitao Pan 2026-03-14 23:31:43 +08:00
parent f6b78cbbc7
commit ea7a45845b
2 changed files with 101 additions and 15 deletions

View File

@ -0,0 +1,35 @@
import { NextResponse } from "next/server";
import { getBlogPosts } from "@/lib/blogContent";
export const dynamic = "force-dynamic";
const DEFAULT_LIMIT = 7;
const MAX_LIMIT = 20;
function parseLimit(rawLimit: string | null): number {
if (!rawLimit) {
return DEFAULT_LIMIT;
}
const parsedLimit = Number.parseInt(rawLimit, 10);
if (!Number.isFinite(parsedLimit) || parsedLimit <= 0) {
return DEFAULT_LIMIT;
}
return Math.min(parsedLimit, MAX_LIMIT);
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const limit = parseLimit(searchParams.get("limit"));
const posts = await getBlogPosts();
const latestPosts = posts.slice(0, limit).map((post) => ({
slug: post.slug,
title: post.title,
date: post.date,
}));
return NextResponse.json(latestPosts);
}

View File

@ -70,10 +70,12 @@ export default function HomePage() {
<div className="min-h-screen bg-background text-text transition-colors duration-150 flex flex-col">
<UnifiedNavigation />
<div className={cn(
"flex flex-1 relative overflow-hidden",
mode === 'left-sidebar' && isOpen && "flex-row-reverse"
)}>
<div
className={cn(
"flex flex-1 relative overflow-hidden",
mode === "left-sidebar" && isOpen && "flex-row-reverse",
)}
>
<div className="flex-1 overflow-y-auto relative">
<div className="relative mx-auto max-w-6xl px-6 pb-20">
<div
@ -119,7 +121,7 @@ export function HeroSection() {
{user ? (
<div className="flex items-center gap-2 rounded-full border border-success/30 bg-success/10 px-4 py-1.5 text-sm font-medium text-success">
<div className="h-2 w-2 rounded-full bg-success animate-pulse" />
{t.signedIn.replace('{{username}}', user.username)}
{t.signedIn.replace("{{username}}", user.username)}
</div>
) : (
<button className="flex items-center gap-2 rounded-full bg-primary px-6 py-2.5 text-sm font-semibold text-white transition hover:bg-primary-hover">
@ -230,7 +232,7 @@ export function StatsSection() {
refreshInterval: 60 * 60 * 1000,
revalidateOnFocus: false,
shouldRetryOnError: false,
}
},
);
const locale = language === "zh" ? "zh-CN" : "en-US";
@ -243,11 +245,14 @@ export function StatsSection() {
const registeredUsersValue =
typeof data?.registeredUsers === "number"
? numberFormatter.format(data.registeredUsers)
: t.stats[0]?.value ?? "0+";
: (t.stats[0]?.value ?? "0+");
const dailyVisits = typeof data?.visits?.daily === "number" ? data.visits.daily : null;
const weeklyVisits = typeof data?.visits?.weekly === "number" ? data.visits.weekly : null;
const monthlyVisits = typeof data?.visits?.monthly === "number" ? data.visits.monthly : null;
const dailyVisits =
typeof data?.visits?.daily === "number" ? data.visits.daily : null;
const weeklyVisits =
typeof data?.visits?.weekly === "number" ? data.visits.weekly : null;
const monthlyVisits =
typeof data?.visits?.monthly === "number" ? data.visits.monthly : null;
const displayStats = [
{
@ -255,15 +260,24 @@ export function StatsSection() {
label: t.statsLabels.registeredUsers,
},
{
value: typeof dailyVisits === "number" ? compactFormatter.format(dailyVisits) : (t.stats[1]?.value ?? "0+"),
value:
typeof dailyVisits === "number"
? compactFormatter.format(dailyVisits)
: (t.stats[1]?.value ?? "0+"),
label: t.statsLabels.dailyVisits,
},
{
value: typeof weeklyVisits === "number" ? compactFormatter.format(weeklyVisits) : "0+",
value:
typeof weeklyVisits === "number"
? compactFormatter.format(weeklyVisits)
: "0+",
label: t.statsLabels.weeklyVisits,
},
{
value: typeof monthlyVisits === "number" ? compactFormatter.format(monthlyVisits) : "0+",
value:
typeof monthlyVisits === "number"
? compactFormatter.format(monthlyVisits)
: "0+",
label: t.statsLabels.monthlyVisits,
},
t.stats[2],
@ -298,6 +312,37 @@ type HomeStatsResponse = {
export function ShortcutsSection() {
const { language } = useLanguage();
const t = translations[language].marketing.home;
const { data: latestBlogs } = useSWR<LatestBlogPost[]>(
"/api/blogs/latest?limit=7",
async (url: string) => {
const response = await fetch(url, { cache: "no-store" });
if (!response.ok) {
throw new Error(`Failed to load latest blogs: ${response.status}`);
}
return (await response.json()) as LatestBlogPost[];
},
{
refreshInterval: 10 * 60 * 1000,
revalidateOnFocus: false,
shouldRetryOnError: false,
},
);
const shortcutItems =
latestBlogs && latestBlogs.length > 0
? latestBlogs.map((post) => ({
title: post.title,
description: post.date
? new Date(post.date).toLocaleDateString(
language === "zh" ? "zh-CN" : "en-US",
)
: t.shortcuts.subtitle,
href: `/blogs/${post.slug}`,
}))
: t.shortcuts.items.map((item) => ({
...item,
href: "#",
}));
return (
<section className="space-y-4">
@ -321,12 +366,12 @@ export function ShortcutsSection() {
</div>
</div>
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
{t.shortcuts.items.map((item, index: number) => {
{shortcutItems.map((item, index: number) => {
const Icon = getIcon(item.title, Sparkles);
return (
<a
key={index}
href="#"
href={item.href}
className="group flex items-start gap-3 rounded-xl border border-surface-border bg-surface p-4 transition hover:-translate-y-[1px] hover:border-primary/50 hover:bg-surface-hover"
>
<div className="mt-1 flex h-10 w-10 items-center justify-center rounded-full bg-primary/15 text-primary">
@ -350,6 +395,12 @@ export function ShortcutsSection() {
);
}
type LatestBlogPost = {
slug: string;
title: string;
date?: string;
};
function LogoPill({ label }: { label: string }) {
return (
<span className="inline-flex items-center gap-2 rounded-full border border-surface-border bg-surface-muted px-3 py-1 text-xs font-semibold text-text">