mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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> }) {
|
||||
|
||||
@@ -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 } }) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user