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
32 changes: 30 additions & 2 deletions apps/sim/app/(interfaces)/chat/[identifier]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
import { db } from '@sim/db'
import { chat } from '@sim/db/schema'
import { and, eq, isNull } from 'drizzle-orm'
import type { Metadata } from 'next'
import ChatClient from '@/app/(interfaces)/chat/[identifier]/chat'
import { OfficeEmbedInit } from '@/app/(interfaces)/chat/[identifier]/office-embed-init'

export const metadata: Metadata = {
title: 'Chat',
/**
* Only fully public, active deployments are indexable. Auth-gated (password,
* email, SSO) and inactive/nonexistent chats are noindexed at the page level
* so Google never indexes an auth wall — narrower than blocking `/chat/`
* entirely in robots.ts, which would also hide genuinely public deployments.
*/
export async function generateMetadata({
params,
}: {
params: Promise<{ identifier: string }>
}): Promise<Metadata> {
const { identifier } = await params

const [deployment] = await db
.select({ authType: chat.authType, isActive: chat.isActive })
.from(chat)
.where(and(eq(chat.identifier, identifier), isNull(chat.archivedAt)))
.limit(1)

const isIndexable = Boolean(deployment?.isActive && deployment.authType === 'public')

return {
title: 'Chat',
...(!isIndexable && { robots: { index: false, follow: false } }),
}
}

export const dynamic = 'force-dynamic'

export default async function ChatPage({
params,
searchParams,
Expand Down
17 changes: 10 additions & 7 deletions apps/sim/app/(landing)/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import { SITE_URL } from '@/lib/core/utils/urls'
import { Cta } from '@/app/(landing)/components'
import { JsonLd } from '@/app/(landing)/components/json-ld'

/**
* Filtered/paginated variants render genuinely different lists, but only the
* bare index is indexable — same policy as the integrations and models
* catalogs — so canonical always points at the unfiltered page and the
* variant itself is noindexed rather than asking Google to index every
* tag/page permutation.
*/
export async function generateMetadata({
searchParams,
}: {
Expand All @@ -25,11 +32,8 @@ export async function generateMetadata({
? `Sim blog posts tagged "${tag}": insights and guides for building AI agents.`
: 'Announcements, insights, and guides from Sim, the open-source AI workspace, for building, deploying, and managing AI agents.'

const canonicalParams = new URLSearchParams()
if (tag) canonicalParams.set('tag', tag)
if (pageNum > 1) canonicalParams.set('page', String(pageNum))
const qs = canonicalParams.toString()
const canonical = `${SITE_URL}/blog${qs ? `?${qs}` : ''}`
const canonical = `${SITE_URL}/blog`
const isFiltered = Boolean(tag) || pageNum > 1

return {
title,
Expand Down Expand Up @@ -57,11 +61,10 @@ export async function generateMetadata({
description,
site: '@simdotai',
},
...(isFiltered && { robots: { index: false, follow: true } }),
}
}

export const revalidate = 3600

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

export const revalidate = 3600
/**
* `team`/`location` render a genuinely different server-rendered job list (see
* search-params.ts), so filtered URLs are noindexed rather than
* self-canonicalized — same policy as the integrations/models/blog catalogs.
*/
export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<SearchParams>
}): Promise<Metadata> {
const { team, location } = await careersSearchParamsCache.parse(searchParams)
const isFiltered = team !== ALL_FILTER_VALUE || location !== ALL_FILTER_VALUE

export const metadata = buildLandingMetadata({
title: 'Careers at Sim — Build the AI workspace for teams',
description:
'Join Sim, the open-source AI workspace where teams build, deploy, and manage AI agents. See open engineering, design, and go-to-market roles.',
path: '/careers',
keywords: 'Sim careers, Sim jobs, AI workspace jobs, AI agent engineering jobs, open source jobs',
})
const base = buildLandingMetadata({
title: 'Careers | Sim, the AI Workspace',
description:
'Join Sim, the open-source AI workspace where teams build, deploy, and manage AI agents. See open engineering, design, and go-to-market roles.',
path: '/careers',
keywords:
'Sim careers, Sim jobs, AI workspace jobs, AI agent engineering jobs, open source jobs',
})

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

export default function Page({ searchParams }: { searchParams: Promise<SearchParams> }) {
return <Careers searchParams={searchParams} />
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/(landing)/comparison/[provider]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export async function generateMetadata({
}

return buildLandingMetadata({
title: `Sim vs ${competitor.name}: AI Workspace Comparison`,
title: `Sim vs ${competitor.name} | Sim, the AI Workspace`,
description: `Compare Sim, the open-source AI workspace, to ${competitor.name} on platform, AI, integrations, pricing, security, and support. Sourced and dated facts.`,
path: `/comparison/${competitor.id}`,
keywords: [
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/(landing)/comparison/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const faqItems = [
]

export const metadata: Metadata = buildLandingMetadata({
title: 'Sim Comparisons: AI Agent & Workflow Platforms',
title: 'Sim Comparisons | Sim, the AI Workspace',
description:
'Compare Sim, the open-source AI workspace, to n8n, Zapier, Make, and other workflow automation and AI agent platforms. Sourced, dated, fact-checked.',
path: '/comparison',
Expand Down
1 change: 0 additions & 1 deletion apps/sim/app/(landing)/components/footer/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ const RESOURCES_LINKS: FooterItem[] = [
{ label: 'Blog', href: '/blog' },
{ label: 'Docs', href: 'https://docs.sim.ai', external: true },
{ label: 'Compare', href: '/comparison' },
{ label: 'Partners', href: '/partners' },
{ label: 'Careers', href: '/careers' },
{ label: 'Changelog', href: '/changelog' },
{ label: 'Contact', href: '/contact' },
Expand Down
10 changes: 5 additions & 5 deletions apps/sim/app/(landing)/components/logo-shell/logo-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { LogoMark, SimWordmark } from '@/app/(landing)/components/navbar/compone
* The canonical light, logo-only page frame - a Sim wordmark linking home, no
* marketing menus, on the platform's light tokens (the `light` class pins
* light mode regardless of visitor theme). It is the shared base for every
* surface that wants minimal chrome: the global 404, the academy catalog, and
* the `(interfaces)` group (which adds a support footer). The `(auth)` group
* uses its own `AuthShell` with the same look.
* surface that wants minimal chrome: the global 404, and the `(interfaces)`
* group (which adds a support footer). The `(auth)` group uses its own
* `AuthShell` with the same look.
*
* Children decide their own layout: pass `center` for a single centered column
* (404 message, simple gates); omit it for full-width content (catalogs, the
* live chat overlay, which covers this frame entirely). An optional `footer`
* (404 message, simple gates); omit it for full-width content (the live chat
* overlay, which covers this frame entirely). An optional `footer`
* slot renders pinned at the bottom.
*/
interface LogoShellProps {
Expand Down
64 changes: 40 additions & 24 deletions apps/sim/app/(landing)/integrations/(shell)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,30 +78,46 @@ const featured = FEATURED_SLUGS.map((s) => bySlug.get(s)).filter(
(i): i is Integration => i !== undefined
)

export const metadata: Metadata = {
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` },
/**
* `q`/`category` render a genuinely different server-rendered list (see
* search-params.ts), so filtered URLs are noindexed rather than
* self-canonicalized — keeps the single indexable URL as the bare catalog
* page instead of asking Google to index every filter permutation.
*/
export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<SearchParams>
}): Promise<Metadata> {
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.`,
},
alternates: { canonical: `${baseUrl}/integrations` },
...(isFiltered && { robots: { index: false, follow: true } }),
}
}

export default async function IntegrationsPage({
Expand Down
80 changes: 48 additions & 32 deletions apps/sim/app/(landing)/models/(shell)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,38 +60,54 @@ const faqItems = [
},
]

export const metadata: Metadata = {
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`,
},
/**
* `q`/`provider` render a genuinely different server-rendered list (see
* search-params.ts), so filtered URLs are noindexed rather than
* self-canonicalized — keeps the single indexable URL as the bare directory
* page instead of asking Google to index every filter permutation.
*/
export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<SearchParams>
}): Promise<Metadata> {
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`,
},
...(isFiltered && { robots: { index: false, follow: true } }),
}
}

export default async function ModelsPage({
Expand Down
Loading
Loading