- Snippet
- Cách hoạt động (thứ tự ưu tiên)
- Hành vi mặc định
- Yêu cầu tương thích
- Các filter cấu hình (đủ bộ)
- Đổi tiền tố khi dùng heading
- Giới hạn độ dài alt
- Bật ghi đè alt cũ (chuẩn hóa toàn site)
- Tắt tiền tố khi lấy từ heading
- Ví dụ nâng cao: bỏ qua ảnh logo/site-branding
- Ví dụ nâng cao: chỉ ghi đè alt với ảnh không có thuộc tính title
- Quy trình triển khai nhanh
- Test nhanh chất lượng alt
- Lợi ích SEO và truy cập
- Gỡ lỗi thường gặp
- Lưu ý hiệu năng
Snippet
/**
* Auto-fill missing IMG alt in the_content (WP_HTML_Tag_Processor only)
* Ưu tiên:
* 1) Figure caption
* 2) Heading gần nhất phía trên (+ prefix "Ảnh minh họa: ")
* 3) Post title (singular)
* 4) Filename
*
* Tùy biến qua filter:
* - init_auto_alt_prefix (mặc định "Ảnh minh họa: ")
* - init_auto_alt_max_length (mặc định 160)
* - init_auto_alt_overwrite_existing (mặc định false)
* - init_auto_alt_use_prefix_for_heading (mặc định true)
*/
add_filter('the_content', function ($html) {
if (is_admin() || stripos($html, '<img') === false) return $html;
if (!class_exists('WP_HTML_Tag_Processor')) return $html;
// ==== Config ====
$prefix = apply_filters('init_auto_alt_prefix', 'Ảnh minh họa: ');
$max_len = (int) apply_filters('init_auto_alt_max_length', 160);
$overwrite = (bool) apply_filters('init_auto_alt_overwrite_existing', false);
// ==== Helpers ====
$clean_text = function ($text) use ($max_len) {
$text = wp_strip_all_tags((string) $text, true);
$text = trim(preg_replace('/\s+/', ' ', $text));
if ($max_len > 0 && mb_strlen($text) > $max_len) {
$text = mb_substr($text, 0, $max_len - 3) . '...';
}
return $text;
};
$clean_filename_from_url = function ($url) {
if (!$url) return '';
$path = parse_url($url, PHP_URL_PATH);
if (!$path) return '';
$base = urldecode(wp_basename($path));
$base = preg_replace('/\.[a-z0-9]+$/i', '', $base); // bỏ đuôi
$base = preg_replace('/-(\d+x\d+|scaled)$/i', '', $base); // bỏ -300x200, -scaled
$base = preg_replace('/[-_+]+/', ' ', $base); // gạch -> space
$base = trim(preg_replace('/\s+/', ' ', $base));
if ($base === '') return '';
return mb_strtoupper(mb_substr($base, 0, 1)) . mb_substr($base, 1);
};
$post_title = (is_singular() ? get_the_title() : '');
$post_title = $post_title ? $clean_text($post_title) : '';
// Cache attachment title theo ID/URL để tránh query lặp
static $att_title_cache = [];
$get_attachment_title = function ($img_class, $src) use (&$att_title_cache, $clean_text) {
// Từ class wp-image-123
if ($img_class && preg_match('/wp-image-(\d+)/', $img_class, $m)) {
$id = (int) $m[1];
if ($id > 0) {
if (!isset($att_title_cache["id:$id"])) {
$p = get_post($id);
$att_title_cache["id:$id"] = ($p && !is_wp_error($p)) ? $clean_text($p->post_title ?? '') : '';
}
if ($att_title_cache["id:$id"] !== '') return $att_title_cache["id:$id"];
}
}
// Fallback theo URL
if ($src) {
if (!isset($att_title_cache["url:$src"])) {
$aid = function_exists('attachment_url_to_postid') ? attachment_url_to_postid($src) : 0;
if ($aid) {
$p = get_post($aid);
$att_title_cache["url:$src"] = ($p && !is_wp_error($p)) ? $clean_text($p->post_title ?? '') : '';
} else {
$att_title_cache["url:$src"] = '';
}
}
if ($att_title_cache["url:$src"] !== '') return $att_title_cache["url:$src"];
}
return '';
};
// ==== Bước 1: map figure -> caption bằng cách gắn index tạm lên IMG đầu tiên trong figure ====
$figure_map = []; // idx => caption
$auto_idx = 0;
if (preg_match_all('/<figure\b[^>]*>.*?<\/figure>/is', $html, $figs, PREG_OFFSET_CAPTURE)) {
foreach ($figs[0] as $m) {
$frag = $m[0];
if (stripos($frag, '<img') === false || stripos($frag, '<figcaption') === false) continue;
if (!preg_match('/<figcaption\b[^>]*>(.*?)<\/figcaption>/is', $frag, $c)) continue;
$cap = $clean_text($c[1] ?? '');
if ($cap === '') continue;
// chỉ đánh index vào IMG đầu tiên trong figure
$new_frag = preg_replace('/<img\b/i', '<img data-init-auto-idx="'.(++$auto_idx).'" ', $frag, 1);
$figure_map[$auto_idx] = $cap;
// replace vào toàn bộ HTML (an toàn vì dùng chuỗi y hệt)
$html = str_replace($frag, $new_frag, $html);
}
}
// ==== Bước 2: duyệt HTML bằng Tag Processor ====
$processor = new WP_HTML_Tag_Processor($html);
$last_heading = '';
$updated = false;
while ($processor->next_tag()) {
$tag = $processor->get_tag();
// Track heading gần nhất phía trên
if (in_array($tag, ['H1','H2','H3','H4','H5','H6'], true)) {
$last_heading = $clean_text($processor->get_modifiable_text());
continue;
}
if ($tag !== 'IMG') continue;
// Bỏ qua: class .no-auto-alt, role="presentation", aria-hidden="true"
$class_attr = (string) $processor->get_attribute('class');
if ($class_attr && preg_match('/\bno-auto-alt\b/', $class_attr)) continue;
$role = $processor->get_attribute('role');
$aria = $processor->get_attribute('aria-hidden');
if ($role === 'presentation' || $aria === 'true') continue;
// Respect alt sẵn có
$alt_current = (string) $processor->get_attribute('alt');
if (!$overwrite && trim($alt_current) !== '') continue;
// Lấy src / lazy-src
$src = $processor->get_attribute('src');
if (!$src) {
foreach (['data-src','data-lazy-src','data-original'] as $k) {
$src = $processor->get_attribute($k);
if ($src) break;
}
}
// Candidate 1: figure caption qua data-init-auto-idx
$candidate = '';
$idx = $processor->get_attribute('data-init-auto-idx');
if ($idx && isset($figure_map[(int)$idx])) {
$candidate = $figure_map[(int)$idx];
}
// Candidate 2: attachment title hoặc img@title
if ($candidate === '') {
$candidate = $get_attachment_title($class_attr, $src);
if ($candidate === '') {
$img_title = (string) $processor->get_attribute('title');
if ($img_title) $candidate = $clean_text($img_title);
}
}
// Candidate 3: heading gần nhất phía trên + prefix
if ($candidate === '' && $last_heading !== '') {
$candidate = $last_heading;
if ((bool) apply_filters('init_auto_alt_use_prefix_for_heading', true)) {
$candidate = $prefix . $candidate;
}
}
// Candidate 4: post title
if ($candidate === '' && $post_title !== '') {
$candidate = $post_title;
}
// Candidate 5: filename
if ($candidate === '' && $src) {
$candidate = $clean_filename_from_url($src);
}
if ($candidate === '') $candidate = 'Ảnh minh họa';
$processor->set_attribute('alt', $candidate);
$processor->set_attribute('data-init-auto-alt', '1');
$updated = true;
}
return $updated ? $processor->get_updated_html() : $html;
}, 15);
Cách hoạt động (thứ tự ưu tiên)
- Figure caption: Nếu ảnh nằm trong <figure> có <figcaption>, caption sẽ được dùng làm alt.
- Heading gần nhất phía trên: Lấy H1–H6 gần nhất trước ảnh. Mặc định thêm tiền tố “Ảnh minh họa: ”.
- Tiêu đề bài viết: Khi không có caption/heading phù hợp.
- Tên file: Lọc bỏ đuôi, kích thước kiểu
-300x200,-scaled, đổi gạch thành khoảng trắng.
Hành vi mặc định
- Chỉ điền khi
altđang trống (không ghi đè), trừ khi bạn bật ghi đè qua filter. - Bỏ qua ảnh trang trí: nếu
role="presentation"hoặcaria-hidden="true". - Hỗ trợ lazy-src:
data-src,data-lazy-src,data-original. - Có “cửa thoát”: thêm class
no-auto-altvào <img> để bỏ qua.
Yêu cầu tương thích
- WordPress 6.2 trở lên (có lớp
WP_HTML_Tag_Processor). - Chạy ở frontend qua filter
the_content.
Các filter cấu hình (đủ bộ)
Đặt các đoạn sau vào functions.php hoặc một snippet khác (có thể dùng chung với snippet chính):
Đổi tiền tố khi dùng heading
add_filter('init_auto_alt_prefix', function () { return 'Minh họa: '; });
Giới hạn độ dài alt
add_filter('init_auto_alt_max_length', function () { return 120; // mặc định 160 });
Bật ghi đè alt cũ (chuẩn hóa toàn site)
add_filter('init_auto_alt_overwrite_existing', '__return_true');
Tắt tiền tố khi lấy từ heading
add_filter('init_auto_alt_use_prefix_for_heading', '__return_false');
Ví dụ nâng cao: bỏ qua ảnh logo/site-branding
Nếu theme của bạn render logo với class riêng, có thể bỏ qua theo CSS class ngay trong HTML (không cần sửa snippet):
<img class="custom-logo no-auto-alt" src="..." alt="">
Hoặc nếu muốn loại bỏ theo vị trí, bạn có thể bọc vùng đó và không cho chạy tự động bằng cách thêm class vào chính <img>.
Ví dụ nâng cao: chỉ ghi đè alt với ảnh không có thuộc tính title
add_filter('init_auto_alt_overwrite_existing', function () {
// Ví dụ: chỉ ghi đè nếu ảnh KHÔNG có title (tùy theo nhu cầu)
return false;
// vẫn tôn trọng alt hiện hữu
});
Quy trình triển khai nhanh
- Dán snippet chính vào functions.php (hoặc Code Snippets) theo placeholder phía trên.
- Thêm các filter cần thiết (tiền tố, giới hạn độ dài, ghi đè, v.v.).
- Clear cache trang nếu có (plugin cache, server cache, CDN).
- Kiểm tra một bài có nhiều ảnh: ảnh trong figure, ảnh dưới các heading, ảnh không có heading, và ảnh lazy-load.
Test nhanh chất lượng alt
- Kiểm tra HTML đầu ra bằng DevTools để đảm bảo alt được điền theo đúng rule.
- Chạy một trang mẫu chứa: figure có figcaption, các đoạn H2/H3 phân mục, ảnh không có tiêu đề và lazy-src.
- Đảm bảo các ảnh trang trí (icon UI) được bỏ qua bằng
role/aria-hiddenhoặc classno-auto-alt.
Lợi ích SEO và truy cập
- Truy cập: Trình đọc màn hình cần alt để mô tả ảnh cho người dùng khiếm thị.
- SEO hình ảnh: Alt rõ ràng giúp ảnh dễ xuất hiện trong tìm kiếm hình ảnh, tăng lưu lượng dài hạn.
- Độ nhất quán: Tiền tố “Minh họa” hoặc tương đương giúp phân biệt ảnh nội dung với ảnh trang trí.
Gỡ lỗi thường gặp
- Không thấy thay đổi: Bạn đang bật cache hoặc ảnh đã có alt sẵn. Bật ghi đè bằng filter nếu cần.
- WP cũ: Nếu phiên bản < 6.2, lớp Tag Processor không tồn tại. Cập nhật WordPress.
- Ảnh bị bỏ qua: Kiểm tra class
no-auto-alt,role="presentation",aria-hidden="true", hoặc alt đã có.
Lưu ý hiệu năng
- Snippet quét trên chuỗi HTML đã render của
the_content; chi phí chủ yếu là duyệt tag theo một pass. - Không truy vấn DB lặp lại cho cùng ảnh nhờ cache theo ID/URL trong request.
- Giới hạn độ dài alt để tránh payload HTML phình to không cần thiết.
Bình luận