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
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": 411,
"totalStrings": 416,
"matchingEnglish": 113,
"matchingEnglishPercent": 27.5
"matchingEnglishPercent": 27.2
},
"es": {
"totalStrings": 411,
"totalStrings": 416,
"matchingEnglish": 104,
"matchingEnglishPercent": 25.3
"matchingEnglishPercent": 25
},
"fr": {
"totalStrings": 411,
"totalStrings": 416,
"matchingEnglish": 115,
"matchingEnglishPercent": 28
"matchingEnglishPercent": 27.6
},
"id": {
"totalStrings": 411,
"totalStrings": 416,
"matchingEnglish": 149,
"matchingEnglishPercent": 36.3
"matchingEnglishPercent": 35.8
},
"ja": {
"totalStrings": 411,
"totalStrings": 416,
"matchingEnglish": 107,
"matchingEnglishPercent": 26
"matchingEnglishPercent": 25.7
},
"ko": {
"totalStrings": 411,
"totalStrings": 416,
"matchingEnglish": 115,
"matchingEnglishPercent": 28
"matchingEnglishPercent": 27.6
},
"pt": {
"totalStrings": 411,
"totalStrings": 416,
"matchingEnglish": 114,
"matchingEnglishPercent": 27.7
"matchingEnglishPercent": 27.4
},
"ru": {
"totalStrings": 411,
"totalStrings": 416,
"matchingEnglish": 124,
"matchingEnglishPercent": 30.2
"matchingEnglishPercent": 29.8
},
"tr": {
"totalStrings": 411,
"totalStrings": 416,
"matchingEnglish": 125,
"matchingEnglishPercent": 30.4
"matchingEnglishPercent": 30
},
"zh-Hans": {
"totalStrings": 411,
"totalStrings": 416,
"matchingEnglish": 98,
"matchingEnglishPercent": 23.8
"matchingEnglishPercent": 23.6
},
"zh-Hant": {
"totalStrings": 411,
"totalStrings": 416,
"matchingEnglish": 98,
"matchingEnglishPercent": 23.8
"matchingEnglishPercent": 23.6
}
},
"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 | 411 | 113 | 27.5% |
| es | 411 | 104 | 25.3% |
| fr | 411 | 115 | 28% |
| id | 411 | 149 | 36.3% |
| ja | 411 | 107 | 26% |
| ko | 411 | 115 | 28% |
| pt | 411 | 114 | 27.7% |
| ru | 411 | 124 | 30.2% |
| tr | 411 | 125 | 30.4% |
| zh-Hans | 411 | 98 | 23.8% |
| zh-Hant | 411 | 98 | 23.8% |
| de | 416 | 113 | 27.2% |
| es | 416 | 104 | 25% |
| fr | 416 | 115 | 27.6% |
| id | 416 | 149 | 35.8% |
| ja | 416 | 107 | 25.7% |
| ko | 416 | 115 | 27.6% |
| pt | 416 | 114 | 27.4% |
| ru | 416 | 124 | 29.8% |
| tr | 416 | 125 | 30% |
| zh-Hans | 416 | 98 | 23.6% |
| zh-Hant | 416 | 98 | 23.6% |

## Backlog by Issue Type

Expand Down
101 changes: 101 additions & 0 deletions docs/SEO-SEARCH-OPPORTUNITY-MAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Organic Search Opportunity Map

**Measurement window:** April 27–July 26, 2026

**Search market:** Google, English-led with localized landing pages

**Primary goal:** Increase non-brand impressions, top-10 rankings, and organic click-through rate

## Search Performance Baseline

Google Search Console reported:

| Metric | Value |
|---|---:|
| Clicks | 2 |
| Impressions | 601 |
| Click-through rate | 0.3% |
| Average position | 39.2 |

The strongest existing landing pages were:

| Landing page | Clicks | Impressions | Average position | Opportunity |
|---|---:|---:|---:|---|
| `/models` | 1 | 67 | 13.5 | Highest-priority striking-distance page |
| `/model-providers` | 0 | 16 | 14.3 | Striking-distance category page |
| `/` | 1 | 46 | 36.3 | Broad discovery page |
| `/zh-Hant/ides/vscode` | 0 | 108 | 32.6 | High-impression product page |
| `/ides/kiro` | 0 | 77 | 59.8 | Brand/product content opportunity |
| `/zh-Hant/vendors/xai` | 0 | 29 | 33.8 | Brand/vendor content opportunity |

## Keyword-to-Landing-Page Map

Search volume is marked `N/A` because no reliable keyword-volume provider was available. Search Console impressions are first-party observations, not monthly search volume.

| Search theme | Observed queries | Intent | Measured signal | Target landing page | Action |
|---|---|---|---|---|---|
| AI coding models | `AI coding models`, `LLM for coding`, model names | Commercial investigation | `/models`: 67 impressions, position 13.5 | `/models` | Improve title, description, comparison guidance, and links to model comparison/providers |
| Model API providers | `model provider`, `LLM API provider` | Commercial investigation | `/model-providers`: 16 impressions, position 14.3 | `/model-providers` | Expand comparison criteria and provider-selection guidance |
| VS Code with AI | `vscode ai` (69 impressions), `visual code ai ide` (12) | Product discovery | Query positions 24.7 and 19.4 | `/ides/vscode` and localized equivalents | Add AI feature coverage and clarify the relationship between VS Code and AI extensions |
| Codex CLI | `codex cli` (8 impressions), `codex-cli`, `does codex have a cli` | Navigational and informational | Positions 14.9–16.0 | `/clis/codex-cli` | Add a direct answer, setup overview, supported workflows, and official-source references |
| Kiro IDE | `aws kiro` (18), `kiro ai` (17), `kiro ide` (13), `kiro aws` (10) | Product discovery | Positions 53.9–68.7 | `/ides/kiro` | Expand AWS/Kiro entity context, use cases, pricing, and comparison links |
| AI coding tools | `aicoding`, `ai coding stack`, `coding stack` | Broad discovery | Homepage: 46 impressions, position 36.3 | `/` | Target directory and comparison intent in the title and description |
| AI coding stack | `ai coding stack`, `coding stack` | Category education | Query positions 40.0–53.7 | `/ai-coding-stack` | Keep as the ecosystem/category architecture page to avoid homepage cannibalization |
| OpenCode | `open code`, `opencode` | Navigational | Positions 9.0–10.0 | Relevant OpenCode detail page | Preserve exact entity naming and strengthen internal links |

## SERP and Content-Gap Findings

For broad commercial searches, current results are dominated by dated, comparison-led pages with a quick answer, consistent evaluation criteria, pricing, best-fit guidance, trade-offs, and a comparison table. Representative results include:

- [Every AI Coding Tool Compared: The 2026 Matrix](https://www.developersdigest.tech/blog/ai-coding-tools-comparison-matrix-2026)
- [The Best AI Coding Assistants in 2026, Compared](https://daily.dev/blog/best-ai-coding-assistants-comparison/)
- [The 9 best AI coding tools in 2026](https://zapier.com/blog/ai-coding-tools/)
- [Best LLM for Coding in 2026: Ranked by Benchmarks](https://www.tembo.io/blog/best-llm-for-coding)

AI Coding Stack has stronger structured catalog data than many editorial competitors, but its category pages provide less decision guidance. The defensible gap is a source-backed comparison layer built on the existing manifests:

1. Define stable comparison criteria for each category.
2. Explain which fields matter for different workflows.
3. Link category guidance to the existing comparison tools and verified detail pages.
4. Publish recommendation content only when the supporting data and methodology are explicit.

## Prioritized Work

| Priority | Work item | Rationale | Status |
|---|---|---|---|
| P0 | Remove repeated brand names and excessive examples from page titles | Existing list titles exceed normal snippet length and repeat `AI Coding Stack` | Implemented in working tree |
| 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 |
| 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 |
| P2 | Create a source-backed cross-category comparison guide | Broad SERPs reward tested comparison and best-fit guidance | Planned |

## On-Page Audit: `/models`

The baseline score applies the repository's weighted on-page rubric to the production page observed during the measurement period. The working-tree score reflects the implemented metadata and guidance changes; production scoring remains unchanged until deployment.

| Section | Weight | Baseline | Working tree | Finding |
|---|---:|---:|---:|---|
| Title tag | 15 | 4 | 14 | Baseline title was long, stale, and repeated the brand; working title is concise and localized |
| Meta description | 5 | 2 | 5 | Baseline description was too short; working description explains comparison criteria |
| Header structure | 10 | 8 | 9 | One H1 and lifecycle H2s; working tree adds a descriptive comparison H2 |
| Content quality | 25 | 12 | 17 | Catalog data is useful; working tree adds decision guidance but is not yet a full editorial guide |
| Keyword optimization | 15 | 9 | 13 | Working copy covers coding models, context, pricing, lifecycle, providers, and comparison |
| Internal/external links | 10 | 8 | 9 | Working tree adds contextual links to comparison and provider pages |
| Image optimization | 10 | 10 | 10 | The catalog does not depend on content images; route-level social imagery is separate |
| Page-level technical | 10 | 9 | 10 | Canonical and locale alternates remain intact; the comparison destination now receives complete metadata |
| **Overall** | **100** | **62 (C)** | **87 (A)** | Production changes after the GitHub Actions deployment |

## Measurement Plan

After deployment, compare 28-day periods in Search Console:

- `/models`: impressions, CTR, and average position
- `/models/compare`: indexed state and query discovery
- Homepage: non-brand impressions for AI coding tool terms
- `/clis/codex-cli`, `/ides/vscode`, and `/ides/kiro`: query-level position and CTR

The first decision threshold is movement into positions 4–10 for `/models`. The next content batch should use the first 28 days of post-deployment data rather than assumed keyword volume.
17 changes: 9 additions & 8 deletions src/app/[locale]/models/compare/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PageHeader from '@/components/PageHeader'
import type { Locale } from '@/i18n/config'
import { PageLayout } from '@/layouts/PageLayout'
import { modelsData as allModels } from '@/lib/generated'
import { generateStaticPageMetadata } from '@/lib/metadata'
import type { ManifestModel } from '@/types/manifests'
import ComparisonPageClient from './[models]/page.client'

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

return {
title: t('models.title'),
return generateStaticPageMetadata({
locale: locale as Locale,
basePath: 'models/compare',
title: `${t('models.title')} ${new Date().getUTCFullYear()}`,
description: t('models.subtitle'),
openGraph: {
title: t('models.title'),
description: t('models.subtitle'),
type: 'website',
},
}
keywords:
'AI coding model comparison, compare coding LLMs, LLM pricing comparison, context window comparison',
ogType: 'website',
})
}

export default async function ComparePage({ params }: { params: Promise<{ locale: string }> }) {
Expand Down
23 changes: 23 additions & 0 deletions src/app/[locale]/models/page.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,29 @@ export default function ModelsPageClient({ locale }: Props) {

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

<section className="mb-[var(--spacing-lg)] max-w-6xl border-y border-[var(--color-border)] py-[var(--spacing-md)]">
<h2 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-sm)] flex flex-wrap gap-[var(--spacing-md)] text-sm">
<Link
href={buildModelComparisonPath(selectedIds)}
className="text-[var(--color-text-secondary)] underline-offset-4 hover:text-[var(--color-text)] hover:underline"
>
{tPage('guide.compareModels')} →
</Link>
<Link
href="/model-providers"
className="text-[var(--color-text-secondary)] underline-offset-4 hover:text-[var(--color-text)] hover:underline"
>
{tPage('guide.exploreProviders')} →
</Link>
</div>
</section>

{/* Search Box */}
<div className="mb-[var(--spacing-md)]">
<input
Expand Down
2 changes: 1 addition & 1 deletion src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export async function generateMetadata({ params }: LocalePageProps) {
const { locale } = await params
const tPage = await getTranslations({ locale, namespace: 'pages.home.meta' })

const title = buildTitle({ title: tPage('title'), includeSiteName: false })
const title = buildTitle({ title: tPage('title'), includeSiteName: true })
const description = tPage('description')

return generateStaticPageMetadata({
Expand Down
13 changes: 4 additions & 9 deletions src/lib/metadata/generators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { formatPrimaryTokenRate } from '@/lib/model-pricing'
import type { ManifestTokenPricing, ModelLifecycle } from '@/types/manifests'
import {
CATEGORY_DISPLAY_NAMES,
CATEGORY_EXAMPLES,
CATEGORY_SEO_KEYWORDS,
type Category,
type Locale,
Expand Down Expand Up @@ -210,17 +209,13 @@ export async function generateListPageMetadata(options: ListPageMetadataParams):
const tPage = await getTranslations({ locale, namespace: translationNamespace })

const translatedTitle = tPage('title')
const description = tPage('subtitle')
const description = tPage.has('meta.description') ? tPage('meta.description') : tPage('subtitle')

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

const title = buildListPageTitle({
translatedTitle,
categoryName:
CATEGORY_DISPLAY_NAMES[category as keyof typeof CATEGORY_DISPLAY_NAMES] || translatedTitle,
examples: categoryExamples ? [...categoryExamples] : [],
year: METADATA_DEFAULTS.currentYear,
})

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

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

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

// Build title
const title = `${article.title} | ${METADATA_DEFAULTS.siteName} Articles`
const title = article.title

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

// Build title
const title = `${doc.title} | ${METADATA_DEFAULTS.siteName} Documentation`
const title = `${doc.title} Documentation`

// Build description
const description = doc.description
Expand Down
31 changes: 7 additions & 24 deletions src/lib/metadata/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function buildTitle(options: {
const {
title,
suffix,
includeSiteName = true,
includeSiteName = false,
separator = METADATA_DEFAULTS.titleSeparator,
} = options

Expand All @@ -71,30 +71,13 @@ export function buildTitle(options: {
/**
* Builds SEO-optimized list page title
*/
export function buildListPageTitle(options: {
translatedTitle: string
categoryName: string
examples?: string[]
year?: number
}): string {
const {
translatedTitle,
categoryName,
examples = [],
year = METADATA_DEFAULTS.currentYear,
} = options

const parts = [translatedTitle, `-`, `Best ${categoryName} ${year}`]

if (examples.length > 0) {
parts.push(METADATA_DEFAULTS.listSeparator)
parts.push(examples.join(', '))
}

parts.push(METADATA_DEFAULTS.titleSeparator)
parts.push(METADATA_DEFAULTS.siteName)
export function buildListPageTitle(options: { translatedTitle: string; year?: number }): string {
const { translatedTitle, year = METADATA_DEFAULTS.currentYear } = options

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

/**
Expand Down
Loading
Loading