
数日前、ずっとやらなければならなかったものの、長い間先延ばしにしていた作業をようやく完了させました。欠落していた注目画像をすべて復元し、画像に不足していたaltテキストをすべて追加しました。また、欠落していたメタディスクリプションの記述もすべて完了させました。 合計で、自作の一回限りのスニペットを3つ使って、15,000件以上のレコードを追加または編集しました。今日は、欠落していた代表画像を取得するためのスニペットをここで紹介します。このスニペットは、他のサイトでもこの作業を行う際に使用したものです。
ブログをしばらく続けていると、注目画像が設定されていない古い記事がかなりあるのではないでしょうか。 これは、クラシックエディタが使われていた頃によくあったことで、画像を投稿本文に直接挿入したものの、「代表画像」ボックスでその画像を選択するのを忘れてしまうことが多かったからです。
記事に代表画像を設定せずに公開することは、単に見た目の問題にとどまらず、サイトの最適化やユーザー体験にも直接的な影響を及ぼします:
- ソーシャルSEOとオープングラフ:ソーシャルメディア、メッセージングアプリ、またはコンテンツアグリゲーターで投稿を共有する際、オープングラフタグ(
og:image)が表示すべき明確なリソースを見つけられず、CTR(クリック率)が大幅に低下してしまいます。 - テーマの視覚的な統一性:モダンなテンプレート(GeneratePress や GenerateBlocks を使用してデザインされたものなど)は、アーカイブのグリッド、関連記事のリスト、ヘッダーを作成する際に、注目画像に依存しています。注目画像がないと、レイアウトが崩れたり、位置がずれた隙間が生じたりします。
- ディスカバリーとフィード(RSS/Google Discover):Google DiscoverやRSSリーダーなどのツールでは、記事カードの表示効果を高めるため、高解像度のサムネイル画像が添付されたコンテンツを優先的に表示したり、その添付を必須としたりしています。
(このプラグインのようなもの)や処理に時間がかかる方法に頼らずにこの問題を解決するため、WordPressのダッシュボードから直接、各投稿の最初の画像を自動的に「注目画像」に設定する、シンプルで軽量なPHPスニペットを作成しました。投稿に画像が1枚も含まれていない場合は、コードにURLを追加することで、デフォルトの画像を指定することができます。
このコードスニペットは、具体的にはどのような動作をするのでしょうか?
このコードにより、WordPressの投稿一覧(wp-admin/edit.php)に、スマートな組み込みのバッチ処理ツールが追加されます:
- カスタム列を追加する:投稿一覧に「サムネイル画像がない」という列を作成し、どの投稿にサムネイル画像がないかを一目で確認できるようにします。
- 3段階の処理:投稿への割り当てを有効にすると、以下の順序で画像が検索されます:
- メディアライブラリ内で、その投稿に直接添付されている画像があるかどうかを確認してください。
- 直接の添付ファイルがない場合(たとえば、画像ブロックやHTMLを介して挿入された場合など)、コンテンツ内をスキャンして最初の
<img>タグを探し、attachment_url_to_postid()を使用してそのIDを抽出します。 - エントリに画像がまったく含まれていない場合、設定で指定されたデフォルト(代替)画像が割り当てられます。
- AJAXによる一括処理(バッチ処理):ヘッダーに「⚡このページのすべてを割り当てる」というボタンがあり、画面上に表示されているすべてのエントリを、連続した非同期リクエストを用いて順次処理します。また、リスト内の各投稿にある個別のボタンを使用して、手動でこの処理を行うこともできます。
- 下書きをフィルタリング:下書き状態のエントリーを自動的に除外するため、公開済みのコンテンツにのみ集中して取り組むことができます。
- 並べ替え可能な列:この機能を使用すると、リストを並べ替えて、特集画像のないすべてのエントリが上部にまとめて表示されるようにすることができます。
なぜこれほど軽量なのでしょうか?また、サーバーのパフォーマンスには影響しないのでしょうか?
この種の自動化ツールを使用する際によくある懸念として、記事数が数千件に及ぶサイトにおいて、データベースがフリーズしたり、PHPのメモリがオーバーロードしたりする恐れが挙げられます。このスニペットは、以下の最適化基準に沿って設計されています:
1. 管理画面(is_admin())でのみ動作します。
このコードは、管理パネルの入力画面(edit.php)専用にパッケージ化されています。公開サイトへのリクエストに対して、余分な負荷を一切追加することはありません。ユーザーやサイト速度の指標に、いかなる影響も及ぼしません。
2. クエリをメタキャッシュに送信する
投稿に画像が含まれているかどうかを判断するために、このスニペットは各行について複雑なデータベース検索を行うことはありません。代わりに、_thumbnail_idというメタデータキーを使用します。このキーは、WordPress がリストをレンダリングする際に、メタデータキャッシュを介してネイティブにメモリに読み込まれるものです。テーブルの読み込みはほぼ瞬時に行われます。
3. カスケード型非同期処理(順次AJAXループ)
データベースに対して大規模なクエリを実行して700件や1,000件のエントリを一度に更新する(これによりタイムアウトが発生したり、サーバーのメモリをすべて使い果たしたりする恐れがある)のではなく、「すべて割り当て」ボタンはJavaScriptのプロミスを使用して、エントリを1件ずつ処理します:
POST 1 (AJAX)⟶OKレスポンス⟶POST 2 (AJAX)⟶OKレスポンス…
これにより、リソースが安定的かつ一貫して利用されるため、行ごとにリアルタイムで進捗状況を確認することができます。
4. ファイルの重複なし
このスニペットは、ハードドライブ上の画像をダウンロードしたり、再結合したり、複製したりすることはありません。単に、メディアライブラリにすでに存在する画像のIDを取得し、wp_postmetaテーブルに該当するmeta_valueを割り当てることで、その画像を投稿にリンクさせるだけです。
コード(PHP + JavaScript)
このコードは、子テーマのfunctions.phpファイルに追加したり、カスタムプラグインに組み込んだり、あるいは「Code Snippets」などのカスタム関数プラグインを介して追加したりすることができます。
注:56行目の
$default_image_url変数には、本文に画像が含まれていない投稿に割り当てたい汎用画像の完全なURLを設定するようにしてください。
PHP
// 1. Añadir columna personalizada en el listado de entradas
add_filter( 'manage_posts_columns', 'custom_featured_status_column' );
function custom_featured_status_column( $columns )
$columns['featured_missing'] = 'Destacada Faltante';
return $columns;
}
// 2. Hacer la columna ordenable al hacer clic en el encabezado
add_filter( 'manage_edit-post_sortable_columns', 'custom_featured_sortable_column' );
function custom_featured_sortable_column( $columns ) {
$columns['featured_missing'] = 'featured_missing';
return $columns;
}
// 3. Modificar la consulta para ordenar por presencia de _thumbnail_id
add_action( 'pre_get_posts', 'custom_featured_column_orderby' );
function custom_featured_column_orderby( $query ) {
if ( ! is_admin() || ! $query->is_main_query() ) return;
if ( $query->get( 'orderby' ) === 'featured_missing' ) {
$query->set( 'meta_key', '_thumbnail_id' );
$query->set( 'orderby', 'meta_value_num' );
$query->set( 'meta_compare', 'NOT EXISTS' );
}
}
// 3.1. Añadir desplegable de filtro nativo en el listado de entradas
add_action( 'restrict_manage_posts', 'custom_featured_filter_dropdown' );
function custom_featured_filter_dropdown( $post_type ) {
if ( $post_type !== 'post' ) return;
$selected = isset( $_GET['filter_featured_status'] ) ? sanitize_text_field( $_GET['filter_featured_status'] ) : '';
?>
<select name="filter_featured_status" id="filter_featured_status">
<option value=""><?php _e( 'Todas las destacadas', 'textdomain' ); ?></option>
<option value="has_img" <?php selected( $selected, 'has_img' ); ?>>Tiene</option>
<option value="missing_img" <?php selected( $selected, 'missing_img' ); ?>>Falta</option>
</select>
<?php
}
// 3.2. Aplicar el filtro del desplegable a la consulta de WP_Query
add_filter( 'parse_query', 'custom_featured_filter_apply' );
function custom_featured_filter_apply( $query ) {
global $pagenow;
if ( ! is_admin() || $pagenow !== 'edit.php' || ! $query->is_main_query() ) return;
if ( isset( $_GET['filter_featured_status'] ) && $_GET['filter_featured_status'] !== '' ) {
$filter_value = sanitize_text_field( $_GET['filter_featured_status'] );
$meta_query = (array) $query->get( 'meta_query' );
if ( $filter_value === 'has_img' ) {
$meta_query[] = array(
'key' => '_thumbnail_id',
'compare' => 'EXISTS',
);
} elseif ( $filter_value === 'missing_img' ) {
$meta_query[] = array(
'key' => '_thumbnail_id',
'compare' => 'NOT EXISTS',
);
}
$query->set( 'meta_query', $meta_query );
}
}
// 4. Mostrar el estado y el botón rápido en la columna (Excluyendo borradores)
add_action( 'manage_posts_custom_column', 'custom_featured_status_column_content', 10, 2 );
function custom_featured_status_column_content( $column, $post_id ) {
if ( $column === 'featured_missing' ) {
if ( get_post_type( $post_id ) !== 'post' || get_post_status( $post_id ) === 'draft' ) {
echo '<span style="color: #999;">—</span>';
return;
}
$has_thumbnail = get_post_meta( $post_id, '_thumbnail_id', true );
if ( $has_thumbnail ) {
echo '<span style="color: #46b450; font-weight: bold;">✔ Tiene</span>';
} else {
echo '<span style="color: #dc3232; font-weight: bold;">❌ Falta</span> ';
echo '<button type="button" class="button button-small set-first-img-btn" data-postid="' . esc_attr( $post_id ) . '">Asignar imagen</button>';
echo '<span class="set-img-status" style="margin-left:5px;"></span>';
}
}
}
// 5. Procesar la asignación por AJAX (con fallback a la imagen por defecto)
add_action( 'wp_ajax_set_first_image_as_featured', 'ajax_set_first_image_as_featured' );
function ajax_set_first_image_as_featured() {
check_ajax_referer( 'set_first_image_nonce', 'nonce' );
$post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
wp_send_json_error( 'Sin permisos o ID inválido' );
}
if ( get_post_status( $post_id ) === 'draft' ) {
wp_send_json_error( 'La entrada es un borrador' );
}
// =========================================================================
// CONFIGURACIÓN: Reemplaza esta URL por la URL completa de tu imagen por defecto
// =========================================================================
$default_image_url = 'https://tu-dominio.com/wp-content/uploads/imagen-por-defecto.jpg';
// A. Buscar adjuntos directos al post
$attachments = get_posts( array(
'post_type' => 'attachment',
'posts_per_page' => 1,
'post_parent' => $post_id,
'post_mime_type' => 'image',
'orderby' => 'menu_order',
'order' => 'ASC',
'fields' => 'ids'
) );
if ( ! empty( $attachments ) ) {
set_post_thumbnail( $post_id, $attachments[0] );
wp_send_json_success( '¡Asignada primera!' );
}
// B. Si no hay adjuntos, buscar <img> en el contenido
$post = get_post( $post_id );
preg_match_all( '/<img.+?src=[\'"]([^\'"]+)[\'"]/i', $post->post_content, $matches );
if ( isset( $matches[1][0] ) ) {
$attachment_id = attachment_url_to_postid( $matches[1][0] );
if ( $attachment_id ) {
set_post_thumbnail( $post_id, $attachment_id );
wp_send_json_success( '¡Asignada primera!' );
}
}
// C. Si no hay ninguna imagen en el post, asignar la imagen por defecto
if ( ! empty( $default_image_url ) ) {
$default_id = attachment_url_to_postid( $default_image_url );
if ( $default_id ) {
set_post_thumbnail( $post_id, $default_id );
wp_send_json_success( '¡Asignada por defecto!' );
}
}
wp_send_json_error( 'No se encontró ninguna imagen en el post ni ID de fallback' );
}
// 6. Cargar JS y botón masivo en el admin (edit.php)
add_action( 'admin_footer-edit.php', 'custom_featured_ajax_script' );
function custom_featured_ajax_script() {
$screen = get_current_screen();
if ( $screen->post_type !== 'post' ) return;
$nonce = wp_create_nonce( 'set_first_image_nonce' );
?>
<script>
jQuery(document).ready(function($) {
$('<button type="button" id="process-all-page-btn" class="page-title-action" style="margin-left:10px; background:#2271b1; color:#fff; border-color:#2271b1;">⚡ Asignar todas en esta página</button>')
.insertAfter('.wp-heading-inline');
function processButton($btn) {
return new Promise(function(resolve) {
var $status = $btn.siblings('.set-img-status');
var postId = $btn.data('postid');
$btn.prop('disabled', true).text('Procesando...');
$.post(ajaxurl, {
action: 'set_first_image_as_featured',
post_id: postId,
nonce: '<?php echo $nonce; ?>'
}, function(response) {
if (response.success) {
$btn.parent().html('<span style="color: #46b450; font-weight: bold;">✔ ' + response.data + '</span>');
} else {
$status.css('color', '#dc3232').text(response.data);
$btn.prop('disabled', false).text('Asignar imagen');
}
resolve();
}).fail(function() {
$btn.prop('disabled', false).text('Error');
resolve();
});
});
}
$(document).on('click', '.set-first-img-btn', function(e) {
e.preventDefault();
processButton($(this));
});
$('#process-all-page-btn').on('click', async function(e) {
e.preventDefault();
var $batchBtn = $(this);
var $pendingButtons = $('.set-first-img-btn:not(:disabled)');
if ($pendingButtons.length === 0) {
alert('No hay entradas pendientes de asignar en esta página.');
return;
}
$batchBtn.prop('disabled', true).text('Procesando por lote...');
for (var i = 0; i < $pendingButtons.length; i++) {
await processButton($($pendingButtons[i]));
}
$batchBtn.text('¡Lote completado!').css('background', '#46b450');
setTimeout(function() {
$batchBtn.prop('disabled', false).text('⚡ Asignar todas en esta página').css('background', '#2271b1');
}, 3000);
});
});
</script>
<?php
}
使い方はどうなりますか?
- 管理パネルの「投稿」一覧に移動してください。
- 上部の「表示オプション」タブを開き、1ページあたりの表示件数を50件または100件に変更してください。(まずは少ない件数から試してみて、サーバーの応答が良好であれば徐々に増やしてください。)

- 「欠落しているハイライト」列の見出しをクリックすると、未処理のエントリを並べ替えたりグループ分けしたりできます。フィルターでは、「含む」、「欠落」、「すべて」から選択できます。
- 欠落している注目画像を一括で割り当てる準備が整ったら、青い 「 」ボタン (「このページのすべてを割り当てる」と表示されているもの )をクリックし、そのバッチのサムネイル割り当てが完了するまで待ちます。必要に応じて、1つずつ割り当てることも可能です。
- 次のページに進み、ライブラリの整理が完了するまでこの手順を繰り返してください。
オプション:自動割り当てのスニペット
今後この作業を繰り返す必要がないように、この短いスニペットを追加することができます。 これは、投稿を保存または公開したときにのみバックグラウンドで実行されます。手動で注目画像が選択されていないことを検知すると、コンテンツ内の最初の画像(または、$default_image_url = で追加する必要があるデフォルトの代替画像)を自動的に割り当てます。
PHP
// Asignar imagen destacada automáticamente al publicar/guardar nuevas entradas
add_action( 'save_post_post', 'auto_set_featured_image_on_save', 10, 3 );
function auto_set_featured_image_on_save( $post_id, $post, $update ) {
// Evitar autoguardados, revisiones o peticiones AJAX/REST innecesarias
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
if ( wp_is_post_revision( $post_id ) || $post->post_status === 'auto-draft' ) return;
// Si la entrada ya tiene imagen destacada marcada, no hacemos nada
if ( has_post_thumbnail( $post_id ) ) return;
$default_image_url = 'https://tu-dominio.com/wp-content/uploads/imagen-por-defecto.jpg';
// 1. Buscar primera imagen en el contenido
preg_match_all( '/<img.+?src=[\'"]([^\'"]+)[\'"]/i', $post->post_content, $matches );
if ( isset( $matches[1][0] ) ) {
$attachment_id = attachment_url_to_postid( $matches[1][0] );
if ( $attachment_id ) {
set_post_thumbnail( $post_id, $attachment_id );
return;
}
}
// 2. Si el post no tiene imágenes, asignar la imagen por defecto
if ( ! empty( $default_image_url ) ) {
$default_id = attachment_url_to_postid( $default_image_url );
if ( $default_id ) {
set_post_thumbnail( $post_id, $default_id );
}
}
}






