summaryrefslogtreecommitdiff
path: root/src/app/(home)
diff options
context:
space:
mode:
authorBertrand Yuan <bert.yuan@outlook.com>2025-12-16 00:25:04 +0800
committerGitHub <noreply@github.com>2025-12-16 00:25:04 +0800
commit39c83fbb69ef06d2d56790d75abc254ba7e34394 (patch)
treedd006593448c3500bdcb414af3b4656f7a7683d4 /src/app/(home)
parent48b07bc308a35734a6a7a305c8fdccbfa47de7d8 (diff)
parent785371bb3eccca455e5ce5fccbe9b6e3752a03f6 (diff)
Merge pull request #1 from bertyuan/feat-introduce-payloadv1.0
Feat: introduce payload
Diffstat (limited to 'src/app/(home)')
-rw-r--r--src/app/(home)/(mdx)/about/page.mdx24
-rw-r--r--src/app/(home)/_components/call-to-action.tsx23
-rw-r--r--src/app/(home)/_components/hero.tsx98
-rw-r--r--src/app/(home)/_components/posts.tsx40
-rw-r--r--src/app/(home)/actions.ts78
-rw-r--r--src/app/(home)/layout.tsx31
-rw-r--r--src/app/(home)/page.tsx29
-rw-r--r--src/app/(home)/posts/[slug]/page.client.tsx57
-rw-r--r--src/app/(home)/posts/[slug]/page.tsx145
-rw-r--r--src/app/(home)/posts/page.tsx133
-rw-r--r--src/app/(home)/tags/[...slug]/page.tsx180
-rw-r--r--src/app/(home)/tags/page.tsx57
12 files changed, 0 insertions, 895 deletions
diff --git a/src/app/(home)/(mdx)/about/page.mdx b/src/app/(home)/(mdx)/about/page.mdx
deleted file mode 100644
index 675f3a8..0000000
--- a/src/app/(home)/(mdx)/about/page.mdx
+++ /dev/null
@@ -1,24 +0,0 @@
-import MdxLayout from '@/components/mdx-layout';
-
-<MdxLayout title="About" toc={toc} comments={true} slug={"about"}>
-Hey, I'm a **Senior Software Engineer** at Company. I focus on building fast, accessible, and visually polished front-end experiences using tools like **Next.js**, **TypeScript**, and whatever cool thing just dropped on GitHub.
-
-This portfolio is built with **Next.js** and powered by [Fumadocs](https://fumadocs.vercel.app/), which makes writing pages in Markdown feel like second nature. It lets me focus on content without wrestling with layout.
-
-### Journey
-
-I didn't grow up writing code. In fact, I didn't even know what a `<div>` was until much later. My curiosity started with tweaking themes on forums, editing little bits of CSS without knowing what CSS even meant.
-
-Eventually, that curiosity turned into late nights spent debugging JavaScript errors and falling in love with how code could bring ideas to life.
-
-I started building side projects, contributing to open source, and slowly leveling up. My first "real" job involved wrangling legacy jQuery code. It wasn't glamorous, but it taught me the fundamentals — and just how far modern frameworks have come.
-
-Since then, I've worked across startups and mid-sized teams, shipped production code to thousands of users, and mentored new developers along the way.
-
-### Socials
-
-- Twitter [@yourname](https://twitter.com/yourname)
-- GitHub [@yourname](https://github.com/yourname)
-- Instagram [@yourname](https://instagram.com/yourname)
-- Email your@name.com
-</MdxLayout> \ No newline at end of file
diff --git a/src/app/(home)/_components/call-to-action.tsx b/src/app/(home)/_components/call-to-action.tsx
deleted file mode 100644
index b75298e..0000000
--- a/src/app/(home)/_components/call-to-action.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { NewsletterForm } from '@/components/newsletter-form';
-import { Section } from '@/components/section';
-import type React from 'react';
-
-export function CTA(): React.ReactElement {
- return (
- <Section className='relative grid gap-8 px-4 py-10 sm:grid-cols-2 md:py-14 lg:px-6 lg:py-16'>
- <div className='max-w-xl space-y-2'>
- <h2 className='font-semibold text-2xl md:text-3xl lg:text-4xl'>
- Subscribe to the Newsletter
- </h2>
- <p className='text-muted-foreground text-sm md:text-base'>
- Get the latest articles and updates delivered straight to your inbox.
- No spam, unsubscribe anytime.
- </p>
- </div>
-
- <div className='flex w-full items-center'>
- <NewsletterForm />
- </div>
- </Section>
- );
-}
diff --git a/src/app/(home)/_components/hero.tsx b/src/app/(home)/_components/hero.tsx
deleted file mode 100644
index 8ac251b..0000000
--- a/src/app/(home)/_components/hero.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { baseOptions, linkItems } from '@/app/layout.config';
-import { Icons } from '@/components/icons/icons';
-import { Section } from '@/components/section';
-import { buttonVariants } from '@/components/ui/button';
-import { cn } from '@/lib/utils';
-import { getLinks } from 'fumadocs-ui/layouts/shared';
-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';
-
-const Hero = () => {
- const links = getLinks(linkItems, baseOptions.githubUrl);
- const navItems = links.filter((item) =>
- ['nav', 'all'].includes(item.on ?? 'all'),
- );
-
- return (
- <Section className='relative flex flex-col items-center justify-center gap-6 overflow-hidden bg-dashed px-4 py-16 sm:px-16 sm:py-24 md:py-32'>
- <motion.div
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{
- duration: 0.4,
- scale: { type: 'spring', visualDuration: 0.4, bounce: 0.5 },
- }}
- whileInView={{ opacity: 1 }}
- viewport={{ once: true }}
- className='-z-10 absolute inset-0 h-full w-full'
- >
- <Image
- src={heroImage}
- alt='Hero Background'
- height={600}
- width={704}
- className='pointer-events-none absolute right-0 bottom-0 h-[900px] w-[1004px] max-w-[1004px] translate-x-1/2 translate-y-1/2 select-none opacity-80 dark:opacity-100'
- priority
- />
- </motion.div>
- <div className='flex items-center justify-center space-x-2'>
- <Icons.code className='h-6 w-6 text-primary transition-transform hover:scale-125' />
- <span className='font-medium text-muted-foreground text-sm'>
- Full-Stack Developer & Tech Writer
- </span>
- </div>
- <h1 className='max-w-3xl text-center font-bold text-4xl leading-tight tracking-tighter sm:text-5xl md:max-w-4xl md:text-6xl lg:leading-[1.1]'>
- <Balancer>I'm John Doe , a Full-Stack Developer.</Balancer>
- </h1>
- <p className='max-w-xl text-center text-muted-foreground md:max-w-2xl md:text-lg'>
- <Balancer>
- I write about web development, software engineering, and the latest
- technologies. I also create fun projects and tutorials to help you
- learn and grow as a developer.
- </Balancer>
- </p>
-
- <div className='flex flex-wrap items-center justify-center gap-4'>
- <Link
- className={cn(
- buttonVariants({
- variant: 'default',
- size: 'lg',
- }),
- 'group rounded-full bg-primary hover:bg-primary/90',
- )}
- href='/posts'
- >
- Browse Posts
- <Icons.arrowUpRight className='group-hover:-rotate-12 ml-2 size-5 transition-transform' />
- </Link>
-
- <div className='flex items-center space-x-4'>
- {navItems
- .filter((item) => item.type === 'icon')
- .map((item, i) => (
- <Link
- key={i.toString()}
- href={item.url}
- className={cn(
- buttonVariants({
- variant: 'ghost',
- size: 'icon',
- }),
- 'rounded-full',
- )}
- >
- {item.icon}
- <span className='sr-only'>{item.text}</span>
- </Link>
- ))}
- </div>
- </div>
- </Section>
- );
-};
-
-export default Hero;
diff --git a/src/app/(home)/_components/posts.tsx b/src/app/(home)/_components/posts.tsx
deleted file mode 100644
index 8c8dc33..0000000
--- a/src/app/(home)/_components/posts.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-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 Link from 'next/link';
-
-export default function Posts({ posts }: { posts: Page[] }) {
- 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}
- />
- );
- })}
- <Link
- href='/posts'
- className={buttonVariants({
- variant: 'default',
- className: 'group rounded-none py-4 sm:py-8',
- })}
- >
- View More
- <Icons.arrowUpRight className='group-hover:-rotate-12 ml-2 size-5 transition-transform' />
- </Link>
- </div>
- </Section>
- );
-}
diff --git a/src/app/(home)/actions.ts b/src/app/(home)/actions.ts
deleted file mode 100644
index fdb16ca..0000000
--- a/src/app/(home)/actions.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-'use server';
-
-import { getContact, sendWelcomeEmail, 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';
-
-const resend = new Resend(process.env.RESEND_API_KEY as string);
-const audienceId = process.env.RESEND_AUDIENCE_ID as string;
-
-const splitName = (name = '') => {
- const [firstName, ...lastName] = name.split(' ').filter(Boolean);
- return {
- firstName: firstName,
- lastName: lastName.join(' '),
- };
-};
-
-export const subscribeUser = actionClient
- .schema(NewsletterSchema)
- .action(async ({ parsedInput: { email } }) => {
- const session = await getSession();
- const fullName = session?.user.name || '';
- const { firstName, lastName } = fullName
- ? splitName(fullName)
- : { firstName: '', lastName: '' };
-
- try {
- const contact = await getContact({ email, audienceId });
-
- if (contact) {
- await updateContact({
- email,
- firstName,
- lastName,
- audienceId,
- unsubscribed: false,
- });
-
- return {
- success: true,
- message: 'You are already subscribed to our newsletter!',
- };
- }
-
- const { data, error } = await resend.contacts.create({
- email,
- audienceId,
- firstName,
- lastName,
- unsubscribed: false,
- });
-
- if (!data || error) {
- throw new Error(
- `Failed to create contact: ${error?.message || 'Unknown error'}`,
- );
- }
-
- const posts = getSortedByDatePosts();
- await sendWelcomeEmail({
- posts,
- to: email,
- firstName: firstName || 'there',
- });
-
- return {
- success: true,
- message: 'You are now subscribed to our newsletter!',
- };
- } catch (error) {
- console.error('Failed to subscribe:', error);
- if (error instanceof ActionError) throw error;
- throw new ActionError('Oops, something went wrong while subscribing.');
- }
- });
diff --git a/src/app/(home)/layout.tsx b/src/app/(home)/layout.tsx
deleted file mode 100644
index bd641df..0000000
--- a/src/app/(home)/layout.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Footer } from '@/components/sections/footer';
-import { Header } from '@/components/sections/header';
-import { HomeLayout } from 'fumadocs-ui/layouts/home';
-import { getLinks } from 'fumadocs-ui/layouts/shared';
-import type { ReactNode } from 'react';
-import { baseOptions, linkItems } from '../layout.config';
-
-const Layout = ({ children }: { children: ReactNode }) => {
- return (
- <HomeLayout
- {...baseOptions}
- links={linkItems}
- nav={{
- component: (
- <Header
- finalLinks={getLinks(linkItems, baseOptions.githubUrl)}
- {...baseOptions}
- />
- ),
- }}
- className='pt-0'
- >
- <main className='flex flex-1 flex-col divide-y divide-dashed divide-border/70 border-border/70 border-dashed sm:border-b dark:divide-border dark:border-border'>
- {children}
- <Footer />
- </main>
- </HomeLayout>
- );
-};
-
-export default Layout;
diff --git a/src/app/(home)/page.tsx b/src/app/(home)/page.tsx
deleted file mode 100644
index da7da0f..0000000
--- a/src/app/(home)/page.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import Hero from '@/app/(home)/_components/hero';
-import Posts from '@/app/(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 { CTA } from './_components/call-to-action';
-
-export default function Home() {
- const posts = getSortedByDatePosts().slice(0, 3);
-
- return (
- <>
- <Hero />
- <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{' '}
- <Icons.posts className='size-10 fill-fd-primary/30 text-fd-primary transition-transform hover:rotate-12 hover:scale-125' />
- </span>
- </h2>
- </Section>
- <Separator />
- <Posts posts={posts} />
- <Separator />
- <CTA />
- </>
- );
-}
diff --git a/src/app/(home)/posts/[slug]/page.client.tsx b/src/app/(home)/posts/[slug]/page.client.tsx
deleted file mode 100644
index 7a97f56..0000000
--- a/src/app/(home)/posts/[slug]/page.client.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-'use client';
-import {
- UploadIcon as ShareIcon,
- type UploadIconHandle as ShareIconHandle,
-} from '@/components/icons/animated/upload';
-import { Icons } from '@/components/icons/icons';
-import { Button } from '@/components/ui/button';
-import { cn } from '@/lib/utils';
-import { Comments } from '@fuma-comment/react';
-import { redirect } from 'next/navigation';
-import { useRef } from 'react';
-import { toast } from 'sonner';
-import { useCopyToClipboard } from 'usehooks-ts';
-
-export function Share({ url }: { url: string }): React.ReactElement {
- const iconRef = useRef<ShareIconHandle>(null);
- const [_, copyToClipboard] = useCopyToClipboard();
-
- const onClick = async (): Promise<void> => {
- await copyToClipboard(`${window.location.origin}${url}`);
- toast.success('Copied to clipboard!', {
- icon: <Icons.copied className='size-4' />,
- description: 'The post link has been copied to your clipboard.',
- });
- };
-
- return (
- <Button
- className={cn('group gap-2')}
- variant={'secondary'}
- onClick={onClick}
- onMouseEnter={() => iconRef.current?.startAnimation?.()}
- onMouseLeave={() => iconRef.current?.stopAnimation?.()}
- >
- <ShareIcon className='size-4 hover:bg-transparent' ref={iconRef} />
- Share Post
- </Button>
- );
-}
-
-export function PostComments({
- slug,
- className,
-}: { slug: string; className?: string }) {
- return (
- <Comments
- page={slug}
- className={cn('w-full', className)}
- auth={{
- type: 'api',
- signIn: () => {
- redirect('/login');
- },
- }}
- />
- );
-}
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/(home)/posts/page.tsx b/src/app/(home)/posts/page.tsx
deleted file mode 100644
index fd0f912..0000000
--- a/src/app/(home)/posts/page.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import { postsPerPage } from '@/app/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 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,
-}: {
- startIndex: number;
- endIndex: number;
-}) => {
- const start = startIndex + 1;
- const end = endIndex < totalPosts ? endIndex : totalPosts;
- if (start === end) return <span>({start})</span>;
- return (
- <span>
- ({start}-{end})
- </span>
- );
-};
-
-const Pagination = ({ pageIndex }: { pageIndex: number }) => {
- const handlePageChange = async (page: number) => {
- 'use server';
- redirect(`/posts?page=${page}`);
- };
-
- return (
- <Section className='bg-dashed'>
- <NumberedPagination
- currentPage={pageIndex + 1}
- totalPages={pageCount}
- paginationItemsToDisplay={5}
- onPageChange={handlePageChange}
- />
- </Section>
- );
-};
-
-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
- : 0;
- if (pageIndex < 0 || pageIndex >= pageCount) notFound();
-
- const startIndex = pageIndex * postsPerPage;
- const endIndex = startIndex + postsPerPage;
- const posts = getSortedByDatePosts().slice(startIndex, endIndex);
-
- 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} />
- </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();
- 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}
- />
- );
- })}
- </div>
- </Section>
- {pageCount > 1 && <Pagination pageIndex={pageIndex} />}
- </>
- );
-}
-
-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 }>;
-};
-
-export async function generateMetadata(
- props: Props,
- parent: ResolvingMetadata,
-): Promise<Metadata> {
- const params = await props.params;
- const searchParams = await props.searchParams;
-
- const pageIndex = searchParams.page
- ? Number.parseInt(searchParams.page as string, 10)
- : 1;
-
- const isFirstPage = pageIndex === 1 || !searchParams.page;
- const pageTitle = isFirstPage ? 'Posts' : `Posts - Page ${pageIndex}`;
- const canonicalUrl = isFirstPage ? '/posts' : `/posts?page=${pageIndex}`;
-
- return createMetadata({
- title: pageTitle,
- description: `Posts${!isFirstPage ? ` - Page ${pageIndex}` : ''}`,
- openGraph: {
- url: canonicalUrl,
- },
- alternates: {
- canonical: canonicalUrl,
- },
- });
-}
diff --git a/src/app/(home)/tags/[...slug]/page.tsx b/src/app/(home)/tags/[...slug]/page.tsx
deleted file mode 100644
index 9479705..0000000
--- a/src/app/(home)/tags/[...slug]/page.tsx
+++ /dev/null
@@ -1,180 +0,0 @@
-import { postsPerPage } from '@/app/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 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,
-}: {
- startIndex: number;
- endIndex: number;
- tag: string;
-}) => {
- const total = totalPosts(tag);
- const start = startIndex + 1;
- const end = endIndex < total ? endIndex : total;
- if (start === end) return <span>({start})</span>;
- return (
- <span>
- ({start}-{end})
- </span>
- );
-};
-
-const Header = ({
- tag,
- startIndex,
- endIndex,
-}: {
- tag: string;
- startIndex: number;
- endIndex: number;
-}) => (
- <Section className='p-4 lg:p-6'>
- <div className='flex items-center gap-2'>
- <Icons.tag
- size={20}
- className='text-muted-foreground transition-transform hover:rotate-12 hover:scale-125'
- />
- <h1 className='font-bold text-3xl leading-tight tracking-tighter md:text-4xl'>
- {tag} <span className='text-muted-foreground'>Posts</span>{' '}
- <CurrentPostsCount
- startIndex={startIndex}
- endIndex={endIndex}
- tag={tag}
- />
- </h1>
- </div>
- </Section>
-);
-
-const Pagination = ({ pageIndex, tag }: { pageIndex: number; tag: string }) => {
- const handlePageChange = async (page: number) => {
- 'use server';
- redirect(`/tags/${tag}?page=${page}`);
- };
-
- return (
- <Section className='bg-dashed'>
- <NumberedPagination
- currentPage={pageIndex + 1}
- totalPages={pageCount(tag)}
- paginationItemsToDisplay={5}
- onPageChange={handlePageChange}
- />
- </Section>
- );
-};
-
-export default async function Page(props: {
- params: Promise<{ slug: string[] }>;
- searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
-}) {
- const params = await props.params;
- const searchParams = await props.searchParams;
-
- const tag = params.slug[0];
- if (!tag) return notFound();
-
- const pageIndex = searchParams.page
- ? Number.parseInt(searchParams.page[0] ?? '', 10) - 1
- : 0;
-
- if (pageIndex < 0 || pageIndex >= pageCount(tag)) notFound();
-
- const startIndex = pageIndex * postsPerPage;
- const endIndex = startIndex + postsPerPage;
- const posts = getPostsByTag(tag).slice(startIndex, endIndex);
-
- return (
- <>
- <Header tag={tag} startIndex={startIndex} endIndex={endIndex} />
- <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();
- 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}
- />
- );
- })}
- </div>
- </Section>
- {pageCount(tag) > 1 && <Pagination pageIndex={pageIndex} tag={tag} />}
- <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()],
- })),
- ),
- ];
-};
-
-type Props = {
- params: Promise<{ slug: string[] }>;
- searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
-};
-
-export async function generateMetadata(
- props: Props,
- parent: ResolvingMetadata,
-): Promise<Metadata> {
- const params = await props.params;
- const searchParams = await props.searchParams;
-
- const tag = params.slug[0];
- const pageIndex = searchParams.page
- ? Number.parseInt(searchParams.page.toString(), 10)
- : 1;
-
- const isFirstPage = pageIndex === 1 || !searchParams.page;
- const pageTitle = isFirstPage
- ? `${tag} Posts`
- : `${tag} Posts - Page ${pageIndex}`;
- const canonicalUrl = isFirstPage
- ? `/tags/${tag}`
- : `/tags/${tag}?page=${pageIndex}`;
-
- return createMetadata({
- title: pageTitle,
- description: `Posts tagged with ${tag}${
- !isFirstPage ? ` - Page ${pageIndex}` : ''
- }`,
- openGraph: {
- url: canonicalUrl,
- },
- alternates: {
- canonical: canonicalUrl,
- },
- });
-}
diff --git a/src/app/(home)/tags/page.tsx b/src/app/(home)/tags/page.tsx
deleted file mode 100644
index 54fb423..0000000
--- a/src/app/(home)/tags/page.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { title as homeTitle } from '@/app/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 { cn } from '@/lib/utils';
-import type { Metadata } from 'next';
-
-export default function Page() {
- const tags = getTags();
-
- return (
- <>
- <Section className='p-4 lg:p-6'>
- <h1 className='font-bold text-3xl leading-tight tracking-tighter md:text-4xl'>
- Tags
- </h1>
- </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) => (
- <TagCard
- key={tag}
- displayCount={true}
- name={tag}
- 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',
- )}
- />
- ))}
- {tags.length % 2 === 1 && (
- <div className='size-full border-border/70 border-dashed bg-dashed sm:border-b sm:border-l dark:border-border' />
- )}
- </div>
- </Section>
- </>
- );
-}
-
-export async function generateMetadata(props: {
- params: Promise<{ slug?: string[] }>;
-}): Promise<Metadata> {
- const params = await props.params;
- const description = `Explore all the tags on ${homeTitle}.`;
-
- return createMetadata({
- title: 'Tags',
- description,
- openGraph: {
- url: '/tags',
- },
- alternates: {
- canonical: '/tags',
- },
- });
-}