From 318681ece1b5770f5695a546cb2d6a8f58960c15 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 4 Nov 2026 17:53:23 -0800 Subject: [PATCH] improvement(comparisons): render every comparison question section through one component 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. --- .../(landing)/comparisons/[provider]/page.tsx | 78 ++++++------------- apps/sim/app/(landing)/comparisons/utils.ts | 39 +++++----- apps/sim/app/llms-full.txt/route.ts | 8 +- apps/sim/lib/compare/data/types.ts | 4 +- 4 files changed, 50 insertions(+), 79 deletions(-) diff --git a/apps/sim/app/(landing)/comparisons/[provider]/page.tsx b/apps/sim/app/(landing)/comparisons/[provider]/page.tsx index 000827730f5..f0c18db4341 100644 --- a/apps/sim/app/(landing)/comparisons/[provider]/page.tsx +++ b/apps/sim/app/(landing)/comparisons/[provider]/page.tsx @@ -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' @@ -96,6 +97,13 @@ export default async function ComparisonProviderPage({ } const faqs = buildComparisonFaqs(competitor) + const questionGroups = getQuestionSectionGroups(competitor) + const { lead } = questionGroups + const leadAnswer = competitor.leadAnswer ? ( +

+ +

+ ) : null const relatedPosts = await getLibraryPostsForCompetitor(competitor) const verdict = buildBottomLine(competitor) const CompetitorIcon = competitor.brand?.icon @@ -200,7 +208,7 @@ export default async function ComparisonProviderPage({ > Sim vs {competitor.name} - {competitor.leadQuestion && competitor.leadAnswer ? ( + {lead ? (
- {competitor.leadQuestion} + {lead.question} -

- -

-

- Every fact below is sourced and dated, last verified {verifiedDate}. -

+ {leadAnswer}
) : ( - <> - {competitor.leadAnswer ? ( -

- -

- ) : 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}. -

- + leadAnswer )} +

+ {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}. +

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 @@ -254,25 +250,7 @@ export default async function ComparisonProviderPage({

) : null} - {competitor.betterThanAnswer ? ( - <> -
-

- {betterThanQuestion(competitor)} -

-

- -

-
-
- - ) : null} - {competitor.questions?.intro ? ( - - ) : null} +
) : null} - {competitor.questions?.glance ? ( - - ) : null} +

- {competitor.questions?.closing ? ( - - ) : null} +

- {competitor.questions ? null : ( + {answersQuestionsInPlace(competitor) ? null : (

({ - 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 @@ -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 ?? []), ], @@ -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('') } diff --git a/apps/sim/app/llms-full.txt/route.ts b/apps/sim/app/llms-full.txt/route.ts index f955ffdf4c7..f1ed68e438a 100644 --- a/apps/sim/app/llms-full.txt/route.ts +++ b/apps/sim/app/llms-full.txt/route.ts @@ -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) @@ -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 ? [ diff --git a/apps/sim/lib/compare/data/types.ts b/apps/sim/lib/compare/data/types.ts index fa9af61bb9e..68e82d57727 100644 --- a/apps/sim/lib/compare/data/types.ts +++ b/apps/sim/lib/compare/data/types.ts @@ -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 /**