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
55 changes: 29 additions & 26 deletions apps/sim/app/(landing)/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Link from 'next/link'
import { getAllPostMeta } from '@/lib/blog/registry'
import { buildCollectionPageJsonLd } from '@/lib/blog/seo'
import { SITE_URL } from '@/lib/core/utils/urls'
import { withFilteredNoindex } from '@/lib/landing/seo'
import { Cta } from '@/app/(landing)/components'
import { JsonLd } from '@/app/(landing)/components/json-ld'

Expand Down Expand Up @@ -35,34 +36,36 @@ export async function generateMetadata({
const canonical = `${SITE_URL}/blog`
const isFiltered = Boolean(tag) || pageNum > 1

return {
title,
description,
alternates: { canonical },
openGraph: {
title: `${title} | Sim`,
return withFilteredNoindex(
{
title,
description,
url: canonical,
siteName: 'Sim',
locale: 'en_US',
type: 'website',
images: [
{
url: `${SITE_URL}/logo/primary/medium.png`,
width: 1200,
height: 630,
alt: 'Sim Blog',
},
],
alternates: { canonical },
openGraph: {
title: `${title} | Sim`,
description,
url: canonical,
siteName: 'Sim',
locale: 'en_US',
type: 'website',
images: [
{
url: `${SITE_URL}/logo/primary/medium.png`,
width: 1200,
height: 630,
alt: 'Sim Blog',
},
],
},
twitter: {
card: 'summary_large_image',
title: `${title} | Sim`,
description,
site: '@simdotai',
},
},
twitter: {
card: 'summary_large_image',
title: `${title} | Sim`,
description,
site: '@simdotai',
},
...(isFiltered && { robots: { index: false, follow: true } }),
}
isFiltered
)
}

export default async function BlogIndex({
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/(landing)/careers/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Metadata } from 'next'
import type { SearchParams } from 'nuqs/server'
import { buildLandingMetadata } from '@/lib/landing/seo'
import { buildLandingMetadata, withFilteredNoindex } from '@/lib/landing/seo'
import Careers from '@/app/(landing)/careers/careers'
import { ALL_FILTER_VALUE, careersSearchParamsCache } from '@/app/(landing)/careers/search-params'

Expand All @@ -26,7 +26,7 @@ export async function generateMetadata({
'Sim careers, Sim jobs, AI workspace jobs, AI agent engineering jobs, open source jobs',
})

return { ...base, ...(isFiltered && { robots: { index: false, follow: true } }) }
return withFilteredNoindex(base, isFiltered)
}

export default function Page({ searchParams }: { searchParams: Promise<SearchParams> }) {
Expand Down
53 changes: 28 additions & 25 deletions apps/sim/app/(landing)/integrations/(shell)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type Integration,
POPULAR_WORKFLOWS,
} from '@/lib/integrations'
import { withFilteredNoindex } from '@/lib/landing/seo'
import { JsonLd } from '@/app/(landing)/components/json-ld'
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
import { IntegrationCard } from '@/app/(landing)/integrations/components/integration-card'
Expand Down Expand Up @@ -92,32 +93,34 @@ export async function generateMetadata({
const { q, category } = await integrationsSearchParamsCache.parse(searchParams)
const isFiltered = Boolean(q || category)

return {
title: 'Integrations',
description: `Connect ${INTEGRATION_COUNT}+ apps and services in Sim's AI workspace. Build agents that automate real work with ${TOP_NAMES.join(', ')}, and more.`,
keywords: [
'AI workspace integrations',
'AI agent integrations',
'AI agent builder integrations',
...TOP_NAMES.flatMap((n) => [`${n} integration`, `${n} automation`]),
...allIntegrations.slice(0, 20).map((i) => `${i.name} automation`),
],
// og:image/twitter:image come from the sibling opengraph-image.tsx -
// Next serves it at a hash-suffixed URL, so hardcoding it here 404s.
openGraph: {
title: 'Integrations | Sim AI Workspace',
description: `Connect ${INTEGRATION_COUNT}+ apps in Sim's AI workspace. Build agents that link ${TOP_NAMES.join(', ')}, and every tool your team uses.`,
url: `${baseUrl}/integrations`,
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Integrations | Sim',
description: `Connect ${INTEGRATION_COUNT}+ apps in Sim's AI workspace.`,
return withFilteredNoindex(
{
title: 'Integrations',
description: `Connect ${INTEGRATION_COUNT}+ apps and services in Sim's AI workspace. Build agents that automate real work with ${TOP_NAMES.join(', ')}, and more.`,
keywords: [
'AI workspace integrations',
'AI agent integrations',
'AI agent builder integrations',
...TOP_NAMES.flatMap((n) => [`${n} integration`, `${n} automation`]),
...allIntegrations.slice(0, 20).map((i) => `${i.name} automation`),
],
// og:image/twitter:image come from the sibling opengraph-image.tsx -
// Next serves it at a hash-suffixed URL, so hardcoding it here 404s.
openGraph: {
title: 'Integrations | Sim AI Workspace',
description: `Connect ${INTEGRATION_COUNT}+ apps in Sim's AI workspace. Build agents that link ${TOP_NAMES.join(', ')}, and every tool your team uses.`,
url: `${baseUrl}/integrations`,
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Integrations | Sim',
description: `Connect ${INTEGRATION_COUNT}+ apps in Sim's AI workspace.`,
},
alternates: { canonical: `${baseUrl}/integrations` },
},
alternates: { canonical: `${baseUrl}/integrations` },
...(isFiltered && { robots: { index: false, follow: true } }),
}
isFiltered
)
}

export default async function IntegrationsPage({
Expand Down
69 changes: 36 additions & 33 deletions apps/sim/app/(landing)/models/(shell)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next'
import type { SearchParams } from 'nuqs/server'
import { SITE_URL } from '@/lib/core/utils/urls'
import { withFilteredNoindex } from '@/lib/landing/seo'
import { JsonLd } from '@/app/(landing)/components/json-ld'
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
import { ModelComparisonCharts } from '@/app/(landing)/models/components/model-comparison-charts'
Expand Down Expand Up @@ -74,40 +75,42 @@ export async function generateMetadata({
const { q, provider } = await modelsSearchParamsCache.parse(searchParams)
const isFiltered = Boolean(q || provider)

return {
title: 'AI Models Directory',
description: `Compare ${TOTAL_MODELS}+ AI models across ${TOTAL_MODEL_PROVIDERS} providers in Sim's AI workspace. Compare pricing, context windows, and capabilities for your agents.`,
keywords: [
'AI models directory',
'AI model comparison',
'LLM model list',
'model pricing',
'context window comparison',
'OpenAI models',
'Anthropic models',
'Google Gemini models',
'xAI Grok models',
'Mistral models',
...TOP_MODEL_PROVIDERS.map((provider) => `${provider} models`),
],
// og:image/twitter:image come from the sibling opengraph-image.tsx -
// Next serves it at a hash-suffixed URL, so hardcoding it here 404s.
openGraph: {
title: 'AI Models Directory | Sim',
description: `Explore ${TOTAL_MODELS}+ AI models across ${TOTAL_MODEL_PROVIDERS} providers with pricing, context windows, and capability details.`,
url: `${baseUrl}/models`,
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'AI Models Directory | Sim',
description: `Search ${TOTAL_MODELS}+ AI models across ${TOTAL_MODEL_PROVIDERS} providers.`,
},
alternates: {
canonical: `${baseUrl}/models`,
return withFilteredNoindex(
{
title: 'AI Models Directory',
description: `Compare ${TOTAL_MODELS}+ AI models across ${TOTAL_MODEL_PROVIDERS} providers in Sim's AI workspace. Compare pricing, context windows, and capabilities for your agents.`,
keywords: [
'AI models directory',
'AI model comparison',
'LLM model list',
'model pricing',
'context window comparison',
'OpenAI models',
'Anthropic models',
'Google Gemini models',
'xAI Grok models',
'Mistral models',
...TOP_MODEL_PROVIDERS.map((provider) => `${provider} models`),
],
// og:image/twitter:image come from the sibling opengraph-image.tsx -
// Next serves it at a hash-suffixed URL, so hardcoding it here 404s.
openGraph: {
title: 'AI Models Directory | Sim',
description: `Explore ${TOTAL_MODELS}+ AI models across ${TOTAL_MODEL_PROVIDERS} providers with pricing, context windows, and capability details.`,
url: `${baseUrl}/models`,
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'AI Models Directory | Sim',
description: `Search ${TOTAL_MODELS}+ AI models across ${TOTAL_MODEL_PROVIDERS} providers.`,
},
alternates: {
canonical: `${baseUrl}/models`,
},
},
...(isFiltered && { robots: { index: false, follow: true } }),
}
isFiltered
)
}

export default async function ModelsPage({
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/(landing)/pricing/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Metadata } from 'next'
import type { SearchParams } from 'nuqs/server'
import { buildLandingMetadata } from '@/lib/landing/seo'
import { buildLandingMetadata, withFilteredNoindex } from '@/lib/landing/seo'
import Pricing from '@/app/(landing)/pricing/pricing'
import { pricingSearchParamsCache } from '@/app/(landing)/pricing/search-params'

Expand Down Expand Up @@ -29,7 +29,7 @@ export async function generateMetadata({
'Sim pricing, AI workspace pricing, AI agent platform pricing, build AI agents, Pro plan, Max plan, Enterprise plan, open-source AI agents, LLM pricing',
})

return { ...base, ...(isFiltered && { robots: { index: false, follow: true } }) }
return withFilteredNoindex(base, isFiltered)
}

export default async function Page({ searchParams }: { searchParams: Promise<SearchParams> }) {
Expand Down
13 changes: 13 additions & 0 deletions apps/sim/lib/landing/seo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,16 @@ export function buildLandingMetadata({
category: 'technology',
}
}

/**
* Google's documented pattern for faceted/filtered navigation: keep the single
* unfiltered listing indexable and `noindex` (but still `follow`) any
* filtered or paginated variant, so link equity flows through without asking
* Google to index every query-param permutation. Used by every catalog page
* that serves distinct content per query param (integrations, models, blog,
* careers, pricing) — `metadata.alternates.canonical` on all of them still
* points at the bare URL regardless of `isFiltered`.
*/
export function withFilteredNoindex(metadata: Metadata, isFiltered: boolean): Metadata {
return { ...metadata, ...(isFiltered && { robots: { index: false, follow: true } }) }
}
Loading