
Vài ngày trước, tôi đã hoàn thành một công việc đã bị trì hoãn quá lâu, vốn cần phải làm từ lâu nhưng tôi cứ trì hoãn mãi. Tôiđã khôi phục lạitất cả các hình ảnh nổi bật còn thiếu và bổ sung văn bản thay thế (alt text) cho các hình ảnh đó. Tôi cũng đã hoàn thành việc viết tất cả các mô tả meta còn thiếu. Tổng cộng, tôi đã thêm hoặc chỉnh sửa hơn 15.000 bản ghi bằng cách sử dụng ba đoạn mã (snippet) do chính tôi viết riêng. Hôm nay, tôi chia sẻ đoạn mã dùng để khôi phục các hình ảnh nổi bật còn thiếu tại đây, đoạn mã mà tôi cũng đã sử dụng để thực hiện công việc này trên các trang web khác.
Nếu bạn đã viết blog được một thời gian, rất có thể bạn sẽ có khá nhiều bài viết cũ chưa được gán hình ảnh nổi bật. Tình trạng này từng xảy ra rất thường xuyên vào thời kỳ sử dụng trình soạn thảo cổ điển, khi chúng ta chèn ảnh trực tiếp vào nội dung bài viết nhưng lại quên chọn ảnh đó trong ô ‘Hình ảnh nổi bật’.
Việc đăng bài mà không có hình ảnh nổi bật không chỉ là vấn đề về mặt thẩm mỹ, mà còn ảnh hưởng trực tiếp đến việc tối ưu hóa trang web của bạn và trải nghiệm người dùng:
- SEO trên mạng xã hội và Open Graph: Khi chia sẻ bài đăng trên mạng xã hội, các ứng dụng nhắn tin hoặc các nền tảng tổng hợp nội dung, các thẻ Open Graph (
og:image) không thể tìm thấy nguồn tài nguyên rõ ràng để hiển thị, dẫn đến việc tỷ lệ nhấp chuột (CTR) giảm mạnh. - Tính nhất quán về mặt hình ảnh trong chủ đề: Các mẫu thiết kế hiện đại (chẳng hạn như những mẫu được thiết kế bằng GeneratePress hoặc GenerateBlocks) phụ thuộc vào hình ảnh nổi bật để tạo lưới bài viết, danh sách bài viết liên quan hoặc các tiêu đề. Nếu thiếu hình ảnh này, bố cục sẽ bị lỗi hoặc xuất hiện các khoảng trống không đều.
- Tìm kiếm và nguồn cấp dữ liệu (RSS / Google Discover): Các công cụ như Google Discover hoặc trình đọc RSS sẽ ưu tiên hoặc yêu cầu nội dung có hình thu nhỏ độ phân giải cao đi kèm để làm nổi bật thẻ bài viết.
Để giải quyết vấn đề này mà không cần dùng đến các plugin (như plugin này) hay các quy trình chậm chạp, tôi đã tạo ra đoạn mã PHP đơn giản và gọn nhẹ này, giúp tự động đặt hình ảnh đầu tiên trong mỗi bài viết làm hình ảnh nổi bật, trực tiếp từ bảng điều khiển WordPress. Nếu bài viết của bạn không có hình ảnh nào, bạn có thể chỉ định một hình ảnh mặc định bằng cách thêm URL của hình ảnh đó vào đoạn mã.
Đoạn mã này cụ thể có chức năng gì?
Mã này bổ sung một công cụ xử lý hàng loạt thông minh và tích hợp sẵn vào danh sách bài viết của WordPress (wp-admin/edit.php):
- Thêm một cột tùy chỉnh: Tạo cột “Thiếu hình ảnhnổi bật ” trong danh sách bài viết để có thể nhận biết ngay những bài viết nào đang thiếu hình thu nhỏ.
- Quy trình xử lý 3 bước: Khi bạn kích hoạt tính năng gán hình ảnh cho một bài đăng, hệ thống sẽ tìm kiếm hình ảnh theo đúng thứ tự sau đây:
- Hãy kiểm tra xem có hình ảnh nào được đính kèm trực tiếp vào bài đăng trong Thư viện phương tiện hay không.
- Nếu không có tệp đính kèm trực tiếp (ví dụ: nếu chúng được chèn qua khối hình ảnh hoặc HTML), hãy quét nội dung để tìm thẻ
<img>đầu tiên nhằm trích xuất ID của nó bằnghàm attachment_url_to_postid(). - Nếu mục đó hoàn toàn không chứa hình ảnh nào, hệ thống sẽ gán hình ảnh mặc định (dự phòng) được chỉ định trong phần cấu hình.
- Xử lý hàng loạt bằng AJAX (xử lý theo lô): Bao gồm một nút ở phần đầu trang (“⚡ Gán tất cả trên trang này”) để xử lý lần lượt tất cả các mục hiển thị trên màn hình bằng các yêu cầu không đồng bộ liên tiếp. Bạn cũng có thể thực hiện việc này thủ công bằng cách sử dụng nút riêng cho từng bài đăng trong danh sách.
- Lọc các bản nháp: Tự động loại trừ các bài viết đang ở trạng thái bản nháp để bạn có thể tập trung hoàn toàn vào nội dung đã được xuất bản.
- Cột có thể sắp xếp: Cho phép bạn sắp xếp danh sách sao cho tất cả các mục không có hình ảnh nổi bật được nhóm lại với nhau ở phía trên cùng.
Tại sao nó lại nhẹ như vậy và liệu điều đó có ảnh hưởng đến hiệu suất máy chủ không?
Một lo ngại phổ biến khi sử dụng loại công cụ tự động này là nguy cơ làm treo cơ sở dữ liệu hoặc gây quá tải bộ nhớ PHP trên các trang web có hàng nghìn bài viết. Đoạn mã này đã được thiết kế tuân theo các tiêu chí tối ưu hóa sau đây:
1. Chức năng này chỉ hoạt động trong phần Quản trị (is_admin())
Mã này được đóng gói dành riêng cho màn hình nhập liệu của bảng điều khiển quản trị (edit.php). Nó không làm tăng thêm bất kỳ dòng mã thừa nào cho các yêu cầu trên trang web công khai. Người dùng của bạn và các chỉ số về tốc độ sẽ không bị ảnh hưởng dưới bất kỳ hình thức nào.
2. Gửi các truy vấn trực tiếp đến bộ nhớ đệm Meta
Để xác định liệu một bài đăng có chứa hình ảnh hay không, đoạn mã này không thực hiện các thao tác tìm kiếm phức tạp trong cơ sở dữ liệu cho từng hàng. Thay vào đó, nó sử dụng khóa siêu dữ liệu _thumbnail_id, mà WordPress tự động tải vào bộ nhớ thông qua bộ đệm siêu dữ liệu khi hiển thị danh sách. Bảng dữ liệu được tải gần như ngay lập tức.
3. Xử lý không đồng bộ theo chuỗi (vòng lặp AJAX tuần tự)
Thay vì thực hiện một truy vấn quy mô lớn trên cơ sở dữ liệu để cập nhật 700 hoặc 1.000 bản ghi cùng một lúc (điều này có thể dẫn đến tình trạng hết thời gian chờ hoặc làm cạn kiệt toàn bộ bộ nhớ của máy chủ), nút “Assign All” sẽ xử lý các bản ghi lần lượt bằng cách sử dụng các hứa hẹn (promises) của JavaScript:
POST 1 (AJAX)⟶Phản hồi OK⟶POST 2 (AJAX)⟶Phản hồi OK…
Điều này đảm bảo việc sử dụng tài nguyên một cách ổn định và nhất quán, giúp bạn theo dõi tiến độ theo thời gian thực, từng hàng một.
4. Không trùng lặp tệp
Tiện ích này không tải xuống, ghép lại hay sao chép hình ảnh trên ổ cứng của bạn. Nó chỉ lấy ID của hình ảnh đã có sẵn trong Thư viện phương tiện của bạn và liên kết hình ảnh đó với bài viết bằng cách gán giá trị meta_value tương ứng trong bảng wp_postmeta.
Mã nguồn (PHP + JavaScript)
Bạn có thể thêm đoạn mã này vào tệp functions.php của chủ đề con, vào một plugin tùy chỉnh, hoặc thông qua một plugin chức năng tùy chỉnh như Code Snippets hoặc các plugin tương tự.
Lưu ý: Hãy nhớ thiết lập biến
$default_image_urlở dòng 56 thành URL đầy đủ của hình ảnh chung mà bạn muốn gán cho các bài viết không chứa hình ảnh nào trong nội dung.
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
}
Sử dụng nó như thế nào?
- Vào danh sách “Bài viết” trong bảng điều khiển quản trị của bạn.
- Mở tab “Tùy chọn hiển thị” ở phía trên và thay đổi số lượng mục hiển thị thành 50 hoặc 100 mục mỗi trang. (Hãy bắt đầu bằng cách thử các con số nhỏ hơn và tăng dần lên nếu máy chủ của bạn phản hồi tốt.)

- Nhấp vào tiêu đề cột “Các điểm nổi bật còn thiếu” để sắp xếp và nhóm các mục còn thiếu. Trong phần bộ lọc, bạn có thể chọn giữa “Có”, “Còn thiếu” hoặc “Tất cả”
- Khi bạn đã chắc chắn rằng mình đã sẵn sàng để bắt đầu gán hàng loạt các hình ảnh nổi bật còn thiếu, hãy nhấp vào nút màu xanh dương có tên “ ” (Gán tất cả trên trang này) và để quá trình tự động hoàn tất việc gán các hình thu nhỏ cho lô đó. Nếu muốn, bạn có thể gán chúng từng cái một.
- Hãy chuyển sang trang tiếp theo và lặp lại cho đến khi bạn dọn dẹp xong thư viện của mình.
Tùy chọn: đoạn mã gán giá trị tự động
Để tránh phải lặp lại tác vụ này trong tương lai, bạn có thể thêm đoạn mã ngắn này. Mã này chỉ chạy ở chế độ nền khi bạn lưu hoặc xuất bản bài viết: nếu phát hiện ra rằng bạn chưa chọn thủ công hình ảnh nổi bật, nó sẽ tự động gán hình ảnh đầu tiên trong nội dung (hoặc hình ảnh dự phòng mặc định, mà bạn cần thêm vào $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 );
}
}
}







