improvement(seo): extract shared withFilteredNoindex helper (#5392)

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.
This commit is contained in:
Waleed
2026-07-03 13:18:08 -07:00
committed by GitHub
parent 8937005076
commit 4fe372b583
6 changed files with 110 additions and 88 deletions
+29 -26
View File
@@ -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({
+2 -2
View File
@@ -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<SearchParams> }) {
@@ -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',
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` },
},
twitter: {
card: 'summary_large_image',
title: 'Integrations | Sim',
description: `Connect ${INTEGRATION_COUNT}+ apps in Sim's AI workspace.`,
},
alternates: { canonical: `${baseUrl}/integrations` },
...(isFiltered && { robots: { index: false, follow: true } }),
}
isFiltered
)
}
export default async function IntegrationsPage({
+36 -33
View File
@@ -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',
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`,
},
},
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({
+2 -2
View File
@@ -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<SearchParams> }) {
+13
View File
@@ -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 } }) }
}