Merge remote-tracking branch 'origin/main' into cloudflare
This commit is contained in:
commit
b6836db12d
12
README.md
12
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.
|
||||
|
14
env.example
14
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=""
|
||||
|
@ -32,7 +32,7 @@ export async function generateMetadata({
|
||||
return constructMetadata({
|
||||
title: t('title'),
|
||||
description: t('description'),
|
||||
canonicalUrl: getUrlWithLocale('/', locale),
|
||||
canonicalUrl: getUrlWithLocale('', locale),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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<Metadata | undefined> {
|
||||
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 (
|
||||
<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/${resolvedParams.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: 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 (
|
||||
<BlogGridWithPagination
|
||||
posts={paginatedPosts}
|
||||
totalPages={totalPages}
|
||||
routePrefix={`/blog/category/${slug}`}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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<Metadata | undefined> {
|
||||
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 (
|
||||
<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'}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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<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' });
|
||||
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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 (
|
||||
<html suppressHydrationWarning lang={locale}>
|
||||
<head>
|
||||
<AffonsoScript />
|
||||
<PromotekitScript />
|
||||
</head>
|
||||
<body
|
||||
suppressHydrationWarning
|
||||
className={cn(
|
||||
|
@ -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,
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
|
33
src/components/affiliate/affonso.tsx
Normal file
33
src/components/affiliate/affonso.tsx
Normal file
@ -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 (
|
||||
<Script
|
||||
src="https://affonso.io/js/pixel.min.js"
|
||||
strategy="afterInteractive"
|
||||
data-affonso={affiliateId}
|
||||
data-cookie_duration="30"
|
||||
/>
|
||||
);
|
||||
}
|
34
src/components/affiliate/promotekit.tsx
Normal file
34
src/components/affiliate/promotekit.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import Script from 'next/script';
|
||||
|
||||
/**
|
||||
* PromoteKit
|
||||
*
|
||||
* https://www.promotekit.com
|
||||
*/
|
||||
export default function PromotekitScript() {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!websiteConfig.features.enablePromotekitAffiliate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const promotekitKey = process.env.NEXT_PUBLIC_AFFILIATE_PROMOTEKIT_ID;
|
||||
if (!promotekitKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
src="https://cdn.promotekit.com/promotekit.js"
|
||||
data-promotekit={promotekitKey}
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -13,6 +13,7 @@ import {
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { authClient } from '@/lib/auth-client';
|
||||
import { getUrlWithLocaleInCallbackUrl } from '@/lib/urls/urls';
|
||||
import { DEFAULT_LOGIN_REDIRECT, Routes } from '@/routes';
|
||||
@ -97,6 +98,13 @@ export const RegisterForm = ({
|
||||
// sign up success, user information stored in ctx.data
|
||||
// console.log("register, success:", ctx.data);
|
||||
setSuccess(t('checkEmail'));
|
||||
|
||||
// add affonso affiliate
|
||||
// https://affonso.io/app/affiliate-program/connect
|
||||
if (websiteConfig.features.enableAffonsoAffiliate) {
|
||||
console.log('register, affonso affiliate:', values.email);
|
||||
window.Affonso.signup(values.email);
|
||||
}
|
||||
},
|
||||
onError: (ctx) => {
|
||||
// sign up fail, display the error message
|
||||
|
33
src/components/blog/blog-grid-with-pagination.tsx
Normal file
33
src/components/blog/blog-grid-with-pagination.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import type { Post } from 'content-collections';
|
||||
import EmptyGrid from '../shared/empty-grid';
|
||||
import CustomPagination from '../shared/pagination';
|
||||
import BlogGrid from './blog-grid';
|
||||
|
||||
interface BlogGridWithPaginationProps {
|
||||
posts: Post[];
|
||||
totalPages: number;
|
||||
routePrefix: string;
|
||||
}
|
||||
|
||||
export default function BlogGridWithPagination({
|
||||
posts,
|
||||
totalPages,
|
||||
routePrefix,
|
||||
}: BlogGridWithPaginationProps) {
|
||||
return (
|
||||
<div>
|
||||
{posts.length === 0 && <EmptyGrid />}
|
||||
{posts.length > 0 && (
|
||||
<div>
|
||||
<BlogGrid posts={posts} />
|
||||
<div className="mt-8 flex items-center justify-center">
|
||||
<CustomPagination
|
||||
routePrefix={routePrefix}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -10,6 +10,7 @@ import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar';
|
||||
import { getSidebarLinks } from '@/config/sidebar-config';
|
||||
import { LocaleLink } from '@/i18n/navigation';
|
||||
@ -31,6 +32,7 @@ export function DashboardSidebar({
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { data: session, isPending } = authClient.useSession();
|
||||
const currentUser = session?.user;
|
||||
const { state } = useSidebar();
|
||||
// console.log('sidebar currentUser:', currentUser);
|
||||
|
||||
const sidebarLinks = getSidebarLinks();
|
||||
@ -73,8 +75,8 @@ export function DashboardSidebar({
|
||||
{/* Only show UI components when not in loading state */}
|
||||
{!isPending && mounted && (
|
||||
<>
|
||||
{/* show upgrade card if user is not a member */}
|
||||
{currentUser && <UpgradeCard />}
|
||||
{/* show upgrade card if user is not a member, and sidebar is not collapsed */}
|
||||
{currentUser && state !== 'collapsed' && <UpgradeCard />}
|
||||
|
||||
{/* show user profile if user is logged in */}
|
||||
{currentUser && <SidebarUser user={currentUser} />}
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { usePayment } from '@/hooks/use-payment';
|
||||
import { LocaleLink } from '@/i18n/navigation';
|
||||
import { Routes } from '@/routes';
|
||||
@ -16,6 +17,10 @@ import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function UpgradeCard() {
|
||||
if (!websiteConfig.features.enableUpgradeCard) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const t = useTranslations('Dashboard.upgrade');
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { isLoading, currentPlan, subscription } = usePayment();
|
||||
|
@ -188,10 +188,15 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
|
||||
<ul className="w-full px-4">
|
||||
{menuLinks?.map((item) => {
|
||||
const isActive = item.href
|
||||
? localePathname.startsWith(item.href)
|
||||
? item.href === '/'
|
||||
? localePathname === '/'
|
||||
: localePathname.startsWith(item.href)
|
||||
: item.items?.some(
|
||||
(subItem) =>
|
||||
subItem.href && localePathname.startsWith(subItem.href)
|
||||
subItem.href &&
|
||||
(subItem.href === '/'
|
||||
? localePathname === '/'
|
||||
: localePathname.startsWith(subItem.href))
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -60,7 +60,7 @@ export function Navbar({ scroll }: NavBarProps) {
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'sticky inset-x-0 top-0 z-40 py-4 transition-all duration-300',
|
||||
'sticky inset-x-0 top-0 z-100 py-4 transition-all duration-300',
|
||||
scroll
|
||||
? scrolled
|
||||
? 'bg-background/80 backdrop-blur-md border-b supports-backdrop-filter:bg-background/60'
|
||||
@ -193,7 +193,9 @@ export function Navbar({ scroll }: NavBarProps) {
|
||||
asChild
|
||||
active={
|
||||
item.href
|
||||
? localePathname.startsWith(item.href)
|
||||
? item.href === '/'
|
||||
? localePathname === '/'
|
||||
: localePathname.startsWith(item.href)
|
||||
: false
|
||||
}
|
||||
className={customNavigationMenuTriggerStyle}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import { createCheckoutAction } from '@/actions/create-checkout-session';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { Loader2Icon } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
@ -50,12 +51,50 @@ export function CheckoutButton({
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const mergedMetadata = metadata ? { ...metadata } : {};
|
||||
|
||||
// add promotekit_referral to metadata if enabled promotekit affiliate
|
||||
if (websiteConfig.features.enablePromotekitAffiliate) {
|
||||
const promotekitReferral =
|
||||
typeof window !== 'undefined'
|
||||
? (window as any).promotekit_referral
|
||||
: undefined;
|
||||
if (promotekitReferral) {
|
||||
console.log(
|
||||
'create checkout button, promotekitReferral:',
|
||||
promotekitReferral
|
||||
);
|
||||
mergedMetadata.promotekit_referral = promotekitReferral;
|
||||
}
|
||||
}
|
||||
|
||||
// add affonso_referral to metadata if enabled affonso affiliate
|
||||
if (websiteConfig.features.enableAffonsoAffiliate) {
|
||||
const affonsoReferral =
|
||||
typeof document !== 'undefined'
|
||||
? (() => {
|
||||
const match = document.cookie.match(
|
||||
/(?:^|; )affonso_referral=([^;]*)/
|
||||
);
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
})()
|
||||
: null;
|
||||
if (affonsoReferral) {
|
||||
console.log(
|
||||
'create checkout button, affonsoReferral:',
|
||||
affonsoReferral
|
||||
);
|
||||
mergedMetadata.affonso_referral = affonsoReferral;
|
||||
}
|
||||
}
|
||||
|
||||
// Create checkout session using server action
|
||||
const result = await createCheckoutAction({
|
||||
userId,
|
||||
planId,
|
||||
priceId,
|
||||
metadata,
|
||||
metadata:
|
||||
Object.keys(mergedMetadata).length > 0 ? mergedMetadata : undefined,
|
||||
});
|
||||
|
||||
// Redirect to checkout page
|
||||
|
@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { DiscordIcon } from '@/components/icons/discord';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import { useMediaQuery } from '@/hooks/use-media-query';
|
||||
import WidgetBot from '@widgetbot/react-embed';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
@ -11,6 +12,10 @@ import { useEffect, useRef, useState } from 'react';
|
||||
* https://docs.widgetbot.io/embed/react-embed/
|
||||
*/
|
||||
export default function DiscordWidget() {
|
||||
if (!websiteConfig.features.enableDiscordWidget) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const serverId = process.env.NEXT_PUBLIC_DISCORD_WIDGET_SERVER_ID as string;
|
||||
const channelId = process.env.NEXT_PUBLIC_DISCORD_WIDGET_CHANNEL_ID as string;
|
||||
if (!serverId || !channelId) {
|
||||
|
@ -9,26 +9,38 @@ 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;
|
||||
routePreix: string;
|
||||
routePrefix: string;
|
||||
};
|
||||
|
||||
export default function CustomPagination({
|
||||
totalPages,
|
||||
routePreix,
|
||||
routePrefix,
|
||||
}: 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(routePrefix);
|
||||
} else {
|
||||
// Go to /blog/page/x or /blog/category/[slug]/page/x
|
||||
router.push(`${routePrefix}/page/${pageNum}`);
|
||||
}
|
||||
};
|
||||
|
||||
const allPages = generatePagination(currentPage, totalPages);
|
||||
|
@ -32,6 +32,12 @@ export const websiteConfig: WebsiteConfig = {
|
||||
youtube: 'https://mksaas.link/youtube',
|
||||
},
|
||||
},
|
||||
features: {
|
||||
enableDiscordWidget: false,
|
||||
enableUpgradeCard: true,
|
||||
enableAffonsoAffiliate: false,
|
||||
enablePromotekitAffiliate: false,
|
||||
},
|
||||
routes: {
|
||||
defaultLoginRedirect: '/dashboard',
|
||||
},
|
||||
|
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,
|
||||
};
|
||||
}
|
@ -37,7 +37,7 @@ export function constructMetadata({
|
||||
url: canonicalUrl,
|
||||
title,
|
||||
description,
|
||||
siteName: title,
|
||||
siteName: defaultMessages.Metadata.name,
|
||||
images: [ogImageUrl.toString()],
|
||||
},
|
||||
twitter: {
|
||||
|
11
src/types/index.d.ts
vendored
11
src/types/index.d.ts
vendored
@ -5,6 +5,7 @@ import type { ReactNode } from 'react';
|
||||
*/
|
||||
export type WebsiteConfig = {
|
||||
metadata: MetadataConfig;
|
||||
features: FeaturesConfig;
|
||||
routes: RoutesConfig;
|
||||
analytics: AnalyticsConfig;
|
||||
auth: AuthConfig;
|
||||
@ -60,6 +61,16 @@ export interface SocialConfig {
|
||||
telegram?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Website features
|
||||
*/
|
||||
export interface FeaturesConfig {
|
||||
enableDiscordWidget?: boolean; // Whether to enable the discord widget
|
||||
enableUpgradeCard?: boolean; // Whether to enable the upgrade card in the sidebar
|
||||
enableAffonsoAffiliate?: boolean; // Whether to enable affonso affiliate
|
||||
enablePromotekitAffiliate?: boolean; // Whether to enable promotekit affiliate
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes configuration
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user