technical seo rails

1,300 pages from one database

Ahmed Nadar · · 4 min read

Someone shared a SolveTO report on Twitter last night. The card showed the title and description but no image. Just a generic icon. I looked at the HTML and found the problem: the og:image meta tag had a relative URL. Twitter’s crawler needs a full https:// URL. A one-line bug that made every shared link look broken.

While fixing it, I realized SolveTO had a bigger problem. We had maybe 185 indexable pages. Individual reports and ward cards. That’s it. No sitemap. Empty robots.txt. No structured data. If someone searched “potholes danforth toronto,” we wouldn’t show up.

The NomadList lesson

Pieter Levels runs NomadList. It has over 55,000 indexed pages. From one database. The trick is simple: take one entity (a city), create multiple URL facets from it, then generate combinatorial pages between entities.

Toronto has 1,379 cities in its database. Each city gets 9 pages: /toronto, /cost-of-living/in/toronto, /coworking/toronto, /near/toronto, /compare/toronto/lisbon. Then the compare pages alone are 9,900 (top 100 cities against each other). Every page has a unique title, data-driven description, and JSON-LD structured data.

SolveTO has 25 wards, 30 issue types, and 25 councillors. That’s our database.

What we generated

Page type URL example Count
Ward cards /wards/toronto-danforth 25
Ward + issue combos /wards/toronto-danforth/pothole up to 750
Issue type pages /issues/pothole 30
Councillor scorecards /councillors/paula-fletcher 25
Ward comparisons /compare/spadina-fort-york/toronto-danforth up to 300

Total: ~1,300 pages from the same data we already had.

Every page is unique

This isn’t content spinning. Each page has real data.

A ward+issue page like /wards/toronto-danforth/pothole shows: how many potholes were reported in that ward, how many were resolved, the average resolution time, and a list of actual reports. The title is “Pothole Reports in Toronto-Danforth (Ward 14) — 47 Reports.” The description includes real numbers. Twitter cards show “Status: 12 open” and “Resolution Rate: 74%.”

A councillor page shows their performance score, fix rate, response rate, and ward stats. A comparison page puts two wards side by side.

Pages with fewer than 3 reports don’t get generated. Google penalizes thin content. Better to have 800 strong pages than 1,300 weak ones.

The cross-linking web

Every page links to related pages. A pothole page in Ward 14 links to graffiti in Ward 14, potholes in Ward 10, and the Ward 14 councillor page. A councillor page links to the ward card. A comparison page links to both wards.

This is the part most people miss. The pages themselves are half the value. The internal link network is the other half. It’s how Google distributes authority across all 1,300 pages instead of concentrating it on the homepage.

Ward slugs

I also changed ward URLs from /wards/14 to /wards/toronto-danforth. Human-readable, keyword-rich, and better for SEO. Old numeric URLs 301 redirect to the slug.

Small change, but “toronto-danforth” in the URL tells Google what the page is about before it even reads the content.

The bet

None of this matters if the pages are empty. A page for “graffiti in Ward 7” with zero reports is worse than no page at all. So every page has a minimum threshold: if there aren’t at least 3 reports for that combination, the page doesn’t exist. Better to have 800 real pages than 1,300 hollow ones.

The comparison pages are the wildest part. 300 pages from 25 wards, pure math. “Ward 14 vs Ward 13” might not be a query anyone searches today. But if someone in Toronto-Danforth ever wonders how their ward stacks up against Toronto Centre, there’s now a page for that. And we’re the only ones who have it.

Report an issue: solveto.ca

Support SolveTO: solveto.ca/support

Questions: support@solveto.ca