返回 AiToEarn
metadata.ts
根目录 / project / aitoearn-web / src / utils / metadata.ts
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
88 lines TYPESCRIPT