| 1 | import type { Metadata } from 'next' |
| 2 | import { getHreflang, languages } from '@/app/i18n/languageConfig' |
| 3 | import { getPageTitle } from './title' |
| 4 | |
| 5 | const BRAND_KEYWORDS = ['AiToEarn', 'aitoearn', 'Ai To Earn', 'ai to earn', 'AITOEARN', 'earn'] |
| 6 | |
| 7 | /** 确保品牌关键词变体在 keywords 最前面,避免重复 */ |
| 8 | function prependBrandKeyword(keywords: Metadata['keywords']): string { |
| 9 | const raw = Array.isArray(keywords) ? keywords.join(', ') : (keywords || '') |
| 10 | const brandSet = new Set(BRAND_KEYWORDS.map(keyword => keyword.toLowerCase().replace(/\s+/g, ''))) |
| 11 | const filtered = raw |
| 12 | .split(',') |
| 13 | .map(keyword => keyword.trim()) |
| 14 | .filter(keyword => !brandSet.has(keyword.toLowerCase().replace(/\s+/g, ''))) |
| 15 | |
| 16 | return [...BRAND_KEYWORDS, ...filtered].join(', ') |
| 17 | } |
| 18 | |
| 19 | /** |
| 20 | * 生成页面 Metadata(符合 SEO 最佳实践)。 |
| 21 | * @param props 基础 Metadata 配置 |
| 22 | * @param lng 当前语言 |
| 23 | * @param path 页面路径(不含语言前缀),如 '/accounts' |
| 24 | */ |
| 25 | export async function getMetadata(props: Metadata, lng: string, path?: string): Promise<Metadata> { |
| 26 | const pagePath = path || '/' |
| 27 | const baseUrl = process.env.NEXT_PUBLIC_HOST_URL || 'https://aitoearn.ai' |
| 28 | const title = await getPageTitle(typeof props.title === 'string' ? props.title : '', lng) |
| 29 | const description = typeof props.description === 'string' ? props.description : '' |
| 30 | const languageAlternates = languages.reduce( |
| 31 | (acc, lang) => { |
| 32 | const hreflang = lang === 'en' ? 'x-default' : getHreflang(lang) |
| 33 | acc[hreflang] = `${baseUrl}/${lang}${pagePath}` |
| 34 | return acc |
| 35 | }, |
| 36 | {} as Record<string, string>, |
| 37 | ) |
| 38 | const defaultOgImage = `${baseUrl}/og-image.png` |
| 39 | |
| 40 | return { |
| 41 | ...props, |
| 42 | title, |
| 43 | description, |
| 44 | keywords: prependBrandKeyword(props.keywords), |
| 45 | referrer: 'no-referrer', |
| 46 | alternates: { |
| 47 | canonical: `${baseUrl}/${lng}${pagePath}`, |
| 48 | languages: languageAlternates, |
| 49 | ...props.alternates, |
| 50 | }, |
| 51 | openGraph: { |
| 52 | title, |
| 53 | description, |
| 54 | url: `${baseUrl}/${lng}${pagePath}`, |
| 55 | siteName: 'AiToEarn', |
| 56 | locale: lng, |
| 57 | type: 'website', |
| 58 | images: [ |
| 59 | { |
| 60 | url: defaultOgImage, |
| 61 | width: 1200, |
| 62 | height: 630, |
| 63 | alt: title, |
| 64 | }, |
| 65 | ], |
| 66 | ...props.openGraph, |
| 67 | }, |
| 68 | twitter: { |
| 69 | card: 'summary_large_image', |
| 70 | title, |
| 71 | description, |
| 72 | images: [defaultOgImage], |
| 73 | ...props.twitter, |
| 74 | }, |
| 75 | robots: props.robots || { |
| 76 | index: true, |
| 77 | follow: true, |
| 78 | googleBot: { |
| 79 | 'index': true, |
| 80 | 'follow': true, |
| 81 | 'max-video-preview': -1, |
| 82 | 'max-image-preview': 'large', |
| 83 | 'max-snippet': -1, |
| 84 | }, |
| 85 | }, |
| 86 | } |
| 87 | } |
| 88 |