Looking to join a great team — open to 186/482 visa sponsorship in Australia. Learn more
How to Add Virtual Video Pages to the Default WordPress Sitemap

How to Add Virtual Video Pages to the Default WordPress Sitemap

In a previous article, I covered how to resolve the “Video isn’t on a watch page” error in Google Search Console. The fix led us to build a virtual video section on our WordPress site — including an archive page at /video/ and individual pages like /video/abc123/ for each embedded YouTube video.

The next logical step was to make these virtual URLs discoverable by search engines via sitemap.xml. Since WordPress generates its own sitemap automatically as of version 5.5, the challenge was to integrate these dynamic, non-post URLs into the native system — without relying on plugins or hacks.

Table of Contents

  1. Why These Pages Aren’t in Your Sitemap by Default
  2. What We’re Trying to Achieve
  3. How to Generate a Custom Sitemap File with Video URLs
  4. How to Add It to Your WordPress Sitemap Index
  5. Final Working Code (Copy & Paste)

1. Why These Pages Aren’t in Your Sitemap by Default

The WordPress sitemap system only includes “real” pages — such as posts, pages, and public custom post types. Since our /video/ and /video/{code}/ pages are dynamically generated based on YouTube embeds and not registered in the database, they are invisible to the native sitemap engine.

That means search engines won’t find them unless we explicitly include them.

2. What We’re Trying to Achieve

We want our sitemap to include:

  • The virtual archive page /video/
  • All discovered video-only pages like /video/4JZ4yXwoiI

And we want them to appear under /wp-sitemap-custom.xml — automatically added to the main wp-sitemap.xml.

3. How to Generate a Custom Sitemap File with Video URLs

We scan through all published posts, extract every YouTube video ID found in the content (via youtube.com/watch?v=, youtube.com/embed/, or youtu.be/), and generate one unique URL per video code.

The result is served dynamically at /wp-sitemap-custom.xml with valid XML structure and no trailing slashes.

4. How to Add It to Your WordPress Sitemap Index

WordPress doesn’t let you register raw XML files in the sitemap index via API, but we can buffer the output of wp-sitemap.xml and inject our custom entry before the closing tag.

This way, Google and Bing will see it just like any other native sitemap.

5. Final Working Code (Copy & Paste)

// 1. Register custom sitemap URL
add_action( 'init', function() {
    add_rewrite_rule( '^wp-sitemap-custom\.xml$', 'index.php?custom_sitemap=1', 'top' );
    add_rewrite_tag( '%custom_sitemap%', '1' );
});

// 2. Output dynamic XML with all /video/ pages
add_action( 'template_redirect', function() {
    if ( get_query_var( 'custom_sitemap' ) ) {
        header( 'Content-Type: application/xml; charset=UTF-8' );

        echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
        echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";

        $args = [
            'post_type' => 'post',
            'post_status' => 'publish',
            'posts_per_page' => -1,
            's' => 'youtube.com', // faster lookup
        ];

        $query = new WP_Query($args);
        $seen_codes = [];

        $lastmod = get_the_modified_time( 'Y-m-d\TH:i:s+00:00' );
        echo "<url>\n";
        echo "  <loc>" . home_url( '/video/' ) . "</loc>\n";
        echo "  <lastmod>$lastmod</lastmod>\n";
        echo "</url>\n";

        while ( $query->have_posts() ) {
            $query->the_post();
            $content = get_the_content();

            preg_match_all( '/(?:youtube\.com\/(?:embed\/|watch\?v=)|youtu\.be\/)([a-zA-Z0-9_-]{6,})/', $content, $matches );

            if ( ! empty( $matches[1] ) ) {
                foreach ( $matches[1] as $code ) {
                    if ( in_array( $code, $seen_codes ) ) continue;
                    $seen_codes[] = $code;

                    $url = home_url( '/video/' . $code . '/' );
                    $lastmod = get_the_modified_time( 'Y-m-d\TH:i:s+00:00' );

                    echo "<url>\n";
                    echo "  <loc>$url</loc>\n";
                    echo "  <lastmod>$lastmod</lastmod>\n";
                    echo "</url>\n";
                }
            }
        }

        wp_reset_postdata();
        echo "</urlset>\n";
        exit;
    }
});

// 3. Disable trailing slash redirect
add_filter( 'redirect_canonical', function( $redirect_url, $requested_url ) {
    if ( strpos( $_SERVER['REQUEST_URI'], 'wp-sitemap-custom.xml' ) !== false ) {
        return false;
    }
    return $redirect_url;
}, 10, 2 );

// 4. Inject link into the main wp-sitemap.xml
add_action( 'template_redirect', function() {
    if ( $_SERVER['REQUEST_URI'] === '/wp-sitemap.xml' ) {
        ob_start( function( $output ) {
            $custom_sitemap_url = home_url( '/wp-sitemap-custom.xml' );
            $custom_sitemap = <<<XML
<sitemap>
  <loc>{$custom_sitemap_url}</loc>
</sitemap>
XML;
            return str_replace('</sitemapindex>', $custom_sitemap . "\n</sitemapindex>", $output);
        } );
    }
});

Wrapping Up

This method is clean, plugin-free, and plays nicely with WordPress core features. Once deployed, you’ll see your /video/ pages indexed correctly — and errors in Google Search Console will finally go away.

If you’re building virtual routes in WordPress — for videos, filters, or dynamic archives — this is a scalable way to make sure they’re discoverable by search engines.

Have a similar use case or a different dynamic section to index? Drop me a message — happy to help.