Skip to content

Commit 5fe2c39

Browse files
ericyangpanclaude
andcommitted
refactor(pages): replace entity templates with direct component composition
Refactor all entity detail pages to use PageLayout and compose components directly instead of relying on nested template hierarchy. This simplifies the architecture and makes page composition explicit. Pages updated: - ai-coding-landscape/page.tsx - articles/[slug]/page.tsx - clis/[slug]/page.tsx + clis/comparison/page.client.tsx - extensions/[slug]/page.tsx + extensions/comparison/page.client.tsx - ides/[slug]/page.tsx + ides/comparison/page.client.tsx - model-providers/[slug]/page.tsx - models/[slug]/page.tsx + models/comparison/page.client.tsx - vendors/[slug]/page.tsx Deleted components: - src/components/product/index.ts - src/components/vendor/VendorModels.tsx - src/components/vendor/VendorProducts.tsx 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 9cd350c commit 5fe2c39

File tree

15 files changed

+586
-298
lines changed

15 files changed

+586
-298
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getTranslations } from 'next-intl/server'
2-
import { BackToNavigation } from '@/components/controls/BackToNavigation'
32
import Footer from '@/components/Footer'
43
import Header from '@/components/Header'
4+
import { BackToNavigation } from '@/components/navigation/BackToNavigation'
55
import PageHeader from '@/components/PageHeader'
66
import { buildVendorMatrix } from '@/lib/landscape-data'
77
import { buildCanonicalUrl, buildOpenGraph, buildTitle, buildTwitterCard } from '@/lib/metadata'

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { notFound } from 'next/navigation'
22
import { getTranslations } from 'next-intl/server'
3-
import { Breadcrumb } from '@/components/controls/Breadcrumb'
43
import Footer from '@/components/Footer'
54
import Header from '@/components/Header'
5+
import { Breadcrumb } from '@/components/navigation/Breadcrumb'
66
import type { Locale } from '@/i18n/config'
77
import { Link } from '@/i18n/navigation'
88
import { getArticle } from '@/lib/data/fetchers'

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

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import { notFound } from 'next/navigation'
22
import { getTranslations } from 'next-intl/server'
3+
import { BackToNavigation } from '@/components/navigation/BackToNavigation'
4+
import { Breadcrumb } from '@/components/navigation/Breadcrumb'
5+
import { ProductCommands } from '@/components/product/ProductCommands'
6+
import { ProductHero } from '@/components/product/ProductHero'
7+
import { ProductLinks } from '@/components/product/ProductLinks'
8+
import { ProductPricing } from '@/components/product/ProductPricing'
9+
import { RelatedProducts } from '@/components/product/RelatedProducts'
310
import type { Locale } from '@/i18n/config'
4-
import { getCLI } from '@/lib/data/fetchers'
11+
import { PageLayout } from '@/layouts/PageLayout'
12+
import { getCLI, getRelatedProducts } from '@/lib/data/fetchers'
513
import { clisData as clis } from '@/lib/generated'
14+
import { getGithubStars } from '@/lib/generated/github-stars'
615
import { translateLicenseText } from '@/lib/license'
716
import { generateSoftwareDetailMetadata } from '@/lib/metadata'
8-
import { ProductDetailTemplate } from '@/templates'
17+
import { generateSoftwareDetailSchema } from '@/lib/metadata/schemas'
18+
import { transformCommunityUrls, transformResourceUrls } from '@/lib/product-utils'
919

1020
export const revalidate = 3600
1121

@@ -61,20 +71,60 @@ export default async function CLIPage({
6171
const t = await getTranslations({ locale, namespace: 'pages.cliDetail' })
6272
const tGlobal = await getTranslations({ locale })
6373

74+
// Transform URLs
75+
const websiteUrl = cli.websiteUrl || cli.resourceUrls?.download || undefined
76+
const docsUrl = cli.docsUrl || undefined
77+
const downloadUrl = cli.resourceUrls?.download || undefined
78+
79+
const resourceUrls = transformResourceUrls(cli.resourceUrls)
80+
const communityUrls = transformCommunityUrls(cli.communityUrls)
81+
82+
// Generate JSON-LD schema
83+
const schema = await generateSoftwareDetailSchema({
84+
product: {
85+
name: cli.name,
86+
description: cli.description,
87+
vendor: cli.vendor,
88+
websiteUrl,
89+
downloadUrl,
90+
version: cli.latestVersion,
91+
platforms: cli.platforms,
92+
pricing: cli.pricing,
93+
license: cli.license ? translateLicenseText(cli.license, tGlobal) : undefined,
94+
},
95+
category: 'clis',
96+
locale: locale as Locale,
97+
})
98+
99+
// Fetch related products
100+
const relatedProducts = await getRelatedProducts(cli.relatedProducts || [], locale as Locale)
101+
102+
// Breadcrumb items
103+
const breadcrumbItems = [
104+
{ name: tGlobal('shared.common.aiCodingStack'), href: '/ai-coding-stack' },
105+
{ name: tGlobal('shared.stacks.clis'), href: '/clis' },
106+
{ name: cli.name, href: `clis/${cli.id}` },
107+
]
108+
64109
return (
65-
<ProductDetailTemplate
66-
product={cli}
67-
productType="cli"
68-
locale={locale as Locale}
69-
category="clis"
70-
translations={{
71-
categoryLabel: t('categoryLabel'),
72-
allProductsLabel: t('allCLIs'),
73-
breadcrumbs: {
74-
home: tGlobal('shared.common.aiCodingStack'),
75-
category: tGlobal('shared.stacks.clis'),
76-
},
77-
productHero: {
110+
<PageLayout schema={schema}>
111+
<Breadcrumb items={breadcrumbItems} />
112+
113+
<ProductHero
114+
name={cli.name}
115+
description={cli.description}
116+
vendor={cli.vendor}
117+
category="CLI"
118+
categoryLabel={t('categoryLabel')}
119+
verified={cli.verified ?? false}
120+
latestVersion={cli.latestVersion}
121+
license={cli.license}
122+
githubStars={getGithubStars('clis', cli.id)}
123+
platforms={cli.platforms?.map(p => p.os)}
124+
websiteUrl={websiteUrl}
125+
docsUrl={docsUrl}
126+
downloadUrl={downloadUrl}
127+
labels={{
78128
vendor: t('vendor'),
79129
version: t('version'),
80130
license: t('license'),
@@ -83,8 +133,18 @@ export default async function CLIPage({
83133
visitWebsite: t('visitWebsite'),
84134
documentation: t('documentation'),
85135
download: t('download'),
86-
},
87-
}}
88-
/>
136+
}}
137+
/>
138+
139+
{relatedProducts.length > 0 && <RelatedProducts products={relatedProducts} />}
140+
141+
<ProductPricing pricing={cli.pricing} pricingUrl={resourceUrls.pricing} />
142+
143+
<ProductLinks resourceUrls={resourceUrls} communityUrls={communityUrls} />
144+
145+
<ProductCommands install={cli.installCommand} launch={cli.launchCommand} />
146+
147+
<BackToNavigation href="/clis" title={t('allCLIs')} />
148+
</PageLayout>
89149
)
90150
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import { Download, FileText, Github, Home, Linkedin, Twitter, Youtube } from 'lucide-react'
44
import { useTranslations } from 'next-intl'
55
import ComparisonTable, { type ComparisonColumn } from '@/components/ComparisonTable'
6-
import { Breadcrumb } from '@/components/controls/Breadcrumb'
7-
import { AppleIcon, LinuxIcon, WindowsIcon } from '@/components/controls/PlatformIcons'
86
import Footer from '@/components/Footer'
97
import Header from '@/components/Header'
8+
import { Breadcrumb } from '@/components/navigation/Breadcrumb'
9+
import { AppleIcon, LinuxIcon, WindowsIcon } from '@/components/PlatformIcons'
1010
import { Link } from '@/i18n/navigation'
1111
import { clisData as clis } from '@/lib/generated'
1212
import { getGithubStars } from '@/lib/generated/github-stars'

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

Lines changed: 94 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import { notFound } from 'next/navigation'
22
import { getTranslations } from 'next-intl/server'
3+
import { BackToNavigation } from '@/components/navigation/BackToNavigation'
4+
import { Breadcrumb } from '@/components/navigation/Breadcrumb'
5+
import { ProductCommands } from '@/components/product/ProductCommands'
6+
import { ProductHero } from '@/components/product/ProductHero'
7+
import { ProductLinks } from '@/components/product/ProductLinks'
8+
import { ProductPricing } from '@/components/product/ProductPricing'
9+
import { RelatedProducts } from '@/components/product/RelatedProducts'
310
import type { Locale } from '@/i18n/config'
4-
import { getExtension } from '@/lib/data/fetchers'
11+
import { PageLayout } from '@/layouts/PageLayout'
12+
import { getExtension, getRelatedProducts } from '@/lib/data/fetchers'
513
import { extensionsData as extensions } from '@/lib/generated'
14+
import { getGithubStars } from '@/lib/generated/github-stars'
615
import { translateLicenseText } from '@/lib/license'
716
import { generateSoftwareDetailMetadata } from '@/lib/metadata'
8-
import { ProductDetailTemplate } from '@/templates'
17+
import { generateSoftwareDetailSchema } from '@/lib/metadata/schemas'
18+
import { transformCommunityUrls, transformResourceUrls } from '@/lib/product-utils'
919

1020
export const revalidate = 3600
1121

@@ -66,30 +76,95 @@ export default async function ExtensionPage({
6676
const t = await getTranslations({ locale, namespace: 'pages.extensionDetail' })
6777
const tGlobal = await getTranslations({ locale })
6878

79+
// Transform URLs
80+
const websiteUrl = extension.websiteUrl || extension.resourceUrls?.download || undefined
81+
const docsUrl = extension.docsUrl || undefined
82+
const downloadUrl = extension.resourceUrls?.download || undefined
83+
84+
const resourceUrls = transformResourceUrls(extension.resourceUrls)
85+
const communityUrls = transformCommunityUrls(extension.communityUrls)
86+
87+
// Generate JSON-LD schema
88+
const schema = await generateSoftwareDetailSchema({
89+
product: {
90+
name: extension.name,
91+
description: extension.description,
92+
vendor: extension.vendor,
93+
websiteUrl,
94+
downloadUrl,
95+
version: extension.latestVersion,
96+
platforms: extension.supportedIdes?.map(ide => ({ os: ide.ideId })),
97+
pricing: extension.pricing,
98+
license: extension.license ? translateLicenseText(extension.license, tGlobal) : undefined,
99+
},
100+
category: 'extensions',
101+
locale: locale as Locale,
102+
applicationSubCategory: 'AI Assistant',
103+
compatibleWith: extension.supportedIdes?.map(ide => ide.ideId).join(', ') || undefined,
104+
})
105+
106+
// Fetch related products
107+
const relatedProducts = await getRelatedProducts(
108+
extension.relatedProducts || [],
109+
locale as Locale
110+
)
111+
112+
// Build additional info for ProductHero (supported IDEs)
113+
const additionalInfo =
114+
extension.supportedIdes && extension.supportedIdes.length > 0
115+
? [
116+
{
117+
label: t('supportedIdes') || 'Supported IDEs',
118+
value: extension.supportedIdes.map(ide => ide.ideId).join(', '),
119+
},
120+
]
121+
: undefined
122+
123+
// Breadcrumb items
124+
const breadcrumbItems = [
125+
{ name: tGlobal('shared.common.aiCodingStack'), href: '/ai-coding-stack' },
126+
{ name: tGlobal('shared.stacks.extensions'), href: '/extensions' },
127+
{ name: extension.name, href: `extensions/${extension.id}` },
128+
]
129+
69130
return (
70-
<ProductDetailTemplate
71-
product={extension}
72-
productType="extension"
73-
locale={locale as Locale}
74-
category="extensions"
75-
translations={{
76-
categoryLabel: t('categoryLabel'),
77-
allProductsLabel: t('allExtensions'),
78-
breadcrumbs: {
79-
home: tGlobal('shared.common.aiCodingStack'),
80-
category: tGlobal('shared.stacks.extensions'),
81-
},
82-
productHero: {
131+
<PageLayout schema={schema}>
132+
<Breadcrumb items={breadcrumbItems} />
133+
134+
<ProductHero
135+
name={extension.name}
136+
description={extension.description}
137+
vendor={extension.vendor}
138+
category="IDE"
139+
categoryLabel={t('categoryLabel')}
140+
verified={extension.verified ?? false}
141+
latestVersion={extension.latestVersion}
142+
license={extension.license}
143+
githubStars={getGithubStars('extensions', extension.id)}
144+
additionalInfo={additionalInfo}
145+
websiteUrl={websiteUrl}
146+
docsUrl={docsUrl}
147+
downloadUrl={downloadUrl}
148+
labels={{
83149
vendor: t('vendor'),
84150
version: t('version'),
85151
license: t('license'),
86152
stars: t('stars'),
87-
supportedIdes: t('supportedIdes'),
88153
visitWebsite: t('visitWebsite'),
89154
documentation: t('documentation'),
90155
download: t('download'),
91-
},
92-
}}
93-
/>
156+
}}
157+
/>
158+
159+
{relatedProducts.length > 0 && <RelatedProducts products={relatedProducts} />}
160+
161+
<ProductPricing pricing={extension.pricing} pricingUrl={resourceUrls.pricing} />
162+
163+
<ProductLinks resourceUrls={resourceUrls} communityUrls={communityUrls} />
164+
165+
<ProductCommands install={extension.installCommand} launch={extension.launchCommand} />
166+
167+
<BackToNavigation href="/extensions" title={t('allExtensions')} />
168+
</PageLayout>
94169
)
95170
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import { Download, FileText, Github, Home, Linkedin, Twitter, Youtube } from 'lucide-react'
44
import { useTranslations } from 'next-intl'
55
import ComparisonTable, { type ComparisonColumn } from '@/components/ComparisonTable'
6-
import { Breadcrumb } from '@/components/controls/Breadcrumb'
76
import Footer from '@/components/Footer'
87
import Header from '@/components/Header'
8+
import { Breadcrumb } from '@/components/navigation/Breadcrumb'
99
import { Link } from '@/i18n/navigation'
1010
import { extensionsData as extensions } from '@/lib/generated'
1111
import { getGithubStars } from '@/lib/generated/github-stars'

0 commit comments

Comments
 (0)