Skip to content

Commit ad5ed28

Browse files
authored
improvement(comparisons): render every comparison question section through one component (#9693)
The 'Is Sim better than' answer now renders through QuestionSections with the intro questions, the hero lead shares one answer paragraph and one verified line across both variants, and answersQuestionsInPlace names the rule that hides the FAQ block and sources FAQ data from the page's questions.
1 parent 85e2fd8 commit ad5ed28

4 files changed

Lines changed: 50 additions & 79 deletions

File tree

‎apps/sim/app/(landing)/comparisons/[provider]/page.tsx‎

Lines changed: 23 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import { QuestionSections } from '@/app/(landing)/comparisons/components/questio
1818
import { getLibraryPostsForCompetitor } from '@/app/(landing)/comparisons/library-links'
1919
import {
2020
ALL_COMPETITORS,
21-
betterThanQuestion,
21+
answersQuestionsInPlace,
2222
buildBottomLine,
2323
buildComparisonFaqs,
2424
getCompetitorBySlug,
2525
getLatestVerifiedDate,
26+
getQuestionSectionGroups,
2627
SIM_LATEST_VERIFIED,
2728
} from '@/app/(landing)/comparisons/utils'
2829
import { BackLink, ContentRelatedPosts } from '@/app/(landing)/components'
@@ -96,6 +97,13 @@ export default async function ComparisonProviderPage({
9697
}
9798

9899
const faqs = buildComparisonFaqs(competitor)
100+
const questionGroups = getQuestionSectionGroups(competitor)
101+
const { lead } = questionGroups
102+
const leadAnswer = competitor.leadAnswer ? (
103+
<p className='max-w-[720px] text-[var(--text-body)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
104+
<ProseText prose={competitor.leadAnswer} />
105+
</p>
106+
) : null
99107
const relatedPosts = await getLibraryPostsForCompetitor(competitor)
100108
const verdict = buildBottomLine(competitor)
101109
const CompetitorIcon = competitor.brand?.icon
@@ -200,7 +208,7 @@ export default async function ComparisonProviderPage({
200208
>
201209
Sim vs {competitor.name}
202210
</h1>
203-
{competitor.leadQuestion && competitor.leadAnswer ? (
211+
{lead ? (
204212
<section
205213
id='lead-question'
206214
aria-labelledby='lead-question-heading'
@@ -210,31 +218,19 @@ export default async function ComparisonProviderPage({
210218
id='lead-question-heading'
211219
className='max-w-[720px] text-balance text-[var(--text-primary)] text-xl leading-[120%] tracking-[-0.02em] lg:text-2xl'
212220
>
213-
{competitor.leadQuestion}
221+
{lead.question}
214222
</h2>
215-
<p className='max-w-[720px] text-[var(--text-body)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
216-
<ProseText prose={competitor.leadAnswer} />
217-
</p>
218-
<p className='max-w-[720px] text-[var(--text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
219-
Every fact below is sourced and dated, last verified {verifiedDate}.
220-
</p>
223+
{leadAnswer}
221224
</section>
222225
) : (
223-
<>
224-
{competitor.leadAnswer ? (
225-
<p className='max-w-[720px] text-[var(--text-body)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
226-
<ProseText prose={competitor.leadAnswer} />
227-
</p>
228-
) : null}
229-
<p className='max-w-[720px] text-[var(--text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
230-
Sim is the open-source AI workspace where teams build, deploy, and manage AI
231-
agents visually, conversationally, or with code. Here is how Sim compares to{' '}
232-
{competitor.name} on platform architecture, AI capabilities, integrations,
233-
pricing, security, and support. Every fact below is sourced and dated, last
234-
verified {verifiedDate}.
235-
</p>
236-
</>
226+
leadAnswer
237227
)}
228+
<p className='max-w-[720px] text-[var(--text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
229+
{lead
230+
? null
231+
: `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. `}
232+
Every fact below is sourced and dated, last verified {verifiedDate}.
233+
</p>
238234
<p className='sr-only'>
239235
Sim is an open-source AI workspace for building, deploying, and managing AI agents.
240236
This page compares Sim to {competitor.name} across platform architecture, AI
@@ -254,25 +250,7 @@ export default async function ComparisonProviderPage({
254250
<div className='h-px w-full bg-[var(--border)]' />
255251
</>
256252
) : null}
257-
{competitor.betterThanAnswer ? (
258-
<>
259-
<section aria-labelledby='better-than-heading' className='px-6 py-10'>
260-
<h2
261-
id='better-than-heading'
262-
className='mb-4 text-[var(--text-primary)] text-xl leading-[100%] tracking-[-0.02em] lg:text-2xl'
263-
>
264-
{betterThanQuestion(competitor)}
265-
</h2>
266-
<p className='max-w-[720px] text-[var(--text-body)] text-small leading-[150%]'>
267-
<ProseText prose={competitor.betterThanAnswer} />
268-
</p>
269-
</section>
270-
<div className='h-px w-full bg-[var(--border)]' />
271-
</>
272-
) : null}
273-
{competitor.questions?.intro ? (
274-
<QuestionSections questions={competitor.questions.intro} idPrefix='intro-question' />
275-
) : null}
253+
<QuestionSections questions={questionGroups.opening} idPrefix='opening-question' />
276254
<div className='grid grid-cols-1 sm:grid-cols-2'>
277255
<section
278256
aria-labelledby='what-is-sim-heading'
@@ -318,12 +296,7 @@ export default async function ComparisonProviderPage({
318296
<div className='h-px w-full bg-[var(--border)]' />
319297
</>
320298
) : null}
321-
{competitor.questions?.glance ? (
322-
<QuestionSections
323-
questions={competitor.questions.glance}
324-
idPrefix='glance-question'
325-
/>
326-
) : null}
299+
<QuestionSections questions={questionGroups.glance} idPrefix='glance-question' />
327300

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

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

399-
{competitor.questions?.closing ? (
400-
<QuestionSections
401-
questions={competitor.questions.closing}
402-
idPrefix='closing-question'
403-
/>
404-
) : null}
372+
<QuestionSections questions={questionGroups.closing} idPrefix='closing-question' />
405373

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

461-
{competitor.questions ? null : (
429+
{answersQuestionsInPlace(competitor) ? null : (
462430
<section aria-labelledby='faq-heading' className='px-6 py-10'>
463431
<h2
464432
id='faq-heading'

‎apps/sim/app/(landing)/comparisons/utils.ts‎

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,15 @@ export function buildBottomLine(competitor: CompetitorProfile): ComparisonVerdic
120120
* question/answer pair is independently quotable per the landing GEO rules,
121121
* and every answer names "Sim" and the competitor explicitly. Every answer
122122
* draws on a real, sourced {@link Fact} field rather than a generic claim,
123-
* and no two questions repeat the same answer. A profile that answers its buyer questions as
124-
* page sections gets those questions instead, so its FAQ data matches what the page shows.
123+
* and no two questions repeat the same answer. A profile that {@link answersQuestionsInPlace}
124+
* gets those questions instead, so its FAQ data matches what the page shows.
125125
*/
126126
export function buildComparisonFaqs(competitor: CompetitorProfile): ComparisonFaq[] {
127-
if (competitor.questions) {
128-
return getQuestionSections(competitor).map(({ question, answer }) => ({
129-
question,
130-
answer: proseToText(answer),
131-
}))
127+
if (answersQuestionsInPlace(competitor)) {
128+
const { lead, opening, glance, closing } = getQuestionSectionGroups(competitor)
129+
return [...(lead ? [lead] : []), ...opening, ...glance, ...closing].map(
130+
({ question, answer }) => ({ question, answer: proseToText(answer) })
131+
)
132132
}
133133
const name = competitor.name
134134
const facts = competitor.facts
@@ -178,27 +178,32 @@ export function buildComparisonFaqs(competitor: CompetitorProfile): ComparisonFa
178178
return faqs
179179
}
180180

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

186190
/**
187-
* The buyer questions the page answers in their own sections, grouped by position: `opening`
188-
* (the lead, "better than", and intro questions) ahead of the product definitions, `glance`
191+
* The buyer questions the page answers in their own sections, by position: `lead` in the hero,
192+
* `opening` (the "better than" and intro questions) ahead of the product definitions, `glance`
189193
* after the at-a-glance table, and `closing` after the documented limitations.
190194
*/
191195
export function getQuestionSectionGroups(competitor: CompetitorProfile): {
196+
lead: ComparisonQuestion | undefined
192197
opening: ComparisonQuestion[]
193198
glance: ComparisonQuestion[]
194199
closing: ComparisonQuestion[]
195200
} {
196201
const { leadQuestion, leadAnswer, betterThanAnswer, questions } = competitor
197202
return {
203+
lead: leadQuestion && leadAnswer ? { question: leadQuestion, answer: leadAnswer } : undefined,
198204
opening: [
199-
...(leadQuestion && leadAnswer ? [{ question: leadQuestion, answer: leadAnswer }] : []),
200205
...(betterThanAnswer
201-
? [{ question: betterThanQuestion(competitor), answer: betterThanAnswer }]
206+
? [{ question: `Is Sim better than ${competitor.name}?`, answer: betterThanAnswer }]
202207
: []),
203208
...(questions?.intro ?? []),
204209
],
@@ -207,12 +212,6 @@ export function getQuestionSectionGroups(competitor: CompetitorProfile): {
207212
}
208213
}
209214

210-
/** Every question from {@link getQuestionSectionGroups}, in page order. */
211-
function getQuestionSections(competitor: CompetitorProfile): ComparisonQuestion[] {
212-
const { opening, glance, closing } = getQuestionSectionGroups(competitor)
213-
return [...opening, ...glance, ...closing]
214-
}
215-
216215
function proseToText(prose: Prose): string {
217216
return prose.map((segment) => (typeof segment === 'string' ? segment : segment.text)).join('')
218217
}

‎apps/sim/app/llms-full.txt/route.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ function isoDate(date: Date): string {
8080
return date.toISOString().slice(0, 10)
8181
}
8282

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

87+
/** The key facts of one `/comparisons/{id}` page as markdown, from the same profile data. */
8888
function comparisonMarkdown(competitor: CompetitorProfile): string {
8989
const verdict = buildBottomLine(competitor)
9090
const questions = getQuestionSectionGroups(competitor)
@@ -104,7 +104,11 @@ function comparisonMarkdown(competitor: CompetitorProfile): string {
104104
`### Sim vs ${competitor.name}`,
105105
`URL: ${toSiteUrl(`/comparisons/${competitor.id}`)} · Facts last verified ${isoDate(verified)}`,
106106
`${competitor.name}: ${competitor.oneLiner}`,
107-
competitor.leadAnswer && !competitor.leadQuestion ? proseToMarkdown(competitor.leadAnswer) : '',
107+
questions.lead
108+
? questionMarkdown(questions.lead)
109+
: competitor.leadAnswer
110+
? proseToMarkdown(competitor.leadAnswer)
111+
: '',
108112
...questions.opening.map(questionMarkdown),
109113
competitor.glance
110114
? [

‎apps/sim/lib/compare/data/types.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,8 @@ export interface CompetitorProfile {
258258
*/
259259
leadAnswer?: Prose
260260
/**
261-
* The question {@link leadAnswer} answers. When set, the lead renders under it as the page's
262-
* first section, in place of the generic introduction.
261+
* The question {@link leadAnswer} answers. When set, the hero shows the lead answer under it as
262+
* a question heading, in place of the generic introduction.
263263
*/
264264
leadQuestion?: string
265265
/**

0 commit comments

Comments
 (0)