GenerateSaaS
ContentCompare engine

Hub and pair pages

What a compare section actually serves - the hub as a router or a ranked listicle, the index.md that switches between them, and everything a generated head-to-head page prints.

The fact sheets are the source; these are the pages they publish. Both read every line from those files, so nothing here is authored twice.

The hub: router, or ranked listicle

/compare is a router by default: a hero, one card per published pair, and a card per roundup so nothing is orphaned. Add an index.md beside the product files and the same URL becomes the ranked best {category} listicle - authored H1 and intro with the whole ranking inside the hero (one card per sheet, so a reader sees where every product stands before the table), the matrix, one product block per sheet in order (top pick first, yours badged), the method section, then the FAQ.

title: "The best CRM tools in 2026, ranked" *  # the authored H1, and the whole <title> - never suffixed
description: * ; date: *                       # `date` is collection-enforced, as on every document
primaryKeyword: "crm tools" *                  # the query the intro's first words carry
intro: "One paragraph." *                      # the page's ONLY prose surface
criteria: ["...", "...", "..."] *              # >= 3 lines saying how the ranking was made
faq: [{ q: "...", a: "..." }] *                # 4-6, about the CATEGORY rather than one product
dateModified ; lastVerified    # the hero's dates; lastVerified also draws the facts-verified pill
seo: { title: "...", description: "..." }      # overrides the <title> and the meta description
RuleEffect
A field marked * is missingThe hub drops back to the router, and the build prints /{section} renders as a router, not a ranked listicle, naming the field
index.md is not a productIt never columns the matrix, cards the ranking, or halves a pair URL
hubSlug renames the hub alone{ slug: "compare", template: "compare", hubSlug: "saas-boilerplates" } serves the hub at /saas-boilerplates while every pair stays at /compare/{a}-vs-{b}; /compare 308s to the hub, built in
The .md rendition follows the modeThe authored head, ranking and criteria in listicle mode; the plain product list in router mode

The product block

One component draws every product write-up on the site: here, on both roundup kinds and on both sides of a pair page. Its parts are documented once, with the roundup. Its three link rows are derived, so a link appears exactly when its page does and never outlives it, and every outbound link to a vendor opens in a new tab as nofollow noopener.

What a pair page prints

In order, and every line of it read from the two sheets:

PartRead from
HeroBoth marks and the That is us badge. No dated pill - the page carries its dates in metadata, not over the H1
tldrOne authored sentence, above everything else. On a referee pair it is the verdict: who wins and why, with the winner's name drawn in the accent
The two caseschooseA / chooseB under Choose {product} if, each with that sheet's verify links
oursPitchReferee pairs only: your own case against both, as one card under the two, in your own tint with your mark and price
The tableThe shared matrix, opened on the pair's own two columns - three on a referee pair, so yours is visible
Verdict rowsEach rulings entry closes the rows it rules on, inside the table
Method lineUnder the table, beside the legend. A pair page has no method section
The two blocksThe shared product block, full width, one per side
FAQBoth sheets' questions, six at most, every answer closed

A verdict row names its products, never their positions

"ShipFast is cheaper upfront, ours includes lifetime updates" survives a pair flipping direction; "the first is cheaper" silently means the other product the day it does. The content tests refuse a ruling naming neither side.

Referee pages: two products you do not make

A pair of two other products still publishes, and it argues rather than abstains. Its tldr rules on the pairing - the winner is your own order, so one judgement lives in one place - and oursPitch says what you would have the reader do instead, once, in a card that is visibly yours. Each pitch line names a product and traces to a cell; the content tests refuse a pitch on a pair containing your own product, which already argues its case in its own column. Which referee pairs are worth writing at all is an editorial call, not a mechanical one.

On this page