Skip to content

Commit 8f55771

Browse files
authored
improvement(comparisons): answer Copilot Studio buyer questions in place with an at-a-glance table (#9688)
* 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. * improvement(comparisons): keep the hero summary, section ids, and better-than question in the page's FAQ data Restores the sr-only hero summary for pages that lead with a question, gives every new section an id, includes the 'Is Sim better than' answer in the page's question list so FAQ data matches the page, and asserts every answer and at-a-glance value in the landing SEO E2E. * improvement(comparisons): order llms-full comparison questions as the page does Opening questions lead, glance questions follow the at-a-glance table, and the switching question follows the feature table, so each answer comes after the evidence it builds on.
1 parent e89400f commit 8f55771

12 files changed

Lines changed: 610 additions & 38 deletions

File tree

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

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ 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,
21+
betterThanQuestion,
1922
buildBottomLine,
2023
buildComparisonFaqs,
2124
getCompetitorBySlug,
@@ -117,6 +120,17 @@ export default async function ComparisonProviderPage({
117120
Math.max(SIM_LATEST_VERIFIED.getTime(), getLatestVerifiedDate(competitor).getTime())
118121
)
119122

123+
const verifiedDate = (
124+
<time dateTime={latestVerified.toISOString().slice(0, 10)}>
125+
{latestVerified.toLocaleDateString('en-US', {
126+
month: 'long',
127+
day: 'numeric',
128+
year: 'numeric',
129+
timeZone: 'UTC',
130+
})}
131+
</time>
132+
)
133+
120134
const productComparisonJsonLd = {
121135
'@context': 'https://schema.org',
122136
'@type': 'ItemList',
@@ -186,26 +200,41 @@ export default async function ComparisonProviderPage({
186200
>
187201
Sim vs {competitor.name}
188202
</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>
203+
{competitor.leadQuestion && competitor.leadAnswer ? (
204+
<section
205+
id='lead-question'
206+
aria-labelledby='lead-question-heading'
207+
className='flex flex-col gap-3'
208+
>
209+
<h2
210+
id='lead-question-heading'
211+
className='max-w-[720px] text-balance text-[var(--text-primary)] text-xl leading-[120%] tracking-[-0.02em] lg:text-2xl'
212+
>
213+
{competitor.leadQuestion}
214+
</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>
221+
</section>
222+
) : (
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+
</>
237+
)}
209238
<p className='sr-only'>
210239
Sim is an open-source AI workspace for building, deploying, and managing AI agents.
211240
This page compares Sim to {competitor.name} across platform architecture, AI
@@ -232,7 +261,7 @@ export default async function ComparisonProviderPage({
232261
id='better-than-heading'
233262
className='mb-4 text-[var(--text-primary)] text-xl leading-[100%] tracking-[-0.02em] lg:text-2xl'
234263
>
235-
Is Sim better than {competitor.name}?
264+
{betterThanQuestion(competitor)}
236265
</h2>
237266
<p className='max-w-[720px] text-[var(--text-body)] text-small leading-[150%]'>
238267
<ProseText prose={competitor.betterThanAnswer} />
@@ -241,6 +270,9 @@ export default async function ComparisonProviderPage({
241270
<div className='h-px w-full bg-[var(--border)]' />
242271
</>
243272
) : null}
273+
{competitor.questions?.intro ? (
274+
<QuestionSections questions={competitor.questions.intro} idPrefix='intro-question' />
275+
) : null}
244276
<div className='grid grid-cols-1 sm:grid-cols-2'>
245277
<section
246278
aria-labelledby='what-is-sim-heading'
@@ -280,6 +312,19 @@ export default async function ComparisonProviderPage({
280312

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

315+
{competitor.glance ? (
316+
<>
317+
<GlanceTable rows={competitor.glance} competitorName={competitor.name} />
318+
<div className='h-px w-full bg-[var(--border)]' />
319+
</>
320+
) : null}
321+
{competitor.questions?.glance ? (
322+
<QuestionSections
323+
questions={competitor.questions.glance}
324+
idPrefix='glance-question'
325+
/>
326+
) : null}
327+
283328
<section aria-labelledby='comparison-table-heading' className='px-6 pt-10 pb-4'>
284329
<h2
285330
id='comparison-table-heading'
@@ -351,6 +396,13 @@ export default async function ComparisonProviderPage({
351396

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

399+
{competitor.questions?.closing ? (
400+
<QuestionSections
401+
questions={competitor.questions.closing}
402+
idPrefix='closing-question'
403+
/>
404+
) : null}
405+
354406
<section aria-labelledby='bottom-line-heading' className='px-6 py-10'>
355407
<h2
356408
id='bottom-line-heading'
@@ -406,17 +458,19 @@ export default async function ComparisonProviderPage({
406458
</ul>
407459
</section>
408460

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>
461+
{competitor.questions ? null : (
462+
<section aria-labelledby='faq-heading' className='px-6 py-10'>
463+
<h2
464+
id='faq-heading'
465+
className='mb-4 text-[var(--text-primary)] text-xl leading-[100%] tracking-[-0.02em] lg:text-2xl'
466+
>
467+
Frequently asked questions
468+
</h2>
469+
<div>
470+
<LandingFAQ faqs={faqs} />
471+
</div>
472+
</section>
473+
)}
420474
</div>
421475
</div>
422476

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 id='glance' 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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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 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 sectionId = `${idPrefix}-${index}`
19+
const headingId = `${sectionId}-heading`
20+
return (
21+
<Fragment key={question}>
22+
<section id={sectionId} aria-labelledby={headingId} className='px-6 py-10'>
23+
<h2
24+
id={headingId}
25+
className='mb-4 text-[var(--text-primary)] text-xl leading-[100%] tracking-[-0.02em] lg:text-2xl'
26+
>
27+
{question}
28+
</h2>
29+
<p className='max-w-[720px] text-[var(--text-body)] text-small leading-[150%]'>
30+
<ProseText prose={answer} />
31+
</p>
32+
</section>
33+
<div className='h-px w-full bg-[var(--border)]' />
34+
</Fragment>
35+
)
36+
})
37+
}

‎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(),

0 commit comments

Comments
 (0)