Every Mother

Treating AI Like a Junior Dev: How We Cleaned Up Thousands of WordPress Alt Tags in 30 Minutes

T

Target Audience: Web designers, agency owners, WordPress freelancers, and junior developers transitioning into full-stack workflows.

The Real-World Reality of WordPress Media Libraries

When a WordPress site has been passed between different teams, agencies, and content owners over a decade or more, accessibility is usually the first thing to slip through the cracks. Even on sites with great modern design and clean layouts, the media library often holds years of accumulated clutter and missing metadata from previous eras.

Image alt tags are often left completely empty. Even when teams conscientiously rename files before uploading, WordPress does not automatically parse those filenames into accessibility tags retroactively. Worse, client uploads and exports from design tools bring real chaos:

  • Canva & Figma defaults: Untitled-design-2.png
  • Smartphone camera counters: IMG_1505.jpg or DSC0029.png
  • Stock assets & CMS UUIDs: db28b989-4dc0-4aba-8cb4-3b1a8a5ba401-1.jpg
  • WordPress internal edits: valley-falls-park-scaled-e1726174652545.jpg

Running a naive script that turns IMG_1505 into an alt tag of "Img 1505" ruins accessibility for screen readers and signals low quality to search engines. When a screen reader reads “Img One Five Zero Five” or spells out a 36-character hexadecimal hash, you haven’t fixed accessibility, instead you’ve made the experience worse.

Beyond user experience, ADA and accessibility compliance enforcement is growing, and search engines heavily rely on descriptive alt text to contextualize image relevance. Leaving hundreds of images blank leaves both organic search ranking and legal compliance exposed.


The Reality Check: Treating AI Like a Junior Developer

There’s a popular narrative that AI allows anyone to spin up production code with zero technical background. This project exposed the exact opposite.

To build this tool safely, we had to know:

  • WordPress Postmeta Schema: That WP stores image alt text inside wp_postmeta under the _wp_attachment_image_alt meta key.
  • Server Performance & PHP Execution Limits: That looping through hundreds or thousands of media files in a single request triggers gateway timeouts (max_execution_time) or memory crashes, requiring chunked batching.
  • Defensive Architecture: Refusing to run blind UPDATE queries without an interactive visual dry run.
  • WordPress Security: Using nonces (check_admin_referer) and capability checks (manage_options) to secure administrative actions.

The AI didn’t know our business rules, our brand fallback, or our edge cases. Instead, the AI acted like a lightning-fast junior developer. It generated the boilerplate, regular expressions, and admin page markup in seconds.

My role was the senior engineer running the code review.

When the preview table surfaced edge cases—copy counters on UUIDs (...ab6b-1), Canva exports (Untitled design), or chained edit timestamps (-scaled-e1726174652545), I didn’t rewrite the code manually. I pointed out the exact pattern breaks, directed the AI to add targeted regex guards, and verified the output.

What would have been a 3–4 hour manual PHP coding and debugging session turned into 30 minutes of directed code review and execution. It was fun, efficient, and felt rewarding.


The Bridge: Thinking Like a Full-Stack Problem Solver

Going from “designer” to “full-stack” means recognizing that design doesn’t end in Figma—it ends in production performance, SEO, and screen-reader compliance.

When you uncover an audit with hundreds of missing alt tags, your choices usually look grim:

  1. The Manual Grind: Spend days clicking into 800+ media items individually, or burden the content owner with a massive spreadsheet. (I honestly had already prepped my client that this was coming to them!)
  2. The Paid SaaS Trap: Pay ongoing monthly subscription fees or per-image credit costs for plugins that charge you recurring fees just to do basic string manipulation.
  3. The Naive Automation: Install a generic plugin that blindly copies raw file slugs into alt fields without sanitation.

A full-stack approach allows you as a designer and developer to build a custom, lightweight, intelligent sanitization pipeline that runs right inside WordPress for free, cleans the library, and leaves zero plugin bloat behind:

Here’s the thought process and logic?

[Is Alt Tag Empty?]
        │
        ├──> [Strip WP Garbage: -scaled, -e1726..., dimensions]
        │
        ├──> [Is it a UUID, Hash, or 'Untitled'?] ───────> Set Default Fallback Brand/Location
        │
        ├──> [Is it an iPhone/Camera Prefix or Numbers?] ─> Set Default Fallback Brand/Location
        │
        └──> [Does it contain actual words?] ─────────────> Strip stray digits,
                                                            format cleanly to Title Case

Why the “Dry Run” Admin Page Matters

One of the biggest mistakes developers or AI makes when running database scripts is executing a blind UPDATE query. It’s reckless. If your regular expressions are overly aggressive, you risk overwriting clean metadata across your entire media database.

Instead, I prompted to wrap our batch runner in an admin preview page, allowing for visual, human analysis and iterations before making changes to the database. The included admin page allowed for:

  • Safety First: Seeing a side-by-side comparison of the original filename, the derived alt text, and the exact rule that triggered it before touching a single database record.
  • Chunked Processing: It queries and processes 100 images per batch, preventing shared-hosting PHP timeouts.
  • No Database Pollution: The script runs cleanly via the Code Snippets plugin; once all images are updated, you simply toggle the snippet off or delete it.

We added checkboxes to every row. If an edge-case filename doesn’t fit the automated pattern or needs completely bespoke human copywriting, you simply uncheck it. The script updates the approved items and leaves the unchecked images untouched for manual handling.

Download & Use the Script for Free

I’ve made this entire script freely available below. You can drop it directly into the free Code Snippets plugin. Set it to Only run in administration area, configure the SMART_ALT_DEFAULT_FALLBACK constant at the top to match your brand, company, or target regional SEO location. To review the edits and to approve them, navigate to Media → Smart Alt Tags.

<?php
/**
 * Plugin Name: Smart WordPress Alt Tag Generator & Batch Fixer
 * Description: Scans media library for empty alt tags, derives clean descriptions from filenames, applies fallback to hashes/numbers, and allows skipping individual items.
 * Author: Designer to Full Stack (designertofullstack.com)
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// -----------------------------------------------------------------------------
// CONFIGURATION: Set your default fallback alt text here
// -----------------------------------------------------------------------------
define( 'SMART_ALT_DEFAULT_FALLBACK', 'Your Brand or Location Here' );
define( 'SMART_ALT_BATCH_SIZE', 100 );

add_action( 'admin_menu', function() {
    add_media_page(
        'Smart Alt Tag Tool',
        'Smart Alt Tags',
        'manage_options',
        'smart-alt-tag-tool',
        'render_smart_alt_tag_tool_page'
    );
});

function evaluate_filename_for_alt( $filename, $default_alt = SMART_ALT_DEFAULT_FALLBACK ) {
    // 1. Strip fake extension remnants left in the slug (e.g., .jpeg-1, .png)
    $clean_original = preg_replace( '/\.(jpe?g|png|webp|gif|bmp)/i', '', $filename );

    // 2. Strip WordPress edit timestamps, dimension stamps, scaled, rotated, and copy numbers (-1, -2)
    $clean_original = preg_replace( '/(-e\d{10,}|-\d+x\d+|-scaled|-rotated|-\d+)$/i', '', $clean_original );
    $clean_original = preg_replace( '/(-e\d{10,}|-\d+x\d+|-scaled|-rotated|-\d+)$/i', '', $clean_original );

    // 3. Match UUID / GUID patterns (e.g., 5bea4cda-d372-43a4-97f5-c6ebc335ab6b)
    $uuid_pattern = '/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i';
    if ( preg_match( $uuid_pattern, $clean_original ) ) {
        return [
            'alt'  => $default_alt,
            'rule' => 'Default Fallback (UUID / Hash)'
        ];
    }

    // 4. Catch "untitled" anywhere in the filename (Canva/Figma default export names)
    if ( stripos( $clean_original, 'untitled' ) !== false ) {
        return [
            'alt'  => $default_alt,
            'rule' => 'Default Fallback (Contains Untitled)'
        ];
    }

    // 5. Normalize separators (hyphens, underscores, dots, pluses) to spaces
    $clean = trim( preg_replace( '/[-_.+]+/', ' ', $clean_original ) );

    // 6. Automated camera, stock, and device prefixes followed by numbers/hashes
    $camera_patterns = '/^(img|dsc|dcim|gopr|gh\d*|dji|pexels|unsplash|screenshot|screen shot|scan|document|photo|image|asset|pic)\s*[-_]?\s*[0-9a-z_-]*$/i';

    // 7. Pure hexadecimal strings (e.g., MD5 hashes, random upload tokens)
    $hash_pattern = '/^[a-f0-9]{8,}$/i';

    // 8. Test for purely numeric, camera format, or hash formats
    if (
        preg_match( '/^[\d\s]+$/', $clean ) ||
        preg_match( $camera_patterns, $clean ) ||
        preg_match( $hash_pattern, $clean )
    ) {
        return [
            'alt'  => $default_alt,
            'rule' => 'Default Fallback (Numeric / Generic)'
        ];
    }

    // 9. Strip remaining standalone numbers from the readable name
    $clean = preg_replace( '/\d+/', '', $clean );

    // Collapse multi-spaces left behind by stripped numbers
    $clean = trim( preg_replace( '/\s+/', ' ', $clean ) );

    // 10. Must contain at least 3 letters of real alphabetic content
    if ( ! preg_match( '/[a-zA-Z]{3,}/', $clean ) ) {
        return [
            'alt'  => $default_alt,
            'rule' => 'Default Fallback (No Valid Words)'
        ];
    }

    // Capitalize cleanly
    return [
        'alt'  => ucwords( strtolower( $clean ) ),
        'rule' => 'Derived from Name (Cleaned)'
    ];
}

function render_smart_alt_tag_tool_page() {
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( 'Unauthorized' );
    }

    $default_alt = SMART_ALT_DEFAULT_FALLBACK;
    $batch_size  = SMART_ALT_BATCH_SIZE;
    $is_apply    = isset( $_POST['apply_alt_updates'] ) && check_admin_referer( 'alt_tool_action', 'alt_tool_nonce' );

    $query_args = [
        'post_type'      => 'attachment',
        'post_mime_type' => 'image',
        'post_status'    => 'inherit',
        'posts_per_page' => $batch_size,
        'meta_query'     => [
            'relation' => 'OR',
            [
                'key'     => '_wp_attachment_image_alt',
                'compare' => 'NOT EXISTS',
            ],
            [
                'key'     => '_wp_attachment_image_alt',
                'value'   => '',
                'compare' => '=',
            ],
        ],
    ];

    echo '<div class="wrap">';
    echo '<h1>Smart Image Alt Tag Tool</h1>';

    // Process submitted updates
    if ( $is_apply ) {
        $selected_ids = isset( $_POST['selected_images'] ) && is_array( $_POST['selected_images'] ) 
            ? array_map( 'intval', $_POST['selected_images'] ) 
            : [];

        $updated = 0;

        if ( ! empty( $selected_ids ) ) {
            foreach ( $selected_ids as $attachment_id ) {
                $file_path = get_attached_file( $attachment_id );
                $filename  = pathinfo( $file_path, PATHINFO_FILENAME );

                $result = evaluate_filename_for_alt( $filename, $default_alt );
                update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $result['alt'] ) );
                $updated++;
            }
        }

        echo '<div class="notice notice-success is-dismissible"><p><strong>Success!</strong> Updated ' . intval( $updated ) . ' image(s). Unchecked items were skipped.</p></div>';
    }

    // Fetch images for preview
    $images = get_posts( $query_args );

    if ( empty( $images ) ) {
        echo '<div class="notice notice-info"><p>Great job! No images found with missing alt tags.</p></div>';
        echo '</div>';
        return;
    }

    echo '<p>Review the proposed updates below. <strong>Uncheck any image you wish to skip.</strong> Click <strong>Apply Updates to Selected</strong> when ready.</p>';

    echo '<form method="post" id="alt-tool-form">';
    wp_nonce_field( 'alt_tool_action', 'alt_tool_nonce' );

    echo '<div style="margin-bottom: 15px;">';
    echo '<input type="submit" name="apply_alt_updates" class="button button-primary" value="Apply Updates to Selected Items">';
    echo '</div>';

    echo '<table class="widefat striped" style="max-width: 1050px;">';
    echo '<thead><tr>';
    echo '<th style="width: 30px;"><input type="checkbox" id="select-all-alts" checked></th>';
    echo '<th style="width: 70px;">ID</th>';
    echo '<th>Filename</th>';
    echo '<th>Proposed Alt Text</th>';
    echo '<th>Rule Applied</th>';
    echo '</tr></thead>';
    echo '<tbody>';

    foreach ( $images as $image ) {
        $file_path = get_attached_file( $image->ID );
        $filename  = pathinfo( $file_path, PATHINFO_FILENAME );
        $result    = evaluate_filename_for_alt( $filename, $default_alt );

        $is_fallback = strpos( $result['rule'], 'Default Fallback' ) !== false;
        $badge = $is_fallback
            ? '<span style="color:#d63638; font-weight:600;">' . esc_html( $result['rule'] ) . '</span>'
            : '<span style="color:#2271b1; font-weight:600;">' . esc_html( $result['rule'] ) . '</span>';

        printf(
            '<tr>
                <td><input type="checkbox" name="selected_images[]" value="%d" class="alt-checkbox" checked></td>
                <td>%d</td>
                <td><code>%s</code></td>
                <td><strong>%s</strong></td>
                <td>%s</td>
            </tr>',
            $image->ID,
            $image->ID,
            esc_html( $filename ),
            esc_html( $result['alt'] ),
            $badge
        );
    }

    echo '</tbody></table>';

    echo '<div style="margin-top: 15px;">';
    echo '<input type="submit" name="apply_alt_updates" class="button button-primary" value="Apply Updates to Selected Items">';
    echo '</div>';
    echo '</form>';

    // JavaScript to handle "Select All / Deselect All"
    ?>
    <script>
    document.addEventListener('DOMContentLoaded', function() {
        var masterCheckbox = document.getElementById('select-all-alts');
        var checkboxes = document.querySelectorAll('.alt-checkbox');

        if (masterCheckbox) {
            masterCheckbox.addEventListener('change', function() {
                checkboxes.forEach(function(cb) {
                    cb.checked = masterCheckbox.checked;
                });
            });
        }
    });
    </script>
    <?php

    echo '</div>';
}

Verifying in Production: The Admin Columns Check

Once you click Apply Updates to this Batch, the tool saves the metadata, re-queries, and loads the next set of 100 images until the library is completely clear.

If you’re using a customize your columns plugin, you can verify the work visually across your WordPress dashboard:

  1. Go to Settings → Admin Columns → Media.
  2. Add the Alternative Text column to your media view and save.
  3. Switch your Media → Library into list view.

Every image now displays either a clear, title-cased descriptive name or your chosen fallback brand text—with zero empty entries remaining. Once verified, simply turn the snippet off in Code Snippets. Alternatively, click into images one-by-one and you’ll see the description updates.

(Note: Updating _wp_attachment_image_alt sets the alt tag in the Media Library and across all future embeds. If you have legacy blog posts with raw, hardcoded <img alt="" /> HTML snapshots saved inside old post bodies, those can be updated with a quick post-content search-and-replace or a the_content filter.)


Retroactive Cleanup vs. Future Uploads

Fixing legacy alt tags (like we just did) is only half the battle. Once your media library is clean, you want to prevent new unformatted uploads from slipping in.

  • For Legacy Images (The Past): We use the batch tool snippet above to safely sanitize existing media and handle weird camera names or UUIDs retroactively.
  • For New Uploads (The Future): We pair it with an upload-hook snippet. Whenever a team member or client uploads an image going forward, WordPress can automatically clean the file slug and convert it to title-cased alt text on the fly.

Need Help Cleaning Up Your WordPress Site or Future-Proofing it?

If you’re a business owner, marketing director, or agency team sitting on a messy WordPress library with thousands of unlabeled images, broken metadata, or accessibility warnings…you don’t have to spend your weekend clicking through it one by one.

While the free script above handles standard cleanups, large-scale client sites often come with custom nuances:

  • Images hardcoded into older page builders (Elementor, Divi, Kadence, Gutenberg) that need rendered HTML replacements.
  • Multi-location businesses that require dynamic regional fallbacks instead of a single global string.
  • Complex libraries mixed with PDFs, technical diagrams, scans, and vector assets.

I’m available for contract development, accessibility tune-ups, and site audits. Reach out through Designer to Full Stack to discuss your project, and we can get your media library, accessibility scores, and image SEO dialed in without production downtime.

Happy WordPressing!

About the author

Kelly Barkhurst

Designer to Fullstack is my place to geek out and share tech solutions from my day-to-day as a graphic designer, programmer, and business owner (portfolio). I also write on Arts and Bricks, a parenting blog and decal shop that embraces my family’s love of Art and LEGO bricks!

Elfsight advertisement - widgets for your website
By Kelly Barkhurst October 29, 2026

Recent Posts

Archives

Categories