feat: add pagination and fix blog post links

### Changes to Blog Listing Page (src/app/blog/page.tsx):

1. **Implemented Pagination**
   - Added support for `page` query parameter (e.g., `/blog?page=2`)
   - Displays 10 posts per page
   - Shows pagination controls with Previous/Next buttons
   - Page numbers displayed as clickable buttons
   - Active page highlighted with brand color
   - Handles edge cases (invalid page numbers, out of bounds)

2. **Fixed "Read more" Links**
   - Changed from `/blog` to `/blog/${post.slug}`
   - Each blog card's "Read more" now links to individual article page
   - Clicking "Read more" on Cloud-Neutral-Hybrid-Network-Architecture → `/blog/Cloud-Neutral-Hybrid-Network-Architecture`

3. **Features**
   - **Server-side pagination**: Posts are sliced on the server
   - **URL-based navigation**: Pagination state in URL
   - **Responsive design**: Clean pagination controls
   - **Error handling**: 404 for invalid pages
   - **Navigation**: Previous/Next buttons with disabled states
   - **Accessibility**: aria-disabled attribute on navigation

### Example URLs:
- `/blog` - First page (default)
- `/blog?page=1` - First page
- `/blog?page=2` - Second page
- Each post card → `/blog/[post-slug]`

### Result:
-  Blog list supports pagination
-  "Read more" links work correctly
-  Better UX for browsing multiple blog posts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Haitao Pan 2025-11-11 13:42:11 +08:00
parent f19694cf24
commit 0f727aa36a

View File

@ -1,6 +1,7 @@
import Link from 'next/link'
import { getHomepagePosts } from '@cms/content'
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
export const metadata: Metadata = {
title: 'Blog | Cloud-Neutral',
@ -27,8 +28,23 @@ function formatDate(dateStr: string | undefined, language: 'zh' | 'en'): string
}
}
export default async function BlogPage() {
type PageProps = {
searchParams: { page?: string }
}
export default async function BlogPage({ searchParams }: PageProps) {
const posts = await getHomepagePosts()
const postsPerPage = 10
const currentPage = parseInt(searchParams.page || '1', 10)
const totalPages = Math.ceil(posts.length / postsPerPage)
if (currentPage < 1 || currentPage > totalPages) {
notFound()
}
const startIndex = (currentPage - 1) * postsPerPage
const endIndex = startIndex + postsPerPage
const paginatedPosts = posts.slice(startIndex, endIndex)
return (
<main className="flex min-h-screen flex-col bg-slate-50">
@ -45,48 +61,92 @@ export default async function BlogPage() {
<p className="text-slate-500"></p>
</div>
) : (
<div className="grid gap-8">
{posts.map((post) => (
<article
key={post.slug}
className="rounded-2xl border border-slate-200 bg-white p-8 shadow-sm transition hover:shadow-md"
>
<div className="mb-4 flex items-center justify-between">
<span className="text-sm font-semibold text-brand">Blog</span>
{post.date && (
<time className="text-sm text-slate-500">
{formatDate(post.date, 'en')}
</time>
)}
</div>
<h2 className="mb-4 text-2xl font-bold text-slate-900">{post.title}</h2>
{post.author && (
<p className="mb-4 text-sm text-slate-500">By {post.author}</p>
)}
<p className="mb-6 text-slate-600">{post.excerpt}</p>
<div className="flex items-center gap-4">
{post.tags && post.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{post.tags.map((tag) => (
<span
key={tag}
className="rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-700"
>
{tag}
</span>
))}
</div>
<>
<div className="grid gap-8">
{paginatedPosts.map((post) => (
<article
key={post.slug}
className="rounded-2xl border border-slate-200 bg-white p-8 shadow-sm transition hover:shadow-md"
>
<div className="mb-4 flex items-center justify-between">
<span className="text-sm font-semibold text-brand">Blog</span>
{post.date && (
<time className="text-sm text-slate-500">
{formatDate(post.date, 'en')}
</time>
)}
</div>
<h2 className="mb-4 text-2xl font-bold text-slate-900">{post.title}</h2>
{post.author && (
<p className="mb-4 text-sm text-slate-500">By {post.author}</p>
)}
<p className="mb-6 text-slate-600">{post.excerpt}</p>
<div className="flex items-center gap-4">
{post.tags && post.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{post.tags.map((tag) => (
<span
key={tag}
className="rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-700"
>
{tag}
</span>
))}
</div>
)}
<Link
href={`/blog/${post.slug}`}
className="ml-auto text-sm font-semibold text-brand transition hover:text-brand-dark"
>
Read more
</Link>
</div>
</article>
))}
</div>
{totalPages > 1 && (
<nav className="mt-12 flex items-center justify-center gap-2">
<Link
href={`/blog?page=${currentPage - 1}`}
className={`px-4 py-2 text-sm font-semibold rounded-lg transition ${
currentPage === 1
? 'cursor-not-allowed text-slate-400'
: 'text-brand hover:bg-slate-100'
}`}
aria-disabled={currentPage === 1}
>
Previous
</Link>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<Link
href="/blog"
className="ml-auto text-sm font-semibold text-brand transition hover:text-brand-dark"
key={page}
href={`/blog?page=${page}`}
className={`px-4 py-2 text-sm font-semibold rounded-lg transition ${
page === currentPage
? 'bg-brand text-white'
: 'text-slate-700 hover:bg-slate-100'
}`}
>
Read more
{page}
</Link>
</div>
</article>
))}
</div>
))}
<Link
href={`/blog?page=${currentPage + 1}`}
className={`px-4 py-2 text-sm font-semibold rounded-lg transition ${
currentPage === totalPages
? 'cursor-not-allowed text-slate-400'
: 'text-brand hover:bg-slate-100'
}`}
aria-disabled={currentPage === totalPages}
>
Next
</Link>
</nav>
)}
</>
)}
</div>
</main>