Skip to content

Commit de55dd1

Browse files
committed
fix(seo): align pages with organic search intent
1 parent cdc8990 commit de55dd1

30 files changed

Lines changed: 181 additions & 54 deletions

File tree

src/app/[locale]/models/compare/page.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import PageHeader from '@/components/PageHeader'
44
import type { Locale } from '@/i18n/config'
55
import { PageLayout } from '@/layouts/PageLayout'
66
import { modelsData as allModels } from '@/lib/generated'
7+
import { generateStaticPageMetadata } from '@/lib/metadata'
78
import type { ManifestModel } from '@/types/manifests'
89
import ComparisonPageClient from './[models]/page.client'
910

@@ -17,15 +18,15 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
1718
const { locale } = await params
1819
const t = await getTranslations({ locale, namespace: 'pages.comparison' })
1920

20-
return {
21-
title: t('models.title'),
21+
return generateStaticPageMetadata({
22+
locale: locale as Locale,
23+
basePath: 'models/compare',
24+
title: `${t('models.title')} ${new Date().getUTCFullYear()}`,
2225
description: t('models.subtitle'),
23-
openGraph: {
24-
title: t('models.title'),
25-
description: t('models.subtitle'),
26-
type: 'website',
27-
},
28-
}
26+
keywords:
27+
'AI coding model comparison, compare coding LLMs, LLM pricing comparison, context window comparison',
28+
ogType: 'website',
29+
})
2930
}
3031

3132
export default async function ComparePage({ params }: { params: Promise<{ locale: string }> }) {

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,29 @@ export default function ModelsPageClient({ locale }: Props) {
109109

110110
<StackTabs activeStack="models" locale={locale} />
111111

112+
<section className="mb-[var(--spacing-lg)] max-w-6xl border-y border-[var(--color-border)] py-[var(--spacing-md)]">
113+
<h2 className="mb-[var(--spacing-xs)] text-lg font-semibold tracking-tight">
114+
{tPage('guide.title')}
115+
</h2>
116+
<p className="max-w-4xl text-sm leading-relaxed text-[var(--color-text-secondary)]">
117+
{tPage('guide.description')}
118+
</p>
119+
<div className="mt-[var(--spacing-sm)] flex flex-wrap gap-[var(--spacing-md)] text-sm">
120+
<Link
121+
href={buildModelComparisonPath(selectedIds)}
122+
className="text-[var(--color-text-secondary)] underline-offset-4 hover:text-[var(--color-text)] hover:underline"
123+
>
124+
{tPage('guide.compareModels')}
125+
</Link>
126+
<Link
127+
href="/model-providers"
128+
className="text-[var(--color-text-secondary)] underline-offset-4 hover:text-[var(--color-text)] hover:underline"
129+
>
130+
{tPage('guide.exploreProviders')}
131+
</Link>
132+
</div>
133+
</section>
134+
112135
{/* Search Box */}
113136
<div className="mb-[var(--spacing-md)]">
114137
<input

src/app/[locale]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export async function generateMetadata({ params }: LocalePageProps) {
1616
const { locale } = await params
1717
const tPage = await getTranslations({ locale, namespace: 'pages.home.meta' })
1818

19-
const title = buildTitle({ title: tPage('title'), includeSiteName: false })
19+
const title = buildTitle({ title: tPage('title'), includeSiteName: true })
2020
const description = tPage('description')
2121

2222
return generateStaticPageMetadata({

src/lib/metadata/generators.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { formatPrimaryTokenRate } from '@/lib/model-pricing'
1111
import type { ManifestTokenPricing, ModelLifecycle } from '@/types/manifests'
1212
import {
1313
CATEGORY_DISPLAY_NAMES,
14-
CATEGORY_EXAMPLES,
1514
CATEGORY_SEO_KEYWORDS,
1615
type Category,
1716
type Locale,
@@ -210,17 +209,13 @@ export async function generateListPageMetadata(options: ListPageMetadataParams):
210209
const tPage = await getTranslations({ locale, namespace: translationNamespace })
211210

212211
const translatedTitle = tPage('title')
213-
const description = tPage('subtitle')
212+
const description = tPage.has('meta.description') ? tPage('meta.description') : tPage('subtitle')
214213

215214
// Build SEO-optimized title
216-
const categoryExamples = CATEGORY_EXAMPLES[category as keyof typeof CATEGORY_EXAMPLES]
217215
const categoryKeywords = CATEGORY_SEO_KEYWORDS[category as keyof typeof CATEGORY_SEO_KEYWORDS]
218216

219217
const title = buildListPageTitle({
220218
translatedTitle,
221-
categoryName:
222-
CATEGORY_DISPLAY_NAMES[category as keyof typeof CATEGORY_DISPLAY_NAMES] || translatedTitle,
223-
examples: categoryExamples ? [...categoryExamples] : [],
224219
year: METADATA_DEFAULTS.currentYear,
225220
})
226221

@@ -383,7 +378,7 @@ export async function generateComparisonMetadata(
383378
const tPage = await getTranslations({ locale, namespace: 'pages.comparison' })
384379

385380
// Build title and description using category-specific translations
386-
const title = `${tPage(`${category}.title`)} - ${categoryName} Comparison | ${METADATA_DEFAULTS.siteName}`
381+
const title = `${tPage(`${category}.title`)} ${METADATA_DEFAULTS.currentYear}`
387382
const description = tPage(`${category}.subtitle`)
388383

389384
// Build keywords
@@ -423,7 +418,7 @@ export async function generateArticleMetadata(options: ArticleMetadataParams): P
423418
const { locale, slug, article } = options
424419

425420
// Build title
426-
const title = `${article.title} | ${METADATA_DEFAULTS.siteName} Articles`
421+
const title = article.title
427422

428423
// Build description
429424
const description = article.description
@@ -458,7 +453,7 @@ export async function generateDocsMetadata(options: DocsMetadataParams): Promise
458453
const { locale, slug, doc } = options
459454

460455
// Build title
461-
const title = `${doc.title} | ${METADATA_DEFAULTS.siteName} Documentation`
456+
const title = `${doc.title} Documentation`
462457

463458
// Build description
464459
const description = doc.description

src/lib/metadata/helpers.ts

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function buildTitle(options: {
5353
const {
5454
title,
5555
suffix,
56-
includeSiteName = true,
56+
includeSiteName = false,
5757
separator = METADATA_DEFAULTS.titleSeparator,
5858
} = options
5959

@@ -71,30 +71,13 @@ export function buildTitle(options: {
7171
/**
7272
* Builds SEO-optimized list page title
7373
*/
74-
export function buildListPageTitle(options: {
75-
translatedTitle: string
76-
categoryName: string
77-
examples?: string[]
78-
year?: number
79-
}): string {
80-
const {
81-
translatedTitle,
82-
categoryName,
83-
examples = [],
84-
year = METADATA_DEFAULTS.currentYear,
85-
} = options
86-
87-
const parts = [translatedTitle, `-`, `Best ${categoryName} ${year}`]
88-
89-
if (examples.length > 0) {
90-
parts.push(METADATA_DEFAULTS.listSeparator)
91-
parts.push(examples.join(', '))
92-
}
93-
94-
parts.push(METADATA_DEFAULTS.titleSeparator)
95-
parts.push(METADATA_DEFAULTS.siteName)
74+
export function buildListPageTitle(options: { translatedTitle: string; year?: number }): string {
75+
const { translatedTitle, year = METADATA_DEFAULTS.currentYear } = options
9676

97-
return parts.join(' ').replace(/\s+/g, ' ') // Clean up multiple spaces
77+
// The locale layout appends the site name through its title template.
78+
// Keep the page-specific title concise so search results do not repeat the
79+
// brand or truncate the primary query.
80+
return `${translatedTitle} ${year}`
9881
}
9982

10083
/**

tests/metadata.generators.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { describe, expect, it } from 'vitest'
22
import {
3+
buildListPageTitle,
4+
buildTitle,
35
generateArticleMetadata,
46
generateComparisonMetadata,
57
generateDocsMetadata,
@@ -9,6 +11,21 @@ import {
911
generateStaticPageMetadata,
1012
} from '@/lib/metadata'
1113

14+
describe('Metadata title helpers', () => {
15+
it('leaves the site name to the root layout title template by default', () => {
16+
expect(buildTitle({ title: 'AI Coding Models' })).toBe('AI Coding Models')
17+
})
18+
19+
it('builds a concise localized list title without examples or a repeated brand', () => {
20+
expect(
21+
buildListPageTitle({
22+
translatedTitle: 'AI Coding Models',
23+
year: 2026,
24+
})
25+
).toBe('AI Coding Models 2026')
26+
})
27+
})
28+
1229
/**
1330
* Test suite to verify metadata generators produce correct structure
1431
* after refactoring to use common buildMetadataWithSocial pipeline

translations/de/pages/home.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
},
2323
"meta": {
2424
"description": "Umfassendes Verzeichnis für AI-Coding-Tools. Entdecken, vergleichen und erkunden Sie IDEs, CLIs, Erweiterungen, Modelle und Anbieter.",
25-
"title": "AI Coding Stack - Ihr AI-Coding-Ökosystem-Hub"
25+
"title": "Verzeichnis und Vergleich von AI-Coding-Tools"
2626
},
2727
"readDocs": "Dokumentation Lesen",
2828
"subtitle": "Entdecken, Vergleichen, Schneller Entwickeln.",

translations/de/pages/models.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
{
22
"context": "Kontext:",
3+
"guide": {
4+
"compareModels": "Modelle direkt vergleichen",
5+
"description": "Vergleichen Sie Coding-Modelle nach Kontextfenster, Tokenpreisen, Lebenszyklus und Anbieter. Nutzen Sie den direkten Vergleich für detaillierte Spezifikationen und prüfen Sie anschließend die API-Anbieter auf Modellverfügbarkeit.",
6+
"exploreProviders": "Modellanbieter entdecken",
7+
"title": "So vergleichen Sie AI-Coding-Modelle"
8+
},
39
"lifecycle": {
410
"noResults": "Keine {lifecycle} Modelle entsprechen Ihrer Suche"
511
},
12+
"meta": {
13+
"description": "Vergleichen Sie AI-Coding-Modelle nach Kontextfenster, Tokenpreisen, Lebenszyklus und Anbieter. Entdecken Sie aktuelle Coding-LLMs führender Modellentwickler."
14+
},
615
"pricing": "Preise:",
716
"size": "Größe:",
817
"subtitle": "Große Sprachmodelle optimiert für Code-Generierung und Analyse",

translations/en/pages/home.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
},
2323
"meta": {
2424
"description": "Comprehensive directory for AI coding tools. Discover, compare and explore IDEs, CLIs, extensions, models and providers.",
25-
"title": "AI Coding Stack - Your AI Coding Ecosystem Hub"
25+
"title": "AI Coding Tools Directory & Comparisons"
2626
},
2727
"readDocs": "Read Documentation",
2828
"subtitle": "Discover, Compare, Build Faster.",

translations/en/pages/models.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
{
22
"context": "Context:",
3+
"guide": {
4+
"compareModels": "Compare models side by side",
5+
"description": "Compare coding models by context window, token pricing, lifecycle, and provider. Use the side-by-side comparison for detailed specifications, then review API providers for model availability.",
6+
"exploreProviders": "Explore model providers",
7+
"title": "How to compare AI coding models"
8+
},
39
"lifecycle": {
410
"noResults": "No {lifecycle} models match your search"
511
},
12+
"meta": {
13+
"description": "Compare AI coding models by context window, token pricing, lifecycle, and provider. Explore current coding LLMs from leading model developers."
14+
},
615
"pricing": "Pricing:",
716
"size": "Size:",
817
"subtitle": "Large language models optimized for code generation and analysis",

0 commit comments

Comments
 (0)