Skip to content

Commit 5de20b2

Browse files
ericyangpanclaude
andcommitted
refactor(app): update pages with i18n restructuring and metadata improvements
Update all app pages to use the new i18n namespace structure and improve metadata handling with request memoization. Changes: - Update all pages to use new namespaced i18n translations - Implement React cache() for data fetchers to prevent duplicate fetching in generateMetadata() and page components - Update metadata generation to use auto-detected OG images - Improve type safety with proper Locale type imports - Update all page.tsx, page.client.tsx files across: - CLIs, Extensions, IDEs (list + detail + comparison pages) - Models, Providers, Vendors (list + detail pages) - Articles, Docs, Search, Home, and other pages - Update globals.css with minimalist design improvements - Update layout.tsx with new i18n structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8a3228e commit 5de20b2

File tree

36 files changed

+364
-436
lines changed

36 files changed

+364
-436
lines changed

src/app/[locale]/ai-coding-landscape/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import VendorMatrix from './components/VendorMatrix'
99

1010
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
1111
const { locale } = await params
12-
const tNav = await getTranslations({ locale, namespace: 'header' })
12+
const tNav = await getTranslations({ locale, namespace: 'components.header' })
1313

1414
const canonicalPath = locale === 'en' ? '/ai-coding-landscape' : `/${locale}/ai-coding-landscape`
1515
const title = buildTitle({ title: tNav('aiCodingLandscape') })
@@ -47,8 +47,8 @@ type Props = {
4747

4848
export default async function Page({ params }: Props) {
4949
const { locale } = await params
50-
const tNav = await getTranslations({ locale, namespace: 'header' })
51-
const tOverview = await getTranslations({ locale, namespace: 'stacksPages.overview' })
50+
const tNav = await getTranslations({ locale, namespace: 'components.header' })
51+
const tOverview = await getTranslations({ locale, namespace: 'pages.overview' })
5252

5353
// Build vendor matrix data
5454
const matrixData = buildVendorMatrix()

src/app/[locale]/ai-coding-stack/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { buildCanonicalUrl, buildOpenGraph, buildTitle, buildTwitterCard } from
77

88
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
99
const { locale } = await params
10-
const t = await getTranslations({ locale, namespace: 'stacksPages.overview' })
10+
const t = await getTranslations({ locale, namespace: 'pages.overview' })
1111

1212
const canonicalPath = locale === 'en' ? '/ai-coding-stack' : `/${locale}/ai-coding-stack`
1313
const title = buildTitle({ title: t('title') })
@@ -45,7 +45,7 @@ type Props = {
4545

4646
export default async function AICodingStackPage({ params }: Props) {
4747
const { locale } = await params
48-
const t = await getTranslations({ locale, namespace: 'stacksPages.overview' })
48+
const t = await getTranslations({ locale, namespace: 'pages.overview' })
4949

5050
return (
5151
<>

src/app/[locale]/articles/[slug]/page.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { getTranslations } from 'next-intl/server'
33
import { Breadcrumb } from '@/components/controls/Breadcrumb'
44
import Footer from '@/components/Footer'
55
import Header from '@/components/Header'
6+
import type { Locale } from '@/i18n/config'
67
import { Link } from '@/i18n/navigation'
7-
import { getArticleBySlug, getArticleComponent, getArticles } from '@/lib/generated/articles'
8+
import { getArticle } from '@/lib/data/fetchers'
9+
import { getArticleComponent, getArticles } from '@/lib/generated/articles'
810
import { generateArticleMetadata } from '@/lib/metadata'
911

1012
type Props = {
@@ -30,14 +32,14 @@ export async function generateStaticParams() {
3032

3133
export async function generateMetadata({ params }: Props) {
3234
const { slug, locale } = await params
33-
const article = getArticleBySlug(slug, locale)
35+
const article = await getArticle(slug, locale)
3436

3537
if (!article) {
3638
return { title: 'Article Not Found | AI Coding Stack' }
3739
}
3840

3941
return await generateArticleMetadata({
40-
locale: locale as 'en' | 'zh-Hans',
42+
locale: locale as Locale,
4143
slug,
4244
article: {
4345
title: article.title,
@@ -49,13 +51,13 @@ export async function generateMetadata({ params }: Props) {
4951

5052
export default async function ArticlePage({ params }: Props) {
5153
const { slug, locale } = await params
52-
const article = getArticleBySlug(slug, locale)
54+
const article = await getArticle(slug, locale)
5355

5456
if (!article) {
5557
notFound()
5658
}
5759

58-
const t = await getTranslations({ locale, namespace: 'header' })
60+
const t = await getTranslations({ locale, namespace: 'components.header' })
5961
const ArticleContent = await getArticleComponent(locale, slug)
6062

6163
if (!ArticleContent) {
@@ -94,7 +96,7 @@ export default async function ArticlePage({ params }: Props) {
9496
/>
9597

9698
{/* Article Content */}
97-
<article className="max-w-5xl mx-auto px-[var(--spacing-md)] py-[var(--spacing-xl)]">
99+
<article className="max-w-6xl mx-auto px-[var(--spacing-md)] py-[var(--spacing-xl)]">
98100
{/* Article Header */}
99101
<header className="mb-[var(--spacing-xl)]">
100102
<h1 className="text-4xl font-semibold tracking-[-0.03em] mb-[var(--spacing-sm)] leading-tight">

src/app/[locale]/clis/[slug]/page.tsx

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,15 @@ import { JsonLd } from '@/components/JsonLd'
88
import { ProductCommands, ProductHero, ProductLinks, ProductPricing } from '@/components/product'
99
import type { Locale } from '@/i18n/config'
1010
import { Link } from '@/i18n/navigation'
11+
import { getCLI } from '@/lib/data/fetchers'
1112
import { clisData as clis } from '@/lib/generated'
1213
import { getGithubStars } from '@/lib/generated/github-stars'
1314
import { translateLicenseText } from '@/lib/license'
14-
import { localizeManifestItem } from '@/lib/manifest-i18n'
1515
import { generateSoftwareDetailMetadata } from '@/lib/metadata'
1616
import { getSchemaCurrency, getSchemaPrice } from '@/lib/pricing'
1717
import type {
1818
ComponentCommunityUrls,
1919
ComponentResourceUrls,
20-
ManifestCLI,
2120
ManifestPricingTier,
2221
} from '@/types/manifests'
2322

@@ -35,22 +34,17 @@ export async function generateMetadata({
3534
params: Promise<{ locale: string; slug: string }>
3635
}) {
3736
const { locale, slug } = await params
38-
const cliRaw = clis.find(c => c.id === slug)
37+
const cli = await getCLI(slug, locale as Locale)
3938

40-
if (!cliRaw) {
39+
if (!cli) {
4140
return { title: 'CLI Not Found | AI Coding Stack' }
4241
}
4342

44-
const cli = localizeManifestItem(
45-
cliRaw as unknown as Record<string, unknown>,
46-
locale as Locale
47-
) as unknown as ManifestCLI
48-
const t = await getTranslations({ locale })
49-
50-
const licenseStr = cli.license ? translateLicenseText(cli.license, t) : ''
43+
const tGlobal = await getTranslations({ locale })
44+
const licenseStr = cli.license ? translateLicenseText(cli.license, tGlobal) : ''
5145

5246
return await generateSoftwareDetailMetadata({
53-
locale: locale as 'en' | 'zh-Hans',
47+
locale: locale as Locale,
5448
category: 'clis',
5549
slug,
5650
product: {
@@ -71,20 +65,14 @@ export default async function CLIPage({
7165
params: Promise<{ locale: string; slug: string }>
7266
}) {
7367
const { locale, slug } = await params
74-
const cliRaw = clis.find(c => c.id === slug) as ManifestCLI | undefined
68+
const cli = await getCLI(slug, locale as Locale)
7569

76-
if (!cliRaw) {
70+
if (!cli) {
7771
notFound()
7872
}
7973

80-
const cli = localizeManifestItem(
81-
cliRaw as unknown as Record<string, unknown>,
82-
locale as Locale
83-
) as unknown as ManifestCLI
84-
const t = await getTranslations({ locale })
85-
const tHero = await getTranslations({ locale, namespace: 'components.productHero' })
86-
const tNav = await getTranslations({ locale, namespace: 'stacksPages.clis' })
87-
const tStacks = await getTranslations({ locale, namespace: 'stacks' })
74+
const t = await getTranslations({ locale, namespace: 'pages.cliDetail' })
75+
const tGlobal = await getTranslations({ locale })
8876

8977
const websiteUrl = cli.resourceUrls?.download || cli.websiteUrl
9078
const docsUrl = cli.docsUrl
@@ -135,7 +123,7 @@ export default async function CLIPage({
135123
}
136124
})
137125
: undefined,
138-
license: cli.license ? translateLicenseText(cli.license, t) : undefined,
126+
license: cli.license ? translateLicenseText(cli.license, tGlobal) : undefined,
139127
}
140128

141129
return (
@@ -145,8 +133,8 @@ export default async function CLIPage({
145133

146134
<Breadcrumb
147135
items={[
148-
{ name: tStacks('aiCodingStack'), href: '/ai-coding-stack' },
149-
{ name: tStacks('clis'), href: 'clis' },
136+
{ name: tGlobal('shared.common.aiCodingStack'), href: '/ai-coding-stack' },
137+
{ name: tGlobal('shared.stacks.clis'), href: 'clis' },
150138
{ name: cli.name, href: `clis/${cli.id}` },
151139
]}
152140
/>
@@ -157,7 +145,7 @@ export default async function CLIPage({
157145
description={cli.description}
158146
vendor={cli.vendor}
159147
category="CLI"
160-
categoryLabel={tHero('categories.CLI')}
148+
categoryLabel={t('categoryLabel')}
161149
latestVersion={cli.latestVersion}
162150
license={cli.license}
163151
githubStars={getGithubStars('clis', cli.id)}
@@ -166,14 +154,14 @@ export default async function CLIPage({
166154
docsUrl={docsUrl}
167155
downloadUrl={cli.resourceUrls?.download || undefined}
168156
labels={{
169-
vendor: tHero('vendor'),
170-
version: tHero('version'),
171-
license: tHero('license'),
172-
stars: tHero('stars'),
173-
platforms: tHero('platforms'),
174-
visitWebsite: tHero('visitWebsite'),
175-
documentation: tHero('documentation'),
176-
download: tHero('download'),
157+
vendor: t('vendor'),
158+
version: t('version'),
159+
license: t('license'),
160+
stars: t('stars'),
161+
platforms: t('platforms'),
162+
visitWebsite: t('visitWebsite'),
163+
documentation: t('documentation'),
164+
download: t('download'),
177165
}}
178166
/>
179167

@@ -226,7 +214,7 @@ export default async function CLIPage({
226214
<ProductCommands install={cli.install} launch={cli.launch} />
227215

228216
{/* Navigation */}
229-
<BackToNavigation href="clis" title={tNav('allCLIs')} />
217+
<BackToNavigation href="clis" title={t('allCLIs')} />
230218

231219
<Footer />
232220
</>

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

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,28 @@ type Props = {
1818
}
1919

2020
export default function CLIComparisonPageClient({ locale }: Props) {
21-
const tComparison = useTranslations('comparison')
22-
const tStacks = useTranslations('stacks')
23-
const tCommunity = useTranslations('community')
24-
const t = useTranslations()
21+
const t = useTranslations('pages.comparison')
22+
const tCommunity = useTranslations('shared.platforms')
23+
const tGlobal = useTranslations()
2524

2625
const columns: ComparisonColumn[] = [
2726
{
2827
key: 'vendor',
29-
label: tComparison('columns.vendor'),
28+
label: t('columns.vendor'),
3029
},
3130
{
3231
key: 'license',
33-
label: tComparison('columns.license'),
34-
render: (value: unknown, item: Record<string, unknown>) => renderLicense(value, item, t),
32+
label: t('columns.license'),
33+
render: (value: unknown, item: Record<string, unknown>) =>
34+
renderLicense(value, item, tGlobal),
3535
},
3636
{
3737
key: 'latestVersion',
38-
label: tComparison('columns.version'),
38+
label: t('columns.version'),
3939
},
4040
{
4141
key: 'platforms',
42-
label: tComparison('columns.platforms'),
42+
label: t('columns.platforms'),
4343
render: (value: unknown) => {
4444
const platforms = value as Array<{ os: string }> | string[]
4545
if (!platforms || platforms.length === 0) return '-'
@@ -72,7 +72,7 @@ export default function CLIComparisonPageClient({ locale }: Props) {
7272
},
7373
{
7474
key: 'githubStars',
75-
label: tComparison('columns.githubStars'),
75+
label: t('columns.githubStars'),
7676
render: (_: unknown, item: Record<string, unknown>) => {
7777
const id = item.id as string
7878
const stars = getGithubStars('clis', id)
@@ -106,7 +106,7 @@ export default function CLIComparisonPageClient({ locale }: Props) {
106106
},
107107
{
108108
key: 'links',
109-
label: tComparison('columns.links'),
109+
label: t('columns.links'),
110110
render: (_: unknown, item: Record<string, unknown>) => {
111111
const websiteUrl = item.websiteUrl as string | undefined
112112
const docsUrl = item.docsUrl as string | undefined
@@ -133,7 +133,7 @@ export default function CLIComparisonPageClient({ locale }: Props) {
133133
target="_blank"
134134
rel="noopener"
135135
className="text-[var(--color-text)] hover:text-[var(--color-text-secondary)] transition-colors"
136-
title={tComparison('linkTitles.officialWebsite')}
136+
title={t('linkTitles.officialWebsite')}
137137
>
138138
<Home className="w-3.5 h-3.5" />
139139
</a>
@@ -148,7 +148,7 @@ export default function CLIComparisonPageClient({ locale }: Props) {
148148
target="_blank"
149149
rel="noopener"
150150
className="text-[var(--color-text)] hover:text-[var(--color-text-secondary)] transition-colors"
151-
title={tComparison('linkTitles.download')}
151+
title={t('linkTitles.download')}
152152
>
153153
<Download className="w-3.5 h-3.5" />
154154
</a>
@@ -163,7 +163,7 @@ export default function CLIComparisonPageClient({ locale }: Props) {
163163
target="_blank"
164164
rel="noopener"
165165
className="text-[var(--color-text)] hover:text-[var(--color-text-secondary)] transition-colors"
166-
title={tComparison('linkTitles.documentation')}
166+
title={t('linkTitles.documentation')}
167167
>
168168
<FileText className="w-3.5 h-3.5" />
169169
</a>
@@ -238,7 +238,7 @@ export default function CLIComparisonPageClient({ locale }: Props) {
238238
},
239239
{
240240
key: 'pricing-free',
241-
label: tComparison('columns.freePlan'),
241+
label: t('columns.freePlan'),
242242
render: (_: unknown, item: Record<string, unknown>) => {
243243
const pricing = item.pricing as PricingTier[]
244244
if (!pricing || pricing.length === 0) return '-'
@@ -248,20 +248,20 @@ export default function CLIComparisonPageClient({ locale }: Props) {
248248
},
249249
{
250250
key: 'pricing-min',
251-
label: tComparison('columns.startingPrice'),
251+
label: t('columns.startingPrice'),
252252
render: (_: unknown, item: Record<string, unknown>) => {
253253
const pricing = item.pricing as PricingTier[]
254254
if (!pricing || pricing.length === 0) return '-'
255255
const paidPlans = pricing.filter(p => p.value && p.value > 0)
256-
if (paidPlans.length === 0) return tComparison('pricingValues.freeOnly')
256+
if (paidPlans.length === 0) return t('pricingValues.freeOnly')
257257
const minPrice = Math.min(...paidPlans.map(p => p.value as number))
258258
const minPlan = paidPlans.find(p => p.value === minPrice)
259259
return minPlan ? formatPrice(minPlan) : '-'
260260
},
261261
},
262262
{
263263
key: 'pricing-max',
264-
label: tComparison('columns.maxPrice'),
264+
label: t('columns.maxPrice'),
265265
render: (_: unknown, item: Record<string, unknown>) => {
266266
const pricing = item.pricing as PricingTier[]
267267
if (!pricing || pricing.length === 0) return '-'
@@ -280,20 +280,20 @@ export default function CLIComparisonPageClient({ locale }: Props) {
280280

281281
<Breadcrumb
282282
items={[
283-
{ name: tStacks('aiCodingStack'), href: '/ai-coding-stack' },
284-
{ name: tStacks('clis'), href: '/clis' },
285-
{ name: tStacks('comparison'), href: '/clis/comparison' },
283+
{ name: tGlobal('shared.common.aiCodingStack'), href: '/ai-coding-stack' },
284+
{ name: tGlobal('shared.stacks.clis'), href: '/clis' },
285+
{ name: tGlobal('shared.common.comparison'), href: '/clis/comparison' },
286286
]}
287287
/>
288288

289289
{/* Page Header */}
290290
<section className="py-[var(--spacing-lg)] border-[var(--color-border)]">
291291
<div className="max-w-8xl mx-auto px-[var(--spacing-md)]">
292292
<h1 className="text-3xl font-semibold tracking-[-0.03em] mb-[var(--spacing-sm)]">
293-
{tComparison('clis.title')}
293+
{t('clis.title')}
294294
</h1>
295295
<p className="text-base text-[var(--color-text-secondary)] font-light">
296-
{tComparison('clis.subtitle')}
296+
{t('clis.subtitle')}
297297
</p>
298298
</div>
299299
</section>
@@ -316,7 +316,7 @@ export default function CLIComparisonPageClient({ locale }: Props) {
316316
href={`/${locale}/clis`}
317317
className="inline-flex items-center gap-[var(--spacing-xs)] text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
318318
>
319-
{tComparison('clis.backTo')}
319+
{t('clis.backTo')}
320320
</Link>
321321
</div>
322322
</section>

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Locale } from '@/i18n/config'
12
import { generateComparisonMetadata } from '@/lib/metadata'
23
import CLIComparisonPageClient from './page.client'
34

@@ -9,7 +10,7 @@ export async function generateMetadata({ params }: Props) {
910
const { locale } = await params
1011

1112
return await generateComparisonMetadata({
12-
locale: locale as 'en' | 'zh-Hans',
13+
locale: locale as Locale,
1314
category: 'clis',
1415
})
1516
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type Props = {
1818
}
1919

2020
export default function CLIsPageClient({ locale }: Props) {
21-
const t = useTranslations('stacksPages.clis')
21+
const t = useTranslations('pages.clis')
2222
const tGlobal = useTranslations()
2323
const [sortOrder, setSortOrder] = useState<'default' | 'name-asc' | 'name-desc'>('default')
2424
const [licenseFilters, setLicenseFilters] = useState<string[]>([])

0 commit comments

Comments
 (0)