Auto-Populate Image Alt Text, Caption, and Description on Upload in WordPress

Image alt text is one of the highest-impact SEO and accessibility improvements available on a content-heavy WordPress site — yet most images uploaded to the Media Library are left with an empty alt attribute because editors rarely fill it in manually. WordPress fires the add_attachment action immediately after an image is uploaded and saved, passing the new attachment’s post ID. At this point the post object is fully saved and meta can be written with update_post_meta(). The filename is already stored in post_title (without extension) by WordPress, so there is no need to read from $_REQUEST — using the saved post object is safer, works for programmatic uploads, and avoids any dependency on request context. A second use case is backfilling alt text for images that existed before this code was added: a dedicated function using get_posts() with post_type = 'attachment' can iterate all images and call the same logic for any that are missing alt text.

Problem: Editors on your site upload images without filling in the alt text, caption, or description fields. Images already in the Media Library also have empty alt text. You want both cases handled automatically, without a plugin.

Solution: Hook into add_attachment to auto-fill alt, caption, and description from the filename for new uploads. Write a separate backfill_missing_image_alt() function for existing images, run it once via WP-CLI, then remove it.

<?php
// ── Auto-fill alt, caption, and description for new image uploads ──────
add_action( 'add_attachment', 'auto_fill_image_alt_on_upload' );

function auto_fill_image_alt_on_upload( int $attachment_id ) {
    if ( ! wp_attachment_is_image( $attachment_id ) ) {
        return; // skip PDFs, videos, etc.
    }

    $post = get_post( $attachment_id );
    if ( ! $post ) {
        return;
    }

    // post_title already contains the filename without extension (set by WordPress)
    $label = ucfirst( str_replace( [ '-', '_' ], ' ', $post->post_title ) );
    $label = sanitize_text_field( $label );

    // Alt text (stored as post meta — do not overwrite if already set)
    if ( empty( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) ) {
        update_post_meta( $attachment_id, '_wp_attachment_image_alt', $label );
    }

    // Caption (post_excerpt) and description (post_content)
    $update = [ 'ID' => $attachment_id ];
    if ( empty( $post->post_excerpt ) ) {
        $update['post_excerpt'] = $label;
    }
    if ( empty( $post->post_content ) ) {
        $update['post_content'] = $label;
    }

    if ( count( $update ) > 1 ) { // more than just ID
        // Remove hook to prevent recursion (wp_update_post fires add_attachment-adjacent hooks)
        remove_action( 'add_attachment', 'auto_fill_image_alt_on_upload' );
        wp_update_post( $update );
        add_action( 'add_attachment', 'auto_fill_image_alt_on_upload' );
    }
}

// ── One-time backfill for existing images that have no alt text ────────
// Run once: wp eval 'echo backfill_missing_image_alt() . " images updated.";'
// Then remove this function.
function backfill_missing_image_alt(): int {
    $ids = get_posts( [
        'post_type'              => 'attachment',
        'post_mime_type'         => 'image',
        'post_status'            => 'inherit',
        'posts_per_page'         => -1,
        'no_found_rows'          => true,
        'update_post_term_cache' => false,
        'fields'                 => 'ids',
    ] );

    $updated = 0;
    foreach ( $ids as $id ) {
        if ( ! get_post_meta( $id, '_wp_attachment_image_alt', true ) ) {
            auto_fill_image_alt_on_upload( $id );
            $updated++;
        }
    }
    return $updated;
}

NOTE: The original common approach reads the filename from $_REQUEST['name'] inside the add_attachment callback. This is unreliable — the key name varies between upload methods (media modal vs REST API vs programmatic), and request data is not available during WP-CLI or background imports. Reading from the saved post_title via get_post() works correctly in all contexts. The backfill function should be run once and then deleted — running it repeatedly on a large media library with thousands of images will be slow and issue many database writes. Run it via wp eval in WP-CLI for the fastest execution without PHP timeout limits.

Sources:

  1. https://faqwp.com/question/avto-title-i-alt-v-kartinkah-wordpress