Munchable has 38 recipes, each with its own page, and the index groups them by meal: breakfast, lunch, dinner, snack.
The obvious thing to build instead is a collection per condition. Eight conditions, eight landing pages, eight head terms that people genuinely search, each one a tidy list of recipes that suit that pattern. It is the standard content play and it writes itself.
I measured the overlap first. Here is what the library actually looks like, counted off the live page a few minutes ago:
| Condition | Recipes that suit it, of 38 |
|---|---|
| IBD | 38 |
| low FODMAP | 37 |
| lactose | 37 |
| SIBO | 37 |
| bile acid malabsorption | 35 |
| gastroparesis | 34 |
| reflux | 30 |
| coeliac | 28 |
Eight collection pages. The thinnest would contain 28 of the 38 recipes, the fattest all 38. Six of the eight would contain at least 34.
Those are not eight pages. They are eight orderings of one page.
What that would actually do to us
Three things, none of them good.
They would compete with each other. Eight pages built from the same 38 cards, differing by a handful of rows and a heading, is the textbook shape of near-duplicate content. A search engine picks one of a set of near-duplicates to show, and you do not get to choose which, and the signals you were hoping to concentrate get spread across eight URLs instead.
They would eat crawl budget to say nothing new. Our sitemap is about 500 URLs. Adding eight pages that are 90 percent the same content as each other, and as the index they were built from, means a crawler re-reading the same recipe titles nine times to discover nothing.
They would be the weakest possible answer for the strongest query. Somebody searching a condition head term wants to know what to do about that condition. A list of 34 recipe cards is not that answer. We already have the page that is: the condition guides, one per condition, which explain what the pattern is, why packaged food is the hard part, and what the app reads. Those are substantive pages that already rank, and they link to the recipes. Putting a thin collection page between the query and the guide would be actively worse than having nothing.
So the index is grouped by meal type, because that is the axis where the groups genuinely differ, and because a reader's actual state is "it is half past six and I need dinner" rather than "show me a list filtered by my diagnosis", which is what the app on their phone is for.
A recipe appears in every meal group it fits, so the index renders 58 cards for 38 recipes. The URL set stays 38. Grouping is not routing.
The decision is a comment, and the comment is already out of date
It is written down in the page, which is the part I would repeat:
/** * Grouped by meal rather than by condition, and that is a decision worth * recording: the obvious move is one page per condition, hitting eight * head terms. It would not work yet. Thirty of the thirty-eight recipes suit * reflux, thirty-seven suit low FODMAP, and the thinnest condition still has * thirty, so those pages would be near-duplicates of each other competing for * the same crawl budget. The condition guides already rank for the head terms * and link here; collections become worth building when the library is large * enough that the per-condition lists genuinely diverge. */ The useful part is not the conclusion, it is the trigger: collections become worth building when the per-condition lists diverge. A deferred decision with a stated condition for revisiting it is a decision. A deferred decision with no trigger is procrastination that will be re-argued from scratch in March.
The embarrassing part is that the comment's numbers are already stale. It says the thinnest condition has thirty. Since it was written we added an eighth condition, and the thinnest is now 28. The argument is untouched, 28 of 38 being just as duplicative as 30 of 38, but the evidence in the comment no longer matches the product it describes.
Which is an argument against writing numbers into comments and in favour of a script or a test that prints the overlap table, so that the day the answer changes is the day somebody sees it change. I would rather have been able to re-run the thing above than reconstruct it from the live HTML.
A smaller one from the same file
While you are looking at those cards: the line under each one says things like "Suits all eight conditions" or "Suits every condition except gastroparesis and coeliac".
That inversion is deliberate. A card listing seven of eight conditions by name is a wall of commas that nobody reads, so past a certain point the line flips to naming what is missing instead:
const suitsLine = conditions.length === 0 ? '' : missing.length === 0 ? ALL_CONDITIONS_LINE : missing.length <= 2 ? `every condition except ${joinNames(missing.map((id) => CONDITION_LABELS[id]))}` : joinNames(conditions.map((id) => CONDITION_LABELS[id])); Three phrasings of one fact, chosen by which is shortest to read, and none of the three has a hand-written version anywhere.
The same decision shows up one function earlier, in the generated meta description, where the clause naming the conditions is dropped entirely rather than truncated if the whole sentence would exceed the snippet budget. A description cut mid-list ends on "except gastroparesis and" and tells the reader less than the version that never mentioned conditions at all. That clause got two words too long the week the eighth condition landed, which is exactly the kind of change that will happen again, so the sentence handles it instead of a human shortening one recipe's summary.
Have a look at the index and count the collection pages that are not there. The decision not to build eight of something is the kind of work that leaves no artefact, which is most of why I wrote it down.