Skip to content
Open
50 changes: 35 additions & 15 deletions apps/sim/app/(landing)/comparisons/[provider]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ALL_COMPETITORS,
buildBottomLine,
buildComparisonFaqs,
getComparisonReviewDate,
getCompetitorBySlug,
getLatestVerifiedDate,
SIM_LATEST_VERIFIED,
Expand All @@ -34,11 +35,22 @@ export async function generateStaticParams() {
function factsToProperties(profile: CompetitorProfile) {
return COMPARISON_SECTIONS.flatMap((section) => {
const group = getFactGroup(profile, section.group)
return section.rows.map((row) => ({
'@type': 'PropertyValue',
name: row.label,
value: group[row.key]?.value ?? 'Unknown',
}))
return section.rows.map((row) => {
const fact = group[row.key]
const qualification =
fact?.confidence === 'estimated'
? 'Estimate: '
: fact?.confidence === 'unknown'
? 'Unverified: '
: ''
return {
'@type': 'PropertyValue',
name: row.label,
value: `${qualification}${fact?.value ?? 'Unknown'}`,
description: fact?.detail,
url: fact?.sources[0]?.url,
}
})
})
}

Expand Down Expand Up @@ -104,6 +116,7 @@ export default async function ComparisonProviderPage({
const latestVerified = new Date(
Math.max(SIM_LATEST_VERIFIED.getTime(), getLatestVerifiedDate(competitor).getTime())
)
const reviewDate = getComparisonReviewDate([simProfile, competitor])

const productComparisonJsonLd = {
'@context': 'https://schema.org',
Expand Down Expand Up @@ -177,16 +190,23 @@ export default async function ComparisonProviderPage({
Sim is the open-source AI workspace where teams build, deploy, and manage AI agents
visually, conversationally, or with code. Here is how Sim compares to{' '}
{competitor.name} on platform architecture, AI capabilities, integrations, pricing,
security, and support. Every fact below is sourced and dated, last verified{' '}
<time dateTime={latestVerified.toISOString().slice(0, 10)}>
{latestVerified.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})}
</time>
.
security, and support.{' '}
{reviewDate ? (
<>
Verified against the cited sources as of{' '}
<time dateTime={reviewDate.toISOString().slice(0, 10)}>
{reviewDate.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})}
</time>
.{' '}
</>
) : null}
Estimates and unverified capabilities are labeled. Plan and deployment restrictions
apply as described in the sources.
</p>
<p className='sr-only'>
Sim is an open-source AI workspace for building, deploying, and managing AI agents.
Expand Down
3 changes: 1 addition & 2 deletions apps/sim/app/(landing)/comparisons/comparison-sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,10 @@ export const COMPARISON_SECTIONS: ComparisonSectionDef[] = [
group: 'security',
title: 'Security & compliance',
rows: [
{ key: 'soc2', label: 'SOC 2' },
{ key: 'compliance', label: 'Compliance' },
{ key: 'dataResidency', label: 'Data residency' },
{ key: 'rbac', label: 'Role-based access control' },
{ key: 'auditLogging', label: 'Audit logging' },
{ key: 'additionalCompliance', label: 'Additional compliance' },
{ key: 'modelAndToolGovernance', label: 'Model & tool governance' },
{ key: 'credentialGovernance', label: 'Credential governance' },
{ key: 'sso', label: 'Single sign-on (SSO)' },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* @vitest-environment node
*/
import type { ReactNode } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import type { Fact } from '@/lib/compare/data'

vi.mock('@sim/emcn', () => ({
cn: (...values: Array<string | false | null | undefined>) => values.filter(Boolean).join(' '),
Tooltip: {
Root: ({ children }: { children: ReactNode }) => <>{children}</>,
Trigger: ({ children }: { children: ReactNode }) => <>{children}</>,
Content: ({ children }: { children: ReactNode }) => (
<span data-testid='source-tooltip'>{children}</span>
),
},
}))

vi.mock('@sim/emcn/icons', () => ({
Check: () => <svg data-icon='check' />,
X: () => <svg data-icon='x' />,
}))

import { FactValue } from '@/app/(landing)/comparisons/components/fact-value/fact-value'

const PRIMARY_SOURCE = {
url: 'https://primary.example/compliance',
label: 'Primary compliance source',
asOf: '2026-09-04',
}

function createFact(overrides: Partial<Fact> = {}): Fact {
return {
value: 'Complete compliance statement',
detail: 'Supporting qualification',
shortValue: 'Compact compliance summary',
confidence: 'verified',
sources: [
PRIMARY_SOURCE,
{
url: 'https://secondary.example/compliance',
label: 'Secondary compliance source',
asOf: '2026-09-04',
},
],
...overrides,
}
}

function withoutScreenReaderText(markup: string): string {
return markup.replace(/<span class="sr-only">.*?<\/span>/, '')
}

function screenReaderText(markup: string): string {
const match = markup.match(/<span class="sr-only">(.*?)<\/span>/)
expect(match).not.toBeNull()
return match?.[1] ?? ''
}

describe('FactValue', () => {
it('shows shortValue while exposing the complete value and detail once to screen readers', () => {
const markup = renderToStaticMarkup(<FactValue fact={createFact()} />)
const visibleMarkup = withoutScreenReaderText(markup)
const accessibleText = screenReaderText(markup)

expect(visibleMarkup).toContain('Compact compliance summary')
expect(visibleMarkup).not.toContain('Complete compliance statement')
expect(accessibleText).toBe('Complete compliance statement. Supporting qualification')
expect(accessibleText.match(/Complete compliance statement/g)).toHaveLength(1)
expect(accessibleText.match(/Supporting qualification/g)).toHaveLength(1)
})

it('falls back to value and renders a fact without detail', () => {
const markup = renderToStaticMarkup(
<FactValue
fact={createFact({
value: 'Fallback visible value',
shortValue: undefined,
detail: undefined,
sources: [],
})}
/>
)

expect(withoutScreenReaderText(markup)).toContain('Fallback visible value')
expect(screenReaderText(markup)).toBe('Fallback visible value')
})

it.each([
['Statement.', 'Statement. Detail'],
['Statement!', 'Statement! Detail'],
['Statement?', 'Statement? Detail'],
['Statement.)', 'Statement.) Detail'],
['Statement.”', 'Statement.” Detail'],
['Statement', 'Statement. Detail'],
])('joins %j and detail without duplicate punctuation', (value, expected) => {
const markup = renderToStaticMarkup(
<FactValue fact={createFact({ value, detail: 'Detail', sources: [] })} />
)

expect(screenReaderText(markup)).toBe(expected)
})

it('uses the URL and label from sources[0] for the visible source link', () => {
const markup = renderToStaticMarkup(<FactValue fact={createFact()} />)
const visibleMarkup = withoutScreenReaderText(markup)

expect(visibleMarkup).toContain(`href="${PRIMARY_SOURCE.url}"`)
expect(visibleMarkup).toContain(`aria-label="${PRIMARY_SOURCE.label} (opens source)"`)
expect(visibleMarkup).not.toContain('https://secondary.example/compliance')
expect(visibleMarkup).not.toContain('Secondary compliance source')
expect(visibleMarkup).toContain('Checked 2026-09-04')
})

it.each(['unknown', 'estimated'] as const)(
'preserves negation without a definitive icon for %s claims',
(confidence) => {
const markup = renderToStaticMarkup(
<FactValue
fact={createFact({
value: 'No: self-hosted deployment',
shortValue: undefined,
confidence,
})}
/>
)
expect(markup).not.toContain('data-icon=')
expect(withoutScreenReaderText(markup)).toContain('No: self-hosted deployment')
expect(markup).toContain(confidence === 'unknown' ? '(unverified)' : '(estimate)')
}
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,21 @@ export interface FactValueProps {
fact: Fact
}

const TERMINAL_PUNCTUATION = /[.!?][\])}'"’”]*$/

/**
* Renders one {@link Fact} for a glancing reader while keeping the full
* granular fact server-rendered for crawlers and AI answer engines.
*
* - A true "Yes"/"No" fact renders as an icon alone (a monochrome check or
* muted cross, no colored pass/fail styling), no visible text, since the
* label column and surrounding context already say what's being asked.
* - Any other fact shows its `shortValue` (a compact, pre-authored
* restatement of `value`), never the full sentence.
* - `Tooltip` here is a cursor-following mini-bubble meant for a short
* one-line label (see its own docs/usages: "Refresh", "last updated: X")
* . It is deliberately NOT used to hold paragraph-length detail text, only
* the compact source citation, which is exactly what it's designed for.
* - When a source exists, the visible glance (icon or `shortValue` text)
* IS the hover/click target for that source, via `SourceLink`, rather
* than a separate info-icon next to every value. One affordance per
* fact keeps a 58-row table from reading as icon-cluttered.
* - A `sr-only` span always carries the complete value, detail, and source
* in the initial server-rendered HTML, independent of hover/JS state, so
* an LLM or crawler reading the page gets full granularity even though a
* human sees only the compact glance.
* Keeps the full value and detail in server-rendered text while displaying
* a compact value. Only verified boolean claims use icons; other claims keep
* their confidence labels and fall back to the original value when needed.
* Source tooltips stay brief so qualifications remain available without hover.
*/
export function FactValue({ fact }: FactValueProps) {
const { status, text } = parseFactValue(fact.value)
const isBoolean = status === 'yes' || status === 'no'
const { status } = parseFactValue(fact.value)
const isBoolean = fact.confidence === 'verified' && (status === 'yes' || status === 'no')
const primarySource = fact.sources[0]

const fullText = [fact.value, fact.detail].filter(Boolean).join('. ')
const detailSeparator = TERMINAL_PUNCTUATION.test(fact.value.trimEnd()) ? ' ' : '. '
const fullText = fact.detail ? `${fact.value}${detailSeparator}${fact.detail}` : fact.value

const glance = isBoolean ? (
status === 'yes' ? (
Expand All @@ -44,9 +31,7 @@ export function FactValue({ fact }: FactValueProps) {
)
) : null

// A pure yes/no fact renders as an icon only. The "why" lives in the
// source link and the sr-only text, not cluttering the glance view.
const shortText = isBoolean ? null : (fact.shortValue ?? text)
const shortText = isBoolean ? null : (fact.shortValue ?? fact.value)

const valueNode = glance ?? (
<span className='truncate text-[var(--text-body)] text-small'>{shortText}</span>
Expand All @@ -61,6 +46,11 @@ export function FactValue({ fact }: FactValueProps) {
) : (
valueNode
)}
{fact.confidence !== 'verified' ? (
<span className='shrink-0 text-[var(--text-muted)] text-caption'>
{fact.confidence === 'estimated' ? '(estimate)' : '(unverified)'}
</span>
) : null}
<span className='sr-only'>{fullText}</span>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,8 @@ export interface SourceLinkProps {
}

/**
* Wraps a fact's visible value (or a card's title) so hovering it directly
* shows a one-line "Source: X" tooltip, and clicking it opens the source,
* rather than a separate info-icon affordance next to every value. One
* hover/click target per fact instead of two keeps the dense comparison
* table and card lists from reading as icon-cluttered. Every {@link FactSource}
* carries a real, publicly reachable URL (enforced by the type), so this
* always renders as a link.
* Uses the visible value or title as the citation target to keep dense tables
* and cards compact. The tooltip includes the source label and review date.
*/
export function SourceLink({ source, children, className }: SourceLinkProps) {
return (
Expand All @@ -34,7 +29,9 @@ export function SourceLink({ source, children, className }: SourceLinkProps) {
{children}
</a>
</Tooltip.Trigger>
<Tooltip.Content>Source: {source.label}</Tooltip.Content>
<Tooltip.Content>
Source: {source.label} · Checked {source.asOf}
</Tooltip.Content>
</Tooltip.Root>
)
}
34 changes: 28 additions & 6 deletions apps/sim/app/(landing)/comparisons/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { simProfile } from '@/lib/compare/data'
import { SITE_URL } from '@/lib/core/utils/urls'
import { buildLandingMetadata } from '@/lib/landing/seo'
import { BrandIconTile } from '@/app/(landing)/comparisons/components/brand-icon-tile'
import { ALL_COMPETITORS, ensurePeriod, lowercaseFirst } from '@/app/(landing)/comparisons/utils'
import {
ALL_COMPETITORS,
ensurePeriod,
getComparisonReviewDate,
lowercaseFirst,
} from '@/app/(landing)/comparisons/utils'
import { ChevronArrow } from '@/app/(landing)/components/chevron-arrow'
import { JsonLd } from '@/app/(landing)/components/json-ld'
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
Expand All @@ -17,12 +22,12 @@ const faqItems = [
{
question: 'How does Sim compare to workflow automation and AI agent platforms?',
answer:
'Sim is an open-source AI workspace where teams build, deploy, and manage AI agents visually, conversationally, or with code. Compared to workflow automation tools like n8n, Zapier, and Make, Sim treats AI agents as first-class building blocks rather than an add-on to data routing, and ships a native knowledge base, MCP support, and an in-editor AI Copilot. Compared to enterprise AI builders like Gumloop, Workato, StackAI, and Vellum, Sim is fully open source (Apache 2.0) and self-hostable, so teams can run it on their own infrastructure.',
'Sim combines a visual workflow canvas, natural-language assistance, multiple model providers, a knowledge base, and MCP support. Its core can run on your own infrastructure. Each comparison examines the specific product surfaces, plans, and deployment options offered by Sim and the other platform.',
},
{
question: 'Is Sim open source?',
answer:
'Yes. Sim is released under the Apache License 2.0 and can be self-hosted via Docker or Kubernetes, or used as a managed cloud-hosted service.',
'Sim’s core is Apache-2.0 licensed and can be self-hosted with Docker or Kubernetes. Enterprise features have separate license terms, and some capabilities, including Chat, use external services.',
},
{
question: 'Which AI agent platform should I choose?',
Expand All @@ -31,15 +36,15 @@ const faqItems = [
},
{
question: 'Is Sim free to use?',
answer: `${ensurePeriod(simProfile.facts.pricing.freeTier.value)} Sim is also free to self-host under the Apache 2.0 license with no seat or usage limits beyond your own infrastructure.`,
answer: `${ensurePeriod(simProfile.facts.pricing.freeTier.value)} You can also self-host the Apache-2.0 core. Infrastructure, model providers, external services, and Enterprise licensing may carry separate costs.`,
},
{
question: 'Does Sim support MCP (Model Context Protocol)?',
answer: `${ensurePeriod(simProfile.facts.aiCapabilities.mcpSupport.value)} Sim can also publish any deployed workflow as its own MCP server, so it works as both an MCP client and an MCP server.`,
answer: `${ensurePeriod(simProfile.facts.aiCapabilities.mcpSupport.value)} ${ensurePeriod(simProfile.facts.integrations.mcpPublishing.value)}`,
},
{
question: 'How many integrations does Sim support?',
answer: `Sim ships ${ensurePeriod(lowercaseFirst(simProfile.facts.integrations.integrationCount.value))} Combined with native MCP client support, teams can extend Sim to any service with a public API, not just the built-in catalog.`,
answer: `Sim lists ${ensurePeriod(lowercaseFirst(simProfile.facts.integrations.integrationCount.value))} MCP, API, and custom code connections provide additional extension options. Service counts and individual tool-action counts are different measures.`,
},
]

Expand All @@ -60,6 +65,7 @@ export const metadata: Metadata = buildLandingMetadata({
})

export default function ComparisonHubPage() {
const reviewDate = getComparisonReviewDate([simProfile, ...ALL_COMPETITORS])
const breadcrumbJsonLd = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
Expand Down Expand Up @@ -116,6 +122,22 @@ export default function ComparisonHubPage() {
Sim is the open-source AI workspace where teams build, deploy, and manage AI agents.
See how Sim compares to workflow automation platforms and AI agent builders on
platform architecture, AI capabilities, integrations, pricing, security, and support.
{reviewDate ? (
<>
{' '}
Verified against the cited sources as of{' '}
<time dateTime={reviewDate.toISOString().slice(0, 10)}>
{reviewDate.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})}
</time>
.
</>
) : null}{' '}
Estimates and unverified capabilities are labeled on each comparison.
</p>
<p className='sr-only'>
This directory lists every Sim vs. competitor comparison page, covering workflow
Expand Down
Loading
Loading