Skip to content

Commit c1c0843

Browse files
authored
improvement(landing): search-length titles, model comparison pages, and free model tools (#9184)
* improvement(library): give posts search titles and descriptions that fit search results - Optional seoTitle/seoDescription frontmatter drive <title>, meta description, and Open Graph/X copy; the H1, cards, feeds, and JSON-LD keep title/description - check:library-content ratchets search title (with the " | Sim" suffix) to 60 characters and description to 160, with a shrink-only baseline - Rewrite search titles and descriptions for 12 library pages to match the queries they rank for - Author pages no longer render the brand twice in their title * improvement(library): make search title and description lengths a strict check - Drop the baseline so parallel content PRs never conflict on it; every published post now fits - Add search titles and descriptions to the remaining library, blog, and changelog posts * feat(models): add a free LLM API pricing calculator and token counter - /models/pricing-calculator prices requests and monthly volume per model with the same tier and cached-input rules billing uses (priceTokenUsage, extracted unchanged from the billing cost policy) - /models/token-counter counts tokens in the browser: exact for OpenAI encodings (js-tiktoken loaded on first input), labelled estimates for other providers; pasted text never leaves the page - Both pages link from /models and appear in the sitemap and llms.txt * improvement(models): API pricing titles, context window comparison, and model-vs-model pages - Provider pages target API pricing searches ("Anthropic (Claude) API Pricing & Models") - /models/context-windows compares context window, max output, and price for every current model; retired models are excluded - 47 curated /models/compare pages with a data-derived verdict priced at standard per-call rates; the build fails if a curated model is retired or removed * chore(models): drop exports the combined change left unused * improvement(models): chip links use the outline variant; tighten SEO checks - Model page chip links use ChipLink's outline variant instead of restyling it - ComparisonLinks gets its own component folder - One E2E check covers every model page's search title budget and canonical; a fixture test covers the search length rules * fix(models): leave evaluation models out of prompt tools; landing page structure - Context windows, the pricing tools, and comparisons skip evaluation-only models (native state and questions, not prompts) - Token counter labels an OpenAI count as an estimate when exact encoding fails - New model pages: desktop-first breakpoints, named props interfaces, section ids and labels, sr-only hero summaries; TSDoc on exported comparison helpers * fix(models): format catalog dates in UTC so date-only values don't shift a day * docs(models): say which models the provider largest-context pick covers * improvement(library): search title for the Bedrock and Vertex alternatives post * fix(models): keep tool pages static; provider FAQs keep their largest-context pick - Pricing calculator and token counter render statically; each URL-reading tool sits in a local Suspense whose fallback reserves its space - Largest-context pick breaks ties in catalog order again (retired models still excluded), and the context-window page uses the same pick
1 parent 768141a commit c1c0843

145 files changed

Lines changed: 3340 additions & 144 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/sim/app/(landing)/models/(shell)/[provider]/[model]/page.tsx‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { JsonLd } from '@/app/(landing)/components/json-ld'
88
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
99
import { LANDING_CONTENT_WIDTH, LANDING_GUTTER } from '@/app/(landing)/components/landing-layout'
1010
import { ShareButton } from '@/app/(landing)/components/share-button'
11+
import { getModelComparisonsFor } from '@/app/(landing)/models/comparisons'
12+
import { ComparisonLinks } from '@/app/(landing)/models/components/comparison-links'
1113
import { FeaturedModelCard, ProviderIcon } from '@/app/(landing)/models/components/model-primitives'
1214
import {
1315
buildModelCapabilityFacts,
@@ -109,6 +111,7 @@ export default async function ModelPage({
109111
const capabilityFacts = buildModelCapabilityFacts(model)
110112
const pricingBounds = getPricingBounds(model.pricing)
111113
const relatedModels = getRelatedModels(model, 6)
114+
const comparisons = getModelComparisonsFor(model)
112115

113116
const breadcrumbJsonLd = {
114117
'@context': 'https://schema.org',
@@ -195,7 +198,7 @@ export default async function ModelPage({
195198
<ChipLink variant='primary' href='/'>
196199
Build with this model
197200
</ChipLink>
198-
<ChipLink href={provider.href} className='border border-[var(--border-1)]'>
201+
<ChipLink href={provider.href} variant='outline'>
199202
All {provider.name} models
200203
</ChipLink>
201204
<ShareButton url={`${baseUrl}${model.href}`} title={model.displayName} />
@@ -273,6 +276,21 @@ export default async function ModelPage({
273276
</>
274277
)}
275278

279+
{comparisons.length > 0 && (
280+
<>
281+
<div className='h-px w-full bg-[var(--border)]' />
282+
<nav aria-labelledby='model-comparisons-heading' className='px-6 py-10'>
283+
<h2
284+
id='model-comparisons-heading'
285+
className='mb-6 text-[24px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em] max-lg:text-[20px]'
286+
>
287+
Compare {model.displayName}
288+
</h2>
289+
<ComparisonLinks comparisons={comparisons} />
290+
</nav>
291+
</>
292+
)}
293+
276294
<div className='h-px w-full bg-[var(--border)]' />
277295

278296
<section aria-labelledby='model-faq-heading' className='px-6 py-10'>

‎apps/sim/app/(landing)/models/(shell)/[provider]/opengraph-image.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { notFound } from 'next/navigation'
22
import { COVER_OG_SIZE, createCoverOgImage } from '@/lib/og/cover-image'
3-
import { getProviderBySlug, MODEL_PROVIDERS_WITH_CATALOGS } from '@/app/(landing)/models/utils'
3+
import {
4+
getProviderBySlug,
5+
getProviderPageTitle,
6+
MODEL_PROVIDERS_WITH_CATALOGS,
7+
} from '@/app/(landing)/models/utils'
48

59
export const contentType = 'image/png'
610
export const size = COVER_OG_SIZE
@@ -21,7 +25,7 @@ export default async function Image({ params }: { params: Promise<{ provider: st
2125
}
2226

2327
return createCoverOgImage({
24-
title: `${provider.name} models`,
28+
title: getProviderPageTitle(provider),
2529
subtitle: `Browse ${provider.modelCount} tracked ${provider.name} models with pricing, context windows, default model selection, and capability coverage.`,
2630
})
2731
}

‎apps/sim/app/(landing)/models/(shell)/[provider]/page.tsx‎

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
formatPrice,
1919
formatTokenCount,
2020
getProviderBySlug,
21+
getProviderPageDescription,
22+
getProviderPageTitle,
2123
MODEL_PROVIDERS_WITH_CATALOGS,
2224
MODEL_PROVIDERS_WITH_MODELS,
2325
TOP_MODEL_PROVIDERS,
@@ -50,31 +52,35 @@ export async function generateMetadata({
5052
return {}
5153
}
5254

53-
const providerFaqs = buildProviderFaqs(provider)
55+
const title = getProviderPageTitle(provider)
56+
const description = getProviderPageDescription(provider)
5457

5558
return {
56-
title: `${provider.name} Models`,
57-
description: `Browse ${provider.modelCount} ${provider.name} models tracked in Sim. Compare pricing, context windows, default model selection, and capabilities for ${provider.name}'s AI model lineup.`,
59+
title,
60+
description,
5861
keywords: [
59-
`${provider.name} models`,
60-
`${provider.name} pricing`,
61-
`${provider.name} context window`,
62-
`${provider.name} model list`,
63-
`${provider.name} AI models`,
64-
...provider.models.slice(0, 6).map((model) => model.displayName),
62+
...new Set([
63+
`${provider.apiName} API pricing`,
64+
`${provider.modelFamily} API pricing`,
65+
`${provider.name} pricing`,
66+
`${provider.modelFamily} context window`,
67+
`${provider.name} models`,
68+
`${provider.name} model list`,
69+
...provider.models.slice(0, 6).map((model) => model.displayName),
70+
]),
6571
],
6672
// og:image/twitter:image come from the sibling opengraph-image.tsx -
6773
// Next serves it at a hash-suffixed URL, so hardcoding it here 404s.
6874
openGraph: {
69-
title: `${provider.name} Models | Sim`,
70-
description: `Explore ${provider.modelCount} ${provider.name} models with pricing and capability details.`,
75+
title: `${title} | Sim`,
76+
description,
7177
url: `${baseUrl}${provider.href}`,
7278
type: 'website',
7379
},
7480
twitter: {
7581
card: 'summary_large_image',
76-
title: `${provider.name} Models | Sim`,
77-
description: providerFaqs[0]?.answer ?? provider.summary,
82+
title: `${title} | Sim`,
83+
description,
7884
},
7985
alternates: {
8086
canonical: `${baseUrl}${provider.href}`,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { notFound } from 'next/navigation'
2+
import { COVER_OG_SIZE, createCoverOgImage } from '@/lib/og/cover-image'
3+
import {
4+
buildComparisonVerdict,
5+
getModelComparisonBySlug,
6+
MODEL_COMPARISONS,
7+
} from '@/app/(landing)/models/comparisons'
8+
9+
export const contentType = 'image/png'
10+
export const size = COVER_OG_SIZE
11+
12+
/** Pre-render catalog images separately from their sibling pages. */
13+
export async function generateStaticParams() {
14+
return MODEL_COMPARISONS.map((comparison) => ({ pair: comparison.slug }))
15+
}
16+
17+
export default async function Image({ params }: { params: Promise<{ pair: string }> }) {
18+
const { pair } = await params
19+
const comparison = getModelComparisonBySlug(pair)
20+
21+
if (!comparison) {
22+
notFound()
23+
}
24+
25+
return createCoverOgImage({
26+
title: comparison.title,
27+
subtitle: buildComparisonVerdict(comparison)[0],
28+
})
29+
}

0 commit comments

Comments
 (0)