diff --git a/apps/sim/app/(home)/components/footer/footer.tsx b/apps/sim/app/(home)/components/footer/footer.tsx index 0f167ee787f..33ef8dcf9b0 100644 --- a/apps/sim/app/(home)/components/footer/footer.tsx +++ b/apps/sim/app/(home)/components/footer/footer.tsx @@ -26,6 +26,7 @@ const RESOURCES_LINKS: FooterItem[] = [ { label: 'Blog', href: '/blog' }, // { label: 'Templates', href: '/templates' }, { label: 'Docs', href: 'https://docs.sim.ai', external: true }, + { label: 'Models', href: '/models' }, // { label: 'Academy', href: '/academy' }, { label: 'Partners', href: '/partners' }, { label: 'Careers', href: 'https://jobs.ashbyhq.com/sim', external: true }, diff --git a/apps/sim/app/(landing)/components/landing-faq.tsx b/apps/sim/app/(landing)/components/landing-faq.tsx new file mode 100644 index 00000000000..435d0ca8e90 --- /dev/null +++ b/apps/sim/app/(landing)/components/landing-faq.tsx @@ -0,0 +1,63 @@ +'use client' + +import { useState } from 'react' +import { ChevronDown } from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' + +export interface LandingFAQItem { + question: string + answer: string +} + +interface LandingFAQProps { + faqs: LandingFAQItem[] +} + +export function LandingFAQ({ faqs }: LandingFAQProps) { + const [openIndex, setOpenIndex] = useState(0) + + return ( +
+ {faqs.map(({ question, answer }, index) => { + const isOpen = openIndex === index + + return ( +
+ + + {isOpen && ( +
+

+ {answer} +

+
+ )} +
+ ) + })} +
+ ) +} diff --git a/apps/sim/app/(landing)/integrations/[slug]/components/integration-faq.tsx b/apps/sim/app/(landing)/integrations/[slug]/components/integration-faq.tsx index a0c6a8e1c70..6c71bc3e8e5 100644 --- a/apps/sim/app/(landing)/integrations/[slug]/components/integration-faq.tsx +++ b/apps/sim/app/(landing)/integrations/[slug]/components/integration-faq.tsx @@ -1,8 +1,4 @@ -'use client' - -import { useState } from 'react' -import { ChevronDown } from '@/components/emcn' -import { cn } from '@/lib/core/utils/cn' +import { LandingFAQ } from '@/app/(landing)/components/landing-faq' import type { FAQItem } from '@/app/(landing)/integrations/data/types' interface IntegrationFAQProps { @@ -10,49 +6,5 @@ interface IntegrationFAQProps { } export function IntegrationFAQ({ faqs }: IntegrationFAQProps) { - const [openIndex, setOpenIndex] = useState(0) - - return ( -
- {faqs.map(({ question, answer }, index) => { - const isOpen = openIndex === index - return ( -
- - - {isOpen && ( -
-

- {answer} -

-
- )} -
- ) - })} -
- ) + return } diff --git a/apps/sim/app/(landing)/models/[provider]/[model]/opengraph-image.tsx b/apps/sim/app/(landing)/models/[provider]/[model]/opengraph-image.tsx new file mode 100644 index 00000000000..0d7911dc110 --- /dev/null +++ b/apps/sim/app/(landing)/models/[provider]/[model]/opengraph-image.tsx @@ -0,0 +1,42 @@ +import { notFound } from 'next/navigation' +import { createModelsOgImage } from '@/app/(landing)/models/og-utils' +import { + formatPrice, + formatTokenCount, + getModelBySlug, + getProviderBySlug, +} from '@/app/(landing)/models/utils' + +export const runtime = 'edge' +export const contentType = 'image/png' +export const size = { + width: 1200, + height: 630, +} + +export default async function Image({ + params, +}: { + params: Promise<{ provider: string; model: string }> +}) { + const { provider: providerSlug, model: modelSlug } = await params + const provider = getProviderBySlug(providerSlug) + const model = getModelBySlug(providerSlug, modelSlug) + + if (!provider || !model) { + notFound() + } + + return createModelsOgImage({ + eyebrow: `${provider.name} model`, + title: model.displayName, + subtitle: `${provider.name} pricing, context window, and feature support generated from Sim's model registry.`, + pills: [ + `Input ${formatPrice(model.pricing.input)}/1M`, + `Output ${formatPrice(model.pricing.output)}/1M`, + model.contextWindow ? `${formatTokenCount(model.contextWindow)} context` : 'Unknown context', + model.capabilityTags[0] ?? 'Capabilities tracked', + ], + domainLabel: `sim.ai${model.href}`, + }) +} diff --git a/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx b/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx new file mode 100644 index 00000000000..fd7557e37c7 --- /dev/null +++ b/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx @@ -0,0 +1,390 @@ +import type { Metadata } from 'next' +import Link from 'next/link' +import { notFound } from 'next/navigation' +import { getBaseUrl } from '@/lib/core/utils/urls' +import { LandingFAQ } from '@/app/(landing)/components/landing-faq' +import { + Breadcrumbs, + CapabilityTags, + DetailItem, + ModelCard, + ProviderIcon, + StatCard, +} from '@/app/(landing)/models/components/model-primitives' +import { + ALL_CATALOG_MODELS, + buildModelCapabilityFacts, + buildModelFaqs, + formatPrice, + formatTokenCount, + formatUpdatedAt, + getModelBySlug, + getPricingBounds, + getProviderBySlug, + getRelatedModels, +} from '@/app/(landing)/models/utils' + +const baseUrl = getBaseUrl() + +export async function generateStaticParams() { + return ALL_CATALOG_MODELS.map((model) => ({ + provider: model.providerSlug, + model: model.slug, + })) +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ provider: string; model: string }> +}): Promise { + const { provider: providerSlug, model: modelSlug } = await params + const provider = getProviderBySlug(providerSlug) + const model = getModelBySlug(providerSlug, modelSlug) + + if (!provider || !model) { + return {} + } + + return { + title: `${model.displayName} Pricing, Context Window, and Features`, + description: `${model.displayName} by ${provider.name}: pricing, cached input cost, output cost, context window, and capability support. Explore the full generated model page on Sim.`, + keywords: [ + model.displayName, + `${model.displayName} pricing`, + `${model.displayName} context window`, + `${model.displayName} features`, + `${provider.name} ${model.displayName}`, + `${provider.name} model pricing`, + ...model.capabilityTags, + ], + openGraph: { + title: `${model.displayName} Pricing, Context Window, and Features | Sim`, + description: `${model.displayName} by ${provider.name}: pricing, context window, and model capability details.`, + url: `${baseUrl}${model.href}`, + type: 'website', + images: [ + { + url: `${baseUrl}${model.href}/opengraph-image`, + width: 1200, + height: 630, + alt: `${model.displayName} on Sim`, + }, + ], + }, + twitter: { + card: 'summary_large_image', + title: `${model.displayName} | Sim`, + description: model.summary, + images: [ + { url: `${baseUrl}${model.href}/opengraph-image`, alt: `${model.displayName} on Sim` }, + ], + }, + alternates: { + canonical: `${baseUrl}${model.href}`, + }, + } +} + +export default async function ModelPage({ + params, +}: { + params: Promise<{ provider: string; model: string }> +}) { + const { provider: providerSlug, model: modelSlug } = await params + const provider = getProviderBySlug(providerSlug) + const model = getModelBySlug(providerSlug, modelSlug) + + if (!provider || !model) { + notFound() + } + + const faqs = buildModelFaqs(provider, model) + const capabilityFacts = buildModelCapabilityFacts(model) + const pricingBounds = getPricingBounds(model.pricing) + const relatedModels = getRelatedModels(model, 6) + + const breadcrumbJsonLd = { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { '@type': 'ListItem', position: 1, name: 'Home', item: baseUrl }, + { '@type': 'ListItem', position: 2, name: 'Models', item: `${baseUrl}/models` }, + { '@type': 'ListItem', position: 3, name: provider.name, item: `${baseUrl}${provider.href}` }, + { + '@type': 'ListItem', + position: 4, + name: model.displayName, + item: `${baseUrl}${model.href}`, + }, + ], + } + + const productJsonLd = { + '@context': 'https://schema.org', + '@type': 'Product', + name: model.displayName, + brand: provider.name, + category: 'AI language model', + description: model.summary, + sku: model.id, + offers: { + '@type': 'AggregateOffer', + priceCurrency: 'USD', + lowPrice: pricingBounds.lowPrice.toString(), + highPrice: pricingBounds.highPrice.toString(), + }, + } + + const faqJsonLd = { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: faqs.map((faq) => ({ + '@type': 'Question', + name: faq.question, + acceptedAnswer: { + '@type': 'Answer', + text: faq.answer, + }, + })), + } + + return ( + <> +