refactor: centralize website configuration with dynamic translations

- Replace static `siteConfig` with dynamic `getWebsiteInfo()` function
- Add `Site` section to translation JSON files for multilingual support
- Update types to distinguish between website config and website info
- Remove deprecated `site.ts` configuration file
- Modify components and utilities to use new translation-based configuration
- Ensure consistent website metadata generation across the application
This commit is contained in:
javayhu 2025-03-10 00:48:12 +08:00
parent c7ab08f01d
commit 8b4dde9042
19 changed files with 146 additions and 60 deletions

View File

@ -1,4 +1,10 @@
{
"Site": {
"name": "MkSaaS",
"title": "MkSaaS - The Best AI SaaS Boilerplate",
"tagline": "Make AI SaaS in hours, simply and effortlessly",
"description": "MkSaaS is the best AI SaaS boilerplate. Make AI SaaS in hours, simply and effortlessly"
},
"Common": {
"login": "Login",
"logout": "Log out",

View File

@ -1,4 +1,10 @@
{
"Site": {
"name": "MkSaaS",
"title": "MkSaaS - 最好的 AI SaaS 模板",
"tagline": "使用 MkSaaS 在几小时内轻松构建您的 AI SaaS",
"description": "MkSaaS 是构建 AI SaaS 的最佳模板,使用 MkSaaS 可以在几小时内轻松构建您的 AI SaaS简单且毫不费力。"
},
"Common": {
"login": "登录",
"logout": "退出",

View File

@ -1,6 +1,7 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { siteConfig } from '@/config/site';
import { getWebsiteInfo, websiteConfig } from '@/config';
import { createTranslator } from '@/i18n/translator';
import { MailIcon, UserCircleIcon } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import Image from 'next/image';
@ -10,6 +11,8 @@ import Image from 'next/image';
*/
export default async function AboutPage() {
const t = await getTranslations('AboutPage');
const translator = createTranslator(t);
const websiteInfo = getWebsiteInfo(translator);
return (
<section className="space-y-8 pb-16">
@ -48,7 +51,7 @@ export default async function AboutPage() {
<div className="flex items-center gap-4">
<Button className="rounded-lg">
<MailIcon className="mr-1 size-4" />
<a href={`mailto:${siteConfig.mail}`}>{t('talkWithMe')}</a>
<a href={`mailto:${websiteConfig.mail}`}>{t('talkWithMe')}</a>
</Button>
</div>
</div>

View File

@ -3,7 +3,8 @@ import EmptyGrid from '@/components/shared/empty-grid';
import CustomPagination from '@/components/shared/pagination';
import { POSTS_PER_PAGE } from '@/constants';
import { allPosts, allCategories } from 'content-collections';
import { siteConfig } from '@/config/site';
import { getWebsiteInfo } from '@/config';
import { createTranslator } from '@/i18n/translator';
import { constructMetadata } from '@/lib/metadata';
import type { Metadata } from 'next';
import { NextPageProps } from '@/types/next-page-props';

View File

@ -1,10 +1,11 @@
import { type MetadataRoute } from 'next';
import { siteConfig } from '@/config/site';
import { getWebsiteInfo } from '@/config';
import { createTranslator } from '@/i18n/translator';
/**
* Generates the Web App Manifest for the application
*
* TODO: https://github.com/amannn/next-intl/blob/main/examples/example-app-router/src/app/manifest.ts
* ref: https://github.com/amannn/next-intl/blob/main/examples/example-app-router/src/app/manifest.ts
*
* The manifest.json provides metadata used when the web app is installed on a
* user's mobile device or desktop. See https://web.dev/add-manifest/
@ -12,10 +13,14 @@ import { siteConfig } from '@/config/site';
* @returns {MetadataRoute.Manifest} The manifest configuration object
*/
export default function manifest(): MetadataRoute.Manifest {
// Create a simple translator function for default values
const t = createTranslator((key: string) => key);
const websiteInfo = getWebsiteInfo(t);
return {
name: siteConfig.name,
short_name: siteConfig.name,
description: siteConfig.description,
name: websiteInfo.name,
short_name: websiteInfo.name,
description: websiteInfo.description,
start_url: '/',
display: 'standalone',
background_color: '#ffffff',

View File

@ -26,7 +26,9 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/ui/sidebar';
import { siteConfig } from '@/config/site';
import { getWebsiteInfo } from '@/config';
import { createTranslator } from '@/i18n/translator';
import { useTranslations } from 'next-intl';
import { Logo } from './logo';
const data = {
@ -154,6 +156,9 @@ const data = {
};
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const t = useTranslations();
const websiteInfo = getWebsiteInfo(createTranslator(t));
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
@ -164,9 +169,9 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<Logo className="size-8" />
<div className="grid flex-1 text-left leading-tight">
<span className="truncate font-semibold text-lg">
{siteConfig.name}
{websiteInfo.name}
</span>
{/* <span className="truncate text-xs">{siteConfig.description}</span> */}
{/* <span className="truncate text-xs">{websiteInfo.description}</span> */}
</div>
</a>
</SidebarMenuButton>

View File

@ -4,8 +4,8 @@ import Container from '@/components/container';
import { ThemeSwitcherHorizontal } from '@/components/layout/theme-switcher-horizontal';
import { Logo } from '@/components/logo';
import BuiltWithButton from '@/components/shared/built-with-button';
import { createTranslator, getFooterLinks, getSocialLinks } from '@/config';
import { siteConfig } from '@/config/site';
import { getFooterLinks, getWebsiteInfo, getSocialLinks } from '@/config';
import { createTranslator } from '@/i18n/translator';
import { LocaleLink } from '@/i18n/navigation';
import { cn } from '@/lib/utils';
import { useTranslations } from 'next-intl';
@ -16,6 +16,7 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
const translator = createTranslator(t);
const footerLinks = getFooterLinks(translator);
const socialLinks = getSocialLinks();
const websiteInfo = getWebsiteInfo(translator);
return (
<footer className={cn('border-t', className)}>
@ -26,12 +27,12 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
{/* logo and name */}
<div className="items-center space-x-2 flex">
<Logo />
<span className="text-xl font-semibold">{siteConfig.name}</span>
<span className="text-xl font-semibold">{websiteInfo.name}</span>
</div>
{/* tagline */}
<p className="text-muted-foreground text-base py-2 md:pr-12">
{siteConfig.tagline}
{websiteInfo.tagline}
</p>
{/* social links */}
@ -93,7 +94,7 @@ export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
<div className="border-t py-8">
<Container className="px-4 flex items-center justify-between">
<span className="text-muted-foreground text-sm">
&copy; {new Date().getFullYear()} {siteConfig.name} All Rights
&copy; {new Date().getFullYear()} {websiteInfo.name} All Rights
Reserved.
</span>

View File

@ -9,9 +9,8 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import { getMenuLinks } from '@/config';
import { getMenuLinks, getWebsiteInfo } from '@/config';
import { createTranslator } from '@/i18n/translator';
import { siteConfig } from '@/config/site';
import { LocaleLink, useLocalePathname } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client';
import { cn } from '@/lib/utils';
@ -37,6 +36,9 @@ export function NavbarMobile({
const localePathname = useLocalePathname();
const { data: session, error } = authClient.useSession();
const user = session?.user;
const t = useTranslations();
const translator = createTranslator(t);
const websiteInfo = getWebsiteInfo(translator);
React.useEffect(() => {
const handleRouteChangeStart = () => {
@ -75,7 +77,7 @@ export function NavbarMobile({
{/* navbar left shows logo */}
<LocaleLink href={Routes.Root} className="flex items-center gap-2">
<Logo />
<span className="text-xl font-semibold">{siteConfig.name}</span>
<span className="text-xl font-semibold">{websiteInfo.name}</span>
</LocaleLink>
{/* navbar right shows menu icon */}

View File

@ -17,9 +17,8 @@ import {
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from '@/components/ui/navigation-menu';
import { getMenuLinks } from '@/config';
import { getMenuLinks, getWebsiteInfo } from '@/config';
import { createTranslator } from '@/i18n/translator';
import { siteConfig } from '@/config/site';
import { useScroll } from '@/hooks/use-scroll';
import { LocaleLink, useLocalePathname } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client';
@ -47,6 +46,7 @@ export function Navbar({ scroll }: NavBarProps) {
const t = useTranslations();
const translator = createTranslator(t);
const menuLinks = getMenuLinks(translator);
const websiteInfo = getWebsiteInfo(translator);
const commonTranslations = useTranslations('Common');
const localePathname = useLocalePathname();
@ -70,7 +70,7 @@ export function Navbar({ scroll }: NavBarProps) {
<div className="flex items-center">
<LocaleLink href="/" className="flex items-center space-x-2">
<Logo />
<span className="text-xl font-semibold">{siteConfig.name}</span>
<span className="text-xl font-semibold">{websiteInfo.name}</span>
</LocaleLink>
</div>

View File

@ -17,7 +17,8 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { createTranslator, getAvatarLinks } from '@/config';
import { getAvatarLinks } from '@/config';
import { createTranslator } from '@/i18n/translator';
import { useMediaQuery } from '@/hooks/use-media-query';
import { LocaleLink, useLocaleRouter } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client';

View File

@ -7,7 +7,7 @@ import { TikTokIcon } from '@/components/icons/tiktok';
import { TwitterIcon } from '@/components/icons/twitter';
import { YouTubeIcon } from '@/components/icons/youtube';
import { Routes } from '@/routes';
import { MenuItem, NestedMenuItem } from '@/types';
import { MenuItem, NestedMenuItem, WebsiteConfig, WebsiteInfo } from '@/types';
import { DashboardIcon } from '@radix-ui/react-icons';
import {
AudioLinesIcon,
@ -34,6 +34,26 @@ import {
} from 'lucide-react';
import { TranslationFunction } from './i18n/translator';
export const websiteConfig: WebsiteConfig = {
image: '/og.png',
mail: 'support@mksaas.com',
};
/**
* Get website information with translations
*
* @param t - The translation function
* @returns The website information with translated content
*/
export function getWebsiteInfo(t: TranslationFunction): WebsiteInfo {
return {
name: t('Site.name'),
title: t('Site.title'),
tagline: t('Site.tagline'),
description: t('Site.description'),
};
}
/**
* Get menu links with translations
*

View File

@ -1,11 +0,0 @@
import type { SiteConfig } from '@/types';
export const siteConfig: SiteConfig = {
name: 'MkSaaS',
title: 'MkSaaS - The Best AI SaaS Boilerplate',
tagline: 'Make AI SaaS in hours, simply and effortlessly',
description:
'MkSaaS is the best AI SaaS boilerplate. Make AI SaaS in hours, simply and effortlessly',
image: `og.png`,
mail: 'support@mksaas.com',
};

View File

@ -1,14 +1,19 @@
import { siteConfig } from '@/config/site';
import { getWebsiteInfo } from '@/config';
import db from '@/db/index';
import { account, session, user, verification } from '@/db/schema';
import { createTranslator } from '@/i18n/translator';
import { getLocaleFromRequest } from '@/lib/utils';
import { send } from '@/mail/send';
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { admin } from 'better-auth/plugins';
// Create a simple translator function for default values
const t = createTranslator((key: string) => key);
const websiteInfo = getWebsiteInfo(t);
export const auth = betterAuth({
appName: siteConfig.name,
appName: websiteInfo.name,
database: drizzleAdapter(db, {
provider: 'pg', // or "mysql", "sqlite"
// The schema object that defines the tables and fields

View File

@ -1,4 +1,5 @@
import { siteConfig } from '@/config/site';
import { getWebsiteInfo, websiteConfig } from '@/config';
import { createTranslator } from '@/i18n/translator';
import type { Metadata } from 'next';
import { getBaseUrl } from './urls/get-base-url';
@ -6,20 +7,30 @@ import { getBaseUrl } from './urls/get-base-url';
* Construct the metadata object for the current page (in docs/guides)
*/
export function constructMetadata({
title = siteConfig.name,
description = siteConfig.description,
title,
description,
canonicalUrl,
image = siteConfig.image,
image,
noIndex = false,
locale = 'en',
}: {
title?: string;
description?: string;
canonicalUrl?: string;
image?: string;
noIndex?: boolean;
locale?: string;
} = {}): Metadata {
const fullTitle = title ? `${title} - ${siteConfig.title}` : siteConfig.title;
const ogImageUrl = new URL(`${getBaseUrl()}/${image}`);
// Create a simple translator function for default values
const t = createTranslator((key: string) => key);
const websiteInfo = getWebsiteInfo(t);
title = title || websiteInfo.name;
description = description || websiteInfo.description;
image = image || websiteConfig.image;
const fullTitle = title ? `${title} - ${websiteInfo.title}` : websiteInfo.title;
const ogImageUrl = new URL(`${getBaseUrl()}${image}`);
return {
title: fullTitle,

View File

@ -1,5 +1,6 @@
import { siteConfig } from '@/config/site';
import { getWebsiteInfo } from '@/config';
import { Locale, LOCALE_COOKIE_NAME, routing } from '@/i18n/routing';
import { createTranslator } from '@/i18n/translator';
import { type ClassValue, clsx } from 'clsx';
import { parse as parseCookies } from 'cookie';
import { twMerge } from 'tailwind-merge';
@ -12,17 +13,26 @@ export function cn(...inputs: ClassValue[]) {
* Creates a title for the page
* @param title - The title of the page
* @param addSuffix - Whether to add the app name as a suffix
* @param locale - The locale to use for the title
* @returns The title for the page
*/
export function createTitle(title: string, addSuffix: boolean = true): string {
export function createTitle(
title: string,
addSuffix: boolean = true,
locale: string = 'en'
): string {
// Create a simple translator function for default values
const t = createTranslator((key: string) => key);
const websiteInfo = getWebsiteInfo(t);
if (!addSuffix) {
return title;
}
if (!title) {
return siteConfig.name;
return websiteInfo.name;
}
return `${title} | ${siteConfig.name}`;
return `${title} | ${websiteInfo.name}`;
}
/**

View File

@ -1,4 +1,5 @@
import { siteConfig } from '@/config/site';
import { getWebsiteInfo } from '@/config';
import { createTranslator as createAppTranslator } from '@/i18n/translator';
import EmailButton from '@/mail/components/email-button';
import EmailLayout from '@/mail/components/email-layout';
import { defaultLocale, defaultMessages } from '@/mail/messages';
@ -21,6 +22,10 @@ export function ForgotPassword({
locale,
messages,
});
// Create a simple translator function for site config
const appTranslator = createAppTranslator((key: string) => key);
const websiteInfo = getWebsiteInfo(appTranslator);
return (
<EmailLayout>
@ -36,7 +41,7 @@ export function ForgotPassword({
<br />
<br />
<Text>{t('Mail.common.team', { name: siteConfig.name })}</Text>
<Text>{t('Mail.common.team', { name: websiteInfo.name })}</Text>
<Text>
{t('Mail.common.copyright', { year: new Date().getFullYear() })}
</Text>

View File

@ -1,4 +1,5 @@
import { siteConfig } from '@/config/site';
import { getWebsiteInfo } from '@/config';
import { createTranslator as createAppTranslator } from '@/i18n/translator';
import EmailButton from '@/mail/components/email-button';
import EmailLayout from '@/mail/components/email-layout';
import { defaultLocale, defaultMessages } from '@/mail/messages';
@ -16,6 +17,10 @@ export function VerifyEmail({ url, name, locale, messages }: VerifyEmailProps) {
locale,
messages,
});
// Create a simple translator function for site config
const appTranslator = createAppTranslator((key: string) => key);
const websiteInfo = getWebsiteInfo(appTranslator);
return (
<EmailLayout>
@ -29,7 +34,7 @@ export function VerifyEmail({ url, name, locale, messages }: VerifyEmailProps) {
<br />
<br />
<Text>{t('Mail.common.team', { name: siteConfig.name })}</Text>
<Text>{t('Mail.common.team', { name: websiteInfo.name })}</Text>
<Text>
{t('Mail.common.copyright', { year: new Date().getFullYear() })}
</Text>

View File

@ -1,4 +1,4 @@
import { siteConfig } from '@/config/site';
import { websiteConfig } from '@/config';
import { SendEmailHandler } from '@/mail/types';
import { Resend } from 'resend';
@ -12,7 +12,7 @@ export const sendEmail: SendEmailHandler = async ({ to, subject, html }) => {
Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
},
body: JSON.stringify({
from: siteConfig.mail,
from: websiteConfig.mail,
to,
subject,
html,

25
src/types/index.d.ts vendored
View File

@ -1,18 +1,26 @@
import type { Icons } from '@/components/icons/icons';
import type { ReactNode } from 'react';
/**
* site config
* website config, without translations
*/
export type SiteConfig = {
name: string;
title: string;
tagline: string;
description: string;
export type WebsiteConfig = {
image: string;
mail: string;
};
/**
* website info, with translations
*/
export type WebsiteInfo = {
name: string;
title: string;
tagline: string;
description: string;
};
/**
* menu item
*/
export type MenuItem = {
title: string;
description?: string;
@ -21,6 +29,9 @@ export type MenuItem = {
external?: boolean;
};
/**
* nested menu item, used for navbar links, sidebar links, footer links
*/
export type NestedMenuItem = {
title: string;
description?: string;