Skip to content

Commit c2ffdbf

Browse files
committed
feat(seo): add localized comparison guides
1 parent 974e04a commit c2ffdbf

16 files changed

Lines changed: 3243 additions & 104 deletions

File tree

src/app/[locale]/clis/comparison/page.client.tsx

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ import { AppleIcon, LinuxIcon, WindowsIcon } from '@/components/controls/Platfor
66
import Footer from '@/components/Footer'
77
import Header from '@/components/Header'
88
import { Breadcrumb } from '@/components/navigation/Breadcrumb'
9+
import ComparisonGuide, {
10+
type ComparisonGuideContent,
11+
type ComparisonGuideItem,
12+
type ComparisonProfileItem,
13+
} from '@/components/product/ComparisonGuide'
914
import ComparisonTable, { type ComparisonColumn } from '@/components/product/ComparisonTable'
1015
import { PricingSummaryValue } from '@/components/product/ProductPricing'
11-
import { Link } from '@/i18n/navigation'
1216
import { withVendorCommunityUrlsForCatalog } from '@/lib/community-urls'
1317
import { clisData, vendorsData } from '@/lib/generated'
1418
import { getGithubStars } from '@/lib/generated/github-stars'
@@ -28,6 +32,19 @@ type Props = {
2832
export default function CLIComparisonPageClient({ locale: _locale }: Props) {
2933
const tPage = useTranslations('pages.comparison')
3034
const tShared = useTranslations('shared')
35+
const guideContent: ComparisonGuideContent = {
36+
title: tPage('clis.guide.title'),
37+
intro: tPage('clis.guide.intro', { count: clis.length }),
38+
criteria: tPage.raw('clis.guide.criteria') as ComparisonGuideItem[],
39+
shortlistTitle: tPage('clis.guide.shortlistTitle'),
40+
shortlistIntro: tPage('clis.guide.shortlistIntro'),
41+
steps: tPage.raw('clis.guide.steps') as ComparisonGuideItem[],
42+
profilesTitle: tPage('clis.guide.profilesTitle'),
43+
profilesIntro: tPage('clis.guide.profilesIntro'),
44+
profiles: tPage.raw('clis.guide.profiles') as ComparisonProfileItem[],
45+
faqTitle: tPage('clis.guide.faqTitle'),
46+
faqs: tPage.raw('clis.guide.faqs') as ComparisonGuideItem[],
47+
}
3148

3249
const columns: ComparisonColumn[] = [
3350
{
@@ -279,7 +296,7 @@ export default function CLIComparisonPageClient({ locale: _locale }: Props) {
279296
/>
280297

281298
{/* Page Header */}
282-
<section className="py-[var(--spacing-lg)] border-[var(--color-border)]">
299+
<section className="pt-[var(--spacing-lg)] pb-[var(--spacing-md)]">
283300
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
284301
<h1 className="text-3xl font-semibold tracking-[-0.03em] mb-[var(--spacing-sm)]">
285302
{tPage('clis.title')}
@@ -291,7 +308,10 @@ export default function CLIComparisonPageClient({ locale: _locale }: Props) {
291308
</section>
292309

293310
{/* Comparison Table */}
294-
<section className="pb-[var(--spacing-lg)] border-b border-[var(--color-border)]">
311+
<section
312+
id="comparison-table"
313+
className="scroll-mt-32 pb-[var(--spacing-xl)] border-b border-[var(--color-border)]"
314+
>
295315
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
296316
<ComparisonTable
297317
items={clis as unknown as Record<string, unknown>[]}
@@ -302,17 +322,7 @@ export default function CLIComparisonPageClient({ locale: _locale }: Props) {
302322
</div>
303323
</section>
304324

305-
{/* Back Navigation */}
306-
<section className="py-[var(--spacing-lg)] border-b border-[var(--color-border)]">
307-
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
308-
<Link
309-
href="/clis"
310-
className="inline-flex items-center gap-[var(--spacing-xs)] text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
311-
>
312-
{tPage('clis.backTo')}
313-
</Link>
314-
</div>
315-
</section>
325+
<ComparisonGuide content={guideContent} />
316326

317327
<Footer />
318328
</>

src/app/[locale]/extensions/comparison/page.client.tsx

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ import { useTranslations } from 'next-intl'
55
import Footer from '@/components/Footer'
66
import Header from '@/components/Header'
77
import { Breadcrumb } from '@/components/navigation/Breadcrumb'
8+
import ComparisonGuide, {
9+
type ComparisonGuideContent,
10+
type ComparisonGuideItem,
11+
type ComparisonProfileItem,
12+
} from '@/components/product/ComparisonGuide'
813
import ComparisonTable, { type ComparisonColumn } from '@/components/product/ComparisonTable'
914
import { PricingSummaryValue } from '@/components/product/ProductPricing'
10-
import { Link } from '@/i18n/navigation'
1115
import { withVendorCommunityUrlsForCatalog } from '@/lib/community-urls'
1216
import { extensionsData, vendorsData } from '@/lib/generated'
1317
import { getGithubStars } from '@/lib/generated/github-stars'
@@ -27,6 +31,19 @@ type Props = {
2731
export default function ExtensionComparisonPageClient({ locale: _locale }: Props) {
2832
const tPage = useTranslations('pages.comparison')
2933
const tShared = useTranslations('shared')
34+
const guideContent: ComparisonGuideContent = {
35+
title: tPage('extensions.guide.title'),
36+
intro: tPage('extensions.guide.intro', { count: extensions.length }),
37+
criteria: tPage.raw('extensions.guide.criteria') as ComparisonGuideItem[],
38+
shortlistTitle: tPage('extensions.guide.shortlistTitle'),
39+
shortlistIntro: tPage('extensions.guide.shortlistIntro'),
40+
steps: tPage.raw('extensions.guide.steps') as ComparisonGuideItem[],
41+
profilesTitle: tPage('extensions.guide.profilesTitle'),
42+
profilesIntro: tPage('extensions.guide.profilesIntro'),
43+
profiles: tPage.raw('extensions.guide.profiles') as ComparisonProfileItem[],
44+
faqTitle: tPage('extensions.guide.faqTitle'),
45+
faqs: tPage.raw('extensions.guide.faqs') as ComparisonGuideItem[],
46+
}
3047

3148
const columns: ComparisonColumn[] = [
3249
{
@@ -265,7 +282,7 @@ export default function ExtensionComparisonPageClient({ locale: _locale }: Props
265282
/>
266283

267284
{/* Page Header */}
268-
<section className="py-[var(--spacing-lg)] border-[var(--color-border)]">
285+
<section className="pt-[var(--spacing-lg)] pb-[var(--spacing-md)]">
269286
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
270287
<h1 className="text-3xl font-semibold tracking-[-0.03em] mb-[var(--spacing-sm)]">
271288
{tPage('extensions.title')}
@@ -277,7 +294,7 @@ export default function ExtensionComparisonPageClient({ locale: _locale }: Props
277294
</section>
278295

279296
{/* Comparison Table */}
280-
<section className="pb-[var(--spacing-lg)] border-b border-[var(--color-border)]">
297+
<section className="pb-[var(--spacing-xl)] border-b border-[var(--color-border)]">
281298
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
282299
<ComparisonTable
283300
items={extensions as unknown as Record<string, unknown>[]}
@@ -288,17 +305,7 @@ export default function ExtensionComparisonPageClient({ locale: _locale }: Props
288305
</div>
289306
</section>
290307

291-
{/* Back Navigation */}
292-
<section className="py-[var(--spacing-lg)] border-b border-[var(--color-border)]">
293-
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
294-
<Link
295-
href="/extensions"
296-
className="inline-flex items-center gap-[var(--spacing-xs)] text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
297-
>
298-
{tPage('extensions.backTo')}
299-
</Link>
300-
</div>
301-
</section>
308+
<ComparisonGuide content={guideContent} />
302309

303310
<Footer />
304311
</>

src/app/[locale]/ides/comparison/page.client.tsx

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ import { AppleIcon, LinuxIcon, WindowsIcon } from '@/components/controls/Platfor
66
import Footer from '@/components/Footer'
77
import Header from '@/components/Header'
88
import { Breadcrumb } from '@/components/navigation/Breadcrumb'
9+
import ComparisonGuide, {
10+
type ComparisonGuideContent,
11+
type ComparisonGuideItem,
12+
type ComparisonProfileItem,
13+
} from '@/components/product/ComparisonGuide'
914
import ComparisonTable, { type ComparisonColumn } from '@/components/product/ComparisonTable'
1015
import { PricingSummaryValue } from '@/components/product/ProductPricing'
11-
import { Link } from '@/i18n/navigation'
1216
import { withVendorCommunityUrlsForCatalog } from '@/lib/community-urls'
1317
import { idesData, vendorsData } from '@/lib/generated'
1418
import { getGithubStars } from '@/lib/generated/github-stars'
@@ -28,6 +32,19 @@ type Props = {
2832
export default function IDEComparisonPageClient({ locale: _locale }: Props) {
2933
const tPage = useTranslations('pages.comparison')
3034
const tShared = useTranslations('shared')
35+
const guideContent: ComparisonGuideContent = {
36+
title: tPage('ides.guide.title'),
37+
intro: tPage('ides.guide.intro', { count: ides.length }),
38+
criteria: tPage.raw('ides.guide.criteria') as ComparisonGuideItem[],
39+
shortlistTitle: tPage('ides.guide.shortlistTitle'),
40+
shortlistIntro: tPage('ides.guide.shortlistIntro'),
41+
steps: tPage.raw('ides.guide.steps') as ComparisonGuideItem[],
42+
profilesTitle: tPage('ides.guide.profilesTitle'),
43+
profilesIntro: tPage('ides.guide.profilesIntro'),
44+
profiles: tPage.raw('ides.guide.profiles') as ComparisonProfileItem[],
45+
faqTitle: tPage('ides.guide.faqTitle'),
46+
faqs: tPage.raw('ides.guide.faqs') as ComparisonGuideItem[],
47+
}
3148

3249
const columns: ComparisonColumn[] = [
3350
{
@@ -279,7 +296,7 @@ export default function IDEComparisonPageClient({ locale: _locale }: Props) {
279296
/>
280297

281298
{/* Page Header */}
282-
<section className="py-[var(--spacing-lg)] border-[var(--color-border)]">
299+
<section className="pt-[var(--spacing-lg)] pb-[var(--spacing-md)]">
283300
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
284301
<h1 className="text-3xl font-semibold tracking-[-0.03em] mb-[var(--spacing-sm)]">
285302
{tPage('ides.title')}
@@ -291,7 +308,7 @@ export default function IDEComparisonPageClient({ locale: _locale }: Props) {
291308
</section>
292309

293310
{/* Comparison Table */}
294-
<section className="pb-[var(--spacing-lg)] border-b border-[var(--color-border)]">
311+
<section className="pb-[var(--spacing-xl)] border-b border-[var(--color-border)]">
295312
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
296313
<ComparisonTable
297314
items={ides as unknown as Record<string, unknown>[]}
@@ -302,17 +319,7 @@ export default function IDEComparisonPageClient({ locale: _locale }: Props) {
302319
</div>
303320
</section>
304321

305-
{/* Back Navigation */}
306-
<section className="py-[var(--spacing-lg)] border-b border-[var(--color-border)]">
307-
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
308-
<Link
309-
href="/ides"
310-
className="inline-flex items-center gap-[var(--spacing-xs)] text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
311-
>
312-
{tPage('ides.backTo')}
313-
</Link>
314-
</div>
315-
</section>
322+
<ComparisonGuide content={guideContent} />
316323

317324
<Footer />
318325
</>
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
'use client'
2+
3+
import { Link } from '@/i18n/navigation'
4+
5+
export type ComparisonGuideItem = {
6+
title: string
7+
description: string
8+
}
9+
10+
export type ComparisonProfileItem = ComparisonGuideItem & {
11+
href: string
12+
}
13+
14+
export type ComparisonGuideContent = {
15+
title: string
16+
intro: string
17+
criteria: ComparisonGuideItem[]
18+
shortlistTitle: string
19+
shortlistIntro: string
20+
steps: ComparisonGuideItem[]
21+
profilesTitle: string
22+
profilesIntro: string
23+
profiles: ComparisonProfileItem[]
24+
faqTitle: string
25+
faqs: ComparisonGuideItem[]
26+
}
27+
28+
type Props = {
29+
content: ComparisonGuideContent
30+
}
31+
32+
export default function ComparisonGuide({ content }: Props) {
33+
return (
34+
<>
35+
<section className="border-b border-[var(--color-border)]">
36+
<div className="max-w-6xl mx-auto px-[var(--spacing-md)] py-[var(--spacing-xl)]">
37+
<div className="max-w-3xl">
38+
<h2 className="text-2xl font-semibold tracking-[-0.025em] mb-[var(--spacing-sm)]">
39+
{content.title}
40+
</h2>
41+
<p className="text-base leading-relaxed text-[var(--color-text-secondary)]">
42+
{content.intro}
43+
</p>
44+
</div>
45+
46+
<div className="grid md:grid-cols-2 border-t border-l border-[var(--color-border)] mt-[var(--spacing-lg)]">
47+
{content.criteria.map(item => (
48+
<article
49+
key={item.title}
50+
className="p-[var(--spacing-md)] border-r border-b border-[var(--color-border)]"
51+
>
52+
<h3 className="text-base font-semibold mb-[var(--spacing-xs)]">{item.title}</h3>
53+
<p className="text-sm leading-relaxed text-[var(--color-text-secondary)]">
54+
{item.description}
55+
</p>
56+
</article>
57+
))}
58+
</div>
59+
</div>
60+
</section>
61+
62+
<section className="border-b border-[var(--color-border)]">
63+
<div className="max-w-6xl mx-auto px-[var(--spacing-md)] py-[var(--spacing-xl)]">
64+
<div className="max-w-3xl">
65+
<h2 className="text-2xl font-semibold tracking-[-0.025em] mb-[var(--spacing-sm)]">
66+
{content.shortlistTitle}
67+
</h2>
68+
<p className="text-base leading-relaxed text-[var(--color-text-secondary)]">
69+
{content.shortlistIntro}
70+
</p>
71+
</div>
72+
73+
<ol className="grid md:grid-cols-3 gap-0 border-t border-l border-[var(--color-border)] mt-[var(--spacing-lg)]">
74+
{content.steps.map((step, index) => (
75+
<li
76+
key={step.title}
77+
className="p-[var(--spacing-md)] border-r border-b border-[var(--color-border)]"
78+
>
79+
<span className="block text-xs font-semibold tracking-wider text-[var(--color-text-muted)] mb-[var(--spacing-sm)]">
80+
{String(index + 1).padStart(2, '0')}
81+
</span>
82+
<h3 className="text-base font-semibold mb-[var(--spacing-xs)]">{step.title}</h3>
83+
<p className="text-sm leading-relaxed text-[var(--color-text-secondary)]">
84+
{step.description}
85+
</p>
86+
</li>
87+
))}
88+
</ol>
89+
90+
<div className="mt-[var(--spacing-xl)]">
91+
<h3 className="text-lg font-semibold mb-[var(--spacing-sm)]">
92+
{content.profilesTitle}
93+
</h3>
94+
<p className="text-sm leading-relaxed text-[var(--color-text-secondary)] max-w-3xl mb-[var(--spacing-md)]">
95+
{content.profilesIntro}
96+
</p>
97+
<div className="grid sm:grid-cols-2 border-t border-l border-[var(--color-border)]">
98+
{content.profiles.map(profile => (
99+
<Link
100+
key={profile.href}
101+
href={profile.href}
102+
className="group p-[var(--spacing-md)] border-r border-b border-[var(--color-border)] hover:bg-[var(--color-hover)] transition-colors"
103+
>
104+
<span className="block text-sm font-semibold group-hover:underline">
105+
{profile.title}
106+
</span>
107+
<span className="block text-sm leading-relaxed text-[var(--color-text-secondary)] mt-[var(--spacing-xs)]">
108+
{profile.description}
109+
</span>
110+
</Link>
111+
))}
112+
</div>
113+
</div>
114+
</div>
115+
</section>
116+
117+
<section>
118+
<div className="max-w-6xl mx-auto px-[var(--spacing-md)] pt-[var(--spacing-xl)] pb-[var(--spacing-md)]">
119+
<h2 className="text-2xl font-semibold tracking-[-0.025em] mb-[var(--spacing-lg)]">
120+
{content.faqTitle}
121+
</h2>
122+
<div className="border-t border-[var(--color-border)]">
123+
{content.faqs.map(faq => (
124+
<article
125+
key={faq.title}
126+
className="grid md:grid-cols-[minmax(0,1fr)_minmax(0,2fr)] gap-[var(--spacing-md)] py-[var(--spacing-md)] border-b border-[var(--color-border)]"
127+
>
128+
<h3 className="text-base font-semibold">{faq.title}</h3>
129+
<p className="text-sm leading-relaxed text-[var(--color-text-secondary)]">
130+
{faq.description}
131+
</p>
132+
</article>
133+
))}
134+
</div>
135+
</div>
136+
</section>
137+
</>
138+
)
139+
}

0 commit comments

Comments
 (0)