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
78 changes: 23 additions & 55 deletions apps/sim/app/(landing)/comparisons/[provider]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ import { QuestionSections } from '@/app/(landing)/comparisons/components/questio
import { getLibraryPostsForCompetitor } from '@/app/(landing)/comparisons/library-links'
import {
ALL_COMPETITORS,
betterThanQuestion,
answersQuestionsInPlace,
buildBottomLine,
buildComparisonFaqs,
getCompetitorBySlug,
getLatestVerifiedDate,
getQuestionSectionGroups,
SIM_LATEST_VERIFIED,
} from '@/app/(landing)/comparisons/utils'
import { BackLink, ContentRelatedPosts } from '@/app/(landing)/components'
Expand Down Expand Up @@ -96,6 +97,13 @@ export default async function ComparisonProviderPage({
}

const faqs = buildComparisonFaqs(competitor)
const questionGroups = getQuestionSectionGroups(competitor)
const { lead } = questionGroups
const leadAnswer = competitor.leadAnswer ? (
<p className='max-w-[720px] text-[var(--text-body)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
<ProseText prose={competitor.leadAnswer} />
</p>
) : null
const relatedPosts = await getLibraryPostsForCompetitor(competitor)
const verdict = buildBottomLine(competitor)
const CompetitorIcon = competitor.brand?.icon
Expand Down Expand Up @@ -200,7 +208,7 @@ export default async function ComparisonProviderPage({
>
Sim vs {competitor.name}
</h1>
{competitor.leadQuestion && competitor.leadAnswer ? (
{lead ? (
<section
id='lead-question'
aria-labelledby='lead-question-heading'
Expand All @@ -210,31 +218,19 @@ export default async function ComparisonProviderPage({
id='lead-question-heading'
className='max-w-[720px] text-balance text-[var(--text-primary)] text-xl leading-[120%] tracking-[-0.02em] lg:text-2xl'
>
{competitor.leadQuestion}
{lead.question}
</h2>
<p className='max-w-[720px] text-[var(--text-body)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
<ProseText prose={competitor.leadAnswer} />
</p>
<p className='max-w-[720px] text-[var(--text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
Every fact below is sourced and dated, last verified {verifiedDate}.
</p>
{leadAnswer}
</section>
) : (
<>
{competitor.leadAnswer ? (
<p className='max-w-[720px] text-[var(--text-body)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
<ProseText prose={competitor.leadAnswer} />
</p>
) : null}
<p className='max-w-[720px] text-[var(--text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
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 {verifiedDate}.
</p>
</>
leadAnswer
)}
<p className='max-w-[720px] text-[var(--text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
{lead
? null
: `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 {verifiedDate}.
</p>
<p className='sr-only'>
Sim is an open-source AI workspace for building, deploying, and managing AI agents.
This page compares Sim to {competitor.name} across platform architecture, AI
Expand All @@ -254,25 +250,7 @@ export default async function ComparisonProviderPage({
<div className='h-px w-full bg-[var(--border)]' />
</>
) : null}
{competitor.betterThanAnswer ? (
<>
<section aria-labelledby='better-than-heading' className='px-6 py-10'>
<h2
id='better-than-heading'
className='mb-4 text-[var(--text-primary)] text-xl leading-[100%] tracking-[-0.02em] lg:text-2xl'
>
{betterThanQuestion(competitor)}
</h2>
<p className='max-w-[720px] text-[var(--text-body)] text-small leading-[150%]'>
<ProseText prose={competitor.betterThanAnswer} />
</p>
</section>
<div className='h-px w-full bg-[var(--border)]' />
</>
) : null}
{competitor.questions?.intro ? (
<QuestionSections questions={competitor.questions.intro} idPrefix='intro-question' />
) : null}
<QuestionSections questions={questionGroups.opening} idPrefix='opening-question' />
<div className='grid grid-cols-1 sm:grid-cols-2'>
<section
aria-labelledby='what-is-sim-heading'
Expand Down Expand Up @@ -318,12 +296,7 @@ export default async function ComparisonProviderPage({
<div className='h-px w-full bg-[var(--border)]' />
</>
) : null}
{competitor.questions?.glance ? (
<QuestionSections
questions={competitor.questions.glance}
idPrefix='glance-question'
/>
) : null}
<QuestionSections questions={questionGroups.glance} idPrefix='glance-question' />

<section aria-labelledby='comparison-table-heading' className='px-6 pt-10 pb-4'>
<h2
Expand Down Expand Up @@ -396,12 +369,7 @@ export default async function ComparisonProviderPage({

<div className='h-px w-full bg-[var(--border)]' />

{competitor.questions?.closing ? (
<QuestionSections
questions={competitor.questions.closing}
idPrefix='closing-question'
/>
) : null}
<QuestionSections questions={questionGroups.closing} idPrefix='closing-question' />

<section aria-labelledby='bottom-line-heading' className='px-6 py-10'>
<h2
Expand Down Expand Up @@ -458,7 +426,7 @@ export default async function ComparisonProviderPage({
</ul>
</section>

{competitor.questions ? null : (
{answersQuestionsInPlace(competitor) ? null : (
<section aria-labelledby='faq-heading' className='px-6 py-10'>
<h2
id='faq-heading'
Expand Down
39 changes: 19 additions & 20 deletions apps/sim/app/(landing)/comparisons/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,15 @@ export function buildBottomLine(competitor: CompetitorProfile): ComparisonVerdic
* question/answer pair is independently quotable per the landing GEO rules,
* and every answer names "Sim" and the competitor explicitly. Every answer
* draws on a real, sourced {@link Fact} field rather than a generic claim,
* and no two questions repeat the same answer. A profile that answers its buyer questions as
* page sections gets those questions instead, so its FAQ data matches what the page shows.
* and no two questions repeat the same answer. A profile that {@link answersQuestionsInPlace}
* gets those questions instead, so its FAQ data matches what the page shows.
*/
export function buildComparisonFaqs(competitor: CompetitorProfile): ComparisonFaq[] {
if (competitor.questions) {
return getQuestionSections(competitor).map(({ question, answer }) => ({
question,
answer: proseToText(answer),
}))
if (answersQuestionsInPlace(competitor)) {
const { lead, opening, glance, closing } = getQuestionSectionGroups(competitor)
return [...(lead ? [lead] : []), ...opening, ...glance, ...closing].map(
({ question, answer }) => ({ question, answer: proseToText(answer) })
)
}
const name = competitor.name
const facts = competitor.facts
Expand Down Expand Up @@ -178,27 +178,32 @@ export function buildComparisonFaqs(competitor: CompetitorProfile): ComparisonFa
return faqs
}

/** Heading of the section answering {@link CompetitorProfile.betterThanAnswer}. */
export function betterThanQuestion(competitor: CompetitorProfile): string {
return `Is Sim better than ${competitor.name}?`
/**
* Whether the page answers its buyer questions as sections (see
* {@link CompetitorProfile.questions}). Such a page omits the FAQ block, and its FAQ structured
* data lists those questions instead.
*/
export function answersQuestionsInPlace(competitor: CompetitorProfile): boolean {
return competitor.questions !== undefined
}

/**
* The buyer questions the page answers in their own sections, grouped by position: `opening`
* (the lead, "better than", and intro questions) ahead of the product definitions, `glance`
* The buyer questions the page answers in their own sections, by position: `lead` in the hero,
* `opening` (the "better than" and intro questions) ahead of the product definitions, `glance`
* after the at-a-glance table, and `closing` after the documented limitations.
*/
export function getQuestionSectionGroups(competitor: CompetitorProfile): {
lead: ComparisonQuestion | undefined
opening: ComparisonQuestion[]
glance: ComparisonQuestion[]
closing: ComparisonQuestion[]
} {
const { leadQuestion, leadAnswer, betterThanAnswer, questions } = competitor
return {
lead: leadQuestion && leadAnswer ? { question: leadQuestion, answer: leadAnswer } : undefined,
opening: [
...(leadQuestion && leadAnswer ? [{ question: leadQuestion, answer: leadAnswer }] : []),
...(betterThanAnswer
? [{ question: betterThanQuestion(competitor), answer: betterThanAnswer }]
? [{ question: `Is Sim better than ${competitor.name}?`, answer: betterThanAnswer }]
: []),
...(questions?.intro ?? []),
],
Expand All @@ -207,12 +212,6 @@ export function getQuestionSectionGroups(competitor: CompetitorProfile): {
}
}

/** Every question from {@link getQuestionSectionGroups}, in page order. */
function getQuestionSections(competitor: CompetitorProfile): ComparisonQuestion[] {
const { opening, glance, closing } = getQuestionSectionGroups(competitor)
return [...opening, ...glance, ...closing]
}

function proseToText(prose: Prose): string {
return prose.map((segment) => (typeof segment === 'string' ? segment : segment.text)).join('')
}
Expand Down
8 changes: 6 additions & 2 deletions apps/sim/app/llms-full.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ function isoDate(date: Date): string {
return date.toISOString().slice(0, 10)
}

/** The key facts of one `/comparisons/{id}` page as markdown, from the same profile data. */
function questionMarkdown({ question, answer }: ComparisonQuestion): string {
return `**${question}**\n\n${proseToMarkdown(answer)}`
}

/** The key facts of one `/comparisons/{id}` page as markdown, from the same profile data. */
function comparisonMarkdown(competitor: CompetitorProfile): string {
const verdict = buildBottomLine(competitor)
const questions = getQuestionSectionGroups(competitor)
Expand All @@ -104,7 +104,11 @@ function comparisonMarkdown(competitor: CompetitorProfile): string {
`### Sim vs ${competitor.name}`,
`URL: ${toSiteUrl(`/comparisons/${competitor.id}`)} · Facts last verified ${isoDate(verified)}`,
`${competitor.name}: ${competitor.oneLiner}`,
competitor.leadAnswer && !competitor.leadQuestion ? proseToMarkdown(competitor.leadAnswer) : '',
questions.lead
? questionMarkdown(questions.lead)
: competitor.leadAnswer
? proseToMarkdown(competitor.leadAnswer)
: '',
...questions.opening.map(questionMarkdown),
competitor.glance
? [
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/compare/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,8 @@ export interface CompetitorProfile {
*/
leadAnswer?: Prose
/**
* The question {@link leadAnswer} answers. When set, the lead renders under it as the page's
* first section, in place of the generic introduction.
* The question {@link leadAnswer} answers. When set, the hero shows the lead answer under it as
* a question heading, in place of the generic introduction.
*/
leadQuestion?: string
/**
Expand Down
Loading