<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: kavela</title>
    <description>The latest articles on DEV Community by kavela (@kavelaltd).</description>
    <link>https://dev.to/kavelaltd</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3841559%2Fe6231d4d-9c33-45b2-a533-74abcfbc6048.png</url>
      <title>DEV Community: kavela</title>
      <link>https://dev.to/kavelaltd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kavelaltd"/>
    <language>en</language>
    <item>
      <title>Turning World Bank Data Into 50K+ Searchable Pages with WordPress</title>
      <dc:creator>kavela</dc:creator>
      <pubDate>Tue, 24 Mar 2026 11:28:45 +0000</pubDate>
      <link>https://dev.to/kavelaltd/turning-world-bank-data-into-50k-searchable-pages-with-wordpress-2ojp</link>
      <guid>https://dev.to/kavelaltd/turning-world-bank-data-into-50k-searchable-pages-with-wordpress-2ojp</guid>
      <description>&lt;p&gt;What if you could make decades of World Bank and IMF economic data actually accessible and browsable - not buried in spreadsheets and PDF reports that nobody reads?&lt;/p&gt;

&lt;p&gt;That's what we built with &lt;a href="https://historysaid.com" rel="noopener noreferrer"&gt;historysaid.com&lt;/a&gt;: a programmatic SEO site that transforms raw international development data into &lt;strong&gt;50,000+ structured, searchable pages&lt;/strong&gt;. Every country, every indicator, every year - all queryable, all browsable, all indexed by Google.&lt;/p&gt;

&lt;p&gt;This post covers the architectural thinking behind it and what we learned building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Data Problem
&lt;/h2&gt;

&lt;p&gt;The World Bank and IMF publish some of the richest economic datasets on the planet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GDP, inflation, trade balances, debt levels for 200+ countries&lt;/li&gt;
&lt;li&gt;Time series spanning 60+ years (some indicators go back to the 1960s)&lt;/li&gt;
&lt;li&gt;Hundreds of unique economic indicators covering everything from agricultural output to internet penetration rates&lt;/li&gt;
&lt;li&gt;Regular updates as new data gets published quarterly or annually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the official portals are designed for researchers and economists who already know what they're looking for. You need to know specific indicator codes and use clunky query builders to extract data into spreadsheets.&lt;/p&gt;

&lt;p&gt;There's no way to just... explore. To browse. To stumble upon interesting economic stories by clicking around.&lt;/p&gt;

&lt;p&gt;We wanted to change that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Build a Dashboard?
&lt;/h2&gt;

&lt;p&gt;We considered building a single-page dashboard app with interactive charts and filters. But dashboards have a fundamental SEO problem: they're one URL. Google can't index the state of your filters. If someone searches "Turkey GDP growth history", a dashboard app won't rank because that specific view doesn't have its own URL.&lt;/p&gt;

&lt;p&gt;Programmatic SEO solves this. Each unique combination of country + indicator gets its own page, its own URL, its own title, and its own meta description. Google can index all 50K of them.&lt;/p&gt;

&lt;p&gt;We chose WordPress for the same reasons we used it for startup-cost.com (see our &lt;a href="https://dev.to/kavelaltd/how-we-built-a-programmatic-seo-engine-serving-80k-pages-on-wordpress-without-using-wpposts-2kgn"&gt;previous post&lt;/a&gt;): cheap hosting, familiar ecosystem, and a powerful rewrite engine that nobody uses to its full potential.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture - Overview
&lt;/h2&gt;

&lt;p&gt;We built a custom WordPress plugin that handles everything from data ingestion to page rendering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Pipeline
&lt;/h3&gt;

&lt;p&gt;The data flows through several stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;World Bank API --&amp;gt; Fetch &amp;amp; Parse --&amp;gt; Validate --&amp;gt; Normalize --&amp;gt; MySQL
IMF Data Portal --&amp;gt; Fetch &amp;amp; Parse --&amp;gt; Validate --&amp;gt; Normalize --&amp;gt; MySQL
                                                                  |
MySQL --&amp;gt; Virtual URL Routing --&amp;gt; Template Engine --&amp;gt; HTML Page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each data source has its own ingestion logic because the API formats differ significantly. The World Bank provides a well-documented REST API with JSON responses, while IMF data comes in a different structure. We wrote adapters that normalize both into a common internal format.&lt;/p&gt;

&lt;p&gt;The pipeline runs on a scheduled basis. When new data is published by either source, our next run picks it up automatically and updates the relevant records.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Quality Challenges
&lt;/h3&gt;

&lt;p&gt;Working with international economic data is messier than you'd expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missing values everywhere&lt;/strong&gt; - Some countries don't report certain indicators for certain years. We handle nulls gracefully rather than showing zeros.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delayed reporting&lt;/strong&gt; - Some nations publish data 2-3 years late. Our pages show the most recent available data and clearly indicate the time period.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unit inconsistency&lt;/strong&gt; - Some values are in current USD, some in constant USD, some in percentages. Each indicator carries its unit metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Country code mismatches&lt;/strong&gt; - The World Bank uses ISO 3166-1 alpha-3 codes, the IMF sometimes uses its own codes. Our normalization layer handles the mapping.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Database Design Principles
&lt;/h3&gt;

&lt;p&gt;We use custom MySQL tables (not &lt;code&gt;wp_posts&lt;/code&gt;) following the same pattern from our startup-cost.com engine. The key design decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Proper normalization&lt;/strong&gt; - Countries, indicators, and data points are separate tables with foreign key relationships&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appropriate data types&lt;/strong&gt; - We use high-precision decimal types for economic values because the data ranges from tiny percentages to trillion-dollar GDP figures. Floating point would introduce precision errors that data-savvy users would notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strategic indexing&lt;/strong&gt; - Our most common query patterns (all data for a country+indicator, all countries for an indicator+year) each have compound indexes that resolve in single-digit milliseconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roughly 4 million data points&lt;/strong&gt; in the main table, all queryable in under 10ms thanks to proper indexing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Page Types
&lt;/h3&gt;

&lt;p&gt;Our routing creates four types of pages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Country pages&lt;/strong&gt; - Overview of all available indicators for a country. Shows key stats, latest values, and links to explore each indicator in depth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Country + Indicator pages&lt;/strong&gt; - The core of the site. Detailed time series data with charts, data tables, summary statistics, and trend analysis. This is the bulk of our 50K pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indicator pages&lt;/strong&gt; - Global comparison view. Shows all countries ranked by a specific indicator, with the ability to see how they compare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comparison pages&lt;/strong&gt; - Side-by-side country comparisons for a given indicator. Perfect for searches like "Japan vs South Korea GDP."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Zero rows in &lt;code&gt;wp_posts&lt;/code&gt;. Everything is computed from data tables on each request (with caching for popular pages).&lt;/p&gt;

&lt;h3&gt;
  
  
  Charts and Data Display
&lt;/h3&gt;

&lt;p&gt;Each data page includes an interactive chart (using a lightweight client-side charting library) and a full data table. The chart data is embedded as JSON in the page - fast, cacheable, and SEO-friendly since the actual values are also present in the HTML table.&lt;/p&gt;

&lt;p&gt;We also calculate and display summary statistics: latest value, historical min/max, average, and trend direction. These make each page genuinely informative rather than just a raw data dump.&lt;/p&gt;

&lt;h3&gt;
  
  
  SEO Strategy
&lt;/h3&gt;

&lt;p&gt;Every page gets unique, data-driven SEO elements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic titles&lt;/strong&gt; that include the country name and indicator name&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta descriptions&lt;/strong&gt; that include actual data values ("Turkey's GDP was $X in 2024. Explore the full trend from 1960 to 2024...")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema.org Dataset markup&lt;/strong&gt; so Google understands these are data pages with temporal and spatial coverage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breadcrumb navigation&lt;/strong&gt; for clear site hierarchy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each meta description contains real numbers from the data, making every page genuinely unique in Google's eyes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caching
&lt;/h3&gt;

&lt;p&gt;With 50K+ pages, not everything can be pre-cached. We use a tiered approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Popular combinations&lt;/strong&gt; (major countries + major indicators) get pre-cached with longer TTLs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medium-traffic pages&lt;/strong&gt; are cached on demand&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-tail pages&lt;/strong&gt; have shorter TTLs and are generated fresh when needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also cache aggregated data (like country rankings and regional averages) at the query level since multiple pages reference the same aggregations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Internal Linking
&lt;/h3&gt;

&lt;p&gt;Strong internal linking is essential for a site this large. Without it, search engines would never discover most of the pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each country page links to all available indicators for that country&lt;/li&gt;
&lt;li&gt;Each indicator page links to top countries for that indicator&lt;/li&gt;
&lt;li&gt;Breadcrumbs on every page create clear hierarchy&lt;/li&gt;
&lt;li&gt;Related content suggestions based on geographic and thematic proximity&lt;/li&gt;
&lt;li&gt;Comparison links suggest relevant country pairs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The internal link graph ensures that any page on the site is reachable within 3-4 clicks from the homepage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results After 12 Months
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;50,000+ pages indexed&lt;/strong&gt; in Google Search Console&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Average TTFB: ~150ms&lt;/strong&gt; on shared hosting&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database queries consistently under 10ms&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero editorial work&lt;/strong&gt; - the site runs itself, updated automatically from source data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Growing organic traffic&lt;/strong&gt; from long-tail searches like "Nigeria inflation rate 2015" or "Vietnam GDP per capita history"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Reusable Pattern
&lt;/h2&gt;

&lt;p&gt;This is the same architectural pattern we use across multiple sites at Kavela:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find an interesting, structured dataset&lt;/li&gt;
&lt;li&gt;Design custom tables optimized for the specific data model&lt;/li&gt;
&lt;li&gt;Build a data pipeline that keeps the database fresh&lt;/li&gt;
&lt;li&gt;Use WordPress virtual routing to create SEO-friendly URLs&lt;/li&gt;
&lt;li&gt;Render pages dynamically from the data&lt;/li&gt;
&lt;li&gt;Generate chunked sitemaps and build strong internal links&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key is making sure every page offers genuine value - real data, real calculations, real insights. Template spam with swapped-out city names won't work. Search engines are smart enough to detect that. But if every page genuinely answers a different question with different data, you've built something valuable.&lt;/p&gt;

&lt;p&gt;Explore it yourself: &lt;a href="https://historysaid.com" rel="noopener noreferrer"&gt;historysaid.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by Kavela Ltd - turning data into discoverable web experiences.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>data</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How We Built a Programmatic SEO Engine Serving 80K+ Pages on WordPress (Without Using wp_posts)</title>
      <dc:creator>kavela</dc:creator>
      <pubDate>Tue, 24 Mar 2026 11:28:20 +0000</pubDate>
      <link>https://dev.to/kavelaltd/how-we-built-a-programmatic-seo-engine-serving-80k-pages-on-wordpress-without-using-wpposts-2kgn</link>
      <guid>https://dev.to/kavelaltd/how-we-built-a-programmatic-seo-engine-serving-80k-pages-on-wordpress-without-using-wpposts-2kgn</guid>
      <description>&lt;p&gt;When we set out to build &lt;a href="https://startup-cost.com" rel="noopener noreferrer"&gt;startup-cost.com&lt;/a&gt;, we knew traditional WordPress wouldn't cut it. We needed to serve &lt;strong&gt;79,000+ unique pages&lt;/strong&gt; - one for every combination of 479 cities and 167 business types - with real cost data, real-time calculations, and solid performance.&lt;/p&gt;

&lt;p&gt;Most people hear "80K pages on WordPress" and assume we're crazy. WordPress is a blogging platform, right? Well, yes - but under the hood it's a flexible PHP framework with a powerful rewrite engine. We just had to throw away the parts that don't scale and build our own.&lt;/p&gt;

&lt;p&gt;Here's the story of how we did it without a single row in &lt;code&gt;wp_posts&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with wp_posts at Scale
&lt;/h2&gt;

&lt;p&gt;WordPress stores all content in a single table called &lt;code&gt;wp_posts&lt;/code&gt;. For a blog or a small business site with a few hundred pages, this works fine. But when you start pushing tens of thousands of rows into that table, things fall apart quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Query performance degrades&lt;/strong&gt; - WordPress joins &lt;code&gt;wp_posts&lt;/code&gt; with &lt;code&gt;wp_postmeta&lt;/code&gt; for almost every query. With 80K posts, each with 10+ meta fields, you're looking at 800K+ rows in postmeta alone. Queries that used to take 5ms now take 500ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The admin panel becomes unusable&lt;/strong&gt; - Try loading the "All Posts" screen with 80K entries. WordPress paginates, sure, but even counting the total takes forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revision history eats disk&lt;/strong&gt; - WordPress auto-saves revisions. With programmatic content that gets regenerated, you end up with 3-4x the actual content in revision rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XML sitemaps choke&lt;/strong&gt; - Popular sitemap plugins try to query all posts at once. With 80K rows, they either timeout or consume all available memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Imports and exports break&lt;/strong&gt; - WordPress export generates a single XML file. Good luck with an 80K-post WXR file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We needed a fundamentally different approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Use a Static Site Generator or a Different CMS?
&lt;/h2&gt;

&lt;p&gt;Fair question. We considered Hugo, Next.js, and even a custom Node.js app. But WordPress gave us specific advantages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hosting is dirt cheap&lt;/strong&gt; - Shared WordPress hosting costs a few dollars a month and handles our traffic fine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The plugin ecosystem&lt;/strong&gt; - We still use various utility plugins for SEO settings, caching, and other tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Familiar deployment&lt;/strong&gt; - Our team knows WordPress inside out. No learning curve, no new CI/CD pipeline needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PHP is actually fast enough&lt;/strong&gt; - With opcache and a clean query pattern, PHP 8 serves pages in double-digit milliseconds&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key insight was: we don't have to use WordPress the way it was designed. We can use it as a routing and rendering framework while storing our data however we want.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture - High Level
&lt;/h2&gt;

&lt;p&gt;We built a custom WordPress plugin that bypasses &lt;code&gt;wp_posts&lt;/code&gt; entirely. The concept has three pillars:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Custom Database Tables
&lt;/h3&gt;

&lt;p&gt;Instead of stuffing everything into posts and postmeta, we created dedicated tables with proper schemas, data types, and indexes. Think of it as designing a mini-application database that lives inside WordPress's MySQL instance.&lt;/p&gt;

&lt;p&gt;The key principle: each entity type (cities, business types, cost metrics) gets its own table with columns that match the actual data model - not the generic key-value pairs that postmeta forces you into. This means proper indexing, proper normalization, and queries that hit exactly the data they need.&lt;/p&gt;

&lt;p&gt;The performance difference is massive. A meta-based lookup on 80K posts might take hundreds of milliseconds. A direct indexed query on a purpose-built table returns in single-digit milliseconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Virtual URL Routing
&lt;/h3&gt;

&lt;p&gt;WordPress has a powerful but underused rewrite API. Most developers only interact with it through the permalink settings screen. But under the hood, you can register completely custom URL patterns that map to your own rendering logic.&lt;/p&gt;

&lt;p&gt;We define URL patterns that WordPress recognizes and routes to our plugin. When a request comes in, WordPress matches the URL against our patterns, extracts the relevant slugs, and hands control to our code. No post is created. No database row in &lt;code&gt;wp_posts&lt;/code&gt; is touched. The URL exists purely because we told WordPress to recognize the pattern.&lt;/p&gt;

&lt;p&gt;This approach gives us full control over URL structure while still benefiting from WordPress's request lifecycle, caching hooks, and plugin compatibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Dynamic Content Generation
&lt;/h3&gt;

&lt;p&gt;This is what makes programmatic SEO actually work. We don't just slap the same text on 80K pages with city names swapped - each page has genuinely different data. Real cost figures, real calculations, real comparisons.&lt;/p&gt;

&lt;p&gt;The rendering layer pulls data from our custom tables, runs computations specific to each city-business combination, and outputs a fully formed HTML page with all the SEO elements (unique title, meta description, schema markup, etc.).&lt;/p&gt;

&lt;p&gt;Every page displays different numbers because the underlying data is different. Google is smart enough to detect thin, templated content. Genuine value on each page is what makes programmatic SEO work long-term.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Results
&lt;/h2&gt;

&lt;p&gt;We benchmarked both approaches on the same server (shared hosting, PHP 8.1, MariaDB 10.6):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;wp_posts approach&lt;/th&gt;
&lt;th&gt;Our custom approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Average page load (TTFB)&lt;/td&gt;
&lt;td&gt;~800ms&lt;/td&gt;
&lt;td&gt;~120ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database query time&lt;/td&gt;
&lt;td&gt;~200ms (with meta joins)&lt;/td&gt;
&lt;td&gt;~15ms (indexed lookup)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin dashboard load&lt;/td&gt;
&lt;td&gt;30+ seconds&lt;/td&gt;
&lt;td&gt;Instant (no admin overhead)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sitemap generation&lt;/td&gt;
&lt;td&gt;Timeout at 60s&lt;/td&gt;
&lt;td&gt;~2 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory usage per request&lt;/td&gt;
&lt;td&gt;~64MB&lt;/td&gt;
&lt;td&gt;~12MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The difference is dramatic. Custom tables with proper indexes make WordPress perform like a purpose-built application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sitemap Strategy
&lt;/h2&gt;

&lt;p&gt;Google's sitemap limit is 50,000 URLs per file and 50MB uncompressed. With 80K pages, we need multiple sitemaps plus a sitemap index.&lt;/p&gt;

&lt;p&gt;We generate sitemaps programmatically, chunking URLs into manageable files, and create a sitemap index that references all of them. The generation runs on a weekly cron. Google Search Console picks them up without issues, and we can track indexing progress per sitemap chunk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Internal Linking
&lt;/h2&gt;

&lt;p&gt;With 80K pages, internal linking is critical both for SEO and for helping users navigate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each city page links to all business types available in that city&lt;/li&gt;
&lt;li&gt;Each business page links to top cities for that business type&lt;/li&gt;
&lt;li&gt;Each detail page links to related pages based on geographic and thematic proximity&lt;/li&gt;
&lt;li&gt;Dense internal link graph helps search engines discover and understand the site structure&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Caching
&lt;/h2&gt;

&lt;p&gt;We use a multi-tier caching strategy. Not all 80K pages get equal traffic - popular city/business combinations are pre-cached with longer TTLs, while long-tail pages are cached on demand with shorter TTLs. We also cache at the database query level for frequently accessed aggregations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WordPress can handle massive scale&lt;/strong&gt; - but only if you step outside its default content model. The platform is more flexible than people give it credit for.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom tables are not "hacky"&lt;/strong&gt; - They're the right tool when your data doesn't fit the post/meta pattern. WordPress itself uses custom tables for comments, users, and options.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Virtual routing is powerful&lt;/strong&gt; - WordPress rewrite rules can handle complex URL patterns. You don't need a custom framework just because your URLs don't map to posts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Programmatic SEO needs real value&lt;/strong&gt; - Google detects thin, templated content. Every page needs genuinely different, useful data. That's the difference between spam and a real product.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with the data model&lt;/strong&gt; - We spent more time designing our database schema than writing rendering code. Good data modeling pays off at every layer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monitor, then optimize&lt;/strong&gt; - We started without caching and added it only where monitoring showed bottlenecks. Premature optimization would have added complexity we didn't need.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check out the result: &lt;a href="https://startup-cost.com" rel="noopener noreferrer"&gt;startup-cost.com&lt;/a&gt; - startup cost estimates for 167 business types across 479 cities worldwide.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by Kavela Ltd - we build data-driven web tools at scale.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>seo</category>
      <category>database</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
