# SaaS Comparison Page: Honest vs Pages That Convert

> Build a SaaS comparison page that ranks and converts: a fair verdict, a dated feature table, rows where the rival wins, a switch path and React templates.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Nov 5, 2026
- Category: [Build it: screens and apps](https://wingo-ui.com/blog/category/build)
- Reading time: 10 min
- Canonical: https://wingo-ui.com/blog/saas-comparison-page

## TL;DR

A SaaS comparison page converts when it reads like a fair review: a one-line verdict that says who each product suits, a grouped feature table with dated sources, the rows where the rival wins, a price for a real team size, and the steps to switch. Build the vs page with the Wingo UI Compare Page template, one static route per competitor, and the alternative page with the Switch Page template and its import wizard. Keep the facts in one data file with a checked date so the page stays true.

A SaaS comparison page is where a buyer who already knows your name decides between you and the tool they are using now. Most of them are written by marketing and read like it: every row a check for you and a cross for them, no date, no source. Developers and ops leads open the competitor's pricing page in the next tab, find one wrong row, and leave. This tutorial builds the two pages that meet that buyer, an "Acme vs Rival" page and a "Rival alternative" page that switches people over, in Next.js 16 with two React templates, and keeps every claim sourced and dated.

## What is the difference between a vs page and an alternative page?

They answer different searches, so they need different pages. A vs page answers "acme vs rival": the reader is weighing two products and wants a fair side by side. An alternative page answers "rival alternative": the reader has decided to leave and wants options and a way out. A third page, the switch from competitor page, serves the people who already picked you and need to move their data.

| Page | Typical search | Reader | Must contain | Template |
|---|---|---|---|---|
| Vs page | "acme vs rival" | Choosing between two | Verdict, feature table, price for their team size, FAQ | [Compare Page](https://wingo-ui.com/templates/compare-page) |
| Alternative page | "rival alternative" | Leaving a tool | Why people leave, honest fit, what moves over, how long | [Switch Page](https://wingo-ui.com/templates/switch-page) |
| Switch page | "import from rival" | Already decided | Import steps, field mapping, what does not move | [Switch Page](https://wingo-ui.com/templates/switch-page) |

Keep them all under one folder such as `/compare/` and `/switch/`, one URL per competitor. A single page that compares you with five tools ranks for none of their names.

## What should an honest SaaS comparison page contain?

Seven parts, in the order a buyer asks the questions:

1. **A verdict in one sentence** that names who each product suits, including the case where the rival is the better fit.
2. **Quick facts**: price per user and the two or three differences that decide most deals.
3. **A feature table grouped the way buyers think** (planning, docs, security, pricing), with a note on every value that needs one.
4. **The rows where the rival wins**, left in the table.
5. **A price for a real team size**, with the competitor's plan named and linked.
6. **Switching steps**: what moves, how long it takes, what stays behind.
7. **An FAQ** about switching, and one call to action.

This is close to what Google asks of review content. Its guide to [writing high quality reviews](https://developers.google.com/search/docs/specialty/ecommerce/write-high-quality-reviews) (read November 2026) asks you to discuss benefits and drawbacks, explain what sets each option apart, and say which one suits which use. In the US, the FTC's [statement on comparative advertising](https://www.ftc.gov/legal-library/browse/statement-policy-regarding-comparative-advertising) (issued August 1979, read November 2026) encourages naming competitors, as long as the comparison is clear and does not mislead.

Not everyone keeps the table. Unbounce's [guide to SaaS comparison pages](https://unbounce.com/landing-page-examples/saas-comparison-pages/) (updated April 14, 2026) argues most tables do not help B2B buyers: shared features add clutter, minor gaps crowd out the ones that decide deals, and the rows skew toward what the vendor chose to show. We keep the table and answer those three faults: the Only show differences switch hides matching rows, a note explains any value that needs context, and the rows you lose stay in. Buyers scan for the one feature they need, and a table becomes credible the moment it shows a row where the rival wins.

## How do I install the comparison and switch templates?

Both are page templates: the source is copied into your project, so every section and string is yours to change.

```bash
npx wingo-ui@latest add compare-page switch-page
```

That adds `components/templates/compare-page.tsx` and `compare-page-data.ts`, the switch page and its data file, and the sections they compose: the [FAQ Section](https://wingo-ui.com/components/faq-section), the [Feature Grid](https://wingo-ui.com/components/feature-grid) for the reasons people switch, the [Slider](https://wingo-ui.com/components/slider) for team size, the [Steps](https://wingo-ui.com/components/steps) of the import wizard, and the marketing header and footer. If you came looking for a competitor comparison page template, this is one in the shadcn sense: data in a plain TypeScript file, layout in a component you own.

Try the vs page template first. It ships with three fictional rivals.

> Live demo (Compare Page): Pick Boardify in the switcher, turn on Only show differences, then drag the team size slider: both yearly costs roll to new totals, and on a phone the table turns into one card per feature. Try it at [Compare Page](https://wingo-ui.com/templates/compare-page) and install it with `npx wingo-ui@latest add compare-page`.

## How do I model a real competitor in the data file?

The template reads a `Competitor` object: `{ id, name, icon, tagline, verdict, facts, reasons, groups, pricing, steps, quotes, faq }`. Write one per rival, and add two fields of your own: the date you checked their public pages and the links you checked.

```ts
// app/(marketing)/compare/[competitor]/competitors.ts
import { FileText, Layers } from "lucide-react";
import type { Competitor } from "@/components/templates/compare-page-data";

export type SourcedCompetitor = Competitor & {
  checked: string;
  sources: { label: string; href: string }[];
};

export const competitors: SourcedCompetitor[] = [
  {
    id: "rival",
    name: "Rival",
    // a generic glyph, never their logo
    icon: Layers,
    tagline: "Project tracking for agencies",
    verdict:
      "Rival costs less above 12 people and has time tracking built in. Acme suits smaller teams that want docs and tasks in one place.",
    facts: [
      { label: "Price per user", acme: "$12", competitor: "$8 + $49 / mo" },
      { label: "Time tracking", acme: "Integration", competitor: "Built in" },
      { label: "Docs built in", acme: "Yes", competitor: "No" },
    ],
    reasons: [
      {
        icon: FileText,
        title: "Docs next to the work",
        description: "Specs and meeting notes live beside the tasks they describe.",
      },
    ],
    groups: [
      {
        id: "planning",
        label: "Planning",
        rows: [
          { id: "boards", label: "Boards and lists", acme: true, competitor: true },
          {
            id: "time",
            label: "Time tracking",
            acme: { value: "partial", note: "Through the Harvest integration." },
            competitor: true,
          },
          {
            id: "offline",
            label: "Offline editing on mobile",
            acme: false,
            competitor: { value: true, note: "Rival's apps sync when you reconnect." },
          },
        ],
      },
    ],
    pricing: { plan: "Team", perSeat: 8, platformFee: 49 },
    steps: [
      { title: "Export from Rival", description: "Settings, Export, CSV with comments.", duration: "1 min" },
      { title: "Map the columns", description: "Check the fields Acme could not match.", duration: "2 min" },
      { title: "Invite the team", description: "Everyone keeps their tasks and history.", duration: "30 sec" },
    ],
    quotes: [],
    faq: [
      {
        question: "When is Rival the better choice?",
        answer: "For teams above 12 people who bill by the hour: it costs less at that size and tracks time without an integration.",
      },
    ],
    checked: "2026-11-01",
    sources: [{ label: "Rival pricing page", href: "https://rival.example/pricing" }],
  },
];
```

A cell is `true`, `false`, `"partial"`, a short string, or `{ value, note }`. Use notes generously: "Enterprise only" is a fact, a bare cross is an argument. Write the verdict, the table rows and the FAQ answer about the rival's strengths before you write your reasons. If you cannot find a row where the rival wins, you have not finished the research.

The pricing model is honest by default. The yearly cost is `(perSeat + add-ons) × max(seats, minSeats) × 12`, plus the platform fee times 12. With Acme at $12 per seat, Rival at $8 plus $49 a month costs $1,548 a year for 10 people against Acme's $1,440, and $2,988 for 25 people against Acme's $3,600. From 13 people up Rival is cheaper, and at 25 the band under the price cards reads "Rival costs $612 less a year at this size". Leave that in: the reader would do the same math anyway.

Leave `quotes` empty until real customers who switched agree to be quoted. The template's sample quotes are placeholders, and an invented testimonial on a comparison page is the fastest way to lose the reader.

## How do I give each competitor its own route in Next.js?

Give each rival one statically generated page whose title matches the search. The competitor objects hold icon components and the label overrides are functions, and neither can be passed as props from a server component, so a small client file renders the template while the server page around it handles metadata and the source list.

```tsx
// app/(marketing)/compare/[competitor]/page.tsx
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { competitors } from "./competitors";
import { CompareView } from "./compare-view";

export const dynamicParams = false;

export function generateStaticParams() {
  return competitors.map((c) => ({ competitor: c.id }));
}

type Props = { params: Promise<{ competitor: string }> };

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { competitor } = await params;
  const rival = competitors.find((c) => c.id === competitor);
  if (!rival) return {};
  return {
    title: `Acme vs ${rival.name}: Features, Pricing and Migration`,
    description: rival.verdict,
    alternates: { canonical: `/compare/${rival.id}` },
  };
}

export default async function ComparePageRoute({ params }: Props) {
  const { competitor } = await params;
  const rival = competitors.find((c) => c.id === competitor);
  if (!rival) notFound();
  return (
    <>
      <CompareView id={rival.id} />
      <section className="mx-auto max-w-3xl px-4 py-12 text-sm text-muted-foreground">
        <h2 className="text-base font-semibold text-foreground">Sources, checked {rival.checked}</h2>
        <ul className="mt-3 list-disc space-y-1 pl-5">
          {rival.sources.map((source) => (
            <li key={source.href}>
              <a className="underline underline-offset-4" href={source.href}>
                {source.label}
              </a>
            </li>
          ))}
        </ul>
      </section>
    </>
  );
}
```

```tsx
// app/(marketing)/compare/[competitor]/compare-view.tsx
"use client";

import Link from "next/link";
import { useRouter } from "next/navigation";
import { ComparePage } from "@/components/templates/compare-page";
import { competitors } from "./competitors";

export function CompareView({ id }: { id: string }) {
  const router = useRouter();
  const rival = competitors.find((c) => c.id === id)!;
  return (
    <ComparePage
      header={false}
      footer={false}
      linkComponent={Link}
      competitors={competitors}
      competitor={id}
      onCompetitorChange={(next) => router.push(`/compare/${next}`)}
      showSwitcher={competitors.length > 1}
      showTestimonials={rival.quotes.length > 0}
      acmePricing={{ plan: "Pro", perSeat: 12 }}
      getImportHref={(c) => `/switch?from=${c.id}`}
      labels={{
        tableDescription: (name) =>
          `Checked against ${name}'s public pricing and docs on ${rival.checked}. Where ${name} is ahead, the table says so.`,
      }}
    />
  );
}
```

Replace Acme with your product name through `labels.brand`, `labels.switcher` and every label function that contains it: `title`, `reasonsDescription`, `tableTitle`, `pricingDescription`, `save` and `ctaTitle`. Rewrite the sample claims too, because they become your claims once the page is live: `migrationTitle` promises an import in 3 minutes, `migrationDescription` says every comment and attachment moves, `reasonsDescription` speaks of six reasons, and `ctaDescription` says Acme is free for up to 10 people.

The FAQ inside the template does not emit structured data. You can add a `FAQPage` script from `rival.faq` in the server page for machine readers, but do not expect stars in search: Google [limited FAQ rich results](https://developers.google.com/search/blog/2023/08/howto-faq-changes) to well-known government and health sites in August 2023.

## How do I build the alternative page and the switch flow?

The alternative page has one job beyond the vs page: make leaving feel small. The Switch Page template is built as that page and works as an alternative page template too: a hero with the import drawn as it happens, tiles for the tools people come from, a four-step wizard (connect, map fields, choose what to bring, import), a "What moves over" matrix with full, partial and unsupported marks, and a translator from the old tool's words to yours.

> Live demo (Switch Page): Go to the Map fields step: change a column's Acme field and the three preview cards update; Continue waits until a column maps to Title. Try it at [Switch Page](https://wingo-ui.com/templates/switch-page) and install it with `npx wingo-ui@latest add switch-page`.

Wire the wizard to your import endpoint and pass only the sources you really support. `SWITCH_SOURCES` holds fictional sample tools (Tasklane, Boardly, Notebase, Sheetwise, CSV), so in production you write your own `SwitchSource` objects, a `matrix` and a `faq`; the sample FAQ answers questions about undo, limits and security on Acme's behalf.

```tsx
// app/(marketing)/switch/switch-view.tsx
"use client";

import Link from "next/link";
import { SwitchPage } from "@/components/templates/switch-page";
import { SWITCH_SOURCES } from "@/components/templates/switch-page-data";

export function SwitchView() {
  return (
    <SwitchPage
      header={false}
      footer={false}
      linkComponent={Link}
      // sample tools; replace them with your own SwitchSource objects
      sources={SWITCH_SOURCES.filter((s) => s.id === "tasklane" || s.id === "csv")}
      showOffer={false}
      showTestimonials={false}
      hrefs={{ signUp: "/sign-up?from=switch", contact: "/contact" }}
      onImport={async (config) => {
        const res = await fetch("/api/import", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(config),
        });
        if (!res.ok) throw new Error("Import failed");
      }}
    />
  );
}
```

`onImport` receives `{ source, origin, fileName?, mapping, options, totals }`; a rejected promise shows Try again. The config carries the column mapping and the options, not the rows, so an OAuth import reads from the source's API on your server, and a CSV import needs the file uploaded to your own route. The matrix only shows columns for the sources you pass, so mark every unsupported feature as `"none"` with a note. "Comments do not move from CSV" in the table beats a support ticket a week later.

On an alternative page, show your own plans right there with the [Pricing Table](https://wingo-ui.com/components/pricing-table), so a reader who has decided does not have to go hunting. The [shadcn pricing page tutorial](https://wingo-ui.com/blog/shadcn-pricing-page) covers the plans and the comparison rows.

## How do comparison pages get found and turn into sign-ups?

They get found when both product names sit in the title tag, the h1 and the URL and the first screen answers the question, and they convert when buyers reach them from pages they already read. Link them from these places:

- **The pricing page**: a "Coming from Rival?" line under the plans.
- **The footer**: a Compare column with one link per rival.
- **Docs and changelog**: the import guide links to the switch page, and the changelog entry for a new importer links to both.
- **Sales replies**: when a prospect names a competitor, send the page.

For conversion, keep one primary action and make it specific. "Import from Rival" converts a reader who is ready, and "Start for free" catches the rest; the template's hero and closing band carry both, with `getImportHref` pointing at your switch page. Track which page a sign-up came from, so you know which comparison is worth updating first.

Our own [Wingo UI vs shadcn/ui comparison](https://wingo-ui.com/blog/wingo-ui-vs-shadcn) follows these rules, including a section on when to pick shadcn/ui and stop there. For the rest of the marketing site around these pages, the [SaaS website template guide](https://wingo-ui.com/blog/saas-website-template) covers pricing, about, changelog, contact and legal pages, and more screens are in the [Build it category](https://wingo-ui.com/blog/category/build).

## Where to go from here

Install the templates with `npx wingo-ui@latest add compare-page switch-page`, write one competitor file with its sources and checked date, and ship the vs page before the alternative page. The Compare Page, the Switch Page and the Pricing Table are part of Wingo UI Pro, $8 a month, $80 a year or $150 once as of November 2026 ([pricing](https://wingo-ui.com/pricing)); the FAQ Section is free, and a free account gets you its source and the other 74 free items.

## Components in this post

- [Compare Page](https://wingo-ui.com/templates/compare-page) (Pro): A React comparison page template (Acme vs a competitor): a verdict hero, a feature table, price comparison, migration steps and FAQ. Install: `npx wingo-ui@latest add compare-page`
- [Switch Page](https://wingo-ui.com/templates/switch-page) (Pro): A migration page for teams leaving other tools: an import visual, source tiles, a working 4-step import wizard, a support matrix and offers. Install: `npx wingo-ui@latest add switch-page`
- [Pricing table](https://wingo-ui.com/components/pricing-table) (Pro): A React pricing table: plans side by side with a monthly / yearly switch whose prices roll, a featured plan and a full comparison table. Install: `npx wingo-ui@latest add pricing-table`
- [FAQ Section](https://wingo-ui.com/components/faq-section) (Free): A React FAQ section for marketing pages: an accordion, a split layout, an open grid or expandable cards, with topics and live search. Install: `npx wingo-ui@latest add faq-section`

## FAQ

### What is a SaaS comparison page?

A page that compares your product with one named competitor, usually at /compare/<competitor>, for buyers who are choosing between the two. It holds a verdict, a feature table, a price comparison, switching steps and an FAQ.

### What is the difference between a vs page and an alternative page?

A vs page answers someone who is weighing two products and wants a fair side by side. An alternative page answers someone who has decided to leave a product and wants options and a way out, so it leans on migration: what moves over, how long it takes, and what does not.

### Should a comparison page admit where the competitor is better?

Yes. Buyers check claims against the competitor's own pricing page, and one false row costs you the whole table. Google's guidance on high quality reviews also asks for benefits and drawbacks and for saying which option suits which use.

### How often should I update a competitor comparison page?

Check the competitor's pricing page and changelog at least once a quarter and whenever they announce a pricing change. Show the date you last checked on the page, and keep it in the data file next to the source links.

### Is the Wingo UI compare page template free?

No. The Compare Page and Switch Page templates and the Pricing Table are part of Wingo UI Pro; the FAQ Section they use is free. They install with npx wingo-ui@latest add.

---

Source: https://wingo-ui.com/blog/saas-comparison-page
