
며칠 전, 오랫동안 미뤄왔지만 꼭 해야만 했던 작업을 마쳤습니다. 누락된 대표 이미지를모두 찾아내고, 해당 이미지들에 누락된 대체 텍스트를 모두 추가했습니다. 또한 누락된 메타 설명도 모두 작성했습니다. 총 15,000개 이상의 레코드를 제 스스로 만든 세 가지 일회용 스니펫을 사용하여 추가하거나 수정했습니다. 오늘은 누락된 대표 이미지를 복구하는 데 사용한 스니펫을 이곳에 공유합니다. 이 스니펫은 다른 사이트에서도 같은 작업을 수행하는 데 활용했던 것입니다.
블로그를 한 지 꽤 되었다면, 대표 이미지가 지정되지 않은 오래된 게시물이 꽤 많이 있을 가능성이 높습니다. 이는 클래식 에디터 시절에 자주 발생하던 문제로, 당시에는 이미지를 글 본문에 직접 삽입하곤 했지만 ‘대표 이미지’ 상자에서 해당 이미지를 선택하는 것을 깜빡하는 경우가 많았기 때문입니다.
대표 이미지를 포함하지 않은 게시물을 올리는 것은 단순히 미적 문제뿐만 아니라, 사이트의 검색 엔진 최적화(SEO)와 사용자 경험에 직접적인 영향을 미칩니다:
- 소셜 SEO와 오픈 그래프: 소셜 미디어, 메신저 앱 또는 콘텐츠 집계 사이트에서 게시물을 공유할 때, 오픈 그래프 태그(
og:image)가 표시할 명확한 리소스를 찾지 못해 CTR(클릭률)이 급격히 떨어집니다. - 테마의 시각적 일관성: 모던스타일의 템플릿(GeneratePress나 GenerateBlocks를 사용하여 디자인된 템플릿 등)은 아카이브 그리드, 관련 게시물 목록 또는 헤더를 구성할 때 대표 이미지를 활용합니다. 대표 이미지가 없으면 레이아웃이 깨지거나 정렬이 어긋난 빈 공간이 생깁니다.
- 검색 및 피드(RSS / Google Discover): Google Discover나 RSS 리더와 같은 도구는 기사 카드를 더욱 돋보이게 하기 위해 고해상도 썸네일이 포함된 콘텐츠를 우선적으로 표시하거나 이를 필수 조건으로 삼습니다.
플러그인(예: 이 플러그인)이나 느린 처리 과정을 거치지 않고 이 문제를 해결하기 위해, 워드프레스 대시보드에서 직접 각 게시물의 첫 번째 이미지를 대표 이미지로 자동 설정해 주는 간단하고 가벼운 PHP 스니펫을 만들었습니다. 게시물에 이미지가 하나도 없는 경우, 코드에 URL을 추가하여 기본 이미지를 지정할 수 있습니다.
이 코드 조각은 정확히 어떤 역할을 하나요?
이 코드는 워드프레스 게시물 목록(wp-admin/edit.php)에 스마트한 내장형 일괄 처리 도구를 추가합니다:
- 사용자 정의 열 추가: 게시물 목록에 ‘미설정 대표 이미지’ 열을 생성하여 썸네일이 없는 게시물을 한눈에 확인할 수 있습니다.
- 3단계 처리: 게시물에 대한 할당을 활성화하면, 시스템은 정확히 다음 순서대로 이미지를 검색합니다:
- 미디어 라이브러리에서 해당 게시물에 직접 첨부된 이미지가 있는지 확인해 보세요.
- 직접 첨부 파일이 없는 경우(예: 이미지 블록이나 HTML을 통해 삽입된 경우), 콘텐츠에서 첫 번째
<img>태그를 찾아attachment_url_to_postid() 함수를사용하여 해당 ID를 추출합니다. - 항목에 이미지가 전혀 포함되어 있지 않은 경우, 구성에 지정된 기본(대체) 이미지를 할당합니다.
- AJAX 일괄 처리(배치 처리): 헤더에 “⚡ 이 페이지의 모든 항목 할당” 버튼이 있으며, 이 버튼을 클릭하면 화면에 표시된 모든 항목을 연속적인 비동기 요청을 통해 순차적으로 처리합니다. 또한 목록에 있는 각 게시물 옆에 있는 개별 버튼을 사용하여 수동으로 이 작업을 수행할 수도 있습니다.
- 초안 필터링: ‘초안’ 상태의 게시물을 자동으로 제외하여, 게시된 콘텐츠에만 집중할 수 있도록 합니다.
- 정렬 가능한 열: 추천 이미지가 없는 모든 항목이 상단에 한데 묶이도록 목록을 정렬할 수 있습니다.
왜 이렇게 가벼운 건가요? 그리고 서버 성능에 영향을 주지 않나요?
이러한 유형의 자동화 도구를 사용할 때 흔히 제기되는 우려는, 수천 개의 기사가 있는 사이트에서 데이터베이스가 응답하지 않게 되거나 PHP 메모리가 과부하될까 봐 하는 두려움입니다. 이 코드 조각은 다음과 같은 최적화 기준에 따라 설계되었습니다:
1. 관리자 섹션(is_admin())에서만 실행됩니다.
이 코드는 관리자 패널의 입력 화면(edit.php) 전용으로 패키징되었습니다 . 일반 사이트의 요청에 불필요한 부하를 전혀 주지 않습니다. 사용자 경험이나 속도 지표에는 어떠한 영향도 미치지 않습니다.
2. 메타 캐시에 쿼리를 전송합니다.
게시물에 이미지가 포함되어 있는지 여부를 확인하기 위해, 이 스니펫은 각 행에 대해 복잡한 데이터베이스 검색을 수행하지 않습니다. 대신 _thumbnail_id 메타데이터 키를 사용하는데, 워드프레스는 목록을 렌더링할 때 메타데이터 캐시를 통해 이 키를 기본적으로 메모리에 불러옵니다. 따라서 테이블은 거의 즉시 로드됩니다.
3. 계단식 비동기 처리 (순차적 AJAX 루프)
데이터베이스에 대규모 쿼리를 실행하여 700개나 1,000개의 항목을 한꺼번에 업데이트하는 것(이 경우 타임아웃이 발생하거나 서버의 메모리가 모두 소진될 수 있음) 대신, ‘모두 할당’ 버튼은 자바스크립트 프롬라이스를 사용하여 항목을 하나씩 처리합니다:
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
}
어떻게 사용하나요?
- 관리자 패널에서 ‘게시물’ 목록으로 이동하세요.
- 상단의 ‘표시 옵션’ 탭을 열고, 페이지당 표시할 항목 수를 50개 또는 100개로 변경하세요. (처음에는 더 적은 수로 시작해 보고, 서버 반응이 원활하다면 점차 늘려보세요.)

- ‘누락된 하이라이트’ 열 머리글을 클릭하면 미처리 항목을 정렬하고 그룹화할 수 있습니다. 필터에서 ‘포함’, ‘누락’ 또는 ‘전체’ 중에서 선택할 수 있습니다.
- 누락된 대표 이미지를 일괄적으로 할당할 준비가 되었다고 판단되면, ‘이 페이지의 모든 항목 할당(Assign all on this page)’이라고 표시된 파란색 ‘ ’ 버튼을 클릭하고, 해당 일괄 처리가 완료되어 썸네일 할당이 끝날 때까지 기다리세요. 원하신다면 하나씩 할당할 수도 있습니다.
- 다음 페이지로 이동하여 라이브러리 정리를 모두 마칠 때까지 이 과정을 반복하세요.
선택 사항: 자동 할당 스니펫
앞으로 이 작업을 반복하지 않으려면, 이 짧은 코드 조각을 추가하면 됩니다. 이 코드는 게시물을 저장하거나 게시할 때만 백그라운드에서 실행됩니다. 수동으로 대표 이미지를 선택하지 않은 것으로 감지되면, 콘텐츠의 첫 번째 이미지(또는 $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 );
}
}
}






