Part 5 of 12 · Silver Rates Site

My Most Important Pages Did Not Actually Exist

My city rate pages were the heart of the site, and one day I went looking for them in the WordPress Pages list to edit one, and they were not there. Not hidden, not trashed, structurally absent, because I had built them as virtual pages, and this post is that architecture, why it seduced me, and the rebuild to real pages when its costs surfaced.

Virtual pages work by intercepting URLs. Rewrite rules teach WordPress that a pattern like silver-rate-karachi is a query, and a template hook renders it:

// the virtual era, simplified
add_rewrite_rule(
    '^silver-rate-([a-z-]+)/?$',
    'index.php?srt_metal=silver&srt_city=$matches[1]',
    'top'
);
add_filter('template_include', function($template) {
    if (get_query_var('srt_city')) {
        return SRT_PATH . 'templates/template-metal.php';
    }
    return $template;
});

The seduction is real, fifteen cities times two metals served from one template, add a city to the list and its pages exist instantly, nothing stored, nothing to maintain. And then the costs arrived, all at once, because they share one root, the rest of WordPress cannot see pages that are not posts. The sitemap plugin walks the posts table, my most important URLs were not in it, so search engines were not being told my core pages existed. The Pages list could not show them, so nothing about them could be edited without editing PHP. Elementor could not open them, no post, nothing to design. SEO plugins could not attach per-page meta, no post row to hang it on. Four symptoms, one cause, the pages had no existence in the system every other tool queries.

So version 3.1.0 made them real. On activation, the installer creates thirty actual WordPress pages, fifteen cities by two metals, plus the two national hubs, idempotently, checking by slug before creating:

foreach (srt_default_cities() as $city) {
    foreach (array('silver', 'gold') as $metal) {
        $slug = $metal . '-rate-' . sanitize_title($city);
        if (!get_page_by_path($slug)) {
            wp_insert_post(array(
                'post_type'    => 'page',
                'post_status'  => 'publish',
                'post_title'   => ucfirst($metal) . ' Rate in ' . $city,
                'post_name'    => $slug,
                'post_content' => srt_build_city_content($metal, $city),
            ));
        }
    }
}

The content each page receives is the shortcode composition from the engine post, live rate, table, graph, calculator, plus written copy, so the pages stayed dynamic where it matters, the numbers, while becoming visible to sitemaps, editable in WordPress and Elementor, and addressable by SEO meta. The router file shrank to a one-time flush of the old rewrites. Rows in the posts table, it turns out, are not overhead, they are citizenship.

A few things people ask me about this

When are virtual pages still the right tool? High-cardinality, low-editorial URLs, thousands of filter combinations or API-like endpoints, where per-page editing and sitemap presence genuinely do not matter. Core landing pages are the opposite case.

Why did my rewrite rules only work after visiting Settings, Permalinks? Rules are cached. Visiting the permalinks screen, or calling flush_rewrite_rules once at activation, rebuilds the cache. Never flush on every load, it is expensive.

Next

Thirty real pages surfaced the next problem immediately, they all said nearly the same thing. Fixing thin, templated content city by city is the next post.

Leave a Reply

Your email address will not be published. Required fields are marked *