If you post a lot of videos, your blog list and related posts are full of empty boxes, unless you download every thumbnail and upload it as a featured image. WordPress itself has never done this for you. When thumbnails “just appeared” before, they came from a plugin (Jetpack’s Related Posts does it, for example). When that plugin changes, they disappear.
How it works
YouTube serves a thumbnail for every video at a predictable address: https://i.ytimg.com/vi/VIDEO_ID/hqdefault.jpg. The snippet finds the first YouTube video in the post (a YouTube embed, a youtu.be link or a Short) and, only when the post has no featured image, uses that thumbnail instead.
<?php
// The ID of the first YouTube video in a post, or ''.
$video_id = static function ( $post_id ) {
$content = (string) get_post_field( 'post_content', $post_id );
return preg_match( '~(?:youtube\.com/(?:watch\?(?:[^"\s<]*&(?:amp;)?)?v=|embed/|shorts/|live/)|youtu\.be/)([A-Za-z0-9_-]{11})~', $content, $match ) ? $match[1] : '';
};
// Classic themes ask "is there a featured image?" first.
add_filter(
'has_post_thumbnail',
static function ( $has, $post ) use ( $video_id ) {
$post = get_post( $post );
if ( $has || ! $post || is_admin() || ( is_singular() && get_queried_object_id() === $post->ID ) ) {
return $has;
}
return '' !== $video_id( $post->ID );
},
10,
2
);
add_filter(
'post_thumbnail_html',
static function ( $html, $post_id, $thumbnail_id, $size, $attr ) use ( $video_id ) {
if ( '' !== $html || is_admin() || ( is_singular() && get_queried_object_id() === (int) $post_id ) ) {
return $html; // A real featured image wins; on the post itself the video is already there.
}
$id = $video_id( $post_id );
if ( '' === $id ) {
return $html;
}
$attr = is_array( $attr ) ? $attr : wp_parse_args( $attr );
$style = empty( $attr['style'] ) ? '' : ' style="' . esc_attr( $attr['style'] ) . '"';
return '<img src="' . esc_url( 'https://i.ytimg.com/vi/' . $id . '/hqdefault.jpg' ) . '" alt="' . esc_attr( get_the_title( $post_id ) ) . '" class="wp-post-image" width="480" height="360" loading="lazy" decoding="async"' . $style . '>';
},
10,
5
);
Add it to your child theme’s functions.php or a code snippets plugin.
Details that matter
- A real featured image always wins. The snippet only fills the gap.
- Not on the post itself. There the video is already playing, so a thumbnail above it would be a duplicate. It shows in lists, related posts and query loops.
- Block and classic themes. Block themes use the Featured Image block, which goes through
post_thumbnail_html. Classic themes usually askhas_post_thumbnail()first, so the snippet answers that too. - CSS backgrounds won’t work. Themes that use the featured image’s URL as a background image read it in another way, so they won’t pick it up.
- Privacy: the image loads from YouTube’s servers, so the visitor’s browser contacts Google. In the EU, mention it in your privacy policy.
Tested with
WordPress 7.1 with Twenty Twenty-Five (a query loop) and Storefront (the blog list): YouTube embeds and Shorts get thumbnails, posts without a video are untouched, and the video post’s own page doesn’t show the thumbnail.
In Snipfire, this is “YouTube thumbnail when there’s no featured image” in the Images and media library.