Looking to join a great team — open to 186/482 visa sponsorship in Australia. Learn more
Fixing “Video isn’t on a watch page” in Google Search Console for WordPress

Fixing “Video isn’t on a watch page” in Google Search Console for WordPress

One of the most frustrating video indexing issues in Google Search Console is:

“Video isn’t on a watch page”

This warning means Google sees your video, but refuses to treat the page as a proper video result — which hurts your video SEO.

In this article, I’ll show you how to fix this for WordPress sites by building proper video watch pages, even if your videos are just embedded in regular blog posts.

Table of Contents

  1. Why this error occurs
  2. The solution: dedicated watch pages
  3. Implementation steps
  4. Structured data with YouTube API (optional)
  5. Result: SEO benefits

1. Why this error occurs

Even if you embed a YouTube video in your WordPress post, Google may still refuse to index it as a video, because:

  • The video is not in the initial viewport (e.g. too far down the page).
  • The page contains multiple videos or too much unrelated content.
  • Google doesn’t “see” the video as the main content.

This is where the “Video isn’t on a watch page” warning kicks in.

2. The solution: dedicated watch pages

To convince Google your video deserves its own rich snippet, we need to create a dedicated landing page for each video — even if the original video lives inside a regular blog post.

The idea:

This watch page contains only the video and a backlink to the original post — exactly what Google expects.

Replace embedded YouTube videos with a clickable thumbnail and play button.

When clicked, users are redirected to a dedicated watch page, like /video/_4JZ4yXwoiI/.

3. Implementation

You don’t need plugins. Everything can be done via functions.php and two small templates.

What we implemented:

  • A filter on the_content to replace YouTube embeds with a static thumbnail.
  • A custom rewrite rule for URLs like /video/{youtube-code}/.
  • A dynamic page handler that locates the original post containing that video.
  • A minimalistic “watch page” template that outputs just the video and a backlink.
  • A video archive page at /video/ that lists all available video watch pages.

Let’s go!

3.1 Replace YouTube embeds in post content in functions.php

add_filter('the_content', function($content) {
    return preg_replace_callback(
        '/<iframe.*?src=["\']https?:\/\/(?:www\.)?youtube\.com\/embed\/([a-zA-Z0-9_-]+).*?["\'].*?><\/iframe>/i',
        function ($matches) {
            $code = esc_attr($matches[1]);
            $thumb = "https://img.youtube.com/vi/$code/hqdefault.jpg";
            $link = site_url("/video/$code/");
            return "<div class='youtube-placeholder' style='position:relative;cursor:pointer;' onclick=\"location.href='$link'\">
                        <img src='$thumb' alt='YouTube Video' style='width:100%;height:auto;'>
                        <div style='position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#000;padding:10px 20px;color:#fff;font-weight:bold;'>▶ Play</div>
                    </div>";
        },
        $content
    );
});

3.2 Add custom rewrite rules for /video/ URLs in functions.php

add_action('init', function() {
    add_rewrite_rule('^video/([a-zA-Z0-9_-]+)/?$', 'index.php?ywr_video_code=$matches[1]', 'top');
    add_rewrite_rule('^video/?$', 'index.php?ywr_video_archive=1', 'top');
    flush_rewrite_rules(false);
});

add_filter('query_vars', function($vars) {
    $vars[] = 'ywr_video_code';
    $vars[] = 'ywr_video_archive';
    return $vars;
});

3.3 Template redirect for watch page and 404 fallback in functions.php

add_action('template_redirect', function() {
    $code = get_query_var('ywr_video_code');
    if ($code) {
        $post = ywr_find_post_by_youtube_code($code);
        if ($post) {
            status_header(200);
            get_header();
            setup_postdata($post); ?>
            
            <h1><?php the_title(); ?></h1>
            <div class="video-wrapper">
                <iframe width="100%" height="500" src="https://www.youtube.com/embed/<?php echo esc_attr($code); ?>" frameborder="0" allowfullscreen></iframe>
            </div>
            <p>
              <a href="<?php echo esc_url(get_permalink($post)); ?>">
                ← Back to article &laquo;<?php echo esc_html(get_the_title($post)); ?>&raquo;
              </a>
            </p>
            
            <?php get_footer();
            exit;
        } else {
            global $wp_query;
            $wp_query->set_404();
            status_header(404);
            include get_404_template();
            exit;
        }
    }
});

3.4 Template redirect for video archive page (/video/) in functions.php

add_filter('template_include', function($template) {
    if (get_query_var('ywr_video_archive')) {
        return get_stylesheet_directory() . '/video-archive.php';
    }
    return $template;
});

3.5 Create watch page template (watch-page.php)

<?php
$code = get_query_var('ywr_video_code');
$post = ywr_find_post_by_youtube_code($code);

if (!$post) {
    get_header();
    echo "<h1>Video not found</h1>";
    get_footer();
    exit;
}

setup_postdata($post);

get_header(); ?>

<h1>Video from “<?php echo esc_html(get_the_title($post)); ?>”</h1>

<div class="video-wrapper">
    <iframe width="100%" height="500" src="https://www.youtube.com/embed/<?php echo esc_attr($code); ?>" frameborder="0" allowfullscreen></iframe>
</div>

<p>
  <a href="<?php echo esc_url(get_permalink($post)); ?>">
    ← Back to article &laquo;<?php echo esc_html(get_the_title($post)); ?>&raquo;
  </a>
</p>

<?php get_footer(); ?>

3.6 Add meta-tags for custom pages in functions.php

//
add_filter('pre_get_document_title', function($title) {
    if (get_query_var('ywr_video_code')) {
        $code = get_query_var('ywr_video_code');
        $post = ywr_find_post_by_youtube_code($code);
        if ($post) {
            return 'Watch “' . get_the_title($post) . '” — Video Only View';
        }
    }
    return $title;
});

//
add_action('wp_head', function() {
    if (get_query_var('ywr_video_code')) {
        $code = get_query_var('ywr_video_code');
        $post = ywr_find_post_by_youtube_code($code);
        if ($post) {
            $desc = 'Watch the video from the article ‘' . esc_attr(get_the_title($post)) . '’. This page shows only the video with minimal distractions.';
            echo "<meta name=\"description\" content=\"$desc\">\n";
        }
    }
}, 1);

3.7 Search youtube post by code in functions.php

function ywr_find_post_by_youtube_code($code) {
    global $wpdb;

    $like_embed = "%youtube.com/embed/$code%";
    $like_watch = "%youtube.com/watch?v=$code%";
    $like_short = "%youtu.be/$code%";

    $post_id = $wpdb->get_var($wpdb->prepare(
        "SELECT ID FROM $wpdb->posts 
         WHERE (post_content LIKE %s OR post_content LIKE %s OR post_content LIKE %s)
         AND post_status = 'publish'
         LIMIT 1",
        $like_embed,
        $like_watch,
        $like_short
    ));
    return $post_id ? get_post($post_id) : false;
}

3.7 Create video archive page (video-archive.php)

<?php
get_header();
?>

<h1>Video Archive</h1>

<div class="posts-grid">
<?php
$args = [
    'post_type' => 'post',
    'posts_per_page' => -1
];
$query = new WP_Query($args);

while ($query->have_posts()): $query->the_post();
    if (preg_match('/youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)/', get_the_content(), $m)) {
        $code = $m[1];
        $link = site_url("/video/$code/");
        $thumb = "https://img.youtube.com/vi/$code/hqdefault.jpg";
        ?>
        <div class="post-card">
            <a href="<?php echo esc_url($link); ?>">
                <img src="<?php echo esc_url($thumb); ?>" alt="<?php the_title_attribute(); ?>">
                <h2><?php the_title(); ?></h2>
            </a>
        </div>
        <?php
    }
endwhile;
?>
</div>

<style>
.posts-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 2rem;
}

.post-card {
    padding: 1rem;
    text-align: center;
}

.post-card img {
    max-width: 100%;
    height: auto;
}
</style>

<?php
get_footer();
?>

4. Add structured data with YouTube API in functions.php (optional)

Use YouTube Data API to fetch video metadata and enhance your watch page with schema.org markup:

function fetch_youtube_video_duration($video_id) {
    $transient_key = 'youtube_api_' . $video_id;

    // Verify cache
    $cached = get_transient($transient_key);
    if ($cached !== false) {
        return $cached;
    }

    // YouTube API
    $api_key = 'YOUR-API-KEY';
    $api_url = "https://www.googleapis.com/youtube/v3/videos?part=contentDetails&id={$video_id}&key={$api_key}";
    $response = wp_remote_get($api_url);

    if (is_wp_error($response)) {
        return null;
    }

    $body = json_decode(wp_remote_retrieve_body($response), true);
    $duration = $body['items'][0]['contentDetails']['duration'] ?? null;

    if ($duration) {
        set_transient($transient_key, $duration, 31104000); // 360 days in seconds
    }

    return $duration;
}

Embed the result into JSON-LD:

function add_video_schema() {
    if (!is_single()) return;

    global $post;
    $content = apply_filters('the_content', $post->post_content);

    // Search for YouTube-link
    if (preg_match('/(?:youtube\.com\/(?:embed\/|watch\?v=)|youtu\.be\/)([a-zA-Z0-9_-]+)/', $content, $matches)) {
        $video_id = $matches[1];
        $video_title = get_the_title($post->ID);
        $upload_date = get_the_date('c', $post->ID);
        $embed_url = "https://www.youtube.com/embed/{$video_id}";
        $thumb_url = "https://img.youtube.com/vi/{$video_id}/hqdefault.jpg";
        $duration = fetch_youtube_video_duration($video_id);

        $schema = [
            "@context" => "https://schema.org",
            "@type" => "VideoObject",
            "name" => $video_title,
            "description" => "Video from the article: " . $video_title,
            "thumbnailUrl" => $thumb_url,
            "uploadDate" => $upload_date,
            "embedUrl" => $embed_url
        ];

        if ($duration) {
            $schema["duration"] = $duration;
        }

        echo '<script type="application/ld+json">' .
            json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) .
            '</script>';
    }
}
add_action('wp_footer', 'add_video_schema');

5. Result: SEO benefits

  • Google indexes the video page as a valid watch page
  • You get video snippets in search results
  • Better CTR from SERPs
  • Cleaner user experience