Skip to content

Commit 7f12b04

Browse files
committed
feat: SEO improve
1 parent 18d6c78 commit 7f12b04

26 files changed

Lines changed: 935 additions & 46 deletions

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

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import type { Metadata } from "next";
2+
import Link from "next/link";
3+
import { notFound } from "next/navigation";
4+
import { Footer } from "@/components/home/Footer";
5+
import { Navbar } from "@/components/home/Navbar";
6+
import { getAppData } from "@/lib/data";
7+
import { i18n, type Locale, locales } from "@/lib/i18n";
8+
import { breadcrumbJsonLd, jsonLdScriptContent } from "@/lib/jsonld";
9+
import {
10+
getKeywordPage,
11+
type KeywordPageSlug,
12+
keywordPageSlugs,
13+
localeHref,
14+
} from "@/lib/keyword-pages";
15+
import { pageAlternates } from "@/lib/seo";
16+
17+
export const dynamicParams = false;
18+
19+
function resolveLocale(lang: string): Locale {
20+
return (
21+
locales.includes(lang as Locale) ? lang : i18n.defaultLanguage
22+
) as Locale;
23+
}
24+
25+
function resolveKeywordSlug(slug: string): KeywordPageSlug | null {
26+
return keywordPageSlugs.includes(slug as KeywordPageSlug)
27+
? (slug as KeywordPageSlug)
28+
: null;
29+
}
30+
31+
export async function generateStaticParams() {
32+
return keywordPageSlugs.map((slug) => ({ slug }));
33+
}
34+
35+
export async function generateMetadata({
36+
params,
37+
}: {
38+
params: Promise<{ lang: string; slug: string }>;
39+
}): Promise<Metadata> {
40+
const { lang, slug } = await params;
41+
const locale = resolveLocale(lang);
42+
const keywordSlug = resolveKeywordSlug(slug);
43+
44+
if (!keywordSlug) {
45+
notFound();
46+
}
47+
48+
const page = getKeywordPage(locale, keywordSlug);
49+
50+
return {
51+
title: {
52+
absolute: page.metaTitle,
53+
},
54+
description: page.metaDescription,
55+
keywords: page.keywords,
56+
alternates: pageAlternates(locale, `/${keywordSlug}`),
57+
openGraph: {
58+
title: page.metaTitle,
59+
description: page.metaDescription,
60+
},
61+
};
62+
}
63+
64+
export default async function KeywordLandingPage({
65+
params,
66+
}: {
67+
params: Promise<{ lang: string; slug: string }>;
68+
}) {
69+
const { lang, slug } = await params;
70+
const locale = resolveLocale(lang);
71+
const keywordSlug = resolveKeywordSlug(slug);
72+
73+
if (!keywordSlug) {
74+
notFound();
75+
}
76+
77+
const page = getKeywordPage(locale, keywordSlug);
78+
const appData = await getAppData();
79+
const downloadsHref = localeHref(locale, "/#downloads");
80+
const docsHref = localeHref(locale, "/docs/install");
81+
const breadcrumbItems = [
82+
{ name: "Gopeed", path: "/" },
83+
{ name: page.title, path: `/${keywordSlug}` },
84+
];
85+
86+
return (
87+
<main className="min-h-screen stable-vh overflow-x-clip bg-white dark:bg-[#0A0A0A] text-gray-900 dark:text-gray-100 relative">
88+
<script type="application/ld+json">
89+
{jsonLdScriptContent(breadcrumbJsonLd(locale, breadcrumbItems))}
90+
</script>
91+
<Navbar version={appData.version} stars={appData.stars} />
92+
93+
<article className="relative pt-32 pb-24 lg:pt-40">
94+
<div className="absolute inset-0 bg-gradient-grid bg-[length:48px_48px] opacity-5 dark:opacity-10" />
95+
96+
<div className="max-w-7xl mx-auto px-6 sm:px-8 lg:px-16 relative">
97+
<div className="max-w-4xl mx-auto">
98+
<p className="text-sm font-semibold tracking-[0.18em] uppercase text-primary-600 dark:text-primary-400 mb-4">
99+
{page.eyebrow}
100+
</p>
101+
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-semibold tracking-tight text-gray-950 dark:text-white mb-6">
102+
{page.title}
103+
</h1>
104+
<p className="text-lg sm:text-xl text-gray-600 dark:text-gray-400 leading-8 mb-10">
105+
{page.description}
106+
</p>
107+
108+
<div className="flex flex-col sm:flex-row gap-4 mb-14">
109+
<a
110+
href={downloadsHref}
111+
className="inline-flex items-center justify-center rounded-lg bg-primary-600 px-6 py-3 font-medium text-white transition-colors hover:bg-primary-700"
112+
>
113+
{page.primaryCtaLabel}
114+
</a>
115+
<Link
116+
href={docsHref}
117+
className="inline-flex items-center justify-center rounded-lg border border-gray-300 dark:border-gray-700 px-6 py-3 font-medium text-gray-900 dark:text-gray-100 transition-colors hover:border-primary-500/50 hover:text-primary-600 dark:hover:text-primary-400"
118+
>
119+
{page.secondaryCtaLabel}
120+
</Link>
121+
</div>
122+
123+
<div className="space-y-6 mb-14">
124+
{page.intro.map((paragraph) => (
125+
<p
126+
key={paragraph}
127+
className="text-base sm:text-lg text-gray-700 dark:text-gray-300 leading-8"
128+
>
129+
{paragraph}
130+
</p>
131+
))}
132+
</div>
133+
134+
<section className="mb-14">
135+
<h2 className="text-2xl font-semibold text-gray-950 dark:text-white mb-6">
136+
{page.benefitsTitle}
137+
</h2>
138+
<div className="grid sm:grid-cols-2 gap-4">
139+
{page.benefits.map((item) => (
140+
<div
141+
key={item}
142+
className="rounded-2xl border border-gray-200/70 dark:border-gray-800/70 bg-white/90 dark:bg-gray-900/80 p-5"
143+
>
144+
<p className="text-gray-700 dark:text-gray-300 leading-7">
145+
{item}
146+
</p>
147+
</div>
148+
))}
149+
</div>
150+
</section>
151+
152+
<section className="mb-14">
153+
<h2 className="text-2xl font-semibold text-gray-950 dark:text-white mb-6">
154+
{page.stepsTitle}
155+
</h2>
156+
<ol className="space-y-4">
157+
{page.steps.map((step, index) => (
158+
<li
159+
key={step}
160+
className="rounded-2xl border border-gray-200/70 dark:border-gray-800/70 bg-white/90 dark:bg-gray-900/80 p-5"
161+
>
162+
<span className="block text-sm font-semibold text-primary-600 dark:text-primary-400 mb-2">
163+
Step {index + 1}
164+
</span>
165+
<p className="text-gray-700 dark:text-gray-300 leading-7">
166+
{step}
167+
</p>
168+
</li>
169+
))}
170+
</ol>
171+
</section>
172+
173+
<section className="rounded-3xl border border-primary-500/20 bg-primary-50/80 dark:bg-primary-950/20 p-8">
174+
<h2 className="text-2xl font-semibold text-gray-950 dark:text-white mb-4">
175+
{page.ctaTitle}
176+
</h2>
177+
<p className="text-gray-700 dark:text-gray-300 leading-7 mb-6">
178+
{page.ctaDescription}
179+
</p>
180+
<div className="flex flex-col sm:flex-row gap-4">
181+
<a
182+
href={downloadsHref}
183+
className="inline-flex items-center justify-center rounded-lg bg-primary-600 px-6 py-3 font-medium text-white transition-colors hover:bg-primary-700"
184+
>
185+
{page.primaryCtaLabel}
186+
</a>
187+
<Link
188+
href={docsHref}
189+
className="inline-flex items-center justify-center rounded-lg border border-gray-300 dark:border-gray-700 px-6 py-3 font-medium text-gray-900 dark:text-gray-100 transition-colors hover:border-primary-500/50 hover:text-primary-600 dark:hover:text-primary-400"
190+
>
191+
{page.secondaryCtaLabel}
192+
</Link>
193+
</div>
194+
</section>
195+
</div>
196+
</div>
197+
</article>
198+
199+
<Footer />
200+
</main>
201+
);
202+
}

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

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Hero,
88
Navbar,
99
} from "@/components/home";
10+
import { SeoFaq } from "@/components/home/SeoFaq";
1011
import { getAppData } from "@/lib/data";
1112
import { i18n, type Locale, locales } from "@/lib/i18n";
1213
import { getTranslation } from "@/lib/i18n/translations";
@@ -42,23 +43,28 @@ export async function generateMetadata({
4243
};
4344
}
4445

45-
export default async function HomePage() {
46+
export default async function HomePage({
47+
params,
48+
}: {
49+
params: Promise<{ lang: string }>;
50+
}) {
51+
const { lang } = await params;
52+
const locale = (
53+
locales.includes(lang as Locale) ? lang : i18n.defaultLanguage
54+
) as Locale;
55+
4656
// Fetch data during SSR
4757
const appData = await getAppData();
4858

4959
return (
5060
<main className="min-h-screen stable-vh overflow-x-clip bg-white dark:bg-gray-950 text-gray-900 dark:text-gray-100 relative">
5161
{/* JSON-LD: SoftwareApplication + FAQ */}
52-
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD structured data is server-generated and safe */}
53-
<script
54-
type="application/ld+json"
55-
dangerouslySetInnerHTML={{
56-
__html: jsonLdScriptContent(
57-
softwareApplicationJsonLd("en", appData.version),
58-
faqJsonLd("en"),
59-
),
60-
}}
61-
/>
62+
<script type="application/ld+json">
63+
{jsonLdScriptContent(
64+
softwareApplicationJsonLd(locale, appData.version),
65+
faqJsonLd(locale),
66+
)}
67+
</script>
6268
{/* Global background effects - only visible in dark mode */}
6369
<div className="fixed inset-0 -z-10 dark:block hidden">
6470
{/* Main background gradient */}
@@ -76,6 +82,7 @@ export default async function HomePage() {
7682
<Hero version={appData.version} releaseAssets={appData.releaseAssets} />
7783
<Features />
7884
<Extensions />
85+
<SeoFaq locale={locale} />
7986
<Downloads
8087
version={appData.version}
8188
releaseAssets={appData.releaseAssets}

app/[lang]/docs/[[...slug]]/page.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { createRelativeLink } from "fumadocs-ui/mdx";
88
import type { Metadata } from "next";
99
import { notFound } from "next/navigation";
1010
import { i18n, type Locale } from "@/lib/i18n";
11+
import { getTranslation } from "@/lib/i18n/translations";
12+
import { breadcrumbJsonLd, jsonLdScriptContent } from "@/lib/jsonld";
1113
import { pageAlternates } from "@/lib/seo";
1214
import { getPageImage, source } from "@/lib/source";
1315
import { getMDXComponents } from "@/mdx-components";
@@ -27,13 +29,28 @@ export default async function Page(props: PageProps) {
2729
if (!page) notFound();
2830

2931
const MDX = page.data.body;
32+
const docsLabel = getTranslation(locale, "nav.docs");
33+
const breadcrumbItems =
34+
page.url === "/docs"
35+
? [
36+
{ name: "Gopeed", path: "/" },
37+
{ name: docsLabel, path: "/docs" },
38+
]
39+
: [
40+
{ name: "Gopeed", path: "/" },
41+
{ name: docsLabel, path: "/docs" },
42+
{ name: page.data.title, path: page.url },
43+
];
3044

3145
return (
3246
<DocsPage
3347
toc={page.data.toc}
3448
full={page.data.full}
3549
tableOfContent={{ style: "clerk" }}
3650
>
51+
<script type="application/ld+json">
52+
{jsonLdScriptContent(breadcrumbJsonLd(locale, breadcrumbItems))}
53+
</script>
3754
<DocsTitle>{page.data.title}</DocsTitle>
3855
<DocsDescription>{page.data.description}</DocsDescription>
3956
<DocsBody>

app/[lang]/layout.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,16 +101,9 @@ export default async function RootLayout({
101101
<html lang={locale} suppressHydrationWarning>
102102
<head>
103103
{/* 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-
/>
104+
<script type="application/ld+json">
105+
{jsonLdScriptContent(organizationJsonLd(), webSiteJsonLd(locale))}
106+
</script>
114107
</head>
115108
<body className="font-sans antialiased">
116109
<NavigationProgress />

app/global.css

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ body {
8686
}
8787

8888
@keyframes float {
89-
9089
0%,
9190
100% {
9291
transform: translateY(0);
@@ -98,7 +97,6 @@ body {
9897
}
9998

10099
@keyframes pulse-slow {
101-
102100
0%,
103101
100% {
104102
opacity: 0.2;
@@ -110,7 +108,6 @@ body {
110108
}
111109

112110
@keyframes gradient-x {
113-
114111
0%,
115112
100% {
116113
background-size: 200% 200%;
@@ -209,4 +206,4 @@ body {
209206
/* IE and Edge */
210207
scrollbar-width: none;
211208
/* Firefox */
212-
}
209+
}

0 commit comments

Comments
 (0)