Skip to content

Commit 919e975

Browse files
committed
improvement(comparisons): answer Copilot Studio buyer questions in place with an at-a-glance table
The Copilot Studio comparison now leads with its answer under the question buyers ask, answers the tracked buyer questions in their own sections, adds a sourced six-row at-a-glance table, and links one alternatives article instead of repeating it. The page no longer appends a generated FAQ block that restated the same material; its FAQ structured data lists the page's own questions. Profile fields are optional, so every other comparison page renders as before.
1 parent ffa0225 commit 919e975

12 files changed

Lines changed: 563 additions & 41 deletions

File tree

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

Lines changed: 86 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import { BrandIconTile, SimIconTile } from '@/app/(landing)/comparisons/componen
1111
import { ComparisonCards } from '@/app/(landing)/comparisons/components/comparison-cards'
1212
import { ComparisonLinkRow } from '@/app/(landing)/comparisons/components/comparison-link-row'
1313
import { ComparisonTable } from '@/app/(landing)/comparisons/components/comparison-table'
14+
import { GlanceTable } from '@/app/(landing)/comparisons/components/glance-table'
1415
import { MigrationSection } from '@/app/(landing)/comparisons/components/migration-section'
1516
import { ProseText } from '@/app/(landing)/comparisons/components/prose-text'
17+
import { QuestionSections } from '@/app/(landing)/comparisons/components/question-sections'
1618
import { getLibraryPostsForCompetitor } from '@/app/(landing)/comparisons/library-links'
1719
import {
1820
ALL_COMPETITORS,
@@ -117,6 +119,17 @@ export default async function ComparisonProviderPage({
117119
Math.max(SIM_LATEST_VERIFIED.getTime(), getLatestVerifiedDate(competitor).getTime())
118120
)
119121

122+
const verifiedDate = (
123+
<time dateTime={latestVerified.toISOString().slice(0, 10)}>
124+
{latestVerified.toLocaleDateString('en-US', {
125+
month: 'long',
126+
day: 'numeric',
127+
year: 'numeric',
128+
timeZone: 'UTC',
129+
})}
130+
</time>
131+
)
132+
120133
const productComparisonJsonLd = {
121134
'@context': 'https://schema.org',
122135
'@type': 'ItemList',
@@ -186,32 +199,43 @@ export default async function ComparisonProviderPage({
186199
>
187200
Sim vs {competitor.name}
188201
</h1>
189-
{competitor.leadAnswer ? (
190-
<p className='max-w-[720px] text-[var(--text-body)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
191-
<ProseText prose={competitor.leadAnswer} />
192-
</p>
193-
) : null}
194-
<p className='max-w-[720px] text-[var(--text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
195-
Sim is the open-source AI workspace where teams build, deploy, and manage AI agents
196-
visually, conversationally, or with code. Here is how Sim compares to{' '}
197-
{competitor.name} on platform architecture, AI capabilities, integrations, pricing,
198-
security, and support. Every fact below is sourced and dated, last verified{' '}
199-
<time dateTime={latestVerified.toISOString().slice(0, 10)}>
200-
{latestVerified.toLocaleDateString('en-US', {
201-
month: 'long',
202-
day: 'numeric',
203-
year: 'numeric',
204-
timeZone: 'UTC',
205-
})}
206-
</time>
207-
.
208-
</p>
209-
<p className='sr-only'>
210-
Sim is an open-source AI workspace for building, deploying, and managing AI agents.
211-
This page compares Sim to {competitor.name} across platform architecture, AI
212-
capabilities, integrations, pricing, security and compliance, observability, and
213-
support, using sourced, dated facts for buyers evaluating both platforms.
214-
</p>
202+
{competitor.leadQuestion && competitor.leadAnswer ? (
203+
<section aria-labelledby='lead-question-heading' className='flex flex-col gap-3'>
204+
<h2
205+
id='lead-question-heading'
206+
className='max-w-[720px] text-balance text-[var(--text-primary)] text-xl leading-[120%] tracking-[-0.02em] lg:text-2xl'
207+
>
208+
{competitor.leadQuestion}
209+
</h2>
210+
<p className='max-w-[720px] text-[var(--text-body)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
211+
<ProseText prose={competitor.leadAnswer} />
212+
</p>
213+
<p className='max-w-[720px] text-[var(--text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
214+
Every fact below is sourced and dated, last verified {verifiedDate}.
215+
</p>
216+
</section>
217+
) : (
218+
<>
219+
{competitor.leadAnswer ? (
220+
<p className='max-w-[720px] text-[var(--text-body)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
221+
<ProseText prose={competitor.leadAnswer} />
222+
</p>
223+
) : null}
224+
<p className='max-w-[720px] text-[var(--text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
225+
Sim is the open-source AI workspace where teams build, deploy, and manage AI
226+
agents visually, conversationally, or with code. Here is how Sim compares to{' '}
227+
{competitor.name} on platform architecture, AI capabilities, integrations,
228+
pricing, security, and support. Every fact below is sourced and dated, last
229+
verified {verifiedDate}.
230+
</p>
231+
<p className='sr-only'>
232+
Sim is an open-source AI workspace for building, deploying, and managing AI
233+
agents. This page compares Sim to {competitor.name} across platform architecture,
234+
AI capabilities, integrations, pricing, security and compliance, observability,
235+
and support, using sourced, dated facts for buyers evaluating both platforms.
236+
</p>
237+
</>
238+
)}
215239
</div>
216240
</div>
217241

@@ -241,6 +265,9 @@ export default async function ComparisonProviderPage({
241265
<div className='h-px w-full bg-[var(--border)]' />
242266
</>
243267
) : null}
268+
{competitor.questions?.intro ? (
269+
<QuestionSections questions={competitor.questions.intro} idPrefix='intro-question' />
270+
) : null}
244271
<div className='grid grid-cols-1 sm:grid-cols-2'>
245272
<section
246273
aria-labelledby='what-is-sim-heading'
@@ -280,6 +307,19 @@ export default async function ComparisonProviderPage({
280307

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

310+
{competitor.glance ? (
311+
<>
312+
<GlanceTable rows={competitor.glance} competitorName={competitor.name} />
313+
<div className='h-px w-full bg-[var(--border)]' />
314+
</>
315+
) : null}
316+
{competitor.questions?.glance ? (
317+
<QuestionSections
318+
questions={competitor.questions.glance}
319+
idPrefix='glance-question'
320+
/>
321+
) : null}
322+
283323
<section aria-labelledby='comparison-table-heading' className='px-6 pt-10 pb-4'>
284324
<h2
285325
id='comparison-table-heading'
@@ -351,6 +391,13 @@ export default async function ComparisonProviderPage({
351391

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

394+
{competitor.questions?.closing ? (
395+
<QuestionSections
396+
questions={competitor.questions.closing}
397+
idPrefix='closing-question'
398+
/>
399+
) : null}
400+
354401
<section aria-labelledby='bottom-line-heading' className='px-6 py-10'>
355402
<h2
356403
id='bottom-line-heading'
@@ -406,17 +453,19 @@ export default async function ComparisonProviderPage({
406453
</ul>
407454
</section>
408455

409-
<section aria-labelledby='faq-heading' className='px-6 py-10'>
410-
<h2
411-
id='faq-heading'
412-
className='mb-4 text-[var(--text-primary)] text-xl leading-[100%] tracking-[-0.02em] lg:text-2xl'
413-
>
414-
Frequently asked questions
415-
</h2>
416-
<div>
417-
<LandingFAQ faqs={faqs} />
418-
</div>
419-
</section>
456+
{competitor.questions ? null : (
457+
<section aria-labelledby='faq-heading' className='px-6 py-10'>
458+
<h2
459+
id='faq-heading'
460+
className='mb-4 text-[var(--text-primary)] text-xl leading-[100%] tracking-[-0.02em] lg:text-2xl'
461+
>
462+
Frequently asked questions
463+
</h2>
464+
<div>
465+
<LandingFAQ faqs={faqs} />
466+
</div>
467+
</section>
468+
)}
420469
</div>
421470
</div>
422471

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { cn } from '@sim/emcn'
2+
import type { GlanceRow } from '@/lib/compare/data'
3+
import { ProseText } from '@/app/(landing)/comparisons/components/prose-text'
4+
5+
interface GlanceTableProps {
6+
rows: GlanceRow[]
7+
competitorName: string
8+
}
9+
10+
const HEADER_CELL_CLASS =
11+
'border-[var(--border)] border-l bg-[var(--surface-1)] px-4 py-3 font-medium text-[var(--text-muted)] text-small'
12+
const VALUE_CELL_CLASS =
13+
'align-top text-[var(--text-body)] text-small leading-[150%] max-lg:block max-lg:px-4 max-lg:py-1 lg:border-[var(--border)] lg:border-t lg:border-l lg:px-4 lg:py-3'
14+
const STACKED_LABEL_CLASS = 'block font-medium text-[var(--text-muted)] text-caption lg:hidden'
15+
16+
/**
17+
* The main buying decision points as one table, each cell a sourced sentence with its citation
18+
* links. Below `lg` each row stacks with labeled values, as the feature-by-feature table does.
19+
* Server-rendered, so answer engines can extract the table without hydration.
20+
*/
21+
export function GlanceTable({ rows, competitorName }: GlanceTableProps) {
22+
const heading = `Sim vs ${competitorName}: at a glance`
23+
return (
24+
<section aria-labelledby='glance-heading' className='px-6 py-10'>
25+
<h2
26+
id='glance-heading'
27+
className='mb-4 text-[var(--text-primary)] text-xl leading-[100%] tracking-[-0.02em] lg:text-2xl'
28+
>
29+
{heading}
30+
</h2>
31+
<div className='w-full overflow-hidden rounded-xl border border-[var(--border)]'>
32+
<table className='w-full border-separate border-spacing-0 text-left max-lg:block lg:table-fixed'>
33+
<caption className='sr-only'>{heading}</caption>
34+
<colgroup>
35+
<col className='w-[22%]' />
36+
<col />
37+
<col />
38+
</colgroup>
39+
<thead className='max-lg:hidden'>
40+
<tr>
41+
<th
42+
scope='col'
43+
className='bg-[var(--surface-1)] px-4 py-3 font-medium text-[var(--text-muted)] text-small'
44+
>
45+
Decision point
46+
</th>
47+
<th scope='col' className={HEADER_CELL_CLASS}>
48+
Sim
49+
</th>
50+
<th scope='col' className={HEADER_CELL_CLASS}>
51+
{competitorName}
52+
</th>
53+
</tr>
54+
</thead>
55+
<tbody className='max-lg:block'>
56+
{rows.map((row, index) => (
57+
<tr
58+
key={row.label}
59+
className={cn(
60+
'max-lg:block',
61+
index > 0 && 'max-lg:border-[var(--border)] max-lg:border-t'
62+
)}
63+
>
64+
<th
65+
scope='row'
66+
className='align-top font-medium text-[var(--text-primary)] text-small leading-[150%] max-lg:block max-lg:px-4 max-lg:pt-3 max-lg:pb-1 lg:border-[var(--border)] lg:border-t lg:px-4 lg:py-3'
67+
>
68+
{row.label}
69+
</th>
70+
<td className={VALUE_CELL_CLASS}>
71+
<span className={STACKED_LABEL_CLASS}>Sim</span>
72+
<ProseText prose={row.sim} />
73+
</td>
74+
<td className={cn(VALUE_CELL_CLASS, 'max-lg:pb-3')}>
75+
<span className={STACKED_LABEL_CLASS}>{competitorName}</span>
76+
<ProseText prose={row.competitor} />
77+
</td>
78+
</tr>
79+
))}
80+
</tbody>
81+
</table>
82+
</div>
83+
</section>
84+
)
85+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { GlanceTable } from './glance-table'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { QuestionSections } from './question-sections'
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Fragment } from 'react'
2+
import type { ComparisonQuestion } from '@/lib/compare/data'
3+
import { ProseText } from '@/app/(landing)/comparisons/components/prose-text'
4+
5+
interface QuestionSectionsProps {
6+
questions: ComparisonQuestion[]
7+
/** Prefix for each section's heading id, unique on the page. */
8+
idPrefix: string
9+
}
10+
11+
/**
12+
* One section per buyer question, its heading worded as the buyer asks it and its answer
13+
* directly beneath, each followed by a divider. Server-rendered, so answer engines can quote
14+
* every answer on its own.
15+
*/
16+
export function QuestionSections({ questions, idPrefix }: QuestionSectionsProps) {
17+
return questions.map(({ question, answer }, index) => {
18+
const headingId = `${idPrefix}-${index}-heading`
19+
return (
20+
<Fragment key={question}>
21+
<section aria-labelledby={headingId} className='px-6 py-10'>
22+
<h2
23+
id={headingId}
24+
className='mb-4 text-[var(--text-primary)] text-xl leading-[100%] tracking-[-0.02em] lg:text-2xl'
25+
>
26+
{question}
27+
</h2>
28+
<p className='max-w-[720px] text-[var(--text-body)] text-small leading-[150%]'>
29+
<ProseText prose={answer} />
30+
</p>
31+
</section>
32+
<div className='h-px w-full bg-[var(--border)]' />
33+
</Fragment>
34+
)
35+
})
36+
}

‎apps/sim/app/(landing)/comparisons/library-links.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,16 @@ function topByScore<T>(items: T[], score: (item: T) => number): T[] {
7676
}
7777

7878
/**
79-
* Library articles about, linking to, or naming `competitor`, strongest match first, then
80-
* newest first.
79+
* The profile's `relatedReading` articles when it sets them; otherwise library articles about,
80+
* linking to, or naming `competitor`, strongest match first, then newest first.
8181
*/
8282
export async function getLibraryPostsForCompetitor(
8383
competitor: CompetitorProfile
8484
): Promise<ContentMeta[]> {
85+
if (competitor.relatedReading) {
86+
const postsBySlug = new Map((await getAllPostMeta()).map((post) => [post.slug, post]))
87+
return competitor.relatedReading.flatMap((slug) => postsBySlug.get(slug) ?? [])
88+
}
8589
const patterns = buildMentionPatterns(competitor)
8690
const [posts, linkingPosts] = await Promise.all([
8791
getAllPostMeta(),

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
type ComparisonQuestion,
23
type CompetitorProfile,
34
claudeCoworkProfile,
45
crewaiProfile,
@@ -12,6 +13,7 @@ import {
1213
n8nProfile,
1314
openaiAgentkitProfile,
1415
openClawProfile,
16+
type Prose,
1517
pipedreamProfile,
1618
powerAutomateProfile,
1719
retoolProfile,
@@ -118,9 +120,16 @@ export function buildBottomLine(competitor: CompetitorProfile): ComparisonVerdic
118120
* question/answer pair is independently quotable per the landing GEO rules,
119121
* and every answer names "Sim" and the competitor explicitly. Every answer
120122
* draws on a real, sourced {@link Fact} field rather than a generic claim,
121-
* and no two questions repeat the same answer.
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.
122125
*/
123126
export function buildComparisonFaqs(competitor: CompetitorProfile): ComparisonFaq[] {
127+
if (competitor.questions) {
128+
return getQuestionSections(competitor).map(({ question, answer }) => ({
129+
question,
130+
answer: proseToText(answer),
131+
}))
132+
}
124133
const name = competitor.name
125134
const facts = competitor.facts
126135
const faqs: ComparisonFaq[] = [
@@ -169,6 +178,24 @@ export function buildComparisonFaqs(competitor: CompetitorProfile): ComparisonFa
169178
return faqs
170179
}
171180

181+
/**
182+
* Every buyer question the page answers in its own section, in page order: the lead question,
183+
* then the intro, glance, and closing questions.
184+
*/
185+
export function getQuestionSections(competitor: CompetitorProfile): ComparisonQuestion[] {
186+
const { leadQuestion, leadAnswer, questions } = competitor
187+
return [
188+
...(leadQuestion && leadAnswer ? [{ question: leadQuestion, answer: leadAnswer }] : []),
189+
...(questions?.intro ?? []),
190+
...(questions?.glance ?? []),
191+
...(questions?.closing ?? []),
192+
]
193+
}
194+
195+
function proseToText(prose: Prose): string {
196+
return prose.map((segment) => (typeof segment === 'string' ? segment : segment.text)).join('')
197+
}
198+
172199
function buildKeyDifferenceAnswer(competitor: CompetitorProfile): string {
173200
const topFeature = competitor.standoutFeatures[0]
174201
const topLimitation = competitor.limitations[0]

0 commit comments

Comments
 (0)