Skip to content

feat(playground): add chat app comparison pages#2779

Merged
smakosh merged 5 commits into
mainfrom
worktree-playground-comparison-pages
Jun 21, 2026
Merged

feat(playground): add chat app comparison pages#2779
smakosh merged 5 commits into
mainfrom
worktree-playground-comparison-pages

Conversation

@smakosh

@smakosh smakosh commented Jun 21, 2026

Copy link
Copy Markdown
Member

What

Adds an SEO-focused set of comparison pages to the Playground (LLM Gateway Chat) under /compare, positioning it against the chat apps people evaluate it against:

  • Single-vendor assistants — ChatGPT, Claude, Google Gemini
  • Multi-model apps — Poe, T3 Chat
  • Answer engines — Perplexity
  • Developer tools — OpenRouter

Each LLM Gateway Chat vs [X] page also targets the [X] alternative keyword, so one strong page captures both "vs" and "alternative" search intent instead of thin duplicate pages.

Why

These are high-intent, bottom-of-funnel keywords ("ChatGPT alternative", "Claude vs …", "Poe alternative"). The pages lean on our real wedge — one subscription, every frontier model, transparent provider-rate credits — while staying honest about where each competitor genuinely wins (ChatGPT's native media, Claude's coding, Gemini's Google integration, Perplexity's cited search, OpenRouter's API economics). Honesty is the conversion strategy on comparison pages; readers verify claims.

How it's built

  • Single source of truthsrc/lib/comparisons.ts holds one structured profile per competitor (at-a-glance table, in-depth paragraph sections with a fair "bottom line", who-should-choose-each, an alternative/migration block, and FAQ). Pricing facts mirror @llmgateway/shared chat plans, so a price change propagates to every page.
  • Dynamic route/compare/[slug] with generateStaticParams + generateMetadata (title, description, canonical, OpenGraph). Each page emits FAQPage and BreadcrumbList JSON-LD.
  • Hub/compare groups competitors by category with a logo line-up and cards.
  • Real brand logoscompare/brand-icons.tsx. ChatGPT/Claude/Gemini/Perplexity reuse the app's existing provider icons; Poe and OpenRouter come from simpleicons.org and the T3 mark from svgl.app. Icons with their own background render full-bleed; transparent marks sit brand-tinted on a neutral chip.
  • Discoverability — a Pricing · Compare vs ChatGPT, Claude & more footer link row on the logged-out welcome card (auth-dialog.tsx), which renders on every studio page. Compare routes are also in sitemap.ts and linked from the home PlaygroundSeoSection.
  • Mobile welcome card fix — the card grew unbounded and pushed the primary CTA off-screen. Rebuilt as a bounded sheet with a scrollable body + pinned action footer, plus a compact 2-column studio grid.

Notes on accuracy

Competitor pricing/positioning was researched against current (mid-2026) sources. To stay defensible, pages avoid stating volatile rate-limit caps as official and lean on durable facts (single-vendor lock-in, opaque points/limits, what each tier costs). Easy to refresh later since everything lives in one data file.

Test plan

  • turbo run build --filter=playground passes; /compare, /compare/[slug], and the logos render (server-rendered).
  • pnpm format / lint-staged clean.
  • Mobile welcome card verified: primary CTA + footer links stay within the viewport.
  • All 7 brand logos verified present in SSR output.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Launched a full /compare catalog plus per-competitor comparison pages with side-by-side tables, detailed sections, “choose if” panels, and an FAQ.
  • SEO & Discoverability
    • Enhanced compare page metadata, added schema.org structured data, updated sitemap coverage, and introduced dedicated Open Graph images for compare routes.
  • UI Improvements
    • Reworked the auth dialog layout and added quick links to Pricing and Compare.

Add SEO comparison pages under /compare positioning LLM Gateway Chat
against ChatGPT, Claude, Gemini, Poe, T3 Chat, Perplexity, and OpenRouter.

- Single source of truth in src/lib/comparisons.ts (one profile per
  competitor with table rows, paragraph sections, who-it's-for, migration,
  and FAQ), mirroring chat-plan pricing from @llmgateway/shared
- Dynamic /compare/[slug] vs-pages with generateStaticParams,
  generateMetadata, plus FAQPage and BreadcrumbList JSON-LD; each page also
  targets '[competitor] alternative' intent
- /compare hub grouping competitors by category
- Reusable logo face-off and comparison-table components
- Wire compare routes into sitemap.ts and link the hub from the home SEO section

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: efc37657-9129-4193-b203-fbad13c35062

📥 Commits

Reviewing files that changed from the base of the PR and between 66a92ce and 9ffaf7e.

📒 Files selected for processing (5)
  • apps/playground/src/app/compare/[slug]/opengraph-image.tsx
  • apps/playground/src/app/compare/opengraph-image.tsx
  • apps/playground/src/app/compare/page.tsx
  • apps/playground/src/components/compare/og-shared.tsx
  • apps/playground/src/lib/comparisons.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/playground/src/lib/comparisons.ts

Walkthrough

Adds a complete /compare section to the playground app: a TypeScript data library (comparisons.ts) defining competitor comparison content, a brand icon system mapping provider slugs to rendering modes, logo tile components (UsTile, ThemTile, FaceOff) with dynamic competitor branding, a ComparisonTable UI component, a /compare index catalog page, a dynamic /compare/[slug] detail page with static params/metadata/JSON-LD, OpenGraph image generation for both index and detail pages, and sitemap/SEO/auth dialog integration.

Changes

Compare Feature

Layer / File(s) Summary
Comparison data model, US constant, and content
apps/playground/src/lib/comparisons.ts
Defines exported TypeScript types (CompetitorCategory, ComparisonRow, ComparisonSection, FaqItem, Comparison), the US product constant with plan pricing, a large comparisons array of per-competitor entries, and getComparison/getComparisonSlugs lookup helpers.
Brand icon system for competitor logos
apps/playground/src/components/compare/brand-icons.tsx
Adds custom SVG icons (PoeIcon, OpenRouterIcon, T3ChatIcon), a BrandRenderMode type, a BRANDS lookup table mapping competitor slugs to icon entries with render modes and optional color classes, and a getBrand(slug) accessor.
Logo faceoff tile components
apps/playground/src/components/compare/logo-faceoff.tsx
Implements UsTile (LLM Gateway branding), ThemTile (dynamic brand rendering via getBrand or initials fallback from competitor name), and FaceOff composition with centered "vs" label; ThemTile now accepts slug and competitor to render brand-specific tiles.
ComparisonTable grid component
apps/playground/src/components/compare/comparison-table.tsx
Adds a responsive comparison grid with branded column headers (UsTile and ThemTile), feature rows with optional usWins emerald highlighting, and alternating row backgrounds.
Compare index catalog page
apps/playground/src/app/compare/page.tsx
Adds the /compare index with static metadata, category-ordered comparison card grid, competitor logo lineup hero, CTAs, and a catalog note referencing US.modelCount.
Dynamic compare detail page
apps/playground/src/app/compare/[slug]/page.tsx
Implements /compare/[slug] with generateStaticParams, generateMetadata (canonical, OpenGraph, Twitter), notFound guard, JSON-LD FAQ/breadcrumb script injection, and full page sections: hero with FaceOff, ComparisonTable, deep sections, choose-us/them panels, migration/switch copy, pricing recap, expandable FAQ, final CTA, and related comparisons grid.
OpenGraph image generation
apps/playground/src/components/compare/og-shared.tsx, apps/playground/src/app/compare/opengraph-image.tsx, apps/playground/src/app/compare/[slug]/opengraph-image.tsx
Adds shared OG constants/components (LgMark, Pill, clipText); generates static OG image for /compare index with header, hero, competitor lineup, and footer; generates dynamic OG images for /compare/[slug] detail pages with competitor monogram and formatted credits.
Sitemap and SEO link integration
apps/playground/src/app/sitemap.ts, apps/playground/src/components/seo/playground-seo-section.tsx
Extends staticEntries in sitemap.ts with /pricing, /compare index, and dynamic /compare/{slug} entries; adds a /compare related-tool link to the chat SEO section.
Auth dialog navigation to compare
apps/playground/src/components/playground/auth-dialog.tsx
Restructures auth dialog with scrollable content and pinned footer; adds links to /pricing and /compare below the signup CTA.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Possibly related PRs

  • theopenco/llmgateway#2306: Modifies playground-seo-section.tsx to add related tool navigation, directly preceding the /compare link entry added in this PR.
  • theopenco/llmgateway#2759: Also modifies apps/playground/src/components/playground/auth-dialog.tsx to adjust responsive layout and styling for smaller screens.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(playground): add chat app comparison pages' directly and clearly describes the main change—adding comparison pages for chat apps to the Playground. It accurately summarizes the primary objective without being vague or off-topic.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-playground-comparison-pages

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
apps/playground/src/components/compare/comparison-table.tsx (1)

23-23: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Remove section-label JSX comments to match repo TSX guidelines.

These comments are purely structural and can be dropped to keep the file aligned with the project’s “no unnecessary code comments” rule.

As per coding guidelines: **/*.{ts,tsx} → “No unnecessary code comments”.

Also applies to: 41-41

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/components/compare/comparison-table.tsx` at line 23,
Remove the unnecessary JSX section-label comments from the comparison-table.tsx
file. Specifically, delete the comment at line 23 that reads `{/* Column headers
*/}` and the similar comment at line 41, as these purely structural comments do
not align with the project's coding guidelines that prohibit unnecessary code
comments in TSX files.

Source: Coding guidelines

apps/playground/src/app/compare/[slug]/page.tsx (1)

119-119: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Remove section-heading JSX comments to comply with TSX style rules.

These comments are organizational labels only; removing them keeps the page aligned with the repository’s comment policy.

As per coding guidelines: **/*.{ts,tsx} → “No unnecessary code comments”.

Also applies to: 140-140, 150-150, 175-175, 188-188, 229-229, 270-270, 294-294, 350-350, 370-370, 392-392

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/app/compare/`[slug]/page.tsx at line 119, Remove all
unnecessary JSX organizational comments from the page component in the compare
page file. Specifically, delete the section-heading comments like {/* Hero */},
{/* ... */} and similar organizational labels at the specified line numbers
(119, 140, 150, 175, 188, 229, 270, 294, 350, 370, 392) as they violate the
repository's policy against unnecessary code comments in TSX files and should
not be included as they provide no functional value or documentation.

Source: Coding guidelines

apps/playground/src/app/compare/page.tsx (1)

49-49: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Drop non-essential JSX section comments in this page.

The section labels are not adding implementation detail and should be removed for consistency with the TSX comment guideline.

As per coding guidelines: **/*.{ts,tsx} → “No unnecessary code comments”.

Also applies to: 85-85, 117-117, 172-172

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/app/compare/page.tsx` at line 49, The file contains
non-essential JSX section comments like {/* Hero */} that serve only as
organizational labels without providing implementation details, which violates
the TSX comment guidelines. Remove all these section label comments found at
lines 49, 85, 117, 172 and any similar comments that function only as section
markers or organizational dividers, while retaining any comments that explain
implementation logic or non-obvious code behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/playground/src/lib/comparisons.ts`:
- Around line 89-104: The `US` object in this file has a manually duplicated
`plans` property that mirrors the chat plans from the shared module at
packages/shared/src/chat-plans.ts. This duplication causes pricing updates in
the shared source to not automatically propagate to the comparison page. Import
the chat plans from the shared module instead of hardcoding them, and derive the
`US.plans` property directly from that shared source so that plan updates
automatically sync across the application.

---

Nitpick comments:
In `@apps/playground/src/app/compare/`[slug]/page.tsx:
- Line 119: Remove all unnecessary JSX organizational comments from the page
component in the compare page file. Specifically, delete the section-heading
comments like {/* Hero */}, {/* ... */} and similar organizational labels at the
specified line numbers (119, 140, 150, 175, 188, 229, 270, 294, 350, 370, 392)
as they violate the repository's policy against unnecessary code comments in TSX
files and should not be included as they provide no functional value or
documentation.

In `@apps/playground/src/app/compare/page.tsx`:
- Line 49: The file contains non-essential JSX section comments like {/* Hero
*/} that serve only as organizational labels without providing implementation
details, which violates the TSX comment guidelines. Remove all these section
label comments found at lines 49, 85, 117, 172 and any similar comments that
function only as section markers or organizational dividers, while retaining any
comments that explain implementation logic or non-obvious code behavior.

In `@apps/playground/src/components/compare/comparison-table.tsx`:
- Line 23: Remove the unnecessary JSX section-label comments from the
comparison-table.tsx file. Specifically, delete the comment at line 23 that
reads `{/* Column headers */}` and the similar comment at line 41, as these
purely structural comments do not align with the project's coding guidelines
that prohibit unnecessary code comments in TSX files.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 01b61842-c2ee-4470-b380-54f36380eef7

📥 Commits

Reviewing files that changed from the base of the PR and between f65aa33 and 7784297.

📒 Files selected for processing (7)
  • apps/playground/src/app/compare/[slug]/page.tsx
  • apps/playground/src/app/compare/page.tsx
  • apps/playground/src/app/sitemap.ts
  • apps/playground/src/components/compare/comparison-table.tsx
  • apps/playground/src/components/compare/logo-faceoff.tsx
  • apps/playground/src/components/seo/playground-seo-section.tsx
  • apps/playground/src/lib/comparisons.ts

Comment thread apps/playground/src/lib/comparisons.ts
Follow-ups to the comparison pages:

- Render real brand SVG logos in the comparison face-offs and table. OpenAI,
  Anthropic, Google AI and Perplexity reuse the app's existing provider icons;
  Poe and OpenRouter come from simpleicons.org and the T3 mark from svgl.app
  (new compare/brand-icons.tsx). Icons with their own background render
  full-bleed; transparent marks sit tinted on a neutral chip. Drops the
  placeholder monogram/tileClass data fields.
- Make the pages discoverable: add a Pricing · Compare footer link row to the
  logged-out welcome card (auth-dialog.tsx), which renders on every studio.
- Fix the welcome card on mobile: it grew unbounded and pushed the primary CTA
  off-screen. Rebuilt as a bounded sheet with a scrollable body and a pinned
  action footer, plus a compact 2-column studio grid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/playground/src/components/compare/logo-faceoff.tsx (1)

102-112: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider removing className from FaceOffProps if not needed.

FaceOffProps extends TileProps, which includes className, but the FaceOff component does not destructure or use className. This creates an unused prop. If FaceOff is not intended to accept custom styling, consider defining FaceOffProps without extending TileProps:

interface FaceOffProps {
  slug: string;
  competitor: string;
  size?: number;
  radius?: number;
}

Alternatively, if future styling is planned, destructure and apply className to the wrapper div at line 114.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/components/compare/logo-faceoff.tsx` around lines 102 -
112, The `FaceOffProps` interface extends `TileProps` which includes a
`className` property, but the `FaceOff` component function does not destructure
or use this prop, making it an unused property. Either remove the `TileProps`
extension from the `FaceOffProps` interface and define it with only the needed
properties (slug, competitor, size, and radius), or if styling support is
intended, destructure the `className` parameter in the `FaceOff` function and
apply it to the wrapper div element to utilize the inherited property.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/playground/src/components/compare/logo-faceoff.tsx`:
- Around line 79-82: The initials variable in the logo-faceoff component's
initials generation logic may result in an empty string or a single character if
the competitor name lacks sufficient alphanumeric characters. After the current
chain of operations (replace, slice, toUpperCase) on the competitor string, add
a check to ensure initials has a minimum length or provide a sensible default
fallback value (such as a single character or a placeholder) when the result is
empty or shorter than expected.

---

Nitpick comments:
In `@apps/playground/src/components/compare/logo-faceoff.tsx`:
- Around line 102-112: The `FaceOffProps` interface extends `TileProps` which
includes a `className` property, but the `FaceOff` component function does not
destructure or use this prop, making it an unused property. Either remove the
`TileProps` extension from the `FaceOffProps` interface and define it with only
the needed properties (slug, competitor, size, and radius), or if styling
support is intended, destructure the `className` parameter in the `FaceOff`
function and apply it to the wrapper div element to utilize the inherited
property.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 67e8ff10-e0fb-4a0c-8dea-8dedfda1ea39

📥 Commits

Reviewing files that changed from the base of the PR and between 7784297 and 66a92ce.

📒 Files selected for processing (7)
  • apps/playground/src/app/compare/[slug]/page.tsx
  • apps/playground/src/app/compare/page.tsx
  • apps/playground/src/components/compare/brand-icons.tsx
  • apps/playground/src/components/compare/comparison-table.tsx
  • apps/playground/src/components/compare/logo-faceoff.tsx
  • apps/playground/src/components/playground/auth-dialog.tsx
  • apps/playground/src/lib/comparisons.ts
💤 Files with no reviewable changes (1)
  • apps/playground/src/lib/comparisons.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/playground/src/app/compare/page.tsx
  • apps/playground/src/components/compare/comparison-table.tsx
  • apps/playground/src/app/compare/[slug]/page.tsx

Comment on lines +79 to +82
const initials = competitor
.replace(/[^A-Za-z0-9]/g, "")
.slice(0, 2)
.toUpperCase();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Consider handling edge cases in the initials fallback.

If competitor contains no alphanumeric characters, initials will be an empty string. If it contains only one alphanumeric character, initials will be a single character. While unlikely for typical competitor names, adding a minimum-length check or default value would make the fallback more robust.

🛡️ Suggested defensive handling
 const initials = competitor
   .replace(/[^A-Za-z0-9]/g, "")
   .slice(0, 2)
   .toUpperCase();
+if (initials.length === 0) {
+  // Fallback to first two chars of original competitor name if no alphanumeric
+  return (
+    <div
+      className={cn(
+        "flex shrink-0 items-center justify-center border border-border/60 bg-muted font-bold tracking-tight text-muted-foreground",
+        className,
+      )}
+      style={{
+        width: size,
+        height: size,
+        borderRadius: radius,
+        fontSize: size * 0.32,
+      }}
+      aria-label={competitor}
+    >
+      {competitor.slice(0, 2).toUpperCase() || "??"}
+    </div>
+  );
+}
 return (
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/components/compare/logo-faceoff.tsx` around lines 79 -
82, The initials variable in the logo-faceoff component's initials generation
logic may result in an empty string or a single character if the competitor name
lacks sufficient alphanumeric characters. After the current chain of operations
(replace, slice, toUpperCase) on the competitor string, add a check to ensure
initials has a minimum length or provide a sensible default fallback value (such
as a single character or a placeholder) when the result is empty or shorter than
expected.

smakosh and others added 3 commits June 21, 2026 23:32
- Dynamic OpenGraph images: opengraph-image.tsx for /compare and
  /compare/[slug] via next/og (shared compare/og-shared.tsx), matching the
  share-image look. Wire the index twitter:image to the dynamic image too.
- Derive US.plans from @llmgateway/shared (CHAT_PLAN_PRICES +
  CHAT_PLAN_CREDITS_MULTIPLIERS) instead of hardcoding, so pricing changes in
  the shared source propagate to the comparison pages automatically.
- SEO: trim all meta descriptions to <=160 chars (were 188-217 and would
  truncate in SERPs) and add ItemList schema to the /compare hub.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@smakosh smakosh enabled auto-merge June 21, 2026 21:43
@smakosh smakosh added this pull request to the merge queue Jun 21, 2026
Merged via the queue into main with commit 93c3325 Jun 21, 2026
11 checks passed
@smakosh smakosh deleted the worktree-playground-comparison-pages branch June 21, 2026 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant