summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app/(home)/posts/[slug]/page.tsx145
-rw-r--r--src/app/(main)/(auth)/login/page.tsx (renamed from src/app/(auth)/login/page.tsx)2
-rw-r--r--src/app/(main)/(home)/(mdx)/about/page.mdx (renamed from src/app/(home)/(mdx)/about/page.mdx)0
-rw-r--r--src/app/(main)/(home)/_components/call-to-action.tsx (renamed from src/app/(home)/_components/call-to-action.tsx)0
-rw-r--r--src/app/(main)/(home)/_components/hero.tsx (renamed from src/app/(home)/_components/hero.tsx)4
-rw-r--r--src/app/(main)/(home)/_components/posts.tsx (renamed from src/app/(home)/_components/posts.tsx)35
-rw-r--r--src/app/(main)/(home)/actions.ts (renamed from src/app/(home)/actions.ts)15
-rw-r--r--src/app/(main)/(home)/layout.tsx (renamed from src/app/(home)/layout.tsx)0
-rw-r--r--src/app/(main)/(home)/page.tsx (renamed from src/app/(home)/page.tsx)12
-rw-r--r--src/app/(main)/(home)/posts/[slug]/page.client.tsx (renamed from src/app/(home)/posts/[slug]/page.client.tsx)0
-rw-r--r--src/app/(main)/(home)/posts/[slug]/page.tsx129
-rw-r--r--src/app/(main)/(home)/posts/page.tsx (renamed from src/app/(home)/posts/page.tsx)74
-rw-r--r--src/app/(main)/(home)/tags/[...slug]/page.tsx (renamed from src/app/(home)/tags/[...slug]/page.tsx)82
-rw-r--r--src/app/(main)/(home)/tags/page.tsx (renamed from src/app/(home)/tags/page.tsx)15
-rw-r--r--src/app/(main)/api/auth/[...all]/route.ts (renamed from src/app/api/auth/[...all]/route.ts)0
-rw-r--r--src/app/(main)/api/comments/[...comment]/route.ts (renamed from src/app/api/comments/[...comment]/route.ts)0
-rw-r--r--src/app/(main)/api/search/route.ts20
-rw-r--r--src/app/(main)/banner.png/fonts/geist-regular-otf.json (renamed from src/app/banner.png/fonts/geist-regular-otf.json)0
-rw-r--r--src/app/(main)/banner.png/fonts/geist-semibold-otf.json (renamed from src/app/banner.png/fonts/geist-semibold-otf.json)0
-rw-r--r--src/app/(main)/banner.png/fonts/geistmono-regular-otf.json (renamed from src/app/banner.png/fonts/geistmono-regular-otf.json)0
-rw-r--r--src/app/(main)/banner.png/og.tsx (renamed from src/app/banner.png/og.tsx)0
-rw-r--r--src/app/(main)/banner.png/route.tsx (renamed from src/app/banner.png/route.tsx)2
-rw-r--r--src/app/(main)/layout.client.tsx (renamed from src/app/layout.client.tsx)0
-rw-r--r--src/app/(main)/layout.config.tsx (renamed from src/app/layout.config.tsx)0
-rw-r--r--src/app/(main)/layout.tsx (renamed from src/app/layout.tsx)6
-rw-r--r--src/app/(main)/not-found.tsx (renamed from src/app/not-found.tsx)0
-rw-r--r--src/app/(main)/og/[...slug]/fonts/geist-regular-otf.json (renamed from src/app/og/[...slug]/fonts/geist-regular-otf.json)0
-rw-r--r--src/app/(main)/og/[...slug]/fonts/geist-semibold-otf.json (renamed from src/app/og/[...slug]/fonts/geist-semibold-otf.json)0
-rw-r--r--src/app/(main)/og/[...slug]/fonts/geistmono-regular-otf.json (renamed from src/app/og/[...slug]/fonts/geistmono-regular-otf.json)0
-rw-r--r--src/app/(main)/og/[...slug]/og.tsx (renamed from src/app/og/[...slug]/og.tsx)0
-rw-r--r--src/app/(main)/og/[...slug]/route.tsx (renamed from src/app/og/[...slug]/route.tsx)50
-rw-r--r--src/app/(main)/provider.tsx (renamed from src/app/provider.tsx)0
-rw-r--r--src/app/(main)/rss.xml/route.ts (renamed from src/app/rss.xml/route.ts)40
-rw-r--r--src/app/(payload)/admin/[[...segments]]/not-found.tsx24
-rw-r--r--src/app/(payload)/admin/[[...segments]]/page.tsx24
-rw-r--r--src/app/(payload)/admin/importMap.js49
-rw-r--r--src/app/(payload)/api/[...slug]/route.ts20
-rw-r--r--src/app/(payload)/api/graphql-playground/route.ts6
-rw-r--r--src/app/(payload)/api/graphql/route.ts6
-rw-r--r--src/app/(payload)/custom.scss1
-rw-r--r--src/app/(payload)/layout.tsx30
-rw-r--r--src/app/api/search/route.ts11
-rw-r--r--src/app/icon.pngbin212793 -> 0 bytes
-rw-r--r--src/components/docs.tsx2
-rw-r--r--src/components/json-ld.tsx33
-rw-r--r--src/components/mdx-layout.tsx2
-rw-r--r--src/components/newsletter-form.tsx2
-rw-r--r--src/components/rich-text/index.tsx42
-rw-r--r--src/components/rich-text/node-format.ts11
-rw-r--r--src/components/rich-text/serialize.tsx201
-rw-r--r--src/components/sections/footer.tsx32
-rw-r--r--src/components/sections/header/menu.tsx2
-rw-r--r--src/components/sections/header/navbar.tsx2
-rw-r--r--src/components/tags/tag-card.tsx9
-rw-r--r--src/lib/metadata-image.ts7
-rw-r--r--src/lib/payload-posts.ts192
-rw-r--r--src/lib/source.ts40
-rw-r--r--src/migrations/20251215_093857.json1264
-rw-r--r--src/migrations/20251215_093857.ts171
-rw-r--r--src/migrations/index.ts9
-rw-r--r--src/payload/collections/Media.ts23
-rw-r--r--src/payload/collections/Posts.ts93
-rw-r--r--src/payload/collections/Users.ts20
-rw-r--r--src/server/db/index.ts10
64 files changed, 2558 insertions, 411 deletions
diff --git a/src/app/(home)/posts/[slug]/page.tsx b/src/app/(home)/posts/[slug]/page.tsx
deleted file mode 100644
index 15a6bfd..0000000
--- a/src/app/(home)/posts/[slug]/page.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import { PostComments, Share } from '@/app/(home)/posts/[slug]/page.client';
-import { PostJsonLd } from '@/components/json-ld';
-import { Section } from '@/components/section';
-import { TagCard } from '@/components/tags/tag-card';
-import { createMetadata } from '@/lib/metadata';
-import { metadataImage } from '@/lib/metadata-image';
-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 'src/app/layout.config';
-
-function Header(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>
- );
-}
-
-export default async function Page(props: {
- params: Promise<{ slug: string }>;
-}) {
- const params = await props.params;
- const page = getPost([params.slug]);
-
- if (!page) notFound();
- const { body: Mdx, toc, tags, lastModified } = page.data;
-
- const lastUpdate = lastModified ? new Date(lastModified) : undefined;
-
- return (
- <>
- <Header 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={params.slug}
- 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} />
- </>
- );
-}
-
-export async function generateMetadata(props: {
- params: Promise<{ slug: string }>;
-}): Promise<Metadata> {
- const params = await props.params;
- const page = getPost([params.slug]);
-
- if (!page) notFound();
-
- const title = page.data.title;
- const description = page.data.description ?? homeDescription;
-
- return createMetadata(
- metadataImage.withImage(page.slugs, {
- title,
- description,
- openGraph: {
- url: `/posts/${page.slugs.join('/')}`,
- },
- alternates: {
- canonical: page.url,
- },
- }),
- );
-}
-
-export function generateStaticParams(): { slug: string | undefined }[] {
- return getPosts().map((page) => ({
- slug: page.slugs[0],
- }));
-}
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(main)/(auth)/login/page.tsx
index 2469097..3ff59d3 100644
--- a/src/app/(auth)/login/page.tsx
+++ b/src/app/(main)/(auth)/login/page.tsx
@@ -1,6 +1,6 @@
'use client';
-import { baseOptions, linkItems } from '@/app/layout.config';
+import { baseOptions, linkItems } from '@/app/(main)/layout.config';
import { Icons } from '@/components/icons/icons';
import { Header } from '@/components/sections/header';
import { Button } from '@/components/ui/button';
diff --git a/src/app/(home)/(mdx)/about/page.mdx b/src/app/(main)/(home)/(mdx)/about/page.mdx
index 675f3a8..675f3a8 100644
--- a/src/app/(home)/(mdx)/about/page.mdx
+++ b/src/app/(main)/(home)/(mdx)/about/page.mdx
diff --git a/src/app/(home)/_components/call-to-action.tsx b/src/app/(main)/(home)/_components/call-to-action.tsx
index b75298e..b75298e 100644
--- a/src/app/(home)/_components/call-to-action.tsx
+++ b/src/app/(main)/(home)/_components/call-to-action.tsx
diff --git a/src/app/(home)/_components/hero.tsx b/src/app/(main)/(home)/_components/hero.tsx
index 8ac251b..04371ca 100644
--- a/src/app/(home)/_components/hero.tsx
+++ b/src/app/(main)/(home)/_components/hero.tsx
@@ -1,4 +1,4 @@
-import { baseOptions, linkItems } from '@/app/layout.config';
+import { baseOptions, linkItems } from '@/app/(main)/layout.config';
import { Icons } from '@/components/icons/icons';
import { Section } from '@/components/section';
import { buttonVariants } from '@/components/ui/button';
@@ -8,7 +8,7 @@ import * as motion from 'motion/react-client';
import Image from 'next/image';
import Link from 'next/link';
import Balancer from 'react-wrap-balancer';
-import heroImage from '../../../../public/images/gradient-noise-purple-azure-light.png';
+import heroImage from '../../../../../public/images/gradient-noise-purple-azure-light.png';
const Hero = () => {
const links = getLinks(linkItems, baseOptions.githubUrl);
diff --git a/src/app/(home)/_components/posts.tsx b/src/app/(main)/(home)/_components/posts.tsx
index 8c8dc33..0eacce1 100644
--- a/src/app/(home)/_components/posts.tsx
+++ b/src/app/(main)/(home)/_components/posts.tsx
@@ -2,28 +2,29 @@ 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';
-export default function Posts({ posts }: { posts: Page[] }) {
+interface PostsProps {
+ posts: BlogPost[];
+}
+
+export default function Posts({ posts }: PostsProps) {
return (
<Section>
<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();
- return (
- <PostCard
- title={post.data.title}
- description={post.data.description ?? ''}
- image={post.data.image}
- url={post.url}
- date={date}
- key={post.url}
- author={post.data.author}
- tags={post.data.tags}
- />
- );
- })}
+ {posts.map((post) => (
+ <PostCard
+ title={post.title}
+ description={post.description}
+ image={post.image}
+ url={post.url}
+ date={post.date.toDateString()}
+ key={post.id}
+ author={post.author}
+ tags={post.tags}
+ />
+ ))}
<Link
href='/posts'
className={buttonVariants({
diff --git a/src/app/(home)/actions.ts b/src/app/(main)/(home)/actions.ts
index fdb16ca..5b0c456 100644
--- a/src/app/(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/(home)/layout.tsx b/src/app/(main)/(home)/layout.tsx
index bd641df..bd641df 100644
--- a/src/app/(home)/layout.tsx
+++ b/src/app/(main)/(home)/layout.tsx
diff --git a/src/app/(home)/page.tsx b/src/app/(main)/(home)/page.tsx
index da7da0f..0718da9 100644
--- a/src/app/(home)/page.tsx
+++ b/src/app/(main)/(home)/page.tsx
@@ -1,13 +1,13 @@
-import Hero from '@/app/(home)/_components/hero';
-import Posts from '@/app/(home)/_components/posts';
+import Hero from '@/app/(main)/(home)/_components/hero';
+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 function Home() {
- const posts = getSortedByDatePosts().slice(0, 3);
+export default async function Home() {
+ const { posts } = await getPublishedPosts({ limit: 3 });
return (
<>
@@ -15,7 +15,7 @@ export default function Home() {
<Section className='py-8 sm:py-16'>
<h2 className='text-center font-semibold text-2xl sm:text-3xl md:text-4xl lg:text-5xl'>
<span className='inline-flex items-center gap-3'>
- Posts{' '}
+ Posts
<Icons.posts className='size-10 fill-fd-primary/30 text-fd-primary transition-transform hover:rotate-12 hover:scale-125' />
</span>
</h2>
diff --git a/src/app/(home)/posts/[slug]/page.client.tsx b/src/app/(main)/(home)/posts/[slug]/page.client.tsx
index 7a97f56..7a97f56 100644
--- a/src/app/(home)/posts/[slug]/page.client.tsx
+++ b/src/app/(main)/(home)/posts/[slug]/page.client.tsx
diff --git a/src/app/(main)/(home)/posts/[slug]/page.tsx b/src/app/(main)/(home)/posts/[slug]/page.tsx
new file mode 100644
index 0000000..5960f06
--- /dev/null
+++ b/src/app/(main)/(home)/posts/[slug]/page.tsx
@@ -0,0 +1,129 @@
+import {
+ PostComments,
+ Share,
+} from '@/app/(main)/(home)/posts/[slug]/page.client';
+import { PostJsonLd } from '@/components/json-ld';
+import { RichText } from '@/components/rich-text';
+import { Section } from '@/components/section';
+import { TagCard } from '@/components/tags/tag-card';
+import { createMetadata } from '@/lib/metadata';
+import {
+ getPostBySlug,
+ getAllPostSlugs,
+ type BlogPost,
+} from '@/lib/payload-posts';
+import { cn } from '@/lib/utils';
+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';
+
+function PostHeader(props: { post: BlogPost }) {
+ const { post } = 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>{post.title}</Balancer>
+ </h1>
+ <p className="mx-auto max-w-4xl">
+ <Balancer>{post.description}</Balancer>
+ </p>
+ </div>
+ <div className="flex flex-wrap gap-2">
+ {post.tags?.map((tag) => (
+ <TagCard name={tag} key={tag} className=" border border-border " />
+ ))}
+ </div>
+ </div>
+ </Section>
+ );
+}
+
+function PostContent({ post }: { post: BlogPost }) {
+ return (
+ <>
+ <PostHeader post={post} />
+
+ <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">
+ <RichText
+ content={post.content as Record<string, unknown>}
+ className="flex-1 px-4"
+ enableProse={true}
+ />
+ <PostComments
+ slug={post.slug}
+ 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">{post.author}</p>
+ </div>
+ <div>
+ <p className="mb-1 text-fd-muted-foreground text-sm">Created At</p>
+ <p className="font-medium">{post.date.toDateString()}</p>
+ </div>
+ <div>
+ <p className="mb-1 text-fd-muted-foreground text-sm">Updated At</p>
+ <p className="font-medium">{post.updatedAt.toDateString()}</p>
+ </div>
+ <Share url={post.url} />
+ </div>
+ </article>
+ </Section>
+ <PostJsonLd post={post} />
+ </>
+ );
+}
+
+export default async function Page(props: {
+ params: Promise<{ slug: string }>;
+}) {
+ const params = await props.params;
+ const post = await getPostBySlug(params.slug);
+
+ if (!post) {
+ 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);
+
+ if (!post) {
+ 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 }[]> {
+ const slugs = await getAllPostSlugs();
+ return slugs.map((slug) => ({ slug }));
+}
diff --git a/src/app/(home)/posts/page.tsx b/src/app/(main)/(home)/posts/page.tsx
index fd0f912..40ebcda 100644
--- a/src/app/(home)/posts/page.tsx
+++ b/src/app/(main)/(home)/posts/page.tsx
@@ -1,23 +1,20 @@
-import { postsPerPage } from '@/app/layout.config';
+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 { getSortedByDatePosts } from '@/lib/source';
+import { getPublishedPosts } from '@/lib/payload-posts';
import type { Metadata, ResolvingMetadata } from 'next';
import { notFound, redirect } from 'next/navigation';
-export const dynamicParams = false;
-
-const totalPosts = getSortedByDatePosts().length;
-const pageCount = Math.ceil(totalPosts / postsPerPage);
-
const CurrentPostsCount = ({
startIndex,
endIndex,
+ totalPosts,
}: {
startIndex: number;
endIndex: number;
+ totalPosts: number;
}) => {
const start = startIndex + 1;
const end = endIndex < totalPosts ? endIndex : totalPosts;
@@ -29,7 +26,13 @@ const CurrentPostsCount = ({
);
};
-const Pagination = ({ pageIndex }: { pageIndex: number }) => {
+const Pagination = ({
+ pageIndex,
+ pageCount,
+}: {
+ pageIndex: number;
+ pageCount: number;
+}) => {
const handlePageChange = async (page: number) => {
'use server';
redirect(`/posts?page=${page}`);
@@ -51,55 +54,63 @@ export default async function Page(props: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const searchParams = await props.searchParams;
+
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) 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 = getSortedByDatePosts().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{' '}
- <CurrentPostsCount startIndex={startIndex} endIndex={endIndex} />
+ All {totalDocs} Posts{' '}
+ <CurrentPostsCount
+ startIndex={startIndex}
+ endIndex={endIndex}
+ totalPosts={totalDocs}
+ />
</h1>
</Section>
<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 > 1 && <Pagination pageIndex={pageIndex} />}
+ {totalPages > 1 && <Pagination pageIndex={pageIndex} pageCount={totalPages} />}
</>
);
}
-export const generateStaticParams = () => {
- const slugs = Array.from({ length: pageCount }, (_, index) => ({
- slug: [(index + 1).toString()],
- }));
-
- return [{ slug: [] }, ...slugs];
-};
-
type Props = {
params: Promise<{ slug: string[] }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
@@ -107,9 +118,8 @@ type Props = {
export async function generateMetadata(
props: Props,
- parent: ResolvingMetadata,
+ parent: ResolvingMetadata
): Promise<Metadata> {
- const params = await props.params;
const searchParams = await props.searchParams;
const pageIndex = searchParams.page
diff --git a/src/app/(home)/tags/[...slug]/page.tsx b/src/app/(main)/(home)/tags/[...slug]/page.tsx
index 9479705..71615cb 100644
--- a/src/app/(home)/tags/[...slug]/page.tsx
+++ b/src/app/(main)/(home)/tags/[...slug]/page.tsx
@@ -1,32 +1,25 @@
-import { postsPerPage } from '@/app/layout.config';
+import { postsPerPage } from '@/app/(main)/layout.config';
import { Icons } from '@/components/icons/icons';
import { TagJsonLd } from '@/components/json-ld';
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/(home)/tags/page.tsx b/src/app/(main)/(home)/tags/page.tsx
index 54fb423..6db13fc 100644
--- a/src/app/(home)/tags/page.tsx
+++ b/src/app/(main)/(home)/tags/page.tsx
@@ -1,13 +1,13 @@
-import { title as homeTitle } from '@/app/layout.config';
+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/api/auth/[...all]/route.ts b/src/app/(main)/api/auth/[...all]/route.ts
index 677b24c..677b24c 100644
--- a/src/app/api/auth/[...all]/route.ts
+++ b/src/app/(main)/api/auth/[...all]/route.ts
diff --git a/src/app/api/comments/[...comment]/route.ts b/src/app/(main)/api/comments/[...comment]/route.ts
index 1da87db..1da87db 100644
--- a/src/app/api/comments/[...comment]/route.ts
+++ b/src/app/(main)/api/comments/[...comment]/route.ts
diff --git a/src/app/(main)/api/search/route.ts b/src/app/(main)/api/search/route.ts
new file mode 100644
index 0000000..cc2a1c8
--- /dev/null
+++ b/src/app/(main)/api/search/route.ts
@@ -0,0 +1,20 @@
+import { getPublishedPosts } from '@/lib/payload-posts';
+import { createSearchAPI } from 'fumadocs-core/search/server';
+
+// 动态生成搜索索引
+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/banner.png/fonts/geist-regular-otf.json b/src/app/(main)/banner.png/fonts/geist-regular-otf.json
index f220c87..f220c87 100644
--- a/src/app/banner.png/fonts/geist-regular-otf.json
+++ b/src/app/(main)/banner.png/fonts/geist-regular-otf.json
diff --git a/src/app/banner.png/fonts/geist-semibold-otf.json b/src/app/(main)/banner.png/fonts/geist-semibold-otf.json
index c119360..c119360 100644
--- a/src/app/banner.png/fonts/geist-semibold-otf.json
+++ b/src/app/(main)/banner.png/fonts/geist-semibold-otf.json
diff --git a/src/app/banner.png/fonts/geistmono-regular-otf.json b/src/app/(main)/banner.png/fonts/geistmono-regular-otf.json
index f4200df..f4200df 100644
--- a/src/app/banner.png/fonts/geistmono-regular-otf.json
+++ b/src/app/(main)/banner.png/fonts/geistmono-regular-otf.json
diff --git a/src/app/banner.png/og.tsx b/src/app/(main)/banner.png/og.tsx
index 1a520c0..1a520c0 100644
--- a/src/app/banner.png/og.tsx
+++ b/src/app/(main)/banner.png/og.tsx
diff --git a/src/app/banner.png/route.tsx b/src/app/(main)/banner.png/route.tsx
index 1cd53ac..d3bfdc8 100644
--- a/src/app/banner.png/route.tsx
+++ b/src/app/(main)/banner.png/route.tsx
@@ -1,4 +1,4 @@
-import { generateOGImage } from '@/app/banner.png/og';
+import { generateOGImage } from '@/app/(main)/banner.png/og';
async function loadAssets(): Promise<
{ name: string; data: Buffer; weight: 400 | 600; style: 'normal' }[]
diff --git a/src/app/layout.client.tsx b/src/app/(main)/layout.client.tsx
index 35726ba..35726ba 100644
--- a/src/app/layout.client.tsx
+++ b/src/app/(main)/layout.client.tsx
diff --git a/src/app/layout.config.tsx b/src/app/(main)/layout.config.tsx
index f9efebb..f9efebb 100644
--- a/src/app/layout.config.tsx
+++ b/src/app/(main)/layout.config.tsx
diff --git a/src/app/layout.tsx b/src/app/(main)/layout.tsx
index c7d2aaf..c9e7dee 100644
--- a/src/app/layout.tsx
+++ b/src/app/(main)/layout.tsx
@@ -5,9 +5,9 @@ import type { ReactNode } from 'react';
import '@/styles/globals.css';
import 'katex/dist/katex.css';
import { baseUrl } from '@/lib/constants';
-import { Body } from './layout.client';
-import { description as homeDescription } from './layout.config';
-import { Provider } from './provider';
+import { Body } from '@/app/(main)/layout.client';
+import { description as homeDescription } from '@/app/(main)/layout.config';
+import { Provider } from '@/app/(main)/provider';
const geistSans = Geist({
variable: '--font-geist-sans',
diff --git a/src/app/not-found.tsx b/src/app/(main)/not-found.tsx
index ecec57a..ecec57a 100644
--- a/src/app/not-found.tsx
+++ b/src/app/(main)/not-found.tsx
diff --git a/src/app/og/[...slug]/fonts/geist-regular-otf.json b/src/app/(main)/og/[...slug]/fonts/geist-regular-otf.json
index f220c87..f220c87 100644
--- a/src/app/og/[...slug]/fonts/geist-regular-otf.json
+++ b/src/app/(main)/og/[...slug]/fonts/geist-regular-otf.json
diff --git a/src/app/og/[...slug]/fonts/geist-semibold-otf.json b/src/app/(main)/og/[...slug]/fonts/geist-semibold-otf.json
index c119360..c119360 100644
--- a/src/app/og/[...slug]/fonts/geist-semibold-otf.json
+++ b/src/app/(main)/og/[...slug]/fonts/geist-semibold-otf.json
diff --git a/src/app/og/[...slug]/fonts/geistmono-regular-otf.json b/src/app/(main)/og/[...slug]/fonts/geistmono-regular-otf.json
index f4200df..f4200df 100644
--- a/src/app/og/[...slug]/fonts/geistmono-regular-otf.json
+++ b/src/app/(main)/og/[...slug]/fonts/geistmono-regular-otf.json
diff --git a/src/app/og/[...slug]/og.tsx b/src/app/(main)/og/[...slug]/og.tsx
index 5754e96..5754e96 100644
--- a/src/app/og/[...slug]/og.tsx
+++ b/src/app/(main)/og/[...slug]/og.tsx
diff --git a/src/app/og/[...slug]/route.tsx b/src/app/(main)/og/[...slug]/route.tsx
index 8738616..77ae7f8 100644
--- a/src/app/og/[...slug]/route.tsx
+++ b/src/app/(main)/og/[...slug]/route.tsx
@@ -1,6 +1,7 @@
-import { generateOGImage } from '@/app/og/[...slug]/og';
-import { metadataImage } from '@/lib/metadata-image';
+import { generateOGImage } from '@/app/(main)/og/[...slug]/og';
+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/provider.tsx b/src/app/(main)/provider.tsx
index 085bf50..085bf50 100644
--- a/src/app/provider.tsx
+++ b/src/app/(main)/provider.tsx
diff --git a/src/app/rss.xml/route.ts b/src/app/(main)/rss.xml/route.ts
index 6a3acf6..3507948 100644
--- a/src/app/rss.xml/route.ts
+++ b/src/app/(main)/rss.xml/route.ts
@@ -1,10 +1,10 @@
-import { description, title } from '@/app/layout.config';
-import { owner } from '@/app/layout.config';
+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, '&apos;');
};
-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/app/(payload)/admin/[[...segments]]/not-found.tsx b/src/app/(payload)/admin/[[...segments]]/not-found.tsx
new file mode 100644
index 0000000..b28df09
--- /dev/null
+++ b/src/app/(payload)/admin/[[...segments]]/not-found.tsx
@@ -0,0 +1,24 @@
+/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
+/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
+import type { Metadata } from 'next';
+
+import config from '@payload-config';
+import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views';
+import { importMap } from '../importMap';
+
+type Args = {
+ params: Promise<{
+ segments: string[];
+ }>;
+ searchParams: Promise<{
+ [key: string]: string | string[];
+ }>;
+};
+
+export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
+ generatePageMetadata({ config, params, searchParams });
+
+const NotFound = ({ params, searchParams }: Args) =>
+ NotFoundPage({ config, importMap, params, searchParams });
+
+export default NotFound;
diff --git a/src/app/(payload)/admin/[[...segments]]/page.tsx b/src/app/(payload)/admin/[[...segments]]/page.tsx
new file mode 100644
index 0000000..f04f258
--- /dev/null
+++ b/src/app/(payload)/admin/[[...segments]]/page.tsx
@@ -0,0 +1,24 @@
+/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
+/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
+import type { Metadata } from 'next';
+
+import config from '@payload-config';
+import { RootPage, generatePageMetadata } from '@payloadcms/next/views';
+import { importMap } from '../importMap';
+
+type Args = {
+ params: Promise<{
+ segments: string[];
+ }>;
+ searchParams: Promise<{
+ [key: string]: string | string[];
+ }>;
+};
+
+export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
+ generatePageMetadata({ config, params, searchParams });
+
+const Page = ({ params, searchParams }: Args) =>
+ RootPage({ config, params, searchParams, importMap });
+
+export default Page;
diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js
new file mode 100644
index 0000000..5bc8ec3
--- /dev/null
+++ b/src/app/(payload)/admin/importMap.js
@@ -0,0 +1,49 @@
+import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
+import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
+import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
+import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
+
+export const importMap = {
+ "@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
+ "@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
+ "@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
+ "@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
+ "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864
+}
diff --git a/src/app/(payload)/api/[...slug]/route.ts b/src/app/(payload)/api/[...slug]/route.ts
new file mode 100644
index 0000000..c3de612
--- /dev/null
+++ b/src/app/(payload)/api/[...slug]/route.ts
@@ -0,0 +1,20 @@
+/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
+/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
+import config from '@payload-config'
+import '@payloadcms/next/css'
+import {
+ REST_DELETE,
+ REST_GET,
+ REST_OPTIONS,
+ REST_PATCH,
+ REST_POST,
+ REST_PUT,
+} from '@payloadcms/next/routes'
+
+export const GET = REST_GET(config)
+export const POST = REST_POST(config)
+export const DELETE = REST_DELETE(config)
+export const PATCH = REST_PATCH(config)
+
+export const PUT = REST_PUT(config)
+export const OPTIONS = REST_OPTIONS(config)
diff --git a/src/app/(payload)/api/graphql-playground/route.ts b/src/app/(payload)/api/graphql-playground/route.ts
new file mode 100644
index 0000000..c14156d
--- /dev/null
+++ b/src/app/(payload)/api/graphql-playground/route.ts
@@ -0,0 +1,6 @@
+/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
+/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
+import config from '@payload-config';
+import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes';
+
+export const GET = GRAPHQL_PLAYGROUND_GET(config);
diff --git a/src/app/(payload)/api/graphql/route.ts b/src/app/(payload)/api/graphql/route.ts
new file mode 100644
index 0000000..65fcf23
--- /dev/null
+++ b/src/app/(payload)/api/graphql/route.ts
@@ -0,0 +1,6 @@
+/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
+/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
+import config from '@payload-config';
+import { GRAPHQL_POST } from '@payloadcms/next/routes';
+
+export const POST = GRAPHQL_POST(config);
diff --git a/src/app/(payload)/custom.scss b/src/app/(payload)/custom.scss
new file mode 100644
index 0000000..f38c2f0
--- /dev/null
+++ b/src/app/(payload)/custom.scss
@@ -0,0 +1 @@
+/* Add custom Payload admin styles here */
diff --git a/src/app/(payload)/layout.tsx b/src/app/(payload)/layout.tsx
new file mode 100644
index 0000000..f14247a
--- /dev/null
+++ b/src/app/(payload)/layout.tsx
@@ -0,0 +1,30 @@
+/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
+/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
+import config from '@payload-config';
+import '@payloadcms/next/css';
+import type { ServerFunctionClient } from 'payload';
+import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts';
+import React from 'react';
+import { importMap } from './admin/importMap.js';
+import './custom.scss';
+
+type Args = {
+ children: React.ReactNode;
+};
+
+const serverFunction: ServerFunctionClient = async function (args) {
+ 'use server';
+ return handleServerFunctions({
+ ...args,
+ config,
+ importMap,
+ });
+};
+
+const Layout = ({ children }: Args) => (
+ <RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
+ {children}
+ </RootLayout>
+);
+
+export default Layout;
diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts
deleted file mode 100644
index 3c99f5c..0000000
--- a/src/app/api/search/route.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { getPosts } from '@/lib/source';
-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,
- })),
-});
diff --git a/src/app/icon.png b/src/app/icon.png
deleted file mode 100644
index 7532d9b..0000000
--- a/src/app/icon.png
+++ /dev/null
Binary files differ
diff --git a/src/components/docs.tsx b/src/components/docs.tsx
index 038a52b..0dec2dd 100644
--- a/src/components/docs.tsx
+++ b/src/components/docs.tsx
@@ -1,5 +1,5 @@
import type { PageTree } from 'fumadocs-core/server';
-import { cn } from 'fumadocs-ui/components/api';
+import { cn } from '@/lib/utils';
import {
type SidebarOptions,
checkPageTree,
diff --git a/src/components/json-ld.tsx b/src/components/json-ld.tsx
index cd30274..58cb0ba 100644
--- a/src/components/json-ld.tsx
+++ b/src/components/json-ld.tsx
@@ -1,33 +1,30 @@
-import { title as homeTitle } from '@/app/layout.config';
-import { owner } from '@/app/layout.config';
+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/mdx-layout.tsx b/src/components/mdx-layout.tsx
index bc68a93..f984dfc 100644
--- a/src/components/mdx-layout.tsx
+++ b/src/components/mdx-layout.tsx
@@ -1,4 +1,4 @@
-import { PostComments } from '@/app/(home)/posts/[slug]/page.client';
+import { PostComments } from '@/app/(main)/(home)/posts/[slug]/page.client';
import type { TOCItemType } from 'fumadocs-core/server';
import { InlineTOC } from 'fumadocs-ui/components/inline-toc';
import type { ReactNode } from 'react';
diff --git a/src/components/newsletter-form.tsx b/src/components/newsletter-form.tsx
index 7dd0cc0..83a9bee 100644
--- a/src/components/newsletter-form.tsx
+++ b/src/components/newsletter-form.tsx
@@ -18,7 +18,7 @@ import { useForm } from 'react-hook-form';
import { Alert, AlertTitle } from '@/components/ui/alert';
-import { subscribeUser } from '@/app/(home)/actions';
+import { subscribeUser } from '@/app/(main)/(home)/actions';
import { Icons } from '@/components/icons/icons';
export const NewsletterForm = () => {
diff --git a/src/components/rich-text/index.tsx b/src/components/rich-text/index.tsx
new file mode 100644
index 0000000..556942c
--- /dev/null
+++ b/src/components/rich-text/index.tsx
@@ -0,0 +1,42 @@
+import { cn } from '@/lib/utils'
+import React from 'react'
+import { serializeLexical } from './serialize'
+
+type Props = {
+ className?: string
+ content: Record<string, unknown>
+ enableProse?: boolean
+}
+
+export function RichText({
+ className,
+ content,
+ enableProse = true,
+}: Props) {
+ if (!content) {
+ return null
+ }
+
+ return (
+ <div
+ className={cn(
+ {
+ 'prose min-w-0 dark:prose-invert': enableProse,
+ },
+ className
+ )}
+ >
+ {content &&
+ !Array.isArray(content) &&
+ typeof content === 'object' &&
+ 'root' in content &&
+ serializeLexical({
+ nodes: (content.root as { children: unknown[] })?.children as Parameters<
+ typeof serializeLexical
+ >[0]['nodes'],
+ })}
+ </div>
+ )
+}
+
+export default RichText
diff --git a/src/components/rich-text/node-format.ts b/src/components/rich-text/node-format.ts
new file mode 100644
index 0000000..84e7d7e
--- /dev/null
+++ b/src/components/rich-text/node-format.ts
@@ -0,0 +1,11 @@
+// From Lexical: https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts
+
+// Text node formatting
+export const IS_BOLD = 1
+export const IS_ITALIC = 1 << 1
+export const IS_STRIKETHROUGH = 1 << 2
+export const IS_UNDERLINE = 1 << 3
+export const IS_CODE = 1 << 4
+export const IS_SUBSCRIPT = 1 << 5
+export const IS_SUPERSCRIPT = 1 << 6
+export const IS_HIGHLIGHT = 1 << 7
diff --git a/src/components/rich-text/serialize.tsx b/src/components/rich-text/serialize.tsx
new file mode 100644
index 0000000..d75c40f
--- /dev/null
+++ b/src/components/rich-text/serialize.tsx
@@ -0,0 +1,201 @@
+import React, { Fragment, type JSX } from 'react'
+import Link from 'next/link'
+import {
+ IS_BOLD,
+ IS_CODE,
+ IS_ITALIC,
+ IS_STRIKETHROUGH,
+ IS_SUBSCRIPT,
+ IS_SUPERSCRIPT,
+ IS_UNDERLINE,
+} from './node-format'
+
+// Lexical 节点类型
+interface LexicalNode {
+ type: string
+ format?: number
+ text?: string
+ tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'ul' | 'ol'
+ listType?: 'bullet' | 'number' | 'check'
+ checked?: boolean
+ value?: number
+ children?: LexicalNode[]
+ fields?: {
+ linkType?: 'internal' | 'custom'
+ url?: string
+ newTab?: boolean
+ doc?: {
+ value?: {
+ slug?: string
+ }
+ relationTo?: string
+ }
+ }
+ language?: string
+ version?: number
+}
+
+type Props = {
+ nodes: LexicalNode[]
+}
+
+export function serializeLexical({ nodes }: Props): JSX.Element {
+ return (
+ <Fragment>
+ {nodes?.map((node, index): JSX.Element | null => {
+ if (node == null) {
+ return null
+ }
+
+ if (node.type === 'text') {
+ let text = <React.Fragment key={index}>{node.text}</React.Fragment>
+ const format = node.format || 0
+
+ if (format & IS_BOLD) {
+ text = <strong key={index}>{text}</strong>
+ }
+ if (format & IS_ITALIC) {
+ text = <em key={index}>{text}</em>
+ }
+ if (format & IS_STRIKETHROUGH) {
+ text = (
+ <span key={index} style={{ textDecoration: 'line-through' }}>
+ {text}
+ </span>
+ )
+ }
+ if (format & IS_UNDERLINE) {
+ text = (
+ <span key={index} style={{ textDecoration: 'underline' }}>
+ {text}
+ </span>
+ )
+ }
+ if (format & IS_CODE) {
+ text = <code key={index}>{node.text}</code>
+ }
+ if (format & IS_SUBSCRIPT) {
+ text = <sub key={index}>{text}</sub>
+ }
+ if (format & IS_SUPERSCRIPT) {
+ text = <sup key={index}>{text}</sup>
+ }
+
+ return text
+ }
+
+ // 处理子节点
+ const serializedChildrenFn = (node: LexicalNode): JSX.Element | null => {
+ if (node.children == null) {
+ return null
+ } else {
+ // 处理 checkbox list
+ if (node?.type === 'list' && node?.listType === 'check') {
+ for (const item of node.children) {
+ if ('checked' in item) {
+ if (!item?.checked) {
+ item.checked = false
+ }
+ }
+ }
+ }
+ return serializeLexical({ nodes: node.children })
+ }
+ }
+
+ const serializedChildren = 'children' in node ? serializedChildrenFn(node) : ''
+
+ switch (node.type) {
+ case 'linebreak': {
+ return <br key={index} />
+ }
+ case 'paragraph': {
+ return <p key={index}>{serializedChildren}</p>
+ }
+ case 'heading': {
+ const Tag = node?.tag || 'h2'
+ return <Tag key={index}>{serializedChildren}</Tag>
+ }
+ case 'list': {
+ const Tag = node?.tag || 'ul'
+ return <Tag key={index}>{serializedChildren}</Tag>
+ }
+ case 'listitem': {
+ if (node?.checked != null) {
+ return (
+ <li
+ aria-checked={node.checked ? 'true' : 'false'}
+ key={index}
+ role="checkbox"
+ tabIndex={-1}
+ value={node?.value}
+ >
+ <input
+ type="checkbox"
+ checked={node.checked}
+ readOnly
+ className="mr-2"
+ />
+ {serializedChildren}
+ </li>
+ )
+ } else {
+ return (
+ <li key={index} value={node?.value}>
+ {serializedChildren}
+ </li>
+ )
+ }
+ }
+ case 'quote': {
+ return <blockquote key={index}>{serializedChildren}</blockquote>
+ }
+ case 'link': {
+ const fields = node.fields
+
+ if (fields?.linkType === 'internal' && fields?.doc?.value?.slug) {
+ const href =
+ fields.doc.relationTo === 'posts'
+ ? `/posts/${fields.doc.value.slug}`
+ : `/${fields.doc.value.slug}`
+
+ return (
+ <Link key={index} href={href}>
+ {serializedChildren}
+ </Link>
+ )
+ }
+
+ return (
+ <a
+ key={index}
+ href={fields?.url || '#'}
+ target={fields?.newTab ? '_blank' : undefined}
+ rel={fields?.newTab ? 'noopener noreferrer' : undefined}
+ >
+ {serializedChildren}
+ </a>
+ )
+ }
+ case 'code': {
+ // 代码块
+ return (
+ <pre key={index} className="overflow-x-auto">
+ <code>{serializedChildren}</code>
+ </pre>
+ )
+ }
+ case 'horizontalrule': {
+ return <hr key={index} />
+ }
+ default:
+ // 如果有子节点,递归渲染
+ if (node.children) {
+ return <Fragment key={index}>{serializedChildren}</Fragment>
+ }
+ return null
+ }
+ })}
+ </Fragment>
+ )
+}
diff --git a/src/components/sections/footer.tsx b/src/components/sections/footer.tsx
index 8d0d876..a982810 100644
--- a/src/components/sections/footer.tsx
+++ b/src/components/sections/footer.tsx
@@ -1,18 +1,19 @@
-import { baseOptions, linkItems, postsPerPage } from '@/app/layout.config';
+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/sections/header/menu.tsx b/src/components/sections/header/menu.tsx
index 57ddf12..7eb6644 100644
--- a/src/components/sections/header/menu.tsx
+++ b/src/components/sections/header/menu.tsx
@@ -2,7 +2,7 @@
import { cva } from 'class-variance-authority';
import Link from 'fumadocs-core/link';
-import { cn } from 'fumadocs-ui/components/api';
+import { cn } from '@/lib/utils';
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
import {
NavigationMenuContent,
diff --git a/src/components/sections/header/navbar.tsx b/src/components/sections/header/navbar.tsx
index 25850a0..bac7d3d 100644
--- a/src/components/sections/header/navbar.tsx
+++ b/src/components/sections/header/navbar.tsx
@@ -1,7 +1,7 @@
'use client';
import Link, { type LinkProps } from 'fumadocs-core/link';
-import { cn } from 'fumadocs-ui/components/api';
+import { cn } from '@/lib/utils';
import {
NavigationMenu,
NavigationMenuLink,
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/payload-posts.ts b/src/lib/payload-posts.ts
new file mode 100644
index 0000000..7cf1a56
--- /dev/null
+++ b/src/lib/payload-posts.ts
@@ -0,0 +1,192 @@
+import { getPayload } from 'payload'
+import config from '@payload-config'
+import type { Media } from '@/../payload-types'
+
+// Payload 文章类型
+export interface PayloadPost {
+ id: number
+ title: string
+ slug: string
+ description?: string
+ content: unknown // Lexical 富文本内容
+ featuredImage?: Media | number
+ author?: string
+ tags?: { tag: string; id?: string }[]
+ status: 'draft' | 'published'
+ publishedAt?: string
+ createdAt: string
+ updatedAt: string
+}
+
+// 转换后的文章类型(用于前端展示)
+export interface BlogPost {
+ id: number
+ title: string
+ slug: string
+ url: string
+ description: string
+ content: unknown
+ image?: string
+ author: string
+ tags: string[]
+ date: Date
+ createdAt: Date
+ updatedAt: Date
+}
+
+// 获取 Payload 实例
+async function getPayloadClient() {
+ return getPayload({ config })
+}
+
+// 将 Payload 文章转换为博客文章格式
+function transformPost(post: PayloadPost): BlogPost {
+ // 处理封面图片 URL
+ let imageUrl: string | undefined
+ if (post.featuredImage && typeof post.featuredImage === 'object') {
+ imageUrl = post.featuredImage.url || undefined
+ }
+
+ // 处理标签数组
+ const tags = post.tags?.map((t) => t.tag).filter(Boolean) || []
+
+ // 处理日期
+ const date = post.publishedAt ? new Date(post.publishedAt) : new Date(post.createdAt)
+
+ return {
+ id: post.id,
+ title: post.title,
+ slug: post.slug,
+ url: `/posts/${post.slug}`,
+ description: post.description || '',
+ content: post.content,
+ image: imageUrl,
+ author: post.author || 'Admin',
+ tags,
+ date,
+ createdAt: new Date(post.createdAt),
+ updatedAt: new Date(post.updatedAt),
+ }
+}
+
+// 获取所有已发布的文章(按发布时间排序)
+export async function getPublishedPosts(options?: {
+ limit?: number
+ page?: number
+}): Promise<{ posts: BlogPost[]; totalDocs: number; totalPages: number }> {
+ const payload = await getPayloadClient()
+
+ const result = await payload.find({
+ collection: 'posts',
+ where: {
+ status: { equals: 'published' },
+ },
+ sort: '-publishedAt',
+ limit: options?.limit || 10,
+ page: options?.page || 1,
+ depth: 1, // 获取关联的 media 数据
+ })
+
+ return {
+ posts: result.docs.map((doc) => transformPost(doc as unknown as PayloadPost)),
+ totalDocs: result.totalDocs,
+ totalPages: result.totalPages,
+ }
+}
+
+// 根据 slug 获取单篇文章
+export async function getPostBySlug(slug: string): Promise<BlogPost | null> {
+ const payload = await getPayloadClient()
+
+ const result = await payload.find({
+ collection: 'posts',
+ where: {
+ slug: { equals: slug },
+ status: { equals: 'published' },
+ },
+ limit: 1,
+ depth: 1,
+ })
+
+ if (result.docs.length === 0) {
+ return null
+ }
+
+ return transformPost(result.docs[0] as unknown as PayloadPost)
+}
+
+// 获取所有已发布文章的 slug(用于静态生成)
+export async function getAllPostSlugs(): Promise<string[]> {
+ const payload = await getPayloadClient()
+
+ const result = await payload.find({
+ collection: 'posts',
+ where: {
+ status: { equals: 'published' },
+ },
+ limit: 1000,
+ depth: 0,
+ })
+
+ return result.docs.map((doc) => (doc as unknown as PayloadPost).slug)
+}
+
+// 根据标签获取文章
+export async function getPostsByTag(
+ tag: string,
+ options?: { limit?: number; page?: number }
+): Promise<{ posts: BlogPost[]; totalDocs: number; totalPages: number }> {
+ const payload = await getPayloadClient()
+
+ const result = await payload.find({
+ collection: 'posts',
+ where: {
+ and: [
+ { status: { equals: 'published' } },
+ { 'tags.tag': { equals: tag } },
+ ],
+ },
+ sort: '-publishedAt',
+ limit: options?.limit || 10,
+ page: options?.page || 1,
+ depth: 1,
+ })
+
+ return {
+ posts: result.docs.map((doc) => transformPost(doc as unknown as PayloadPost)),
+ totalDocs: result.totalDocs,
+ totalPages: result.totalPages,
+ }
+}
+
+// 获取所有标签
+export async function getAllTags(): Promise<{ tag: string; count: number }[]> {
+ const payload = await getPayloadClient()
+
+ const result = await payload.find({
+ collection: 'posts',
+ where: {
+ status: { equals: 'published' },
+ },
+ limit: 1000,
+ depth: 0,
+ })
+
+ // 统计标签
+ const tagCounts = new Map<string, number>()
+
+ for (const doc of result.docs) {
+ const post = doc as unknown as PayloadPost
+ if (post.tags) {
+ for (const { tag } of post.tags) {
+ if (tag) {
+ tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1)
+ }
+ }
+ }
+ }
+
+ return Array.from(tagCounts.entries())
+ .map(([tag, count]) => ({ tag, count }))
+ .sort((a, b) => b.count - a.count)
+}
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>;
diff --git a/src/migrations/20251215_093857.json b/src/migrations/20251215_093857.json
new file mode 100644
index 0000000..fad24c8
--- /dev/null
+++ b/src/migrations/20251215_093857.json
@@ -0,0 +1,1264 @@
+{
+ "id": "af74e03d-43f6-47cf-8c1d-570aacc337d5",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "payload.users_sessions": {
+ "name": "users_sessions",
+ "schema": "payload",
+ "columns": {
+ "_order": {
+ "name": "_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "_parent_id": {
+ "name": "_parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "users_sessions_order_idx": {
+ "name": "users_sessions_order_idx",
+ "columns": [
+ {
+ "expression": "_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_sessions_parent_id_idx": {
+ "name": "users_sessions_parent_id_idx",
+ "columns": [
+ {
+ "expression": "_parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "users_sessions_parent_id_fk": {
+ "name": "users_sessions_parent_id_fk",
+ "tableFrom": "users_sessions",
+ "tableTo": "users",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "_parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.users": {
+ "name": "users",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reset_password_token": {
+ "name": "reset_password_token",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reset_password_expiration": {
+ "name": "reset_password_expiration",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "salt": {
+ "name": "salt",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hash": {
+ "name": "hash",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "login_attempts": {
+ "name": "login_attempts",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "lock_until": {
+ "name": "lock_until",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "users_updated_at_idx": {
+ "name": "users_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_created_at_idx": {
+ "name": "users_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_email_idx": {
+ "name": "users_email_idx",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.posts_tags": {
+ "name": "posts_tags",
+ "schema": "payload",
+ "columns": {
+ "_order": {
+ "name": "_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "_parent_id": {
+ "name": "_parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "tag": {
+ "name": "tag",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "posts_tags_order_idx": {
+ "name": "posts_tags_order_idx",
+ "columns": [
+ {
+ "expression": "_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_tags_parent_id_idx": {
+ "name": "posts_tags_parent_id_idx",
+ "columns": [
+ {
+ "expression": "_parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "posts_tags_parent_id_fk": {
+ "name": "posts_tags_parent_id_fk",
+ "tableFrom": "posts_tags",
+ "tableTo": "posts",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "_parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.posts": {
+ "name": "posts",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content": {
+ "name": "content",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "featured_image_id": {
+ "name": "featured_image_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "author": {
+ "name": "author",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'Admin'"
+ },
+ "status": {
+ "name": "status",
+ "type": "enum_posts_status",
+ "typeSchema": "payload",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'draft'"
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "posts_slug_idx": {
+ "name": "posts_slug_idx",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_featured_image_idx": {
+ "name": "posts_featured_image_idx",
+ "columns": [
+ {
+ "expression": "featured_image_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_updated_at_idx": {
+ "name": "posts_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_created_at_idx": {
+ "name": "posts_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "posts_featured_image_id_media_id_fk": {
+ "name": "posts_featured_image_id_media_id_fk",
+ "tableFrom": "posts",
+ "tableTo": "media",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "featured_image_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.media": {
+ "name": "media",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "alt": {
+ "name": "alt",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "url": {
+ "name": "url",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "thumbnail_u_r_l": {
+ "name": "thumbnail_u_r_l",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "filename": {
+ "name": "filename",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "filesize": {
+ "name": "filesize",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "width": {
+ "name": "width",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "height": {
+ "name": "height",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "focal_x": {
+ "name": "focal_x",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "focal_y": {
+ "name": "focal_y",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "media_updated_at_idx": {
+ "name": "media_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "media_created_at_idx": {
+ "name": "media_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "media_filename_idx": {
+ "name": "media_filename_idx",
+ "columns": [
+ {
+ "expression": "filename",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.payload_kv": {
+ "name": "payload_kv",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "payload_kv_key_idx": {
+ "name": "payload_kv_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.payload_locked_documents": {
+ "name": "payload_locked_documents",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "global_slug": {
+ "name": "global_slug",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "payload_locked_documents_global_slug_idx": {
+ "name": "payload_locked_documents_global_slug_idx",
+ "columns": [
+ {
+ "expression": "global_slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_updated_at_idx": {
+ "name": "payload_locked_documents_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_created_at_idx": {
+ "name": "payload_locked_documents_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.payload_locked_documents_rels": {
+ "name": "payload_locked_documents_rels",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "users_id": {
+ "name": "users_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "posts_id": {
+ "name": "posts_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "media_id": {
+ "name": "media_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "payload_locked_documents_rels_order_idx": {
+ "name": "payload_locked_documents_rels_order_idx",
+ "columns": [
+ {
+ "expression": "order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_parent_idx": {
+ "name": "payload_locked_documents_rels_parent_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_path_idx": {
+ "name": "payload_locked_documents_rels_path_idx",
+ "columns": [
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_users_id_idx": {
+ "name": "payload_locked_documents_rels_users_id_idx",
+ "columns": [
+ {
+ "expression": "users_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_posts_id_idx": {
+ "name": "payload_locked_documents_rels_posts_id_idx",
+ "columns": [
+ {
+ "expression": "posts_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_media_id_idx": {
+ "name": "payload_locked_documents_rels_media_id_idx",
+ "columns": [
+ {
+ "expression": "media_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "payload_locked_documents_rels_parent_fk": {
+ "name": "payload_locked_documents_rels_parent_fk",
+ "tableFrom": "payload_locked_documents_rels",
+ "tableTo": "payload_locked_documents",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_locked_documents_rels_users_fk": {
+ "name": "payload_locked_documents_rels_users_fk",
+ "tableFrom": "payload_locked_documents_rels",
+ "tableTo": "users",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "users_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_locked_documents_rels_posts_fk": {
+ "name": "payload_locked_documents_rels_posts_fk",
+ "tableFrom": "payload_locked_documents_rels",
+ "tableTo": "posts",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "posts_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_locked_documents_rels_media_fk": {
+ "name": "payload_locked_documents_rels_media_fk",
+ "tableFrom": "payload_locked_documents_rels",
+ "tableTo": "media",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "media_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.payload_preferences": {
+ "name": "payload_preferences",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "value": {
+ "name": "value",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "payload_preferences_key_idx": {
+ "name": "payload_preferences_key_idx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_preferences_updated_at_idx": {
+ "name": "payload_preferences_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_preferences_created_at_idx": {
+ "name": "payload_preferences_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.payload_preferences_rels": {
+ "name": "payload_preferences_rels",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "users_id": {
+ "name": "users_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "payload_preferences_rels_order_idx": {
+ "name": "payload_preferences_rels_order_idx",
+ "columns": [
+ {
+ "expression": "order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_preferences_rels_parent_idx": {
+ "name": "payload_preferences_rels_parent_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_preferences_rels_path_idx": {
+ "name": "payload_preferences_rels_path_idx",
+ "columns": [
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_preferences_rels_users_id_idx": {
+ "name": "payload_preferences_rels_users_id_idx",
+ "columns": [
+ {
+ "expression": "users_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "payload_preferences_rels_parent_fk": {
+ "name": "payload_preferences_rels_parent_fk",
+ "tableFrom": "payload_preferences_rels",
+ "tableTo": "payload_preferences",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_preferences_rels_users_fk": {
+ "name": "payload_preferences_rels_users_fk",
+ "tableFrom": "payload_preferences_rels",
+ "tableTo": "users",
+ "schemaTo": "payload",
+ "columnsFrom": [
+ "users_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "payload.payload_migrations": {
+ "name": "payload_migrations",
+ "schema": "payload",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "batch": {
+ "name": "batch",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "payload_migrations_updated_at_idx": {
+ "name": "payload_migrations_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_migrations_created_at_idx": {
+ "name": "payload_migrations_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "payload.enum_posts_status": {
+ "name": "enum_posts_status",
+ "schema": "payload",
+ "values": [
+ "draft",
+ "published"
+ ]
+ }
+ },
+ "schemas": {
+ "payload": "payload"
+ },
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ }
+} \ No newline at end of file
diff --git a/src/migrations/20251215_093857.ts b/src/migrations/20251215_093857.ts
new file mode 100644
index 0000000..1a53568
--- /dev/null
+++ b/src/migrations/20251215_093857.ts
@@ -0,0 +1,171 @@
+import type { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/db-postgres'
+import { sql } from 'drizzle-orm'
+
+export async function up({ db }: MigrateUpArgs): Promise<void> {
+ await db.execute(sql`
+ CREATE TYPE "payload"."enum_posts_status" AS ENUM('draft', 'published');
+ CREATE TABLE "payload"."users_sessions" (
+ "_order" integer NOT NULL,
+ "_parent_id" integer NOT NULL,
+ "id" varchar PRIMARY KEY NOT NULL,
+ "created_at" timestamp(3) with time zone,
+ "expires_at" timestamp(3) with time zone NOT NULL
+ );
+
+ CREATE TABLE "payload"."users" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "name" varchar,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "email" varchar NOT NULL,
+ "reset_password_token" varchar,
+ "reset_password_expiration" timestamp(3) with time zone,
+ "salt" varchar,
+ "hash" varchar,
+ "login_attempts" numeric DEFAULT 0,
+ "lock_until" timestamp(3) with time zone
+ );
+
+ CREATE TABLE "payload"."posts_tags" (
+ "_order" integer NOT NULL,
+ "_parent_id" integer NOT NULL,
+ "id" varchar PRIMARY KEY NOT NULL,
+ "tag" varchar
+ );
+
+ CREATE TABLE "payload"."posts" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "title" varchar NOT NULL,
+ "slug" varchar NOT NULL,
+ "description" varchar,
+ "content" jsonb NOT NULL,
+ "featured_image_id" integer,
+ "author" varchar DEFAULT 'Admin',
+ "status" "payload"."enum_posts_status" DEFAULT 'draft',
+ "published_at" timestamp(3) with time zone,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
+ );
+
+ CREATE TABLE "payload"."media" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "alt" varchar,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "url" varchar,
+ "thumbnail_u_r_l" varchar,
+ "filename" varchar,
+ "mime_type" varchar,
+ "filesize" numeric,
+ "width" numeric,
+ "height" numeric,
+ "focal_x" numeric,
+ "focal_y" numeric
+ );
+
+ CREATE TABLE "payload"."payload_kv" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "key" varchar NOT NULL,
+ "data" jsonb NOT NULL
+ );
+
+ CREATE TABLE "payload"."payload_locked_documents" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "global_slug" varchar,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
+ );
+
+ CREATE TABLE "payload"."payload_locked_documents_rels" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "order" integer,
+ "parent_id" integer NOT NULL,
+ "path" varchar NOT NULL,
+ "users_id" integer,
+ "posts_id" integer,
+ "media_id" integer
+ );
+
+ CREATE TABLE "payload"."payload_preferences" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "key" varchar,
+ "value" jsonb,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
+ );
+
+ CREATE TABLE "payload"."payload_preferences_rels" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "order" integer,
+ "parent_id" integer NOT NULL,
+ "path" varchar NOT NULL,
+ "users_id" integer
+ );
+
+ CREATE TABLE "payload"."payload_migrations" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "name" varchar,
+ "batch" numeric,
+ "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
+ );
+
+ ALTER TABLE "payload"."users_sessions" ADD CONSTRAINT "users_sessions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "payload"."users"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload"."posts_tags" ADD CONSTRAINT "posts_tags_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "payload"."posts"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload"."posts" ADD CONSTRAINT "posts_featured_image_id_media_id_fk" FOREIGN KEY ("featured_image_id") REFERENCES "payload"."media"("id") ON DELETE set null ON UPDATE no action;
+ ALTER TABLE "payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "payload"."payload_locked_documents"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "payload"."users"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_posts_fk" FOREIGN KEY ("posts_id") REFERENCES "payload"."posts"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload"."payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_media_fk" FOREIGN KEY ("media_id") REFERENCES "payload"."media"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload"."payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "payload"."payload_preferences"("id") ON DELETE cascade ON UPDATE no action;
+ ALTER TABLE "payload"."payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "payload"."users"("id") ON DELETE cascade ON UPDATE no action;
+ CREATE INDEX "users_sessions_order_idx" ON "payload"."users_sessions" USING btree ("_order");
+ CREATE INDEX "users_sessions_parent_id_idx" ON "payload"."users_sessions" USING btree ("_parent_id");
+ CREATE INDEX "users_updated_at_idx" ON "payload"."users" USING btree ("updated_at");
+ CREATE INDEX "users_created_at_idx" ON "payload"."users" USING btree ("created_at");
+ CREATE UNIQUE INDEX "users_email_idx" ON "payload"."users" USING btree ("email");
+ CREATE INDEX "posts_tags_order_idx" ON "payload"."posts_tags" USING btree ("_order");
+ CREATE INDEX "posts_tags_parent_id_idx" ON "payload"."posts_tags" USING btree ("_parent_id");
+ CREATE UNIQUE INDEX "posts_slug_idx" ON "payload"."posts" USING btree ("slug");
+ CREATE INDEX "posts_featured_image_idx" ON "payload"."posts" USING btree ("featured_image_id");
+ CREATE INDEX "posts_updated_at_idx" ON "payload"."posts" USING btree ("updated_at");
+ CREATE INDEX "posts_created_at_idx" ON "payload"."posts" USING btree ("created_at");
+ CREATE INDEX "media_updated_at_idx" ON "payload"."media" USING btree ("updated_at");
+ CREATE INDEX "media_created_at_idx" ON "payload"."media" USING btree ("created_at");
+ CREATE UNIQUE INDEX "media_filename_idx" ON "payload"."media" USING btree ("filename");
+ CREATE UNIQUE INDEX "payload_kv_key_idx" ON "payload"."payload_kv" USING btree ("key");
+ CREATE INDEX "payload_locked_documents_global_slug_idx" ON "payload"."payload_locked_documents" USING btree ("global_slug");
+ CREATE INDEX "payload_locked_documents_updated_at_idx" ON "payload"."payload_locked_documents" USING btree ("updated_at");
+ CREATE INDEX "payload_locked_documents_created_at_idx" ON "payload"."payload_locked_documents" USING btree ("created_at");
+ CREATE INDEX "payload_locked_documents_rels_order_idx" ON "payload"."payload_locked_documents_rels" USING btree ("order");
+ CREATE INDEX "payload_locked_documents_rels_parent_idx" ON "payload"."payload_locked_documents_rels" USING btree ("parent_id");
+ CREATE INDEX "payload_locked_documents_rels_path_idx" ON "payload"."payload_locked_documents_rels" USING btree ("path");
+ CREATE INDEX "payload_locked_documents_rels_users_id_idx" ON "payload"."payload_locked_documents_rels" USING btree ("users_id");
+ CREATE INDEX "payload_locked_documents_rels_posts_id_idx" ON "payload"."payload_locked_documents_rels" USING btree ("posts_id");
+ CREATE INDEX "payload_locked_documents_rels_media_id_idx" ON "payload"."payload_locked_documents_rels" USING btree ("media_id");
+ CREATE INDEX "payload_preferences_key_idx" ON "payload"."payload_preferences" USING btree ("key");
+ CREATE INDEX "payload_preferences_updated_at_idx" ON "payload"."payload_preferences" USING btree ("updated_at");
+ CREATE INDEX "payload_preferences_created_at_idx" ON "payload"."payload_preferences" USING btree ("created_at");
+ CREATE INDEX "payload_preferences_rels_order_idx" ON "payload"."payload_preferences_rels" USING btree ("order");
+ CREATE INDEX "payload_preferences_rels_parent_idx" ON "payload"."payload_preferences_rels" USING btree ("parent_id");
+ CREATE INDEX "payload_preferences_rels_path_idx" ON "payload"."payload_preferences_rels" USING btree ("path");
+ CREATE INDEX "payload_preferences_rels_users_id_idx" ON "payload"."payload_preferences_rels" USING btree ("users_id");
+ CREATE INDEX "payload_migrations_updated_at_idx" ON "payload"."payload_migrations" USING btree ("updated_at");
+ CREATE INDEX "payload_migrations_created_at_idx" ON "payload"."payload_migrations" USING btree ("created_at");`)
+}
+
+export async function down({ db }: MigrateDownArgs): Promise<void> {
+ await db.execute(sql`
+ DROP TABLE "payload"."users_sessions" CASCADE;
+ DROP TABLE "payload"."users" CASCADE;
+ DROP TABLE "payload"."posts_tags" CASCADE;
+ DROP TABLE "payload"."posts" CASCADE;
+ DROP TABLE "payload"."media" CASCADE;
+ DROP TABLE "payload"."payload_kv" CASCADE;
+ DROP TABLE "payload"."payload_locked_documents" CASCADE;
+ DROP TABLE "payload"."payload_locked_documents_rels" CASCADE;
+ DROP TABLE "payload"."payload_preferences" CASCADE;
+ DROP TABLE "payload"."payload_preferences_rels" CASCADE;
+ DROP TABLE "payload"."payload_migrations" CASCADE;
+ DROP TYPE "payload"."enum_posts_status";`)
+}
diff --git a/src/migrations/index.ts b/src/migrations/index.ts
new file mode 100644
index 0000000..2a97f8d
--- /dev/null
+++ b/src/migrations/index.ts
@@ -0,0 +1,9 @@
+import * as migration_20251215_093857 from './20251215_093857';
+
+export const migrations = [
+ {
+ up: migration_20251215_093857.up,
+ down: migration_20251215_093857.down,
+ name: '20251215_093857'
+ },
+];
diff --git a/src/payload/collections/Media.ts b/src/payload/collections/Media.ts
new file mode 100644
index 0000000..45525db
--- /dev/null
+++ b/src/payload/collections/Media.ts
@@ -0,0 +1,23 @@
+import type { CollectionConfig } from 'payload';
+
+export const Media: CollectionConfig = {
+ slug: 'media',
+ labels: {
+ singular: 'media',
+ plural: 'medias',
+ },
+ access: {
+ read: () => true,
+ },
+ upload: {
+ staticDir: 'public/media',
+ mimeTypes: ['image/*'],
+ },
+ fields: [
+ {
+ name: 'alt',
+ label: '替代文本',
+ type: 'text',
+ },
+ ],
+};
diff --git a/src/payload/collections/Posts.ts b/src/payload/collections/Posts.ts
new file mode 100644
index 0000000..6ecf898
--- /dev/null
+++ b/src/payload/collections/Posts.ts
@@ -0,0 +1,93 @@
+import type { CollectionConfig } from 'payload';
+
+export const Posts: CollectionConfig = {
+ slug: 'posts',
+ labels: {
+ singular: 'posts',
+ plural: 'posts',
+ },
+ admin: {
+ useAsTitle: 'title',
+ defaultColumns: ['title', 'status', 'publishedAt', 'updatedAt'],
+ },
+ access: {
+ read: () => true,
+ },
+ fields: [
+ {
+ name: 'title',
+ label: 'title',
+ type: 'text',
+ required: true,
+ },
+ {
+ name: 'slug',
+ label: 'slug',
+ type: 'text',
+ required: true,
+ unique: true,
+ admin: {
+ description: 'example:my-first-post',
+ },
+ },
+ {
+ name: 'description',
+ label: 'description',
+ type: 'textarea',
+ },
+ {
+ name: 'content',
+ label: 'content',
+ type: 'richText',
+ required: true,
+ },
+ {
+ name: 'featuredImage',
+ label: 'image',
+ type: 'upload',
+ relationTo: 'media',
+ },
+ {
+ name: 'author',
+ label: 'author',
+ type: 'text',
+ defaultValue: 'Admin',
+ },
+ {
+ name: 'tags',
+ label: 'tag',
+ type: 'array',
+ fields: [
+ {
+ name: 'tag',
+ label: 'tag',
+ type: 'text',
+ },
+ ],
+ },
+ {
+ name: 'status',
+ label: 'status',
+ type: 'select',
+ defaultValue: 'draft',
+ options: [
+ { label: 'draft', value: 'draft' },
+ { label: 'published', value: 'published' },
+ ],
+ admin: {
+ position: 'sidebar',
+ },
+ },
+ {
+ name: 'publishedAt',
+ label: 'publishedAt',
+ type: 'date',
+ admin: {
+ position: 'sidebar',
+ date: {
+ pickerAppearance: 'dayAndTime',
+ },
+ },
+ },
+ ],
+};
diff --git a/src/payload/collections/Users.ts b/src/payload/collections/Users.ts
new file mode 100644
index 0000000..730cde2
--- /dev/null
+++ b/src/payload/collections/Users.ts
@@ -0,0 +1,20 @@
+import type { CollectionConfig } from 'payload';
+
+export const Users: CollectionConfig = {
+ slug: 'users',
+ labels: {
+ singular: 'users',
+ plural: 'users',
+ },
+ admin: {
+ useAsTitle: 'email',
+ },
+ auth: true,
+ fields: [
+ {
+ name: 'name',
+ label: 'name',
+ type: 'text',
+ },
+ ],
+};
diff --git a/src/server/db/index.ts b/src/server/db/index.ts
index 8579424..6a8821e 100644
--- a/src/server/db/index.ts
+++ b/src/server/db/index.ts
@@ -1,13 +1,15 @@
-import { neon } from '@neondatabase/serverless';
-import { drizzle } from 'drizzle-orm/neon-http';
+import { Pool } from 'pg';
+import { drizzle } from 'drizzle-orm/node-postgres';
import { env } from '@/env';
import * as schema from './schema';
-const sql = neon(env.DATABASE_URL);
+const pool = new Pool({
+ connectionString: env.DATABASE_URL,
+});
export const db = drizzle({
- client: sql,
+ client: pool,
schema,
casing: 'snake_case',
});