From 48c2d2ef92477da6b57d16e27464ebb7065fadad Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 3 Jul 2026 13:09:34 -0700 Subject: [PATCH] improvement(seo): extract shared withFilteredNoindex helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `{ ...base, ...(isFiltered && { robots: { index: false, follow: true } }) }` faceted-navigation noindex pattern was duplicated verbatim across five catalog pages (integrations, models, blog, careers, pricing). Extracted to lib/landing/seo.ts alongside buildLandingMetadata, giving the pattern a name and a single point of change. Pure refactor — verified byte-identical rendered output (robots meta + canonical) on baseline and filtered variants of all five pages before and after. --- apps/sim/app/(landing)/blog/page.tsx | 55 ++++++++------- apps/sim/app/(landing)/careers/page.tsx | 4 +- .../(landing)/integrations/(shell)/page.tsx | 53 +++++++------- .../sim/app/(landing)/models/(shell)/page.tsx | 69 ++++++++++--------- apps/sim/app/(landing)/pricing/page.tsx | 4 +- apps/sim/lib/landing/seo.ts | 13 ++++ 6 files changed, 110 insertions(+), 88 deletions(-) diff --git a/apps/sim/app/(landing)/blog/page.tsx b/apps/sim/app/(landing)/blog/page.tsx index 36bd57d6f7d..54a0b72d65d 100644 --- a/apps/sim/app/(landing)/blog/page.tsx +++ b/apps/sim/app/(landing)/blog/page.tsx @@ -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' @@ -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({ diff --git a/apps/sim/app/(landing)/careers/page.tsx b/apps/sim/app/(landing)/careers/page.tsx index 9a17cc0867b..172aeb582ce 100644 --- a/apps/sim/app/(landing)/careers/page.tsx +++ b/apps/sim/app/(landing)/careers/page.tsx @@ -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' @@ -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 }) { diff --git a/apps/sim/app/(landing)/integrations/(shell)/page.tsx b/apps/sim/app/(landing)/integrations/(shell)/page.tsx index a78e903f56f..db36e048358 100644 --- a/apps/sim/app/(landing)/integrations/(shell)/page.tsx +++ b/apps/sim/app/(landing)/integrations/(shell)/page.tsx @@ -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' @@ -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({ diff --git a/apps/sim/app/(landing)/models/(shell)/page.tsx b/apps/sim/app/(landing)/models/(shell)/page.tsx index 624732c9f76..5e0f0eb1e0e 100644 --- a/apps/sim/app/(landing)/models/(shell)/page.tsx +++ b/apps/sim/app/(landing)/models/(shell)/page.tsx @@ -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' @@ -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({ diff --git a/apps/sim/app/(landing)/pricing/page.tsx b/apps/sim/app/(landing)/pricing/page.tsx index 7d8a03e2f81..fe5ee681c86 100644 --- a/apps/sim/app/(landing)/pricing/page.tsx +++ b/apps/sim/app/(landing)/pricing/page.tsx @@ -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' @@ -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 }) { diff --git a/apps/sim/lib/landing/seo.ts b/apps/sim/lib/landing/seo.ts index d0f04f50caf..24b48775870 100644 --- a/apps/sim/lib/landing/seo.ts +++ b/apps/sim/lib/landing/seo.ts @@ -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 } }) } +}