Auto ALT ảnh thông minh với WP_HTML_Tag_Processor cho the_content

Bài này giới thiệu snippet tự động điền alt còn thiếu cho ảnh trong the_content, ưu tiên theo: caption của figure, heading gần nhất phía trên (kèm tiền tố “Ảnh minh họa: …”), tiêu đề bài viết, và tên file. Snippet chỉ dùng WP_HTML_Tag_Processor để an toàn, nhanh và ít phá vỡ HTML.

Auto ALT ảnh thông minh với WP_HTML_Tag_Processor cho the_content

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ặc aria-hidden="true".
  • Hỗ trợ lazy-src: data-src, data-lazy-src, data-original.
  • Có “cửa thoát”: thêm class no-auto-alt và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

  1. Dán snippet chính vào functions.php (hoặc Code Snippets) theo placeholder phía trên.
  2. Thêm các filter cần thiết (tiền tố, giới hạn độ dài, ghi đè, v.v.).
  3. Clear cache trang nếu có (plugin cache, server cache, CDN).
  4. 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-hidden hoặc class no-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


  • Không có bình luận.

Init Toolbox

Nhấn Ctrl + \ trên máy tính, hoặc vuốt sang trái ở bất kỳ đâu trên mobile.

Đăng nhập





Đang tải...