Merge branch 'main' of github.com:MkSaaSHQ/mksaas-template
This commit is contained in:
commit
5f6e75fe93
@ -1,72 +1,38 @@
|
||||
import BlogGrid from '@/components/blog/blog-grid';
|
||||
import EmptyGrid from '@/components/shared/empty-grid';
|
||||
import CustomPagination from '@/components/shared/pagination';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import BlogGridWithPagination from '@/components/blog/blog-grid-with-pagination';
|
||||
import { LOCALES } from '@/i18n/routing';
|
||||
import { getPaginatedBlogPosts } from '@/lib/blog/data';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { allCategories, allPosts } from 'content-collections';
|
||||
import { allCategories } from 'content-collections';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
// Generate all static params for SSG (locale + category + pagination)
|
||||
// Generate all static params for SSG (locale + category)
|
||||
export function generateStaticParams() {
|
||||
const categories = allCategories;
|
||||
const publishedPosts = allPosts.filter((post) => post.published);
|
||||
const paginationSize = websiteConfig.blog.paginationSize;
|
||||
const params: { locale: string; slug: string; page?: string }[] = [];
|
||||
const params: { locale: string; slug: string }[] = [];
|
||||
for (const locale of LOCALES) {
|
||||
const localeCategories = categories.filter((cat) => cat.locale === locale);
|
||||
const localeCategories = allCategories.filter(
|
||||
(category) => category.locale === locale
|
||||
);
|
||||
for (const category of localeCategories) {
|
||||
const filteredPosts = publishedPosts.filter(
|
||||
(post) =>
|
||||
post.locale === locale &&
|
||||
post.categories.some((cat) => cat && cat.slug === category.slug)
|
||||
);
|
||||
const totalCount = filteredPosts.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalCount / paginationSize));
|
||||
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber++) {
|
||||
if (pageNumber === 1) {
|
||||
params.push({ locale, slug: category.slug });
|
||||
} else {
|
||||
params.push({
|
||||
locale,
|
||||
slug: category.slug,
|
||||
page: String(pageNumber),
|
||||
});
|
||||
}
|
||||
}
|
||||
params.push({ locale, slug: category.slug });
|
||||
}
|
||||
}
|
||||
console.log('BlogCategoryPage, generateStaticParams', params);
|
||||
return params;
|
||||
}
|
||||
|
||||
// Generate metadata for each static category page (locale + slug + page)
|
||||
// Generate metadata for each static category page (locale + category)
|
||||
export async function generateMetadata({ params }: BlogCategoryPageProps) {
|
||||
const { locale, slug, page } = await params;
|
||||
// Find category with matching slug and locale
|
||||
const { locale, slug } = await params;
|
||||
const category = allCategories.find(
|
||||
(category) => category.slug === slug && category.locale === locale
|
||||
(category) => category.locale === locale && category.slug === slug
|
||||
);
|
||||
|
||||
if (!category) {
|
||||
console.warn(
|
||||
`generateMetadata, category not found for slug: ${slug}, locale: ${locale}`
|
||||
);
|
||||
return {};
|
||||
notFound();
|
||||
}
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
|
||||
// Build canonical URL with pagination
|
||||
let canonicalPath = `/blog/category/${slug}`;
|
||||
if (page && page !== '1') {
|
||||
canonicalPath += `?page=${page}`;
|
||||
}
|
||||
console.log(
|
||||
`locale: ${locale}, slug: ${slug}, page: ${page}, canonicalPath: ${canonicalPath}`
|
||||
);
|
||||
|
||||
const canonicalPath = `/blog/category/${slug}`;
|
||||
return constructMetadata({
|
||||
title: `${category.name} | ${t('title')}`,
|
||||
description: category.description,
|
||||
@ -76,68 +42,32 @@ export async function generateMetadata({ params }: BlogCategoryPageProps) {
|
||||
|
||||
interface BlogCategoryPageProps {
|
||||
params: Promise<{
|
||||
slug: string;
|
||||
locale: Locale;
|
||||
page?: string;
|
||||
slug: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function BlogCategoryPage({
|
||||
params,
|
||||
}: BlogCategoryPageProps) {
|
||||
const { slug, locale, page } = await params;
|
||||
const currentPage = page ? Number(page) : 1;
|
||||
const paginationSize = websiteConfig.blog.paginationSize;
|
||||
const startIndex = (currentPage - 1) * paginationSize;
|
||||
const endIndex = startIndex + paginationSize;
|
||||
|
||||
// Find category with matching slug and locale
|
||||
const { locale, slug } = await params;
|
||||
const category = allCategories.find(
|
||||
(category) => category.slug === slug && category.locale === locale
|
||||
(category) => category.locale === locale && category.slug === slug
|
||||
);
|
||||
|
||||
// Filter posts by category and locale
|
||||
const filteredPosts = allPosts.filter((post) => {
|
||||
if (!post.published || post.locale !== locale) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if any of the post's categories match the current category slug
|
||||
return post.categories.some(
|
||||
(category) => category && category.slug === slug
|
||||
);
|
||||
if (!category) {
|
||||
notFound();
|
||||
}
|
||||
const currentPage = 1;
|
||||
const { paginatedPosts, totalPages } = getPaginatedBlogPosts({
|
||||
locale,
|
||||
page: currentPage,
|
||||
category: slug,
|
||||
});
|
||||
|
||||
// Sort posts by date (newest first)
|
||||
const sortedPosts = [...filteredPosts].sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
);
|
||||
|
||||
// Paginate posts
|
||||
const paginatedPosts = sortedPosts.slice(startIndex, endIndex);
|
||||
const totalCount = filteredPosts.length;
|
||||
const totalPages = Math.ceil(totalCount / paginationSize);
|
||||
|
||||
// console.log("BlogCategoryPage, totalCount", totalCount, ", totalPages", totalPages);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* when no posts are found */}
|
||||
{paginatedPosts.length === 0 && <EmptyGrid />}
|
||||
|
||||
{/* when posts are found */}
|
||||
{paginatedPosts.length > 0 && (
|
||||
<div>
|
||||
<BlogGrid posts={paginatedPosts} />
|
||||
|
||||
<div className="mt-8 flex items-center justify-center">
|
||||
<CustomPagination
|
||||
routePreix={`/${locale}/blog/category/${slug}`}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<BlogGridWithPagination
|
||||
posts={paginatedPosts}
|
||||
totalPages={totalPages}
|
||||
routePrefix={`/blog/category/${slug}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,84 @@
|
||||
import BlogGridWithPagination from '@/components/blog/blog-grid-with-pagination';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { LOCALES } from '@/i18n/routing';
|
||||
import { getPaginatedBlogPosts } from '@/lib/blog/data';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { allCategories, allPosts } from 'content-collections';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
// Generate all static params for SSG (locale + category + pagination)
|
||||
export function generateStaticParams() {
|
||||
const params: { locale: string; slug: string; page: string }[] = [];
|
||||
for (const locale of LOCALES) {
|
||||
const localeCategories = allCategories.filter(
|
||||
(category) => category.locale === locale
|
||||
);
|
||||
for (const category of localeCategories) {
|
||||
const totalPages = Math.ceil(
|
||||
allPosts.filter(
|
||||
(post) =>
|
||||
post.locale === locale &&
|
||||
post.categories.some((cat) => cat && cat.slug !== category.slug)
|
||||
).length / websiteConfig.blog.paginationSize
|
||||
);
|
||||
for (let page = 2; page <= totalPages; page++) {
|
||||
params.push({ locale, slug: category.slug, page: String(page) });
|
||||
}
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
// Generate metadata for each static category page (locale + category + pagination)
|
||||
export async function generateMetadata({ params }: BlogCategoryPageProps) {
|
||||
const { locale, slug, page } = await params;
|
||||
const category = allCategories.find(
|
||||
(category) => category.slug === slug && category.locale === locale
|
||||
);
|
||||
if (!category) {
|
||||
notFound();
|
||||
}
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
const canonicalPath = `/blog/category/${slug}/page/${page}`;
|
||||
return constructMetadata({
|
||||
title: `${category.name} | ${t('title')}`,
|
||||
description: category.description,
|
||||
canonicalUrl: getUrlWithLocale(canonicalPath, locale),
|
||||
});
|
||||
}
|
||||
|
||||
interface BlogCategoryPageProps {
|
||||
params: Promise<{
|
||||
locale: Locale;
|
||||
slug: string;
|
||||
page: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function BlogCategoryPage({
|
||||
params,
|
||||
}: BlogCategoryPageProps) {
|
||||
const { locale, slug, page } = await params;
|
||||
const currentPage = page;
|
||||
const category = allCategories.find(
|
||||
(category) => category.slug === slug && category.locale === locale
|
||||
);
|
||||
if (!category) {
|
||||
notFound();
|
||||
}
|
||||
const { paginatedPosts, totalPages } = getPaginatedBlogPosts({
|
||||
locale,
|
||||
page: currentPage,
|
||||
category: slug,
|
||||
});
|
||||
return (
|
||||
<BlogGridWithPagination
|
||||
posts={paginatedPosts}
|
||||
totalPages={totalPages}
|
||||
routePrefix={`/blog/category/${slug}`}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,53 +1,20 @@
|
||||
import BlogGrid from '@/components/blog/blog-grid';
|
||||
import EmptyGrid from '@/components/shared/empty-grid';
|
||||
import CustomPagination from '@/components/shared/pagination';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import BlogGridWithPagination from '@/components/blog/blog-grid-with-pagination';
|
||||
import { LOCALES } from '@/i18n/routing';
|
||||
import { getPaginatedBlogPosts } from '@/lib/blog/data';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { allPosts } from 'content-collections';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
// Generate all static params for SSG (locale + pagination)
|
||||
export function generateStaticParams() {
|
||||
const publishedPosts = allPosts.filter((post) => post.published);
|
||||
const paginationSize = websiteConfig.blog.paginationSize;
|
||||
const params: { locale: string; page?: string }[] = [];
|
||||
for (const locale of LOCALES) {
|
||||
const localePosts = publishedPosts.filter((post) => post.locale === locale);
|
||||
if (localePosts.length <= 0) {
|
||||
continue;
|
||||
}
|
||||
const totalCount = localePosts.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalCount / paginationSize));
|
||||
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber++) {
|
||||
if (pageNumber === 1) {
|
||||
params.push({ locale });
|
||||
} else {
|
||||
params.push({ locale, page: String(pageNumber) });
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('BlogPage, generateStaticParams', params);
|
||||
return params;
|
||||
return LOCALES.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
// Generate metadata for each static page (locale + page)
|
||||
export async function generateMetadata({ params }: BlogPageProps) {
|
||||
const { locale, page } = await params;
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
const pt = await getTranslations({ locale, namespace: 'BlogPage' });
|
||||
|
||||
// Build canonical URL with pagination
|
||||
let canonicalPath = '/blog';
|
||||
if (page && page !== '1') {
|
||||
canonicalPath += `?page=${page}`;
|
||||
}
|
||||
console.log(
|
||||
`locale: ${locale}, page: ${page}, canonicalPath: ${canonicalPath}`
|
||||
);
|
||||
|
||||
const canonicalPath = '/blog';
|
||||
return constructMetadata({
|
||||
title: `${pt('title')} | ${t('title')}`,
|
||||
description: pt('description'),
|
||||
@ -58,58 +25,21 @@ export async function generateMetadata({ params }: BlogPageProps) {
|
||||
interface BlogPageProps {
|
||||
params: Promise<{
|
||||
locale: Locale;
|
||||
page?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function BlogPage({ params }: BlogPageProps) {
|
||||
const { locale, page } = await params;
|
||||
const currentPage = page ? Number(page) : 1;
|
||||
const paginationSize = websiteConfig.blog.paginationSize;
|
||||
const startIndex = (currentPage - 1) * paginationSize;
|
||||
const endIndex = startIndex + paginationSize;
|
||||
|
||||
// Filter posts by locale
|
||||
const localePosts = allPosts.filter(
|
||||
(post) => post.locale === locale && post.published
|
||||
);
|
||||
|
||||
// If no posts found for the current locale, show all published posts
|
||||
const filteredPosts =
|
||||
localePosts.length > 0
|
||||
? localePosts
|
||||
: allPosts.filter((post) => post.published);
|
||||
|
||||
// Sort posts by date (newest first)
|
||||
const sortedPosts = [...filteredPosts].sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
);
|
||||
|
||||
// Paginate posts
|
||||
const paginatedPosts = sortedPosts.slice(startIndex, endIndex);
|
||||
const totalCount = filteredPosts.length;
|
||||
const totalPages = Math.ceil(totalCount / paginationSize);
|
||||
|
||||
// console.log("BlogPage, totalCount", totalCount, ", totalPages", totalPages,);
|
||||
|
||||
const { locale } = await params;
|
||||
const currentPage = 1;
|
||||
const { paginatedPosts, totalPages } = getPaginatedBlogPosts({
|
||||
locale,
|
||||
page: currentPage,
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
{/* when no posts are found */}
|
||||
{paginatedPosts.length === 0 && <EmptyGrid />}
|
||||
|
||||
{/* when posts are found */}
|
||||
{paginatedPosts.length > 0 && (
|
||||
<div>
|
||||
<BlogGrid posts={paginatedPosts} />
|
||||
|
||||
<div className="mt-8 flex items-center justify-center">
|
||||
<CustomPagination
|
||||
routePreix={`/${locale}/blog`}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<BlogGridWithPagination
|
||||
posts={paginatedPosts}
|
||||
totalPages={totalPages}
|
||||
routePrefix={'/blog'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,65 @@
|
||||
import BlogGridWithPagination from '@/components/blog/blog-grid-with-pagination';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { LOCALES } from '@/i18n/routing';
|
||||
import { getPaginatedBlogPosts } from '@/lib/blog/data';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import { allPosts } from 'content-collections';
|
||||
import type { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
export function generateStaticParams() {
|
||||
const paginationSize = websiteConfig.blog.paginationSize;
|
||||
const params: { locale: string; page: string }[] = [];
|
||||
for (const locale of LOCALES) {
|
||||
const publishedPosts = allPosts.filter(
|
||||
(post) => post.published && post.locale === locale
|
||||
);
|
||||
const totalPages = Math.max(
|
||||
1,
|
||||
Math.ceil(publishedPosts.length / paginationSize)
|
||||
);
|
||||
for (let pageNumber = 2; pageNumber <= totalPages; pageNumber++) {
|
||||
params.push({
|
||||
locale,
|
||||
page: String(pageNumber),
|
||||
});
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: BlogListPageProps) {
|
||||
const { locale, page } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
const pt = await getTranslations({ locale, namespace: 'BlogPage' });
|
||||
const canonicalPath = `/blog/page/${page}`;
|
||||
return constructMetadata({
|
||||
title: `${pt('title')} | ${t('title')}`,
|
||||
description: pt('description'),
|
||||
canonicalUrl: getUrlWithLocale(canonicalPath, locale),
|
||||
});
|
||||
}
|
||||
|
||||
interface BlogListPageProps {
|
||||
params: Promise<{
|
||||
locale: Locale;
|
||||
page: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function BlogListPage({ params }: BlogListPageProps) {
|
||||
const { page, locale } = await params;
|
||||
const currentPage = Number(page);
|
||||
const { paginatedPosts, totalPages } = getPaginatedBlogPosts({
|
||||
locale,
|
||||
page: currentPage,
|
||||
});
|
||||
return (
|
||||
<BlogGridWithPagination
|
||||
posts={paginatedPosts}
|
||||
totalPages={totalPages}
|
||||
routePrefix={'/blog'}
|
||||
/>
|
||||
);
|
||||
}
|
@ -7,11 +7,11 @@ import { NewsletterCard } from '@/components/newsletter/newsletter-card';
|
||||
import { CustomMDXContent } from '@/components/shared/custom-mdx-content';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { LocaleLink } from '@/i18n/navigation';
|
||||
import { LOCALES } from '@/i18n/routing';
|
||||
import { getTableOfContents } from '@/lib/blog/toc';
|
||||
import { formatDate } from '@/lib/formatter';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
||||
import type { NextPageProps } from '@/types/next-page-props';
|
||||
import { type Post, allPosts } from 'content-collections';
|
||||
import { CalendarIcon, ClockIcon, FileTextIcon } from 'lucide-react';
|
||||
import type { Metadata } from 'next';
|
||||
@ -24,7 +24,8 @@ import '@/styles/mdx.css';
|
||||
|
||||
/**
|
||||
* Gets the blog post from the params
|
||||
* @param props - The props of the page
|
||||
* @param slug - The slug of the blog post
|
||||
* @param locale - The locale of the blog post
|
||||
* @returns The blog post
|
||||
*
|
||||
* How it works:
|
||||
@ -33,16 +34,8 @@ import '@/styles/mdx.css';
|
||||
* slug becomes "first-post" after join('/')
|
||||
* Matches post where slugAsParams === "first-post" AND locale === params.locale
|
||||
*/
|
||||
async function getBlogPostFromParams(props: NextPageProps) {
|
||||
const params = await props.params;
|
||||
if (!params) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const locale = params.locale as string;
|
||||
const slug =
|
||||
(Array.isArray(params.slug) ? params.slug?.join('/') : params.slug) || '';
|
||||
|
||||
async function getBlogPostFromParams(locale: Locale, slug: string) {
|
||||
// console.log('getBlogPostFromParams', locale, slug);
|
||||
// Find post with matching slug and locale
|
||||
const post = allPosts.find(
|
||||
(post) =>
|
||||
@ -78,22 +71,23 @@ async function getRelatedPosts(post: Post) {
|
||||
return relatedPosts;
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return LOCALES.map((locale) => {
|
||||
const posts = allPosts.filter((post) => post.locale === locale);
|
||||
return posts.map((post) => ({
|
||||
locale,
|
||||
slug: post.slugAsParams,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string; locale: Locale }>;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const { slug, locale } = await params;
|
||||
|
||||
const post = await getBlogPostFromParams({
|
||||
params: Promise.resolve({ slug, locale }),
|
||||
searchParams: Promise.resolve({}),
|
||||
});
|
||||
}: BlogPostPageProps): Promise<Metadata | undefined> {
|
||||
const { locale, slug } = await params;
|
||||
const post = await getBlogPostFromParams(locale, slug.join('/'));
|
||||
if (!post) {
|
||||
console.warn(
|
||||
`generateMetadata, post not found for slug: ${slug}, locale: ${locale}`
|
||||
);
|
||||
return {};
|
||||
notFound();
|
||||
}
|
||||
|
||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
||||
@ -106,8 +100,16 @@ export async function generateMetadata({
|
||||
});
|
||||
}
|
||||
|
||||
export default async function BlogPostPage(props: NextPageProps) {
|
||||
const post = await getBlogPostFromParams(props);
|
||||
interface BlogPostPageProps {
|
||||
params: Promise<{
|
||||
locale: Locale;
|
||||
slug: string[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function BlogPostPage(props: BlogPostPageProps) {
|
||||
const { locale, slug } = await props.params;
|
||||
const post = await getBlogPostFromParams(locale, slug.join('/'));
|
||||
if (!post) {
|
||||
notFound();
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { getLocalePathname } from '@/i18n/navigation';
|
||||
import { routing } from '@/i18n/routing';
|
||||
import { source } from '@/lib/docs/source';
|
||||
@ -60,15 +61,73 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
)
|
||||
);
|
||||
|
||||
// add posts
|
||||
sitemapList.push(
|
||||
...allPosts.flatMap((post: { slugAsParams: string }) =>
|
||||
routing.locales.map((locale) => ({
|
||||
url: getUrl(`/blog/${post.slugAsParams}`, locale),
|
||||
// add paginated blog list pages
|
||||
routing.locales.forEach((locale) => {
|
||||
const posts = allPosts.filter(
|
||||
(post) => post.locale === locale && post.published
|
||||
);
|
||||
const totalPages = Math.max(
|
||||
1,
|
||||
Math.ceil(posts.length / websiteConfig.blog.paginationSize)
|
||||
);
|
||||
// /blog/page/[page] (from 2)
|
||||
for (let page = 2; page <= totalPages; page++) {
|
||||
sitemapList.push({
|
||||
url: getUrl(`/blog/page/${page}`, locale),
|
||||
lastModified: new Date(),
|
||||
priority: 0.8,
|
||||
changeFrequency: 'weekly' as const,
|
||||
}))
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// add paginated category pages
|
||||
routing.locales.forEach((locale) => {
|
||||
const localeCategories = allCategories.filter(
|
||||
(category) => category.locale === locale
|
||||
);
|
||||
localeCategories.forEach((category) => {
|
||||
// posts in this category and locale
|
||||
const postsInCategory = allPosts.filter(
|
||||
(post) =>
|
||||
post.locale === locale &&
|
||||
post.published &&
|
||||
post.categories.some((cat) => cat && cat.slug === category.slug)
|
||||
);
|
||||
const totalPages = Math.max(
|
||||
1,
|
||||
Math.ceil(postsInCategory.length / websiteConfig.blog.paginationSize)
|
||||
);
|
||||
// /blog/category/[slug] (first page)
|
||||
sitemapList.push({
|
||||
url: getUrl(`/blog/category/${category.slug}`, locale),
|
||||
lastModified: new Date(),
|
||||
priority: 0.8,
|
||||
changeFrequency: 'weekly' as const,
|
||||
});
|
||||
// /blog/category/[slug]/page/[page] (from 2)
|
||||
for (let page = 2; page <= totalPages; page++) {
|
||||
sitemapList.push({
|
||||
url: getUrl(`/blog/category/${category.slug}/page/${page}`, locale),
|
||||
lastModified: new Date(),
|
||||
priority: 0.8,
|
||||
changeFrequency: 'weekly' as const,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// add posts (single post pages)
|
||||
sitemapList.push(
|
||||
...allPosts.flatMap((post: { slugAsParams: string; locale: string }) =>
|
||||
routing.locales
|
||||
.filter((locale) => post.locale === locale)
|
||||
.map((locale) => ({
|
||||
url: getUrl(`/blog/${post.slugAsParams}`, locale),
|
||||
lastModified: new Date(),
|
||||
priority: 0.8,
|
||||
changeFrequency: 'weekly' as const,
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
|
32
src/components/blog/blog-grid-with-pagination.tsx
Normal file
32
src/components/blog/blog-grid-with-pagination.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import EmptyGrid from '../shared/empty-grid';
|
||||
import CustomPagination from '../shared/pagination';
|
||||
import BlogGrid from './blog-grid';
|
||||
|
||||
interface BlogListWithPaginationProps {
|
||||
posts: any[];
|
||||
totalPages: number;
|
||||
routePrefix: string;
|
||||
}
|
||||
|
||||
export default function BlogGridWithPagination({
|
||||
posts,
|
||||
totalPages,
|
||||
routePrefix,
|
||||
}: BlogListWithPaginationProps) {
|
||||
return (
|
||||
<div>
|
||||
{posts.length === 0 && <EmptyGrid />}
|
||||
{posts.length > 0 && (
|
||||
<div>
|
||||
<BlogGrid posts={posts} />
|
||||
<div className="mt-8 flex items-center justify-center">
|
||||
<CustomPagination
|
||||
routePreix={routePrefix}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -9,8 +9,15 @@ import {
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from '@/components/ui/pagination';
|
||||
import { useLocaleRouter } from '@/i18n/navigation';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useLocalePathname, useLocaleRouter } from '@/i18n/navigation';
|
||||
|
||||
function getCurrentPageFromPath(pathname: string): number {
|
||||
const match = pathname.match(/\/page\/(\d+)$/);
|
||||
if (match?.[1]) {
|
||||
return Number(match[1]);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
type CustomPaginationProps = {
|
||||
totalPages: number;
|
||||
@ -22,13 +29,18 @@ export default function CustomPagination({
|
||||
routePreix,
|
||||
}: CustomPaginationProps) {
|
||||
const router = useLocaleRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const currentPage = Number(searchParams.get('page')) || 1;
|
||||
const pathname = useLocalePathname();
|
||||
const currentPage = getCurrentPageFromPath(pathname);
|
||||
|
||||
const handlePageChange = (page: number | string) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set('page', page.toString());
|
||||
router.push(`${routePreix}?${params.toString()}`);
|
||||
const pageNum = Number(page);
|
||||
if (pageNum === 1) {
|
||||
// Go to /blog or /blog/category/[slug] for page 1
|
||||
router.push(routePreix);
|
||||
} else {
|
||||
// Go to /blog/page/x or /blog/category/[slug]/page/x
|
||||
router.push(`${routePreix}/page/${pageNum}`);
|
||||
}
|
||||
};
|
||||
|
||||
const allPages = generatePagination(currentPage, totalPages);
|
||||
@ -122,3 +134,8 @@ const generatePagination = (currentPage: number, totalPages: number) => {
|
||||
totalPages,
|
||||
];
|
||||
};
|
||||
|
||||
function normalizePath(path: string) {
|
||||
// Remove duplicated locale prefix, e.g. /zh/zh/blog => /zh/blog
|
||||
return path.replace(/^(\/\w{2,3})(?:\1)+/, '$1');
|
||||
}
|
||||
|
42
src/lib/blog/data.ts
Normal file
42
src/lib/blog/data.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { allPosts } from 'content-collections';
|
||||
|
||||
export function getPaginatedBlogPosts({
|
||||
locale,
|
||||
page = 1,
|
||||
category,
|
||||
}: {
|
||||
locale: string;
|
||||
page?: number;
|
||||
category?: string;
|
||||
}) {
|
||||
// Filter posts by locale
|
||||
let filteredPosts = allPosts.filter(
|
||||
(post) => post.locale === locale && post.published
|
||||
);
|
||||
// If no posts found for the current locale, show all published posts
|
||||
if (filteredPosts.length === 0) {
|
||||
filteredPosts = allPosts.filter((post) => post.published);
|
||||
}
|
||||
// Filter by category if category is provided
|
||||
if (category) {
|
||||
filteredPosts = filteredPosts.filter((post) =>
|
||||
post.categories.some((cat) => cat && cat.slug === category)
|
||||
);
|
||||
}
|
||||
// Sort posts by date (newest first)
|
||||
const sortedPosts = [...filteredPosts].sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
);
|
||||
// Paginate posts
|
||||
const paginationSize = websiteConfig.blog.paginationSize;
|
||||
const startIndex = (page - 1) * paginationSize;
|
||||
const endIndex = startIndex + paginationSize;
|
||||
const paginatedPosts = sortedPosts.slice(startIndex, endIndex);
|
||||
const totalPages = Math.ceil(filteredPosts.length / paginationSize);
|
||||
return {
|
||||
paginatedPosts,
|
||||
totalPages,
|
||||
filteredPostsCount: filteredPosts.length,
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user