Next.js Supabase V3 (#463)

Version 3 of the kit:
- Radix UI replaced with Base UI (using the Shadcn UI patterns)
- next-intl replaces react-i18next
- enhanceAction deprecated; usage moved to next-safe-action
- main layout now wrapped with [locale] path segment
- Teams only mode
- Layout updates
- Zod v4
- Next.js 16.2
- Typescript 6
- All other dependencies updated
- Removed deprecated Edge CSRF
- Dynamic Github Action runner
This commit is contained in:
Giancarlo Buomprisco
2026-03-24 13:40:38 +08:00
committed by GitHub
parent 4912e402a3
commit 7ebff31475
840 changed files with 71395 additions and 20095 deletions

View File

@@ -0,0 +1,120 @@
import { cache } from 'react';
import type { Metadata } from 'next';
import { getLocale, getTranslations } from 'next-intl/server';
import { createCmsClient } from '@kit/cms';
import { getLogger } from '@kit/shared/logger';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
// local imports
import { SitePageHeader } from '../_components/site-page-header';
import { BlogPagination } from './_components/blog-pagination';
import { PostPreview } from './_components/post-preview';
interface BlogPageProps {
searchParams: Promise<{ page?: string }>;
}
const BLOG_POSTS_PER_PAGE = 10;
export const generateMetadata = async (
props: BlogPageProps,
): Promise<Metadata> => {
const t = await getTranslations('marketing');
const resolvedLanguage = await getLocale();
const searchParams = await props.searchParams;
const limit = BLOG_POSTS_PER_PAGE;
const page = searchParams.page ? parseInt(searchParams.page) : 0;
const offset = page * limit;
const { total } = await getContentItems(resolvedLanguage, limit, offset);
return {
title: t('blog'),
description: t('blogSubtitle'),
pagination: {
previous: page > 0 ? `/blog?page=${page - 1}` : undefined,
next: offset + limit < total ? `/blog?page=${page + 1}` : undefined,
},
};
};
const getContentItems = cache(
async (language: string | undefined, limit: number, offset: number) => {
const client = await createCmsClient();
const logger = await getLogger();
try {
return await client.getContentItems({
collection: 'posts',
limit,
offset,
language,
content: false,
sortBy: 'publishedAt',
sortDirection: 'desc',
});
} catch (error) {
logger.error({ error }, 'Failed to load blog posts');
return { total: 0, items: [] };
}
},
);
async function BlogPage(props: BlogPageProps) {
const t = await getTranslations('marketing');
const language = await getLocale();
const searchParams = await props.searchParams;
const limit = BLOG_POSTS_PER_PAGE;
const page = searchParams.page ? parseInt(searchParams.page) : 0;
const offset = page * limit;
const { total, items: posts } = await getContentItems(
language,
limit,
offset,
);
return (
<>
<SitePageHeader title={t('blog')} subtitle={t('blogSubtitle')} />
<div className={'container flex flex-col space-y-6 py-8'}>
<If
condition={posts.length > 0}
fallback={<Trans i18nKey="marketing.noPosts" />}
>
<PostsGridList>
{posts.map((post, idx) => {
return <PostPreview key={idx} post={post} />;
})}
</PostsGridList>
<div>
<BlogPagination
currentPage={page}
canGoToNextPage={offset + limit < total}
canGoToPreviousPage={page > 0}
/>
</div>
</If>
</div>
</>
);
}
export default BlogPage;
function PostsGridList({ children }: React.PropsWithChildren) {
return (
<div className="grid grid-cols-1 gap-y-8 md:grid-cols-2 md:gap-x-2 md:gap-y-12 lg:grid-cols-3">
{children}
</div>
);
}