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:
parent
c7ab08f01d
commit
8b4dde9042
@ -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",
|
||||
|
@ -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": "退出",
|
||||
|
@ -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>
|
||||
|
@ -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';
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
© {new Date().getFullYear()} {siteConfig.name} All Rights
|
||||
© {new Date().getFullYear()} {websiteInfo.name} All Rights
|
||||
Reserved.
|
||||
</span>
|
||||
|
||||
|
@ -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 */}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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',
|
||||
};
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
25
src/types/index.d.ts
vendored
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user