diff --git a/README.md b/README.md index f82c30b..6636a54 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ If you found anything that could be improved, please let me know. - πŸ”₯ website: [mksaas.com](https://mksaas.com) - 🌐 demo: [demo.mksaas.com](https://demo.mksaas.com) - πŸ“š documentation: [mksaas.com/docs](https://mksaas.com/docs) -- πŸ—“οΈ roadmap: [mksaas project](https://mksaas.link/roadmap) +- πŸ—“οΈ roadmap: [mksaas roadmap](https://mksaas.link/roadmap) - πŸ‘¨β€πŸ’» discord: [mksaas.link/discord](https://mksaas.link/discord) - πŸ“Ή video (WIP): [mksaas.link/youtube](https://mksaas.link/youtube) @@ -27,17 +27,15 @@ If you found anything that could be improved, please let me know. By default, you should have access to all four repositories. If you find that you’re unable to access any of them, please don’t hesitate to reach out to me, and I’ll assist you in resolving the issue. -- [MkSaaSHQ/mksaas-template](https://github.com/MkSaaSHQ/mksaas-template): https://demo.mksaas.com (ready) -- [MkSaaSHQ/mksaas-blog](https://github.com/MkSaaSHQ/mksaas-blog): https://mksaas.me (ready) -- [MkSaaSHQ/mksaas-app](https://github.com/MkSaaSHQ/mksaas-app): https://mksaas.app (WIP) -- [MkSaaSHQ/mksaas-haitang](https://github.com/MkSaaSHQ/mksaas-haitang): https://haitang.app (WIP) +- [mksaas-template (ready)](https://github.com/MkSaaSHQ/mksaas-template): https://demo.mksaas.com +- [mksaas-blog (ready)](https://github.com/MkSaaSHQ/mksaas-blog): https://mksaas.me +- [mksaas-haitang (ready)](https://github.com/MkSaaSHQ/mksaas-haitang): https://haitang.app +- [mksaas-app (WIP)](https://github.com/MkSaaSHQ/mksaas-app): https://mksaas.app ## Notice > If you have any questions, please [submit an issue](https://github.com/MkSaaSHQ/mksaas-template/issues/new), or contact me at [support@mksaas.com](mailto:support@mksaas.com). -> If you have any feature requests or questions or ideas to share, please [submit it in the discussions](https://github.com/MkSaaSHQ/mksaas-template/discussions). - > If you want to receive notifications whenever code changes, please click `Watch` button in the top right. > When submitting any content to the issues or discussions of the repository, please use **English** as the main Language, so that everyone can read it and help you, thank you for your supports. diff --git a/env.example b/env.example index 1380cb6..29488a6 100644 --- a/env.example +++ b/env.example @@ -129,3 +129,17 @@ NEXT_PUBLIC_DATAFAST_ANALYTICS_DOMAIN="" # ----------------------------------------------------------------------------- NEXT_PUBLIC_DISCORD_WIDGET_SERVER_ID="" NEXT_PUBLIC_DISCORD_WIDGET_CHANNEL_ID="" + +# ----------------------------------------------------------------------------- +# Affiliate +# https://mksaas.com/docs/affiliate +# ----------------------------------------------------------------------------- +# Affonso +# https://affonso.com/ +# ----------------------------------------------------------------------------- +NEXT_PUBLIC_AFFILIATE_AFFONSO_ID="" +# ----------------------------------------------------------------------------- +# PromoteKit +# https://www.promotekit.com/ +# ----------------------------------------------------------------------------- +NEXT_PUBLIC_AFFILIATE_PROMOTEKIT_ID="" diff --git a/src/app/[locale]/(marketing)/(home)/page.tsx b/src/app/[locale]/(marketing)/(home)/page.tsx index 88d79fd..6433930 100644 --- a/src/app/[locale]/(marketing)/(home)/page.tsx +++ b/src/app/[locale]/(marketing)/(home)/page.tsx @@ -32,7 +32,7 @@ export async function generateMetadata({ return constructMetadata({ title: t('title'), description: t('description'), - canonicalUrl: getUrlWithLocale('/', locale), + canonicalUrl: getUrlWithLocale('', locale), }); } diff --git a/src/app/[locale]/(marketing)/blocks/[category]/page.tsx b/src/app/[locale]/(marketing)/blocks/[category]/page.tsx index 8f93db4..cd893d1 100644 --- a/src/app/[locale]/(marketing)/blocks/[category]/page.tsx +++ b/src/app/[locale]/(marketing)/blocks/[category]/page.tsx @@ -26,7 +26,7 @@ export async function generateMetadata({ return constructMetadata({ title: category + ' | ' + t('title'), description: t('description'), - canonicalUrl: getUrlWithLocale('/blocks/${category}', locale), + canonicalUrl: getUrlWithLocale(`/blocks/${category}`, locale), }); } diff --git a/src/app/[locale]/(marketing)/blog/(blog)/category/[slug]/page.tsx b/src/app/[locale]/(marketing)/blog/(blog)/category/[slug]/page.tsx index 5e69f4e..b8d0a41 100644 --- a/src/app/[locale]/(marketing)/blog/(blog)/category/[slug]/page.tsx +++ b/src/app/[locale]/(marketing)/blog/(blog)/category/[slug]/page.tsx @@ -1,103 +1,73 @@ -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 type { NextPageProps } from '@/types/next-page-props'; -import { allCategories, allPosts } from 'content-collections'; -import type { Metadata } from 'next'; +import { allCategories } from 'content-collections'; import type { Locale } from 'next-intl'; import { getTranslations } from 'next-intl/server'; +import { notFound } from 'next/navigation'; -export async function generateMetadata({ - params, -}: { - params: Promise<{ slug: string; locale: Locale }>; -}): Promise { - const { slug, locale } = await params; - - // Find category with matching slug and locale - const category = allCategories.find( - (category) => category.slug === slug && category.locale === locale - ); - - if (!category) { - console.warn( - `generateMetadata, category not found for slug: ${slug}, locale: ${locale}` +// Generate all static params for SSG (locale + category) +export function generateStaticParams() { + const params: { locale: string; slug: string }[] = []; + for (const locale of LOCALES) { + const localeCategories = allCategories.filter( + (category) => category.locale === locale ); - return {}; + for (const category of localeCategories) { + params.push({ locale, slug: category.slug }); + } } + return params; +} +// Generate metadata for each static category page (locale + category) +export async function generateMetadata({ params }: BlogCategoryPageProps) { + const { locale, slug } = await params; + const category = allCategories.find( + (category) => category.locale === locale && category.slug === slug + ); + if (!category) { + notFound(); + } const t = await getTranslations({ locale, namespace: 'Metadata' }); - + const canonicalPath = `/blog/category/${slug}`; return constructMetadata({ title: `${category.name} | ${t('title')}`, description: category.description, - canonicalUrl: getUrlWithLocale('/blog/category/${slug}', locale), + canonicalUrl: getUrlWithLocale(canonicalPath, locale), }); } +interface BlogCategoryPageProps { + params: Promise<{ + locale: Locale; + slug: string; + }>; +} + export default async function BlogCategoryPage({ params, - searchParams, -}: NextPageProps) { - const paginationSize = websiteConfig.blog.paginationSize; - const resolvedParams = await params; - const { slug, locale } = resolvedParams; - const resolvedSearchParams = await searchParams; - const { page } = (resolvedSearchParams as { [key: string]: string }) || {}; - const currentPage = page ? Number(page) : 1; - const startIndex = (currentPage - 1) * paginationSize; - const endIndex = startIndex + paginationSize; - - // Find category with matching slug and locale +}: BlogCategoryPageProps) { + 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 ( -
- {/* when no posts are found */} - {paginatedPosts.length === 0 && } - - {/* when posts are found */} - {paginatedPosts.length > 0 && ( -
- - -
- -
-
- )} -
+ ); } diff --git a/src/app/[locale]/(marketing)/blog/(blog)/category/[slug]/page/[page]/page.tsx b/src/app/[locale]/(marketing)/blog/(blog)/category/[slug]/page/[page]/page.tsx new file mode 100644 index 0000000..7c09807 --- /dev/null +++ b/src/app/[locale]/(marketing)/blog/(blog)/category/[slug]/page/[page]/page.tsx @@ -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: string; + }>; +} + +export default async function BlogCategoryPage({ + params, +}: BlogCategoryPageProps) { + const { locale, slug, page } = await params; + const currentPage = Number(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 ( + + ); +} diff --git a/src/app/[locale]/(marketing)/blog/(blog)/page.tsx b/src/app/[locale]/(marketing)/blog/(blog)/page.tsx index 02266d0..aaf6394 100644 --- a/src/app/[locale]/(marketing)/blog/(blog)/page.tsx +++ b/src/app/[locale]/(marketing)/blog/(blog)/page.tsx @@ -1,84 +1,45 @@ -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 type { NextPageProps } from '@/types/next-page-props'; -import { allPosts } from 'content-collections'; -import type { Metadata } from 'next'; import type { Locale } from 'next-intl'; import { getTranslations } from 'next-intl/server'; -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: Locale }>; -}): Promise { +export function generateStaticParams() { + return LOCALES.map((locale) => ({ locale })); +} + +export async function generateMetadata({ params }: BlogPageProps) { const { locale } = await params; const t = await getTranslations({ locale, namespace: 'Metadata' }); const pt = await getTranslations({ locale, namespace: 'BlogPage' }); + const canonicalPath = '/blog'; return constructMetadata({ - title: pt('title') + ' | ' + t('title'), + title: `${pt('title')} | ${t('title')}`, description: pt('description'), - canonicalUrl: getUrlWithLocale('/blog', locale), + canonicalUrl: getUrlWithLocale(canonicalPath, locale), }); } -export default async function BlogPage({ - params, - searchParams, -}: NextPageProps) { - const paginationSize = websiteConfig.blog.paginationSize; - const resolvedParams = await params; - const { locale } = resolvedParams; - const resolvedSearchParams = await searchParams; - const { page } = (resolvedSearchParams as { [key: string]: string }) || {}; - const currentPage = page ? Number(page) : 1; - 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,); +interface BlogPageProps { + params: Promise<{ + locale: Locale; + }>; +} +export default async function BlogPage({ params }: BlogPageProps) { + const { locale } = await params; + const currentPage = 1; + const { paginatedPosts, totalPages } = getPaginatedBlogPosts({ + locale, + page: currentPage, + }); return ( -
- {/* when no posts are found */} - {paginatedPosts.length === 0 && } - - {/* when posts are found */} - {paginatedPosts.length > 0 && ( -
- - -
- -
-
- )} -
+ ); } diff --git a/src/app/[locale]/(marketing)/blog/(blog)/page/[page]/page.tsx b/src/app/[locale]/(marketing)/blog/(blog)/page/[page]/page.tsx new file mode 100644 index 0000000..b0c42b1 --- /dev/null +++ b/src/app/[locale]/(marketing)/blog/(blog)/page/[page]/page.tsx @@ -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 ( + + ); +} diff --git a/src/app/[locale]/(marketing)/blog/[...slug]/page.tsx b/src/app/[locale]/(marketing)/blog/[...slug]/page.tsx index c90fabb..83233c0 100644 --- a/src/app/[locale]/(marketing)/blog/[...slug]/page.tsx +++ b/src/app/[locale]/(marketing)/blog/[...slug]/page.tsx @@ -5,11 +5,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'; @@ -22,7 +22,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: @@ -31,16 +32,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) => @@ -76,22 +69,23 @@ async function getRelatedPosts(post: Post) { return relatedPosts; } +export function generateStaticParams() { + return LOCALES.flatMap((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 { - const { slug, locale } = await params; - - const post = await getBlogPostFromParams({ - params: Promise.resolve({ slug, locale }), - searchParams: Promise.resolve({}), - }); +}: BlogPostPageProps): Promise { + 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' }); @@ -100,11 +94,20 @@ export async function generateMetadata({ title: `${post.title} | ${t('title')}`, description: post.description, canonicalUrl: getUrlWithLocale(post.slug, locale), + image: post.image, }); } -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(); } @@ -112,6 +115,8 @@ export default async function BlogPostPage(props: NextPageProps) { const publishDate = post.date; const date = formatDate(new Date(publishDate)); const toc = await getTableOfContents(post.content); + + // getTranslations may cause error DYNAMIC_SERVER_USAGE, so we set dynamic to force-static const t = await getTranslations('BlogPage'); // get related posts diff --git a/src/app/[locale]/docs/[[...slug]]/page.tsx b/src/app/[locale]/docs/[[...slug]]/page.tsx index c6ce729..8258d61 100644 --- a/src/app/[locale]/docs/[[...slug]]/page.tsx +++ b/src/app/[locale]/docs/[[...slug]]/page.tsx @@ -16,16 +16,14 @@ import { DocsPage, DocsTitle, } from 'fumadocs-ui/page'; -import type { Metadata } from 'next'; import type { Locale } from 'next-intl'; import { getTranslations } from 'next-intl/server'; import { notFound } from 'next/navigation'; import type { ReactNode } from 'react'; export function generateStaticParams() { - const locales = LOCALES; const slugParams = source.generateParams(); - const params = locales.flatMap((locale) => + const params = LOCALES.flatMap((locale) => slugParams.map((param) => ({ locale, slug: param.slug, diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 0952441..4bb1ee8 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,9 +1,13 @@ +import { Analytics } from '@/analytics/analytics'; import { fontBricolageGrotesque, fontNotoSans, fontNotoSansMono, fontNotoSerif, } from '@/assets/fonts'; +import AffonsoScript from '@/components/affiliate/affonso'; +import PromotekitScript from '@/components/affiliate/promotekit'; +import { TailwindIndicator } from '@/components/layout/tailwind-indicator'; import { routing } from '@/i18n/routing'; import { cn } from '@/lib/utils'; import { type Locale, NextIntlClientProvider, hasLocale } from 'next-intl'; @@ -13,8 +17,6 @@ import { Toaster } from 'sonner'; import { Providers } from './providers'; import '@/styles/globals.css'; -import { Analytics } from '@/analytics/analytics'; -import { TailwindIndicator } from '@/components/layout/tailwind-indicator'; interface LocaleLayoutProps { children: ReactNode; @@ -41,6 +43,10 @@ export default async function LocaleLayout({ return ( + + + + { ) ); - // 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, + })) ) ); diff --git a/src/components/affiliate/affonso.tsx b/src/components/affiliate/affonso.tsx new file mode 100644 index 0000000..1b83bda --- /dev/null +++ b/src/components/affiliate/affonso.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { websiteConfig } from '@/config/website'; +import Script from 'next/script'; + +/** + * Affonso Affiliate + * + * https://affonso.com + */ +export default function AffonsoScript() { + if (process.env.NODE_ENV !== 'production') { + return null; + } + + if (!websiteConfig.features.enableAffonsoAffiliate) { + return null; + } + + const affiliateId = process.env.NEXT_PUBLIC_AFFILIATE_AFFONSO_ID as string; + if (!affiliateId) { + return null; + } + + return ( +