diff --git a/apps/sim/app/(landing)/comparisons/[provider]/opengraph-image.tsx b/apps/sim/app/(landing)/comparisons/[provider]/opengraph-image.tsx new file mode 100644 index 00000000000..04aed75ef27 --- /dev/null +++ b/apps/sim/app/(landing)/comparisons/[provider]/opengraph-image.tsx @@ -0,0 +1,25 @@ +import { notFound } from 'next/navigation' +import { COVER_OG_SIZE, createCoverOgImage } from '@/lib/og/cover-image' +import { ALL_COMPETITORS, getCompetitorBySlug } from '@/app/(landing)/comparisons/utils' + +export const contentType = 'image/png' +export const size = COVER_OG_SIZE + +/** Pre-render every comparison card alongside its page. */ +export async function generateStaticParams() { + return ALL_COMPETITORS.map((competitor) => ({ provider: competitor.id })) +} + +export default async function Image({ params }: { params: Promise<{ provider: string }> }) { + const { provider } = await params + const competitor = getCompetitorBySlug(provider) + + if (!competitor) { + notFound() + } + + return createCoverOgImage({ + title: `Sim vs ${competitor.name}`, + subtitle: 'Platform, AI, integrations, pricing, security, and support, with sourced facts', + }) +} diff --git a/apps/sim/app/(landing)/comparisons/[provider]/page.tsx b/apps/sim/app/(landing)/comparisons/[provider]/page.tsx index d2908ba2d2f..3f690f18624 100644 --- a/apps/sim/app/(landing)/comparisons/[provider]/page.tsx +++ b/apps/sim/app/(landing)/comparisons/[provider]/page.tsx @@ -76,6 +76,7 @@ export async function generateMetadata({ 'AI agent workspace', 'AI workflow automation comparison', ].join(', '), + routeImage: true, }) } diff --git a/apps/sim/app/(landing)/models/(shell)/page.tsx b/apps/sim/app/(landing)/models/(shell)/page.tsx index f7f31e200a2..32accbbc40c 100644 --- a/apps/sim/app/(landing)/models/(shell)/page.tsx +++ b/apps/sim/app/(landing)/models/(shell)/page.tsx @@ -1,3 +1,4 @@ +import { ChipLink } from '@sim/emcn' import type { Metadata } from 'next' import Link from 'next/link' import type { SearchParams } from 'nuqs/server' @@ -20,6 +21,7 @@ import { ALL_CATALOG_MODELS, getPricingBounds, MODEL_PROVIDERS_WITH_CATALOGS, + MODEL_RESELLER_PROVIDERS, TOP_MODEL_PROVIDERS, TOTAL_MODEL_PROVIDERS, TOTAL_MODELS, @@ -273,6 +275,36 @@ export default async function ModelsPage({
+ {MODEL_RESELLER_PROVIDERS.length > 0 && ( + <> +
+

+ Cloud and inference providers +

+

+ The same models served through cloud platforms and inference hosts, each with + its own pricing and model IDs. +

+ +
+ +
+ + )} +

provider.models.length > 0 && !provider.isReseller ) +/** Cloud platforms and inference hosts that serve other providers' models, each with its own page. */ +export const MODEL_RESELLER_PROVIDERS = MODEL_PROVIDERS_WITH_MODELS.filter( + (provider) => provider.isReseller +) export const MODEL_PROVIDERS_WITH_DYNAMIC_CATALOGS = MODEL_CATALOG_PROVIDERS.filter( (provider) => provider.models.length === 0 ) diff --git a/apps/sim/lib/landing/seo.ts b/apps/sim/lib/landing/seo.ts index be3aa0a1490..f2ca22c447a 100644 --- a/apps/sim/lib/landing/seo.ts +++ b/apps/sim/lib/landing/seo.ts @@ -23,6 +23,12 @@ interface LandingMetadataInput { twitterImageAlt?: string /** RSS feed path advertised through ``, e.g. "/changelog.xml". */ feedPath?: string + /** + * The route renders its own card from a sibling `opengraph-image`. The shared + * image is then left out, because an explicit `images` entry would replace the + * route's card on X and Slack, where `twitter:image` falls back to it. + */ + routeImage?: boolean } /** @@ -31,9 +37,10 @@ interface LandingMetadataInput { * landing family, so per-page files declare only what actually varies (title, * description, keywords, path, and any non-default image alt). * - * Note: dynamic and image-bearing routes that resolve their card from a sibling - * `opengraph-image` (integrations, models) and the blog family (`buildPostMetadata` - * in `lib/blog/seo`) intentionally do not use this helper. + * Note: the integration and model routes, which resolve their card from a sibling + * `opengraph-image`, and the blog family (`buildPostMetadata` in `lib/blog/seo`) + * intentionally do not use this helper. A route that otherwise fits it but has its + * own card passes `routeImage`. */ export function buildLandingMetadata({ title, @@ -43,6 +50,7 @@ export function buildLandingMetadata({ imageAlt, twitterImageAlt, feedPath, + routeImage = false, }: LandingMetadataInput): Metadata { const url = `${SITE_URL}${path}` const ogAlt = imageAlt ?? title @@ -63,12 +71,7 @@ export function buildLandingMetadata({ url, siteName: 'Sim', locale: 'en_US', - images: [ - { - ...LANDING_SOCIAL_IMAGE, - alt: ogAlt, - }, - ], + ...(routeImage ? {} : { images: [{ ...LANDING_SOCIAL_IMAGE, alt: ogAlt }] }), }, twitter: { card: 'summary_large_image', @@ -76,7 +79,7 @@ export function buildLandingMetadata({ creator: '@simdotai', title, description, - images: { url: LANDING_SOCIAL_IMAGE.url, alt: twitterAlt }, + ...(routeImage ? {} : { images: { url: LANDING_SOCIAL_IMAGE.url, alt: twitterAlt } }), }, alternates: { canonical: url,