Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@
"ignorePaths": ["cloudflare-env.d.ts", "docs", ".claude"],
"ignoreRegExpList": ["/\\d+-g[0-9a-f]{6,}/g"],
"words": [
"Kesiapan",
"Мультимодельная",
"мультимодельную",
"мультимодельные",
"мультимодельных",
"продакшену",
"confiabilidade",
"gözlemlenebilirliğini",
"modelnya",
"observabilitas",
"observabilidad",
"observabilidade",
"Autorisierungsaufträgen",
"Entwicklungsframework",
"İçgörüleri",
Expand Down
44 changes: 22 additions & 22 deletions data/data-health.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,59 +73,59 @@
},
"translationsByLocale": {
"de": {
"totalStrings": 453,
"totalStrings": 467,
"matchingEnglish": 39,
"matchingEnglishPercent": 8.6
"matchingEnglishPercent": 8.4
},
"es": {
"totalStrings": 453,
"totalStrings": 467,
"matchingEnglish": 25,
"matchingEnglishPercent": 5.5
"matchingEnglishPercent": 5.4
},
"fr": {
"totalStrings": 453,
"totalStrings": 467,
"matchingEnglish": 36,
"matchingEnglishPercent": 7.9
"matchingEnglishPercent": 7.7
},
"id": {
"totalStrings": 453,
"totalStrings": 467,
"matchingEnglish": 30,
"matchingEnglishPercent": 6.6
"matchingEnglishPercent": 6.4
},
"ja": {
"totalStrings": 453,
"totalStrings": 467,
"matchingEnglish": 23,
"matchingEnglishPercent": 5.1
"matchingEnglishPercent": 4.9
},
"ko": {
"totalStrings": 453,
"totalStrings": 467,
"matchingEnglish": 23,
"matchingEnglishPercent": 5.1
"matchingEnglishPercent": 4.9
},
"pt": {
"totalStrings": 453,
"totalStrings": 467,
"matchingEnglish": 31,
"matchingEnglishPercent": 6.8
"matchingEnglishPercent": 6.6
},
"ru": {
"totalStrings": 453,
"totalStrings": 467,
"matchingEnglish": 23,
"matchingEnglishPercent": 5.1
"matchingEnglishPercent": 4.9
},
"tr": {
"totalStrings": 453,
"totalStrings": 467,
"matchingEnglish": 26,
"matchingEnglishPercent": 5.7
"matchingEnglishPercent": 5.6
},
"zh-Hans": {
"totalStrings": 453,
"totalStrings": 467,
"matchingEnglish": 22,
"matchingEnglishPercent": 4.9
"matchingEnglishPercent": 4.7
},
"zh-Hant": {
"totalStrings": 453,
"totalStrings": 467,
"matchingEnglish": 22,
"matchingEnglishPercent": 4.9
"matchingEnglishPercent": 4.7
}
},
"issues": []
Expand Down
22 changes: 11 additions & 11 deletions docs/DATA-HEALTH.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,17 @@ Exact English matches are a triage signal; product names and technical terms can

| Locale | Comparable strings | Exact English matches | Match rate |
| --- | ---: | ---: | ---: |
| de | 453 | 39 | 8.6% |
| es | 453 | 25 | 5.5% |
| fr | 453 | 36 | 7.9% |
| id | 453 | 30 | 6.6% |
| ja | 453 | 23 | 5.1% |
| ko | 453 | 23 | 5.1% |
| pt | 453 | 31 | 6.8% |
| ru | 453 | 23 | 5.1% |
| tr | 453 | 26 | 5.7% |
| zh-Hans | 453 | 22 | 4.9% |
| zh-Hant | 453 | 22 | 4.9% |
| de | 467 | 39 | 8.4% |
| es | 467 | 25 | 5.4% |
| fr | 467 | 36 | 7.7% |
| id | 467 | 30 | 6.4% |
| ja | 467 | 23 | 4.9% |
| ko | 467 | 23 | 4.9% |
| pt | 467 | 31 | 6.6% |
| ru | 467 | 23 | 4.9% |
| tr | 467 | 26 | 5.6% |
| zh-Hans | 467 | 22 | 4.7% |
| zh-Hant | 467 | 22 | 4.7% |

## Backlog by Issue Type

Expand Down
5 changes: 4 additions & 1 deletion docs/SEO-SEARCH-OPPORTUNITY-MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ AI Coding Stack has stronger structured catalog data than many editorial competi
| P0 | Optimize `/models` for comparison intent | It is the strongest striking-distance page | Implemented in working tree |
| P0 | Complete `/models/compare` metadata | The comparison route lacked canonical, hreflang, robots, and complete social metadata | Implemented in working tree |
| P0 | Reposition homepage metadata around “AI coding tools directory and comparisons” | Aligns the broad landing page with non-brand discovery intent | Implemented in working tree |
| P1 | Expand `/model-providers` selection guidance | Average position 14.3 indicates near-term potential | Planned |
| P0 | Add the lowercase domain as a fallback WebSite name | Google indexed pages but exact brand and domain discovery remained weak; the WebSite schema lacked an `alternateName` fallback | Implemented in working tree |
| P0 | Correct `/model-providers` canonical and hreflang paths | Production pointed canonical and alternates to the non-public `/modelProviders` path while the sitemap used `/model-providers` | Implemented in working tree |
| P0 | Remove static meta-keyword lists and product examples | Google Search does not use the keywords meta tag for indexing or ranking; manually curated product names become stale and distract from visible, localized search-intent content | Implemented in working tree |
| P1 | Expand `/model-providers` selection guidance | Average position 14.3 indicates near-term potential | Implemented in working tree |
| P1 | Expand `/clis/codex-cli` with a direct answer and setup coverage | Multiple queries rank between positions 14.9 and 16.0 | Planned |
| P1 | Expand `/ides/vscode` for “VS Code AI” intent | Highest observed product-query impressions | Planned |
| P1 | Expand `/ides/kiro` for AWS/Kiro entity coverage | Multiple related queries already receive impressions | Planned |
Expand Down
2 changes: 0 additions & 2 deletions src/app/[locale]/ai-coding-landscape/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
basePath: 'ai-coding-landscape',
title,
description,
keywords:
'AI coding ecosystem, AI development landscape, AI tools, coding tools visualization, vendor comparison, product matrix',
ogType: 'website',
})
}
Expand Down
1 change: 0 additions & 1 deletion src/app/[locale]/articles/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
basePath: 'articles',
title,
description,
keywords: tPage('keywords'),
ogType: 'website',
})
}
Expand Down
1 change: 0 additions & 1 deletion src/app/[locale]/clis/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
locale: locale as Locale,
category: 'clis',
translationNamespace: 'pages.clis',
additionalKeywords: ['Gemini CLI', 'GitHub Copilot CLI', 'command line AI tools'],
})
}

Expand Down
2 changes: 0 additions & 2 deletions src/app/[locale]/curated-collections/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ export async function generateMetadata({ params }: LocalePageProps) {
basePath: 'curated-collections',
title,
description,
keywords:
'AI coding resources, MCP protocol, Agent2Agent, development standards, AI coding articles, semantic versioning, conventional commits',
ogType: 'website',
})
}
Expand Down
1 change: 0 additions & 1 deletion src/app/[locale]/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
basePath: 'docs',
title,
description,
keywords: tPage('keywords'),
ogType: 'website',
})
}
Expand Down
1 change: 0 additions & 1 deletion src/app/[locale]/extensions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
locale: locale as Locale,
category: 'extensions',
translationNamespace: 'pages.extensions',
additionalKeywords: ['AI code completion', 'VS Code extensions', 'JetBrains AI'],
})
}

Expand Down
10 changes: 0 additions & 10 deletions src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,6 @@ export async function generateMetadata({
locale: locale as Locale,
title,
description,
keywords: [
'AI coding tools',
'AI code editor',
'AI IDE',
'AI coding assistant',
'AI CLI',
'LLM models',
'AI coding directory',
'coding tools comparison',
].join(', '),
canonical: canonicalPath,
languageAlternates: buildLanguageAlternates(''),
openGraph: {
Expand Down
1 change: 0 additions & 1 deletion src/app/[locale]/manifesto/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export async function generateMetadata({ params }: LocalePageProps) {
basePath: 'manifesto',
title: buildTitle({ title: tShared('terms.manifesto') }),
description: tPage('subtitle'),
keywords: 'AI Coding Manifesto, AI development philosophy, AI coding principles',
ogType: 'website',
})
}
Expand Down
1 change: 0 additions & 1 deletion src/app/[locale]/model-intelligence-index/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
basePath: 'model-intelligence-index',
title: buildTitle({ title: tPage('title') }),
description: tPage('description'),
keywords: tPage('keywords'),
ogType: 'website',
})
}
Expand Down
1 change: 0 additions & 1 deletion src/app/[locale]/model-price-intelligence-index/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
basePath: 'model-price-intelligence-index',
title: buildTitle({ title: tPage('title') }),
description: tPage('description'),
keywords: tPage('keywords'),
ogType: 'website',
})
}
Expand Down
97 changes: 97 additions & 0 deletions src/app/[locale]/model-providers/page.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,40 @@ export default function ModelProvidersPageClient({ locale }: Props) {
p => p.type === 'foundation-model-provider'
)
const modelServiceProviders = filteredProviders.filter(p => p.type === 'model-service-provider')
const selectionCriteria = [
{
id: 'model-access',
title: tPage('guide.criteria.modelAccess.title'),
description: tPage('guide.criteria.modelAccess.description'),
},
{
id: 'pricing-limits',
title: tPage('guide.criteria.pricingLimits.title'),
description: tPage('guide.criteria.pricingLimits.description'),
},
{
id: 'production-fit',
title: tPage('guide.criteria.productionFit.title'),
description: tPage('guide.criteria.productionFit.description'),
},
]
const faqItems = [
{
id: 'provider-types',
question: tPage('faq.providerTypes.question'),
answer: tPage('faq.providerTypes.answer'),
},
{
id: 'selection',
question: tPage('faq.selection.question'),
answer: tPage('faq.selection.answer'),
},
{
id: 'first-party',
question: tPage('faq.firstParty.question'),
answer: tPage('faq.firstParty.answer'),
},
]

return (
<>
Expand All @@ -74,6 +108,47 @@ export default function ModelProvidersPageClient({ locale }: Props) {

<StackTabs activeStack="model-providers" locale={locale} />

<section
aria-labelledby="provider-selection-guide"
className="mb-[var(--spacing-lg)] max-w-6xl border-y border-[var(--color-border)] py-[var(--spacing-md)]"
>
<h2
id="provider-selection-guide"
className="mb-[var(--spacing-xs)] text-lg font-semibold tracking-tight"
>
{tPage('guide.title')}
</h2>
<p className="max-w-4xl text-sm leading-relaxed text-[var(--color-text-secondary)]">
{tPage('guide.description')}
</p>
<div className="mt-[var(--spacing-md)] grid grid-cols-1 gap-[var(--spacing-md)] md:grid-cols-3">
{selectionCriteria.map(criterion => (
<article key={criterion.id}>
<h3 className="mb-[var(--spacing-xs)] text-sm font-semibold">
{criterion.title}
</h3>
<p className="text-sm leading-relaxed text-[var(--color-text-secondary)]">
{criterion.description}
</p>
</article>
))}
</div>
<div className="mt-[var(--spacing-md)] flex flex-wrap gap-[var(--spacing-md)] text-sm">
<Link
href="/models"
className="text-[var(--color-text-secondary)] underline-offset-4 hover:text-[var(--color-text)] hover:underline"
>
{tPage('guide.browseModels')}
</Link>
<Link
href="/models/compare"
className="text-[var(--color-text-secondary)] underline-offset-4 hover:text-[var(--color-text)] hover:underline"
>
{tPage('guide.compareModels')}
</Link>
</div>
</section>

{/* Search Box */}
<div className="mb-[var(--spacing-md)]">
<input
Expand Down Expand Up @@ -134,6 +209,28 @@ export default function ModelProvidersPageClient({ locale }: Props) {
))}
</div>
</section>

<section
aria-labelledby="provider-faq"
className="mt-[var(--spacing-xl)] max-w-6xl border-t border-[var(--color-border)] pt-[var(--spacing-lg)]"
>
<h2 id="provider-faq" className="mb-[var(--spacing-sm)] text-lg font-semibold">
{tPage('faq.title')}
</h2>
<div className="divide-y divide-[var(--color-border)]">
{faqItems.map(item => (
<article
key={item.id}
className="grid gap-[var(--spacing-sm)] py-[var(--spacing-md)] md:grid-cols-3"
>
<h3 className="text-sm font-semibold leading-relaxed">{item.question}</h3>
<p className="text-sm leading-relaxed text-[var(--color-text-secondary)] md:col-span-2">
{item.answer}
</p>
</article>
))}
</div>
</section>
</main>
</div>

Expand Down
27 changes: 25 additions & 2 deletions src/app/[locale]/model-providers/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { getTranslations } from 'next-intl/server'
import { JsonLd } from '@/components/JsonLd'
import type { Locale } from '@/i18n/config'
import { generateListPageMetadata } from '@/lib/metadata'
import { generateFAQPageSchema } from '@/lib/metadata/schemas'
import type { LocalePageProps } from '@/types/locale'
import ModelProvidersPageClient from './page.client'

Expand All @@ -12,11 +15,31 @@ export async function generateMetadata({ params }: LocalePageProps) {
locale: locale as Locale,
category: 'modelProviders',
translationNamespace: 'pages.modelProviders',
additionalKeywords: ['OpenAI', 'Anthropic', 'model API', 'AI provider comparison'],
})
}

export default async function ModelProvidersPage({ params }: LocalePageProps) {
const { locale } = await params
return <ModelProvidersPageClient locale={locale} />
const tPage = await getTranslations({ locale, namespace: 'pages.modelProviders' })
const faqSchema = await generateFAQPageSchema([
{
question: tPage('faq.providerTypes.question'),
answer: tPage('faq.providerTypes.answer'),
},
{
question: tPage('faq.selection.question'),
answer: tPage('faq.selection.answer'),
},
{
question: tPage('faq.firstParty.question'),
answer: tPage('faq.firstParty.answer'),
},
])

return (
<>
<JsonLd data={faqSchema} />
<ModelProvidersPageClient locale={locale} />
</>
)
}
Loading
Loading