Bulk Updating Alt Text for 2,846 Images with WP-CLI
· 9 min read
An SEO audit on a UK pet food and accessories WooCommerce store flagged something I already suspected but hadn't quantified: 2,846 product images had no alt text. Not poor alt text — no alt text at all. Every product image, lifestyle photo, and team headshot was invisible to search engines and screen readers alike.
Fixing this manually through the WordPress media library was not an option. At 30 seconds per image — find it, click edit, type something reasonable, save — that's roughly 24 hours of mind-numbing work. The store had over 400 products, each with multiple images, and the client needed this done before their next marketing push.
So I built a WP-CLI script to do it intelligently.
The Approach: Filenames Are Data
Most WordPress images have reasonably descriptive filenames. Product photographers and designers tend to name files things like Duck-Feet-Premium-Mockup.png or salmon-oil-500ml-bottle.jpg. That filename, with some cleanup, makes a perfectly good alt text.
The logic is straightforward: strip the file extension, remove size suffixes that WordPress adds during upload, replace hyphens and underscores with spaces, drop common junk words like "mockup", "final", "v2", and title-case the result. For images where the filename is genuinely useless — IMG_4521.jpg, photo.png — fall back to the WordPress attachment title, which is often set from the original upload name before WordPress sanitises it.
The Script
Here's the core of what I used:
#!/bin/bash
# bulk-alt-text.sh — Generate and apply alt text from filenames via WP-CLI
# Usage: bash bulk-alt-text.sh [--dry-run]
DRY_RUN=false
if [[ "$1" == "--dry-run" ]]; then
DRY_RUN=true
echo "=== DRY RUN MODE — no changes will be made ==="
fi
SITE_PATH="/var/www/html"
SKIP_WORDS="mockup|mock-up|final|copy|v[0-9]+|edit|web|scaled|rotated"
GENERIC_NAMES="img|image|photo|picture|pic|screenshot|screen|file|untitled|dsc|toy|small|thumb|thumbnail"
# Get all attachment IDs with no alt text
ATTACHMENT_IDS=$(wp post list --post_type=attachment --post_mime_type=image \
--fields=ID --format=csv --path="$SITE_PATH" | tail -n +2 | while read -r ID; do
ALT=$(wp post meta get "$ID" _wp_attachment_image_alt --path="$SITE_PATH" 2>/dev/null)
if [[ -z "$ALT" ]]; then
echo "$ID"
fi
done)
TOTAL=$(echo "$ATTACHMENT_IDS" | grep -c .)
echo "Found $TOTAL images with missing alt text."
COUNT=0
UPDATED=0
echo "$ATTACHMENT_IDS" | while read -r ID; do
COUNT=$((COUNT + 1))
# Get the filename from the attachment URL
FILENAME=$(wp post meta get "$ID" _wp_attached_file --path="$SITE_PATH" 2>/dev/null)
FILENAME=$(basename "$FILENAME")
# Strip extension
NAME="${FILENAME%.*}"
# Remove WordPress size suffixes (-150x150, -1024x768, etc.)
NAME=$(echo "$NAME" | sed -E 's/-[0-9]+x[0-9]+$//')
# Remove junk words
NAME=$(echo "$NAME" | sed -E "s/[-_]?(${SKIP_WORDS})[-_]?//gI")
# Replace hyphens and underscores with spaces
NAME=$(echo "$NAME" | tr '-_' ' ')
# Collapse multiple spaces
NAME=$(echo "$NAME" | tr -s ' ')
# Trim whitespace
NAME=$(echo "$NAME" | xargs)
# Check if the result is generic or too short
IS_GENERIC=$(echo "$NAME" | grep -iE "^(${GENERIC_NAMES})[0-9 ]*$")
if [[ -z "$NAME" || ${#NAME} -lt 3 || -n "$IS_GENERIC" ]]; then
# Fall back to the WordPress attachment title
NAME=$(wp post get "$ID" --field=post_title --path="$SITE_PATH" 2>/dev/null)
# If the title is also useless, skip
IS_GENERIC_TITLE=$(echo "$NAME" | grep -iE "^(${GENERIC_NAMES})[0-9 ]*$")
if [[ -z "$NAME" || ${#NAME} -lt 3 || -n "$IS_GENERIC_TITLE" ]]; then
echo "[$COUNT/$TOTAL] SKIP ID $ID — no usable name (filename: $FILENAME)"
continue
fi
fi
# Title case
ALT_TEXT=$(echo "$NAME" | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1')
if $DRY_RUN; then
echo "[$COUNT/$TOTAL] WOULD SET ID $ID: \"$ALT_TEXT\" (from: $FILENAME)"
else
wp post meta update "$ID" _wp_attachment_image_alt "$ALT_TEXT" --path="$SITE_PATH"
echo "[$COUNT/$TOTAL] SET ID $ID: \"$ALT_TEXT\""
fi
UPDATED=$((UPDATED + 1))
done
echo "Done. $UPDATED images ${DRY_RUN:+would be }updated."
The --dry-run flag was critical. I ran it three times before committing to any changes.
Handling Edge Cases
The first dry run exposed several problems I hadn't anticipated.
Size suffixes were the easiest fix. WordPress appends dimensions like -150x150 or -1024x768 to resized images, so duck-feet-premium-150x150.jpg was initially generating "Duck Feet Premium 150x150" as alt text. A single regex strips those.
Generic filenames needed a blocklist. The store had dozens of images named things like toy.png, small.jpg, and DSC_0042.jpg. These came from early product uploads before the client standardised their naming. For these, the script falls back to the attachment title in WordPress, which often retains the original pre-upload name.
Team member photos were named with people's actual names — sarah-operations-manager.jpg. The script handled these correctly, producing "Sarah Operations Manager", which is reasonable alt text for a team page headshot.
Product variants required no special handling. Files like salmon-oil-250ml.jpg and salmon-oil-500ml.jpg naturally produced distinct alt text: "Salmon Oil 250ml" and "Salmon Oil 500ml".
Running It for Real
After three dry-run iterations and manual spot-checking of about 50 results, I ran the script without the flag. The whole process took roughly 60 seconds. WP-CLI is fast when you're just writing postmeta.
Of the 2,846 images, 2,791 got clean alt text from their filenames. 38 used the attachment title fallback. 17 were skipped entirely because neither the filename nor the title produced anything usable — those I flagged for the client to handle manually.
The next crawl showed a significant improvement in the SEO audit scores, and the client's Google Search Console started picking up image search impressions within a couple of weeks.
Why This Matters for Ongoing Maintenance
Alt text is one of those things that drifts. A store adds 20 new products a month, each with 3-5 images, and unless there's a process in place, they all go up without alt text. As part of my WooCommerce maintenance plans, I run periodic audits for missing alt text and other SEO basics that quietly degrade over time.
It's not glamorous work. But 2,846 images going from invisible to indexed is the kind of compounding improvement that adds up.
Need help with something similar? Check out my maintenance plans.
