diff options
| author | Bertrand Yuan <bert.yuan@outlook.com> | 2025-12-16 00:15:04 +0800 |
|---|---|---|
| committer | Bertrand Yuan <bert.yuan@outlook.com> | 2025-12-16 00:15:04 +0800 |
| commit | 785371bb3eccca455e5ce5fccbe9b6e3752a03f6 (patch) | |
| tree | dd006593448c3500bdcb414af3b4656f7a7683d4 /src | |
| parent | 02ae938c238c9d18448d17a8ec92c0edd8c17463 (diff) | |
fix(front-end): bug in viewing posts
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/(main)/(home)/_components/posts.tsx | 58 | ||||
| -rw-r--r-- | src/app/(main)/(home)/actions.ts | 15 | ||||
| -rw-r--r-- | src/app/(main)/(home)/page.tsx | 9 | ||||
| -rw-r--r-- | src/app/(main)/(home)/posts/[slug]/page.tsx | 185 | ||||
| -rw-r--r-- | src/app/(main)/(home)/posts/page.tsx | 84 | ||||
| -rw-r--r-- | src/app/(main)/(home)/tags/[...slug]/page.tsx | 80 | ||||
| -rw-r--r-- | src/app/(main)/(home)/tags/page.tsx | 13 | ||||
| -rw-r--r-- | src/app/(main)/api/search/route.ts | 27 | ||||
| -rw-r--r-- | src/app/(main)/og/[...slug]/route.tsx | 48 | ||||
| -rw-r--r-- | src/app/(main)/rss.xml/route.ts | 36 | ||||
| -rw-r--r-- | src/components/json-ld.tsx | 29 | ||||
| -rw-r--r-- | src/components/sections/footer.tsx | 30 | ||||
| -rw-r--r-- | src/components/tags/tag-card.tsx | 9 | ||||
| -rw-r--r-- | src/lib/metadata-image.ts | 7 | ||||
| -rw-r--r-- | src/lib/source.ts | 40 |
15 files changed, 192 insertions, 478 deletions
diff --git a/src/app/(main)/(home)/_components/posts.tsx b/src/app/(main)/(home)/_components/posts.tsx index 00ada0c..0eacce1 100644 --- a/src/app/(main)/(home)/_components/posts.tsx +++ b/src/app/(main)/(home)/_components/posts.tsx @@ -2,73 +2,25 @@ import { Icons } from '@/components/icons/icons'; import { PostCard } from '@/components/posts/post-card'; import { Section } from '@/components/section'; import { buttonVariants } from '@/components/ui/button'; -import type { Page } from '@/lib/source'; import type { BlogPost } from '@/lib/payload-posts'; import Link from 'next/link'; -// 统一的文章数据格式 -interface UnifiedPost { - title: string; - description: string; - image?: string | null; - url: string; - date: string; - author: string; - tags?: string[]; -} - -// 将 MDX Page 转换为统一格式 -function transformMdxPost(post: Page): UnifiedPost { - return { - title: post.data.title, - description: post.data.description ?? '', - image: post.data.image, - url: post.url, - date: new Date(post.data.date).toDateString(), - author: post.data.author, - tags: post.data.tags, - }; -} - -// 将 Payload BlogPost 转换为统一格式 -function transformPayloadPost(post: BlogPost): UnifiedPost { - return { - title: post.title, - description: post.description, - image: post.image, - url: post.url, - date: post.date.toDateString(), - author: post.author, - tags: post.tags, - }; -} - interface PostsProps { - mdxPosts?: Page[]; - payloadPosts?: BlogPost[]; + posts: BlogPost[]; } -export default function Posts({ mdxPosts = [], payloadPosts = [] }: PostsProps) { - // 转换并合并所有文章 - const allPosts: UnifiedPost[] = [ - ...mdxPosts.map(transformMdxPost), - ...payloadPosts.map(transformPayloadPost), - ]; - - // 按日期排序 - allPosts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); - +export default function Posts({ posts }: PostsProps) { return ( <Section> <div className='grid divide-y divide-dashed divide-border/70 text-left dark:divide-border'> - {allPosts.map((post) => ( + {posts.map((post) => ( <PostCard title={post.title} description={post.description} image={post.image} url={post.url} - date={post.date} - key={post.url} + date={post.date.toDateString()} + key={post.id} author={post.author} tags={post.tags} /> diff --git a/src/app/(main)/(home)/actions.ts b/src/app/(main)/(home)/actions.ts index fdb16ca..5b0c456 100644 --- a/src/app/(main)/(home)/actions.ts +++ b/src/app/(main)/(home)/actions.ts @@ -1,8 +1,7 @@ 'use server'; -import { getContact, sendWelcomeEmail, updateContact } from '@/lib/resend'; +import { getContact, updateContact } from '@/lib/resend'; import { ActionError, actionClient } from '@/lib/safe-action'; -import { getSortedByDatePosts } from '@/lib/source'; import { NewsletterSchema } from '@/lib/validators'; import { getSession } from '@/server/auth'; import { Resend } from 'resend'; @@ -59,12 +58,12 @@ export const subscribeUser = actionClient ); } - const posts = getSortedByDatePosts(); - await sendWelcomeEmail({ - posts, - to: email, - firstName: firstName || 'there', - }); + // const posts = getSortedByDatePosts(); + // await sendWelcomeEmail({ + // posts, + // to: email, + // firstName: firstName || 'there', + // }); return { success: true, diff --git a/src/app/(main)/(home)/page.tsx b/src/app/(main)/(home)/page.tsx index a94193c..0718da9 100644 --- a/src/app/(main)/(home)/page.tsx +++ b/src/app/(main)/(home)/page.tsx @@ -3,16 +3,11 @@ import Posts from '@/app/(main)/(home)/_components/posts'; import { Icons } from '@/components/icons/icons'; import { Section } from '@/components/section'; import Separator from '@/components/separator'; -import { getSortedByDatePosts } from '@/lib/source'; import { getPublishedPosts } from '@/lib/payload-posts'; import { CTA } from './_components/call-to-action'; export default async function Home() { - // 获取 MDX 文章(保留原有功能) - // const mdxPosts = getSortedByDatePosts().slice(0, 3); - - // 获取 Payload CMS 文章 - const { posts: payloadPosts } = await getPublishedPosts({ limit: 3 }); + const { posts } = await getPublishedPosts({ limit: 3 }); return ( <> @@ -26,7 +21,7 @@ export default async function Home() { </h2> </Section> <Separator /> - <Posts mdxPosts={[]} payloadPosts={payloadPosts} /> + <Posts posts={posts} /> <Separator /> <CTA /> </> diff --git a/src/app/(main)/(home)/posts/[slug]/page.tsx b/src/app/(main)/(home)/posts/[slug]/page.tsx index fa096b6..5960f06 100644 --- a/src/app/(main)/(home)/posts/[slug]/page.tsx +++ b/src/app/(main)/(home)/posts/[slug]/page.tsx @@ -7,55 +7,18 @@ import { RichText } from '@/components/rich-text'; import { Section } from '@/components/section'; import { TagCard } from '@/components/tags/tag-card'; import { createMetadata } from '@/lib/metadata'; -import { metadataImage } from '@/lib/metadata-image'; import { getPostBySlug, getAllPostSlugs, type BlogPost, } from '@/lib/payload-posts'; -import { type Page as MDXPage, getPost, getPosts } from '@/lib/source'; import { cn } from '@/lib/utils'; -import { File, Files, Folder } from 'fumadocs-ui/components/files'; -import { InlineTOC } from 'fumadocs-ui/components/inline-toc'; -import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; -import defaultMdxComponents from 'fumadocs-ui/mdx'; import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; import Balancer from 'react-wrap-balancer'; import { description as homeDescription } from '@/app/(main)/layout.config'; -// MDX 文章 Header -function MdxHeader(props: { page: MDXPage; tags?: string[] }) { - const { page, tags } = props; - - return ( - <Section className="p-4 lg:p-6"> - <div - className={cn( - 'flex flex-col items-start justify-center gap-4 py-8 md:gap-6', - 'sm:items-center sm:rounded-lg sm:border sm:bg-muted/70 sm:px-8 sm:py-20 sm:shadow-xs sm:dark:bg-muted' - )} - > - <div className="flex flex-col gap-2 sm:text-center md:gap-4"> - <h1 className="max-w-4xl font-bold text-3xl leading-tight tracking-tight sm:text-4xl sm:leading-tight md:text-5xl md:leading-tight"> - <Balancer>{page.data.title}</Balancer> - </h1> - <p className="mx-auto max-w-4xl"> - <Balancer>{page.data.description}</Balancer> - </p> - </div> - <div className="flex flex-wrap gap-2"> - {tags?.map((tag) => ( - <TagCard name={tag} key={tag} className=" border border-border " /> - ))} - </div> - </div> - </Section> - ); -} - -// Payload 文章 Header -function PayloadHeader(props: { post: BlogPost }) { +function PostHeader(props: { post: BlogPost }) { const { post } = props; return ( @@ -84,70 +47,10 @@ function PayloadHeader(props: { post: BlogPost }) { ); } -// MDX 文章内容 -function MdxContent({ page }: { page: MDXPage }) { - const { body: Mdx, toc, tags, lastModified } = page.data; - const lastUpdate = lastModified ? new Date(lastModified) : undefined; - - return ( - <> - <MdxHeader page={page} tags={tags} /> - - <Section className="h-full" sectionClassName="flex flex-1"> - <article className="flex min-h-full flex-col lg:flex-row"> - <div className="flex flex-1 flex-col gap-4"> - <InlineTOC - items={toc} - className="rounded-none border-0 border-border/70 border-b border-dashed dark:border-border" - /> - <div className="prose min-w-0 flex-1 px-4"> - <Mdx - components={{ - ...defaultMdxComponents, - File, - Files, - Folder, - Tabs, - Tab, - }} - /> - </div> - <PostComments - slug={page.slugs[0] ?? ''} - className="[&_form>div]:!rounded-none rounded-none border-0 border-border/70 border-t border-dashed dark:border-border" - /> - </div> - <div className="flex flex-col gap-4 p-4 text-sm lg:sticky lg:top-[4rem] lg:h-[calc(100vh-4rem)] lg:w-[250px] lg:self-start lg:overflow-y-auto lg:border-border/70 lg:border-l lg:border-dashed lg:dark:border-border"> - <div> - <p className="mb-1 text-fd-muted-foreground">Written by</p> - <p className="font-medium">{page.data.author}</p> - </div> - <div> - <p className="mb-1 text-fd-muted-foreground text-sm">Created At</p> - <p className="font-medium"> - {new Date(page.data.date ?? page.file.name).toDateString()} - </p> - </div> - {lastUpdate && ( - <div> - <p className="mb-1 text-fd-muted-foreground text-sm">Updated At</p> - <p className="font-medium">{lastUpdate.toDateString()}</p> - </div> - )} - <Share url={page.url} /> - </div> - </article> - </Section> - <PostJsonLd page={page} /> - </> - ); -} - -// Payload 文章内容 -function PayloadContent({ post }: { post: BlogPost }) { +function PostContent({ post }: { post: BlogPost }) { return ( <> - <PayloadHeader post={post} /> + <PostHeader post={post} /> <Section className="h-full" sectionClassName="flex flex-1"> <article className="flex min-h-full flex-col lg:flex-row"> @@ -179,6 +82,7 @@ function PayloadContent({ post }: { post: BlogPost }) { </div> </article> </Section> + <PostJsonLd post={post} /> </> ); } @@ -187,80 +91,39 @@ export default async function Page(props: { params: Promise<{ slug: string }>; }) { const params = await props.params; + const post = await getPostBySlug(params.slug); - // 先尝试获取 MDX 文章 - const mdxPage = getPost([params.slug]); - - if (mdxPage) { - return <MdxContent page={mdxPage} />; - } - - // 再尝试获取 Payload 文章 - const payloadPost = await getPostBySlug(params.slug); - - if (payloadPost) { - return <PayloadContent post={payloadPost} />; + if (!post) { + notFound(); } - // 都找不到则 404 - notFound(); + return <PostContent post={post} />; } export async function generateMetadata(props: { params: Promise<{ slug: string }>; }): Promise<Metadata> { const params = await props.params; + const post = await getPostBySlug(params.slug); - // 先尝试 MDX 文章 - const mdxPage = getPost([params.slug]); - - if (mdxPage) { - const title = mdxPage.data.title; - const description = mdxPage.data.description ?? homeDescription; - - return createMetadata( - metadataImage.withImage(mdxPage.slugs, { - title, - description, - openGraph: { - url: `/posts/${mdxPage.slugs.join('/')}`, - }, - alternates: { - canonical: mdxPage.url, - }, - }) - ); + if (!post) { + return {}; } - // 再尝试 Payload 文章 - const payloadPost = await getPostBySlug(params.slug); - - if (payloadPost) { - return createMetadata({ - title: payloadPost.title, - description: payloadPost.description || homeDescription, - openGraph: { - url: `/posts/${payloadPost.slug}`, - }, - alternates: { - canonical: `/posts/${payloadPost.slug}`, - }, - }); - } - - return {}; + return createMetadata({ + title: post.title, + description: post.description || homeDescription, + openGraph: { + url: `/posts/${post.slug}`, + images: post.image ? [{ url: post.image }] : undefined, + }, + alternates: { + canonical: `/posts/${post.slug}`, + }, + }); } export async function generateStaticParams(): Promise<{ slug: string }[]> { - // MDX 文章的 slugs - const mdxSlugs = getPosts() - .map((page) => page.slugs[0]) - .filter((slug): slug is string => !!slug) - .map((slug) => ({ slug })); - - // Payload 文章的 slugs - const payloadSlugs = await getAllPostSlugs(); - const payloadParams = payloadSlugs.map((slug) => ({ slug })); - - return [...mdxSlugs, ...payloadParams]; + const slugs = await getAllPostSlugs(); + return slugs.map((slug) => ({ slug })); } diff --git a/src/app/(main)/(home)/posts/page.tsx b/src/app/(main)/(home)/posts/page.tsx index 00e51c4..40ebcda 100644 --- a/src/app/(main)/(home)/posts/page.tsx +++ b/src/app/(main)/(home)/posts/page.tsx @@ -3,70 +3,10 @@ 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 { getPublishedPosts } from '@/lib/payload-posts'; 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<UnifiedPost[]> { - // 获取 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, @@ -115,11 +55,6 @@ export default async function Page(props: { }) { 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) @@ -129,21 +64,26 @@ export default async function Page(props: { ) - 1 : 0; - if (pageIndex < 0 || (pageCount > 0 && pageIndex >= pageCount)) notFound(); + // 获取文章(带分页) + const { posts, totalDocs, totalPages } = await getPublishedPosts({ + limit: postsPerPage, + page: pageIndex + 1, + }); + + if (pageIndex < 0 || (totalPages > 0 && pageIndex >= totalPages)) notFound(); const startIndex = pageIndex * postsPerPage; - const endIndex = startIndex + postsPerPage; - const posts = allPosts.slice(startIndex, endIndex); + const endIndex = startIndex + posts.length; return ( <> <Section className='p-4 lg:p-6'> <h1 className='font-bold text-3xl leading-tight tracking-tighter md:text-4xl'> - All {totalPosts} Posts{' '} + All {totalDocs} Posts{' '} <CurrentPostsCount startIndex={startIndex} endIndex={endIndex} - totalPosts={totalPosts} + totalPosts={totalDocs} /> </h1> </Section> @@ -166,7 +106,7 @@ export default async function Page(props: { })} </div> </Section> - {pageCount > 1 && <Pagination pageIndex={pageIndex} pageCount={pageCount} />} + {totalPages > 1 && <Pagination pageIndex={pageIndex} pageCount={totalPages} />} </> ); } diff --git a/src/app/(main)/(home)/tags/[...slug]/page.tsx b/src/app/(main)/(home)/tags/[...slug]/page.tsx index c1ce96b..71615cb 100644 --- a/src/app/(main)/(home)/tags/[...slug]/page.tsx +++ b/src/app/(main)/(home)/tags/[...slug]/page.tsx @@ -5,28 +5,21 @@ import { NumberedPagination } from '@/components/numbered-pagination'; import { PostCard } from '@/components/posts/post-card'; import { Section } from '@/components/section'; import { createMetadata } from '@/lib/metadata'; -import { getPostsByTag, getTags } from '@/lib/source'; +import { getPostsByTag, getAllTags } from '@/lib/payload-posts'; import type { Metadata, ResolvingMetadata } from 'next'; import { notFound, redirect } from 'next/navigation'; -export const dynamicParams = false; - -const totalPosts = (title: string) => getPostsByTag(title).length; -const pageCount = (title: string) => - Math.ceil(totalPosts(title) / postsPerPage); - const CurrentPostsCount = ({ startIndex, endIndex, - tag, + totalPosts, }: { startIndex: number; endIndex: number; - tag: string; + totalPosts: number; }) => { - const total = totalPosts(tag); const start = startIndex + 1; - const end = endIndex < total ? endIndex : total; + const end = endIndex < totalPosts ? endIndex : totalPosts; if (start === end) return <span>({start})</span>; return ( <span> @@ -39,10 +32,12 @@ const Header = ({ tag, startIndex, endIndex, + totalPosts, }: { tag: string; startIndex: number; endIndex: number; + totalPosts: number; }) => ( <Section className='p-4 lg:p-6'> <div className='flex items-center gap-2'> @@ -55,14 +50,14 @@ const Header = ({ <CurrentPostsCount startIndex={startIndex} endIndex={endIndex} - tag={tag} + totalPosts={totalPosts} /> </h1> </div> </Section> ); -const Pagination = ({ pageIndex, tag }: { pageIndex: number; tag: string }) => { +const Pagination = ({ pageIndex, tag, totalPages }: { pageIndex: number; tag: string; totalPages: number }) => { const handlePageChange = async (page: number) => { 'use server'; redirect(`/tags/${tag}?page=${page}`); @@ -72,7 +67,7 @@ const Pagination = ({ pageIndex, tag }: { pageIndex: number; tag: string }) => { <Section className='bg-dashed'> <NumberedPagination currentPage={pageIndex + 1} - totalPages={pageCount(tag)} + totalPages={totalPages} paginationItemsToDisplay={5} onPageChange={handlePageChange} /> @@ -91,54 +86,56 @@ export default async function Page(props: { if (!tag) return notFound(); const pageIndex = searchParams.page - ? Number.parseInt(searchParams.page[0] ?? '', 10) - 1 + ? Number.parseInt( + Array.isArray(searchParams.page) + ? searchParams.page[0] ?? '' + : searchParams.page, + 10 + ) - 1 : 0; - if (pageIndex < 0 || pageIndex >= pageCount(tag)) notFound(); + const { posts, totalDocs, totalPages } = await getPostsByTag(tag, { + limit: postsPerPage, + page: pageIndex + 1, + }); + + if (pageIndex < 0 || (totalPages > 0 && pageIndex >= totalPages)) notFound(); const startIndex = pageIndex * postsPerPage; - const endIndex = startIndex + postsPerPage; - const posts = getPostsByTag(tag).slice(startIndex, endIndex); + const endIndex = startIndex + posts.length; return ( <> - <Header tag={tag} startIndex={startIndex} endIndex={endIndex} /> + <Header tag={tag} startIndex={startIndex} endIndex={endIndex} totalPosts={totalDocs} /> <Section className='h-full' sectionClassName='flex flex-1'> <div className='grid divide-y divide-dashed divide-border/70 text-left dark:divide-border'> {posts.map((post) => { - const date = new Date(post.data.date).toDateString(); + const date = post.date.toDateString(); return ( <PostCard - title={post.data.title} - description={post.data.description ?? ''} - image={post.data.image} + title={post.title} + description={post.description} + image={post.image} url={post.url} date={date} - key={post.url} - author={post.data.author} - tags={post.data.tags} + key={post.id} + author={post.author} + tags={post.tags} /> ); })} </div> </Section> - {pageCount(tag) > 1 && <Pagination pageIndex={pageIndex} tag={tag} />} + {totalPages > 1 && <Pagination pageIndex={pageIndex} tag={tag} totalPages={totalPages} />} <TagJsonLd tag={tag} /> </> ); } -export const generateStaticParams = () => { - const tags = getTags(); - return [ - ...tags.map((tag) => ({ slug: [tag] })), - ...tags.flatMap((tag) => - Array.from({ length: pageCount(tag) }, (_, index) => ({ - slug: [tag, (index + 1).toString()], - })), - ), - ]; -}; +export async function generateStaticParams() { + const tags = await getAllTags(); + return tags.map((item) => ({ slug: [item.tag] })); +} type Props = { params: Promise<{ slug: string[] }>; @@ -154,7 +151,12 @@ export async function generateMetadata( const tag = params.slug[0]; const pageIndex = searchParams.page - ? Number.parseInt(searchParams.page.toString(), 10) + ? Number.parseInt( + Array.isArray(searchParams.page) + ? searchParams.page[0] ?? '' + : searchParams.page, + 10 + ) : 1; const isFirstPage = pageIndex === 1 || !searchParams.page; diff --git a/src/app/(main)/(home)/tags/page.tsx b/src/app/(main)/(home)/tags/page.tsx index 9138fde..6db13fc 100644 --- a/src/app/(main)/(home)/tags/page.tsx +++ b/src/app/(main)/(home)/tags/page.tsx @@ -2,12 +2,12 @@ import { title as homeTitle } from '@/app/(main)/layout.config'; import { Section } from '@/components/section'; import { TagCard } from '@/components/tags/tag-card'; import { createMetadata } from '@/lib/metadata'; -import { getTags } from '@/lib/source'; +import { getAllTags } from '@/lib/payload-posts'; import { cn } from '@/lib/utils'; import type { Metadata } from 'next'; -export default function Page() { - const tags = getTags(); +export default async function Page() { + const tags = await getAllTags(); return ( <> @@ -18,11 +18,12 @@ export default function Page() { </Section> <Section className='h-full' sectionClassName='flex flex-1'> <div className='grid grid-cols-1 divide-y divide-dashed divide-border/70 sm:grid-cols-2 lg:grid-cols-4 dark:divide-border'> - {tags.map((tag, index) => ( + {tags.map((item, index) => ( <TagCard - key={tag} + key={item.tag} displayCount={true} - name={tag} + name={item.tag} + count={item.count} className={cn( 'items-center justify-start gap-2 rounded-none border-r-0 bg-card/50 p-6 last:border-border/70 last:border-b last:border-dashed hover:bg-card/80 last:dark:border-border', tags.at(index - 1) && 'border-l', diff --git a/src/app/(main)/api/search/route.ts b/src/app/(main)/api/search/route.ts index 3c99f5c..cc2a1c8 100644 --- a/src/app/(main)/api/search/route.ts +++ b/src/app/(main)/api/search/route.ts @@ -1,11 +1,20 @@ -import { getPosts } from '@/lib/source'; +import { getPublishedPosts } from '@/lib/payload-posts'; import { createSearchAPI } from 'fumadocs-core/search/server'; -export const { GET } = createSearchAPI('advanced', { - indexes: getPosts().map((page) => ({ - title: page.data.title, - structuredData: page.data.structuredData, - id: page.url, - url: page.url, - })), -}); +// 动态生成搜索索引 +export async function GET(request: Request) { + const { posts } = await getPublishedPosts({ limit: 1000 }); + + const indexes = posts.map((post) => ({ + title: post.title, + description: post.description, + id: post.url, + url: post.url, + })); + + const searchAPI = createSearchAPI('advanced', { + indexes, + }); + + return searchAPI.GET(request); +} diff --git a/src/app/(main)/og/[...slug]/route.tsx b/src/app/(main)/og/[...slug]/route.tsx index d713923..77ae7f8 100644 --- a/src/app/(main)/og/[...slug]/route.tsx +++ b/src/app/(main)/og/[...slug]/route.tsx @@ -1,6 +1,7 @@ import { generateOGImage } from '@/app/(main)/og/[...slug]/og'; -import { metadataImage } from '@/lib/metadata-image'; +import { getPostBySlug, getAllPostSlugs } from '@/lib/payload-posts'; import type { ImageResponse } from 'next/og'; +import { notFound } from 'next/navigation'; async function loadAssets(): Promise< { name: string; data: Buffer; weight: 400 | 600; style: 'normal' }[] @@ -39,20 +40,33 @@ async function loadAssets(): Promise< ]; } -export const GET = metadataImage.createAPI( - async (page): Promise<ImageResponse> => { - const [fonts] = await Promise.all([loadAssets()]); - - return generateOGImage({ - title: page.data.title, - description: page.data.description, - fonts, - }); - }, -); - -export function generateStaticParams(): { - slug: string[]; -}[] { - return metadataImage.generateParams(); +export async function GET( + request: Request, + { params }: { params: Promise<{ slug: string[] }> } +): Promise<ImageResponse> { + const { slug } = await params; + const postSlug = slug[0]; + + if (!postSlug) { + notFound(); + } + + const post = await getPostBySlug(postSlug); + + if (!post) { + notFound(); + } + + const fonts = await loadAssets(); + + return generateOGImage({ + title: post.title, + description: post.description, + fonts, + }); +} + +export async function generateStaticParams(): Promise<{ slug: string[] }[]> { + const slugs = await getAllPostSlugs(); + return slugs.map((slug) => ({ slug: [slug, 'image.png'] })); } diff --git a/src/app/(main)/rss.xml/route.ts b/src/app/(main)/rss.xml/route.ts index ee06a40..3507948 100644 --- a/src/app/(main)/rss.xml/route.ts +++ b/src/app/(main)/rss.xml/route.ts @@ -1,10 +1,10 @@ import { description, title } from '@/app/(main)/layout.config'; import { owner } from '@/app/(main)/layout.config'; import { baseUrl } from '@/lib/constants'; -import { getPosts } from '@/lib/source'; +import { getPublishedPosts } from '@/lib/payload-posts'; import { Feed } from 'feed'; -export const dynamic = 'force-static'; +export const dynamic = 'force-dynamic'; const escapeForXML = (str: string) => { return str @@ -15,8 +15,8 @@ const escapeForXML = (str: string) => { .replace(/'/g, '''); }; -export const GET = () => { - const feed = createFeed(); +export const GET = async () => { + const feed = await createFeed(); return new Response(feed.atom1(), { headers: { @@ -25,7 +25,7 @@ export const GET = () => { }); }; -function createFeed(): Feed { +async function createFeed(): Promise<Feed> { const feed = new Feed({ title, description, @@ -39,24 +39,24 @@ function createFeed(): Feed { updated: new Date(), }); - const posts = getPosts(); + const { posts } = await getPublishedPosts({ limit: 1000 }); + for (const post of posts) { feed.addItem({ - title: post.data.title, - description: post.data.description, + title: post.title, + description: post.description, link: new URL(post.url, baseUrl).href, - image: { - title: post.data.title, - type: 'image/png', - url: escapeForXML( - new URL(`/og/${post.slugs.join('/')}/image.png`, baseUrl.href).href, - ), - }, - date: post.data.date, + image: post.image + ? { + title: post.title, + type: 'image/png', + url: escapeForXML(new URL(post.image, baseUrl.href).href), + } + : undefined, + date: post.date, author: [ { - name: post.data.author, - // link: new URL('/about', baseUrl).href, + name: post.author, }, ], }); diff --git a/src/components/json-ld.tsx b/src/components/json-ld.tsx index a7cd884..58cb0ba 100644 --- a/src/components/json-ld.tsx +++ b/src/components/json-ld.tsx @@ -1,33 +1,30 @@ import { title as homeTitle } from '@/app/(main)/layout.config'; import { owner } from '@/app/(main)/layout.config'; import { baseUrl } from '@/lib/constants'; -import type { Post } from '@/lib/source'; +import type { BlogPost } from '@/lib/payload-posts'; import type { BlogPosting, BreadcrumbList, Graph } from 'schema-dts'; -export const PostJsonLd = ({ page }: { page: Post }) => { - if (!page) { +export const PostJsonLd = ({ post }: { post: BlogPost }) => { + if (!post) { return null; } - const url = new URL(page.url, baseUrl.href).href; + const url = new URL(post.url, baseUrl.href).href; - const post: BlogPosting = { + const blogPosting: BlogPosting = { '@type': 'BlogPosting', - headline: page.data.title, - description: page.data.description, - image: new URL(`/og/${page.slugs.join('/')}/image.png`, baseUrl.href).href, - datePublished: new Date(page.data.date).toISOString(), - dateModified: page.data.lastModified - ? new Date(page.data.lastModified).toISOString() - : undefined, + headline: post.title, + description: post.description, + image: post.image ? new URL(post.image, baseUrl.href).href : undefined, + datePublished: post.date.toISOString(), + dateModified: post.updatedAt.toISOString(), mainEntityOfPage: { '@type': 'WebPage', '@id': url, }, author: { '@type': 'Person', - name: page.data.author, - // url: 'https://techwithanirudh.com/', + name: post.author, }, publisher: { '@type': 'Person', @@ -54,7 +51,7 @@ export const PostJsonLd = ({ page }: { page: Post }) => { { '@type': 'ListItem', position: 3, - name: page.data.title, + name: post.title, item: url, }, ], @@ -62,7 +59,7 @@ export const PostJsonLd = ({ page }: { page: Post }) => { const graph: Graph = { '@context': 'https://schema.org', - '@graph': [post, breadcrumbList], + '@graph': [blogPosting, breadcrumbList], }; return ( diff --git a/src/components/sections/footer.tsx b/src/components/sections/footer.tsx index 3a1b51d..a982810 100644 --- a/src/components/sections/footer.tsx +++ b/src/components/sections/footer.tsx @@ -1,18 +1,19 @@ import { baseOptions, linkItems, postsPerPage } from '@/app/(main)/layout.config'; import { InlineLink } from '@/components/inline-link'; -import { getSortedByDatePosts, getTags } from '@/lib/source'; +import { getPublishedPosts, getAllTags } from '@/lib/payload-posts'; import { cn } from '@/lib/utils'; import { getLinks } from 'fumadocs-ui/layouts/shared'; import { ActiveLink } from '../active-link'; -export function Footer() { +export async function Footer() { const links = getLinks(linkItems, baseOptions.githubUrl); const navItems = links.filter((item) => ['nav', 'all'].includes(item.on ?? 'all'), ); - const posts = getSortedByDatePosts(); - const tags = getTags(); + // 从 Payload 获取文章和标签 + const { posts } = await getPublishedPosts({ limit: postsPerPage }); + const tags = await getAllTags(); return ( <footer className={cn('flex flex-col gap-4')}> @@ -54,7 +55,7 @@ export function Footer() { {posts.slice(0, postsPerPage).map((post, i) => ( <li key={post.url}> <ActiveLink key={i.toString()} href={post.url}> - {post.data.title} + {post.title} </ActiveLink> </li> ))} @@ -65,10 +66,10 @@ export function Footer() { <p className='font-medium text-foreground'>Tags</p> <ul className='flex flex-col gap-3'> - {tags.slice(0, postsPerPage).map((name, i) => ( - <li key={`/tags/${name}`}> - <ActiveLink key={i.toString()} href={`/tags/${name}`}> - <span className='capitalize'>{name}</span> + {tags.slice(0, postsPerPage).map((item, i) => ( + <li key={`/tags/${item.tag}`}> + <ActiveLink key={i.toString()} href={`/tags/${item.tag}`}> + <span className='capitalize'>{item.tag}</span> </ActiveLink> </li> ))} @@ -96,17 +97,6 @@ export function Footer() { </ul> </div> </div> - {/* <Design /> */} </footer> ); } - -function Design() { - return ( - <div className='footer'> - <span className='footer-text font-mono'>john•doe</span> - <div className='footer-grid' /> - <div className='footer-gradient' /> - </div> - ); -} diff --git a/src/components/tags/tag-card.tsx b/src/components/tags/tag-card.tsx index a71e4c4..447c73e 100644 --- a/src/components/tags/tag-card.tsx +++ b/src/components/tags/tag-card.tsx @@ -1,19 +1,18 @@ import { Icons } from '@/components/icons/icons'; -import { getPostsByTag } from '@/lib/source'; import { cn } from '@/lib/utils'; import Link from 'next/link'; export const TagCard = ({ name, displayCount = false, + count, className = '', }: { name: string; displayCount?: boolean; + count?: number; className?: string; }) => { - const posts = getPostsByTag(name); - return ( <Link href={`/tags/${name}`} @@ -27,8 +26,8 @@ export const TagCard = ({ className='my-auto text-muted-foreground transition-transform group-hover:rotate-12' /> <span className='text-card-foreground'>{name}</span> - {displayCount && ( - <span className='ml-auto text-muted-foreground'>({posts.length})</span> + {displayCount && count !== undefined && ( + <span className='ml-auto text-muted-foreground'>({count})</span> )} </Link> ); diff --git a/src/lib/metadata-image.ts b/src/lib/metadata-image.ts deleted file mode 100644 index f2b91b6..0000000 --- a/src/lib/metadata-image.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { source } from '@/lib/source'; -import { createMetadataImage } from 'fumadocs-core/server'; - -export const metadataImage = createMetadataImage({ - source, - imageRoute: 'og', -}); diff --git a/src/lib/source.ts b/src/lib/source.ts deleted file mode 100644 index 34bc7ac..0000000 --- a/src/lib/source.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { loader } from 'fumadocs-core/source'; -import type { InferMetaType, InferPageType } from 'fumadocs-core/source'; -import { createMDXSource } from 'fumadocs-mdx'; -import { blog } from '.source'; - -export const source = loader({ - baseUrl: '/posts', - source: createMDXSource(blog), -}); -export const { getPage: getPost, getPages: getPosts, pageTree } = source; - -export type Post = ReturnType<typeof getPost>; - -const posts = getPosts(); - -export const getSortedByDatePosts = () => - posts.toSorted((a, b) => b.data.date.getTime() - a.data.date.getTime()); - -export const getTags = () => { - const tagSet = new Set<string>(); - - for (const post of posts) { - if (post.data.tags) { - for (const tag of post.data.tags) { - tagSet.add(tag); - } - } - } - - return Array.from(tagSet).toSorted(); -}; - -export const getPostsByTag = (tag: string) => { - return posts - .filter((post) => post.data.tags?.includes(tag)) - .toSorted((a, b) => b.data.date.getTime() - a.data.date.getTime()); -}; - -export type Page = InferPageType<typeof source>; -export type Meta = InferMetaType<typeof source>; |
