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.

Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for UK agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.

View Maintenance Plans Get in Touch

Get in Touch to Discuss Your Needs