Skip to content

Commit b5656fe

Browse files
committed
feat: enhance SEO with structured data and improve metadata across pages
1 parent 745c7c8 commit b5656fe

12 files changed

Lines changed: 422 additions & 61 deletions

File tree

app/[lang]/(home)/page.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import {
99
} from "@/components/home";
1010
import { getAppData } from "@/lib/data";
1111
import { i18n, type Locale, locales } from "@/lib/i18n";
12+
import { getTranslation } from "@/lib/i18n/translations";
13+
import {
14+
faqJsonLd,
15+
jsonLdScriptContent,
16+
softwareApplicationJsonLd,
17+
} from "@/lib/jsonld";
1218
import { pageAlternates } from "@/lib/seo";
1319

1420
export async function generateMetadata({
@@ -20,8 +26,17 @@ export async function generateMetadata({
2026
const locale = (
2127
locales.includes(lang as Locale) ? lang : i18n.defaultLanguage
2228
) as Locale;
29+
const t = (key: string) => getTranslation(locale, key);
30+
2331
return {
32+
title: t("site.title"),
33+
description: t("site.description"),
34+
keywords: t("seo.keywords.en"),
2435
alternates: pageAlternates(locale, "/"),
36+
openGraph: {
37+
title: t("site.title"),
38+
description: t("site.description"),
39+
},
2540
};
2641
}
2742

@@ -31,6 +46,17 @@ export default async function HomePage() {
3146

3247
return (
3348
<main className="min-h-screen stable-vh overflow-x-clip bg-white dark:bg-gray-950 text-gray-900 dark:text-gray-100 relative">
49+
{/* JSON-LD: SoftwareApplication + FAQ */}
50+
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD structured data is server-generated and safe */}
51+
<script
52+
type="application/ld+json"
53+
dangerouslySetInnerHTML={{
54+
__html: jsonLdScriptContent(
55+
softwareApplicationJsonLd("en", appData.version),
56+
faqJsonLd("en"),
57+
),
58+
}}
59+
/>
3460
{/* Global background effects - only visible in dark mode */}
3561
<div className="fixed inset-0 -z-10 dark:block hidden">
3662
{/* Main background gradient */}

app/[lang]/(home)/store/[id]/page.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { type Extension, extensions } from "@/db/schema";
1111
import { getAppData } from "@/lib/data";
1212
import { i18n, type Locale, locales } from "@/lib/i18n";
1313
import { getTranslation } from "@/lib/i18n/translations";
14+
import { pageAlternates } from "@/lib/seo";
1415

1516
export async function generateMetadata({
1617
params,
@@ -53,9 +54,14 @@ export async function generateMetadata({
5354
description:
5455
extension.description ||
5556
`${displayTitle} by ${authorName} — Gopeed Extension`,
56-
openGraph: extension.icon
57-
? { images: [{ url: extension.icon }] }
58-
: undefined,
57+
alternates: pageAlternates(locale, `/store/${encodeURIComponent(id)}`),
58+
openGraph: {
59+
title: `${displayTitle} — Gopeed ${getTranslation(locale, "store.title")}`,
60+
description:
61+
extension.description ||
62+
`${displayTitle} by ${authorName} — Gopeed Extension`,
63+
...(extension.icon ? { images: [{ url: extension.icon }] } : {}),
64+
},
5965
};
6066
}
6167

app/[lang]/(home)/store/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ export async function generateMetadata({
2525
return {
2626
title: t("store.title"),
2727
description: t("store.subtitle"),
28+
keywords: t("seo.keywords.store"),
2829
alternates: pageAlternates(locale, "/store"),
30+
openGraph: {
31+
title: `${t("store.title")} | Gopeed`,
32+
description: t("store.subtitle"),
33+
},
2934
};
3035
}
3136

app/[lang]/layout.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import { NavigationProgress } from "@/components/NavigationProgress";
88
import { i18n, type Locale, locales } from "@/lib/i18n";
99
import { localeNames } from "@/lib/i18n/config";
1010
import { getTranslation } from "@/lib/i18n/translations";
11+
import {
12+
jsonLdScriptContent,
13+
organizationJsonLd,
14+
webSiteJsonLd,
15+
} from "@/lib/jsonld";
1116
import { LocaleProvider } from "@/lib/locale-context";
1217
import { BASE_URL, pageAlternates } from "@/lib/seo";
1318

@@ -29,6 +34,7 @@ export async function generateMetadata({
2934
default: t("site.title"),
3035
},
3136
description: t("site.description"),
37+
keywords: t("seo.keywords.en"),
3238
icons: {
3339
icon: "/images/logo.png",
3440
},
@@ -39,9 +45,36 @@ export async function generateMetadata({
3945
title: t("site.title"),
4046
description: t("site.description"),
4147
url: BASE_URL,
48+
images: [
49+
{
50+
url: `${BASE_URL}/images/screenshot.png`,
51+
width: 1200,
52+
height: 900,
53+
alt: "Gopeed Download Manager",
54+
},
55+
],
56+
locale:
57+
locale === "zh" ? "zh_CN" : locale === "zh-TW" ? "zh_TW" : "en_US",
4258
},
4359
twitter: {
4460
card: "summary_large_image",
61+
title: t("site.title"),
62+
description: t("site.description"),
63+
images: [`${BASE_URL}/images/screenshot.png`],
64+
},
65+
robots: {
66+
index: true,
67+
follow: true,
68+
googleBot: {
69+
index: true,
70+
follow: true,
71+
"max-video-preview": -1,
72+
"max-image-preview": "large",
73+
"max-snippet": -1,
74+
},
75+
},
76+
other: {
77+
"google-site-verification": "",
4578
},
4679
};
4780
}
@@ -66,6 +99,19 @@ export default async function RootLayout({
6699

67100
return (
68101
<html lang={locale} suppressHydrationWarning>
102+
<head>
103+
{/* JSON-LD Structured Data: Organization + WebSite */}
104+
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD structured data is server-generated and safe */}
105+
<script
106+
type="application/ld+json"
107+
dangerouslySetInnerHTML={{
108+
__html: jsonLdScriptContent(
109+
organizationJsonLd(),
110+
webSiteJsonLd(locale),
111+
),
112+
}}
113+
/>
114+
</head>
69115
<body className="font-sans antialiased">
70116
<NavigationProgress />
71117
<FirebaseAnalytics />

app/robots.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,21 @@ import { BASE_URL } from "@/lib/seo";
33

44
export default function robots(): MetadataRoute.Robots {
55
return {
6-
rules: {
7-
userAgent: "*",
8-
allow: "/",
9-
// Prevent indexing of internal API routes and OG image generator
10-
disallow: ["/api/", "/og/"],
11-
},
6+
rules: [
7+
{
8+
userAgent: "*",
9+
allow: "/",
10+
// Prevent indexing of internal API routes, OG image generator, and LLM endpoints
11+
disallow: ["/api/", "/og/"],
12+
},
13+
{
14+
// Google-specific: allow maximum crawling for rich results
15+
userAgent: "Googlebot",
16+
allow: "/",
17+
disallow: ["/api/", "/og/"],
18+
},
19+
],
1220
sitemap: `${BASE_URL}/sitemap.xml`,
21+
host: BASE_URL,
1322
};
1423
}

app/sitemap.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import { getCloudflareContext } from "@opennextjs/cloudflare";
12
import type { MetadataRoute } from "next";
3+
import { getDb } from "@/db/client";
4+
import { extensions } from "@/db/schema";
25
import { locales } from "@/lib/i18n";
36
import { canonicalUrl } from "@/lib/seo";
47
import { source } from "@/lib/source";
@@ -18,6 +21,11 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
1821
lastModified: now,
1922
changeFrequency: "weekly",
2023
priority: path === "/" ? 1.0 : 0.8,
24+
alternates: {
25+
languages: Object.fromEntries(
26+
locales.map((l) => [l, canonicalUrl(l, path)]),
27+
),
28+
},
2129
});
2230
}
2331
}
@@ -31,8 +39,44 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
3139
lastModified: now,
3240
changeFrequency: "weekly",
3341
priority: 0.7,
42+
alternates: {
43+
languages: Object.fromEntries(
44+
locales.map((l) => [l, canonicalUrl(l, page.url)]),
45+
),
46+
},
3447
});
3548
}
3649

50+
// ── Extension store detail pages ────────────────────────────────────────
51+
try {
52+
const ctx = await getCloudflareContext({ async: true });
53+
// @ts-expect-error - CF env type
54+
const d1 = ctx.env.DB as D1Database | undefined;
55+
56+
if (d1) {
57+
const db = getDb(d1);
58+
const extensionList = await db.select().from(extensions).all();
59+
60+
for (const ext of extensionList) {
61+
const extPath = `/store/${encodeURIComponent(ext.id)}`;
62+
for (const locale of locales) {
63+
entries.push({
64+
url: canonicalUrl(locale, extPath),
65+
lastModified: ext.updatedAt ? new Date(ext.updatedAt) : now,
66+
changeFrequency: "weekly",
67+
priority: 0.6,
68+
alternates: {
69+
languages: Object.fromEntries(
70+
locales.map((l) => [l, canonicalUrl(l, extPath)]),
71+
),
72+
},
73+
});
74+
}
75+
}
76+
}
77+
} catch (err) {
78+
console.error("Sitemap: failed to load extensions from D1:", err);
79+
}
80+
3781
return entries;
3882
}

components/home/Downloads.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,11 @@ export function Downloads({ version, releaseAssets }: DownloadsProps) {
301301
};
302302

303303
return (
304-
<section id="downloads" className="relative py-20 overflow-hidden">
304+
<section
305+
id="downloads"
306+
className="relative py-20 overflow-hidden"
307+
aria-label={t("downloads.title")}
308+
>
305309
{/* 背景装饰 */}
306310
<div className="absolute inset-0 bg-gradient-grid bg-[length:50px_50px] opacity-5 dark:opacity-10" />
307311

components/home/Extensions.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ export function Extensions() {
4141
];
4242

4343
return (
44-
<section id="extensions" className="relative py-20 overflow-hidden">
44+
<section
45+
id="extensions"
46+
className="relative py-20 overflow-hidden"
47+
aria-label={t("extensions.title")}
48+
>
4549
{/* Background Decoration */}
4650
<div className="absolute inset-0 bg-gradient-grid bg-[length:50px_50px] opacity-5 dark:opacity-10" />
4751

components/home/Features.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ export function Features() {
4848
];
4949

5050
return (
51-
<section id="features" className="relative py-20 overflow-hidden">
51+
<section
52+
id="features"
53+
className="relative py-20 overflow-hidden"
54+
aria-label={t("features.title")}
55+
>
5256
{/* Background Decoration */}
5357
<div className="absolute inset-0 bg-gradient-grid bg-[length:50px_50px] opacity-5 dark:opacity-10" />
5458

components/home/Hero.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export function Hero({ version, releaseAssets }: HeroProps) {
8989
};
9090

9191
return (
92-
<section className="relative pt-18">
92+
<section className="relative pt-18" aria-label="Gopeed Download Manager">
9393
{/* Background Grid */}
9494
<div className="absolute inset-0 bg-gradient-grid bg-[length:50px_50px] opacity-5 dark:opacity-10" />
9595

0 commit comments

Comments
 (0)