From 02ae938c238c9d18448d17a8ec92c0edd8c17463 Mon Sep 17 00:00:00 2001 From: Bertrand Yuan Date: Tue, 16 Dec 2025 00:12:49 +0800 Subject: feat(back-end): introduce payload Payload is the next.js Headless CMS and App Framework, I would like to pick it up and modify it as it is MIT licensed. Many features in Payload is not applicable for our project. So, I modify it so that it is light and clear. --- src/app/(main)/(home)/posts/page.tsx | 203 +++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 src/app/(main)/(home)/posts/page.tsx (limited to 'src/app/(main)/(home)/posts/page.tsx') diff --git a/src/app/(main)/(home)/posts/page.tsx b/src/app/(main)/(home)/posts/page.tsx new file mode 100644 index 0000000..00e51c4 --- /dev/null +++ b/src/app/(main)/(home)/posts/page.tsx @@ -0,0 +1,203 @@ +import { postsPerPage } from '@/app/(main)/layout.config'; +import { NumberedPagination } from '@/components/numbered-pagination'; +import { PostCard } from '@/components/posts/post-card'; +import { Section } from '@/components/section'; +import { createMetadata } from '@/lib/metadata'; +import { getPublishedPosts, type BlogPost } from '@/lib/payload-posts'; +import { getSortedByDatePosts, type Page } from '@/lib/source'; +import type { Metadata, ResolvingMetadata } from 'next'; +import { notFound, redirect } from 'next/navigation'; + +// 统一的文章类型 +interface UnifiedPost { + id: string; + title: string; + description: string; + image?: string; + url: string; + date: Date; + author: string; + tags?: string[]; + source: 'mdx' | 'payload'; +} + +// 将 MDX 文章转换为统一格式 +function transformMdxPost(post: Page): UnifiedPost { + return { + id: `mdx-${post.url}`, + title: post.data.title, + description: post.data.description ?? '', + image: post.data.image, + url: post.url, + date: new Date(post.data.date), + author: post.data.author, + tags: post.data.tags, + source: 'mdx', + }; +} + +// 将 Payload 文章转换为统一格式 +function transformPayloadPost(post: BlogPost): UnifiedPost { + return { + id: `payload-${post.id}`, + title: post.title, + description: post.description, + image: post.image, + url: post.url, + date: post.date, + author: post.author, + tags: post.tags, + source: 'payload', + }; +} + +// 获取所有文章(合并 MDX 和 Payload) +async function getAllPosts(): Promise { + // 获取 MDX 文章 + const mdxPosts = getSortedByDatePosts().map(transformMdxPost); + + // 获取 Payload 文章(获取全部,用于分页计算) + const { posts: payloadPosts } = await getPublishedPosts({ limit: 1000 }); + const transformedPayloadPosts = payloadPosts.map(transformPayloadPost); + + // 合并并按日期排序 + const allPosts = [...mdxPosts, ...transformedPayloadPosts]; + allPosts.sort((a, b) => b.date.getTime() - a.date.getTime()); + + return allPosts; +} + +const CurrentPostsCount = ({ + startIndex, + endIndex, + totalPosts, +}: { + startIndex: number; + endIndex: number; + totalPosts: number; +}) => { + const start = startIndex + 1; + const end = endIndex < totalPosts ? endIndex : totalPosts; + if (start === end) return ({start}); + return ( + + ({start}-{end}) + + ); +}; + +const Pagination = ({ + pageIndex, + pageCount, +}: { + pageIndex: number; + pageCount: number; +}) => { + const handlePageChange = async (page: number) => { + 'use server'; + redirect(`/posts?page=${page}`); + }; + + return ( +
+ +
+ ); +}; + +export default async function Page(props: { + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +}) { + const searchParams = await props.searchParams; + + // 获取所有文章 + const allPosts = await getAllPosts(); + const totalPosts = allPosts.length; + const pageCount = Math.ceil(totalPosts / postsPerPage); + + const pageIndex = searchParams.page + ? Number.parseInt( + Array.isArray(searchParams.page) + ? searchParams.page[0] ?? '' + : searchParams.page, + 10 + ) - 1 + : 0; + + if (pageIndex < 0 || (pageCount > 0 && pageIndex >= pageCount)) notFound(); + + const startIndex = pageIndex * postsPerPage; + const endIndex = startIndex + postsPerPage; + const posts = allPosts.slice(startIndex, endIndex); + + return ( + <> +
+

+ All {totalPosts} Posts{' '} + +

+
+
+
+ {posts.map((post) => { + const date = post.date.toDateString(); + return ( + + ); + })} +
+
+ {pageCount > 1 && } + + ); +} + +type Props = { + params: Promise<{ slug: string[] }>; + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +}; + +export async function generateMetadata( + props: Props, + parent: ResolvingMetadata +): Promise { + const searchParams = await props.searchParams; + + const pageIndex = searchParams.page + ? Number.parseInt(searchParams.page as string, 10) + : 1; + + const isFirstPage = pageIndex === 1 || !searchParams.page; + const pageTitle = isFirstPage ? 'Posts' : `Posts - Page ${pageIndex}`; + const canonicalUrl = isFirstPage ? '/posts' : `/posts?page=${pageIndex}`; + + return createMetadata({ + title: pageTitle, + description: `Posts${!isFirstPage ? ` - Page ${pageIndex}` : ''}`, + openGraph: { + url: canonicalUrl, + }, + alternates: { + canonical: canonicalUrl, + }, + }); +} -- cgit v1.2.3