返回 CodeWhale
page-meta.ts
根目录 / web / lib / page-meta.ts
1 import type { Metadata } from "next";
2 import { locales } from "./i18n/config";
3
4 /** Canonical origin for the production site (no trailing slash). */
5 export const SITE_URL = "https://codewhale.net";
6
7 export const SITE_NAME = "Codewhale";
8
9 /** The one-line product identity, used as the default OG image alt text. */
10 export const IDENTITY_PHRASE =
11 "Dive into the deep so you don't have to — any model, local-first.";
12
13 /** Shared OG card rendered by app/opengraph-image.tsx (1200×630 PNG). */
14 const OG_IMAGE = {
15 url: `${SITE_URL}/opengraph-image`,
16 width: 1200,
17 height: 630,
18 alt: `${SITE_NAME} — ${IDENTITY_PHRASE}`,
19 };
20
21 /** Open Graph locale codes per routed locale (BCP 47 with underscore). */
22 const OG_LOCALE: Record<string, string> = {
23 en: "en_US",
24 zh: "zh_CN",
25 ja: "ja_JP",
26 vi: "vi_VN",
27 ko: "ko_KR",
28 ru: "ru_RU",
29 uk: "uk_UA",
30 es: "es_ES",
31 "pt-BR": "pt_BR",
32 };
33
34 /**
35 * buildPageMetadata — per-page SEO metadata for the localized site.
36 *
37 * Produces a canonical URL for the rendered locale, hreflang alternates for
38 * every routed locale (derived from `locales` in lib/i18n/config.ts, plus
39 * `x-default` pointing at the English page), and matching Open Graph /
40 * Twitter card fields wired to the shared OG image.
41 *
42 * @param path Route path WITHOUT the locale prefix, with a leading
43 * slash: "/" for the homepage, "/install", "/docs", …
44 * @param locale Locale of the page being rendered (a routed locale).
45 * @param title Localized page <title> (full string; no template is applied).
46 * @param description Localized meta description, same locale as `title`.
47 *
48 * Usage in a page or layout:
49 * ```ts
50 * export async function generateMetadata({ params }) {
51 * const { locale } = await params;
52 * const isZh = locale === "zh";
53 * return buildPageMetadata({
54 * path: "/install",
55 * locale,
56 * title: isZh ? "安装 · Codewhale" : "Install · Codewhale",
57 * description: isZh ? "…" : "…",
58 * });
59 * }
60 * ```
61 */
62 export function buildPageMetadata({
63 path,
64 locale,
65 title,
66 description,
67 }: {
68 path: string;
69 locale: string;
70 title: string;
71 description: string;
72 }): Metadata {
73 // "/" → "" so the homepage canonical is /en, not /en/.
74 const suffix = path === "/" ? "" : path.replace(/\/+$/, "");
75 const canonical = `${SITE_URL}/${locale}${suffix}`;
76
77 // hreflang alternates derive from the canonical locale registry, so a new
78 // routed locale appears on every page without touching call sites.
79 const languages: Record<string, string> = {};
80 for (const l of locales) {
81 languages[l] = `${SITE_URL}/${l}${suffix}`;
82 }
83 languages["x-default"] = `${SITE_URL}/en${suffix}`;
84
85 return {
86 metadataBase: new URL(SITE_URL),
87 title,
88 description,
89 alternates: {
90 canonical,
91 languages,
92 },
93 openGraph: {
94 title,
95 description,
96 url: canonical,
97 siteName: SITE_NAME,
98 type: "website",
99 locale: OG_LOCALE[locale] ?? "en_US",
100 images: [OG_IMAGE],
101 },
102 twitter: {
103 card: "summary_large_image",
104 title,
105 description,
106 images: [OG_IMAGE.url],
107 },
108 };
109 }
110
110 lines TYPESCRIPT