SEO & Structured Data

WordPress Alt Text: Where It Lives and Why Your Edits Do Not Show

Alt text in WordPress is one postmeta row, copied into the post markup the moment you insert an image. That single fact explains why editing the library changes nothing on pages you already published, why an audit has two halves, and what to fix first.

WordPress Alt Text: Where It Lives and Why Your Edits Do Not Show

You open the media library, find an image with no alt text, type a proper description, click Update, and reload the page it sits on. The HTML has not changed. Nothing is broken and no cache is at fault. WordPress is doing exactly what it was built to do.

Alt text in WordPress lives in one place and gets copied to another. It lives in a row of post meta on the attachment. It gets copied into the block markup in post_content at the moment you insert the image, and that copy never looks back at the original.

Once you know that, the subject stops being mysterious. An alt text audit is two audits, a fix is two fixes, and the size of the problem is one SQL query away. On this site, 684 image attachments produced 274 with no usable alt text, which is 40 percent.

The advice everyone repeats, describe the image, is the easy half. Here is the other half first.

Where WordPress keeps alt text

Alt text is a single row in the postmeta table with the meta key _wp_attachment_image_alt, attached to the attachment post ID. That is the whole storage mechanism. There is no column for it, no separate table, and no dedicated API beyond ordinary get_post_meta() and update_post_meta().

It is not stored in the image file. JPEG and PNG carry their own metadata blocks, but WordPress never writes alt text into them and never reads it out, which is a separate subject: what EXIF and IPTC data survive an upload. It is also not any of the three fields the media modal stacks next to it, which is where most of the confusion starts.

Every write path in core lands on that same key. Uploads and the REST API, which is what the block editor’s media modal talks to, run the value through sanitize_text_field(). The classic attachment edit screen runs it through wp_strip_all_tags() in wp-admin/includes/post.php. Either way, alt text is one line of plain text and any markup you paste is stripped before it is stored. Reading it back is a single query.

SELECT meta_value
FROM mv0DiJnB_postmeta
WHERE post_id = 1343
  AND meta_key = '_wp_attachment_image_alt';

That mv0DiJnB_ is this site’s table prefix, set by $table_prefix in wp-config.php. Substitute yours in every query below.

The four fields people confuse

The media modal stacks four text fields in a column with almost no indication that they do completely different jobs. Three of them are columns on the attachment post. Only one becomes the alt attribute.

  • Title is post_title, filled from the file name on upload, which is why so many libraries are full of titles like “img 4471”. It is what the media library search matches, so keep it sane, but it does not appear on the page and it describes nothing.
  • Caption is post_excerpt, the one field here that is visibly published. It renders in the <figcaption> under the image, for everybody. Write it for sighted readers.
  • Description is post_content. It appears on the attachment page, which most sites never link to and many themes render badly. In practice it is a private note.
  • Alternative Text is the _wp_attachment_image_alt meta. The only one that becomes the alt attribute, and the only one read out in place of the image.

The common failure is writing a good description into Description, watching it save, and assuming the accessibility box is ticked. It is stored, it is retrievable, and no visitor will ever meet it.

Table of the four media library fields, showing Title, Caption and Description stored as columns on the attachment post while Alternative Text is stored as the _wp_attachment_image_alt meta row, and only Alternative Text replaces the image.

The copy that explains everything

Knowing which box to type in is only half of it. The rest happens at the moment you insert the image.

When you open the media modal, wp_prepare_attachment_for_js() builds a JavaScript object for each attachment carrying 'alt' => get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ) alongside the title, caption and description. The editor now holds the current value.

When you click Insert, the block editor writes an image block into post_content, and the alt goes in as a literal HTML attribute. A real block from this site looks like this, trimmed for width.

<!-- wp:image {"id":1343,"sizeSlug":"full","linkDestination":"none"} -->
<figure class="wp-block-image size-full">
  <img src="https://example.com/wp-content/uploads/2026/07/logo.png"
       alt="The WunderPaint logo in dark blue"
       class="wp-image-1343"/>
</figure>
<!-- /wp:image -->

The attachment ID is right there twice, as "id":1343 in the block attributes and as the wp-image-1343 class. WordPress knows exactly which attachment this is. It still never goes back for the alt text.

Core confirms it twice. The render callback in wp-includes/blocks/image.php calls $processor->get_attribute( 'alt' ) to build its lightbox label, pulling the alt back out of the saved markup rather than out of the meta. And wp_filter_content_tags(), which rewrites content images on the way to the browser, adds srcset, sizes, loading, decoding and fetchpriority, and touches alt not at all.

So the rule, stated plainly: the alt text you type in the media library is copied into the post at insert time, and editing the library value afterwards changes nothing about images already placed in a post. Almost every report of “I fixed my alt text and nothing happened” is this, and nothing else.

Which images update and which do not

The copy is not universal. The question is whether the HTML was written into the database once or is generated fresh on every request, and anything going through wp_get_attachment_image() is generated fresh. Its default attributes, in wp-includes/media.php:

$default_attr = array(
    'src'   => $src,
    'class' => "attachment-$size_class size-$size_class",
    'alt'   => trim( strip_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) ),
);

A fresh get_post_meta() on every page load. That splits every image on your site into three groups.

  • Live from the meta: featured images, Post Featured Image blocks, image widgets, anything a theme or plugin renders with wp_get_attachment_image(). These update the moment you save the library value.
  • Frozen at insert time: images placed in the block editor, images placed in the classic editor, and anything else stored as raw HTML in post_content.
  • Stored somewhere else entirely: page builders, which keep their own JSON in post meta and copy the alt in exactly the same way, just into a different field.

One considerate detail: when WordPress creates a cropped derivative, wp-admin/includes/image.php copies both the caption and _wp_attachment_image_alt from the parent attachment, so the derived image inherits the description. What else one upload turns into is a longer story.

The consequence is what to take away. An alt text audit has two halves: the attachment meta, and the markup already sitting in post_content. A library reporting 100 percent coverage can still serve pages full of empty alt attributes, because those pages were written before it was fixed.

Table comparing where an image came from, showing that block editor, classic editor and page builder images keep stored markup that ignores later library edits, while featured images and theme output read the alt meta on every request, with two code panels contrasting the live read and the frozen copy.

Measuring the library half

The count that matters is not “images without alt text” but “images without usable alt text”, and it needs a LEFT JOIN, because an attachment that has never had alt text has no meta row at all and an inner join silently hides it.

SELECT
  COUNT(*)                                          AS images,
  SUM(m.meta_id IS NULL)                            AS no_alt_row,
  SUM(m.meta_id IS NOT NULL AND TRIM(m.meta_value) = '') AS empty_alt,
  ROUND(100 * SUM(m.meta_id IS NULL OR TRIM(m.meta_value) = '') / COUNT(*), 1) AS pct_unusable
FROM mv0DiJnB_posts p
LEFT JOIN mv0DiJnB_postmeta m
       ON m.post_id  = p.ID
      AND m.meta_key = '_wp_attachment_image_alt'
WHERE p.post_type      = 'attachment'
  AND p.post_status    = 'inherit'
  AND p.post_mime_type LIKE 'image/%';

Here that returns 677 live images, 266 with no meta row, 1 with an empty string, 39.4 percent unusable. Drop the post_status line and the totals become 684 and 274, or 40.1 percent, because seven image attachments sit in the trash. Worth knowing before you quote a figure: inherit is the normal status for an attachment, and trashed attachments keep their rows.

The same numbers through WP-CLI, if SQL makes you nervous. These are ordinary read-only WP_Query calls.

# total images in the library
wp post list --post_type=attachment --post_mime_type=image --format=count

# images with no alt meta row at all
wp post list --post_type=attachment --post_mime_type=image 
  --meta_key=_wp_attachment_image_alt --meta_compare='NOT EXISTS' 
  --format=count

# the same set as a worklist, with IDs and file URLs
wp post list --post_type=attachment --post_mime_type=image 
  --meta_key=_wp_attachment_image_alt --meta_compare='NOT EXISTS' 
  --fields=ID,post_title,guid --format=csv

WP-CLI returned 677 and 266, matching the SQL, because wp post list excludes the trash by default. If your two methods disagree by a small number, the trash is the first place to look.

The last query is the one people skip and usually the most revealing. It finds alt text that exists but is only the file name in disguise, which is what a badly configured bulk tool leaves behind.

SELECT p.ID, m.meta_value AS alt, p.guid
FROM mv0DiJnB_posts p
JOIN mv0DiJnB_postmeta m
  ON m.post_id  = p.ID
 AND m.meta_key = '_wp_attachment_image_alt'
WHERE p.post_type      = 'attachment'
  AND p.post_mime_type LIKE 'image/%'
  AND TRIM(m.meta_value) != ''
  AND (
        m.meta_value REGEXP '\.(jpe?g|png|gif|webp|avif)$'
     OR m.meta_value REGEXP '^(IMG|DSC|DCIM|PXL|Screenshot)[ _-]?[0-9]'
     OR LOWER(REPLACE(TRIM(m.meta_value), ' ', '-'))
        = LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(p.guid, '/', -1), '.', 1))
      )
LIMIT 100;

Three tests in one: alt text carrying a file extension, alt text starting like a camera or phone file name, and alt text that is the slug of its own file with the hyphens turned into spaces. This site returned zero rows, which is the answer you want. A site that has been through an automatic “fill in all the alt text” plugin often returns hundreds, each one a field that looks complete to a reporting tool and reads as gibberish to a person.

Measuring the markup half

Now the images already placed in posts. This is coarser, because you are pattern matching HTML in a text column rather than querying structured data, but it sizes the problem.

SELECT ID, post_title, post_type
FROM mv0DiJnB_posts
WHERE post_status = 'publish'
  AND post_type IN ('post', 'page')
  AND post_content LIKE '%<img %'
  AND (post_content NOT LIKE '% alt=%'
       OR post_content LIKE '%alt=""%')
LIMIT 100;

Two caveats, because a query that lies is worse than no query. It flags a post if any image in it lacks alt, not which one, so it is a triage list rather than a fix list. And alt="" is correct for a decorative image, so the second condition finds candidates, not defects. Here the first condition returned zero posts and the second returned six, all deliberate.

For a real report you want the rendered page, because themes and page builders inject images that never appear in post_content. Run an accessibility checker over the live URLs and use the SQL as a cross-check: when the checker flags a page whose stored content looks clean, the fix belongs in the theme.

Audit summary showing 684 image attachments, 274 with no usable alt text, 40.1 percent of the library and zero alt fields containing a file name, above a table listing five read only queries and the counts each returned.

Measuring the markup half by hand stops being realistic somewhere around the twentieth image. Paste the HTML of a post below and the auditor lists every image with what its alt attribute actually contains.

It flags the ones that matter: no alt at all, alt that is just the file name, alt that starts with “image of”, alt duplicated across a dozen images, and the one that catches most people, an empty alt on an image that is itself a link, which leaves the link with no accessible name at all.

Alt text auditor

Paste the HTML of a post and get one row per image, with the alt text faults that actually turn up in a WordPress library: no alt at all, the file name repeated, the caption copied, or a sentence nobody would want read out. The HTML is parsed in a detached document, no picture is ever requested, and nothing leaves this browser tab.

Or drop a file here
Drop an .html or .txt file
or press Enter to pick one. The file is read in this tab and never uploaded.
0Images
0No alt
0With faults
0Decorative
0Clean
Nothing to check yet
FileAlt textLengthFindings

Rows with faults come first. Grey marks are notes, not faults.

How to write one
  1. Say what the picture shows for the paragraph it sits next to, in one sentence, and stop there.
  2. Leave the alt empty when the picture only decorates: an empty alt is a decision, a missing one is an oversight.
  3. Stay under 125 characters and drop "image of", the file name and the list of keywords.
  4. When the picture is the only thing inside a link, the alt has to name where the link goes.
WunderPaint
The Dynamic Design and Automation Studio
WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

Filling them in without making it worse

The honest warning first. Wrong alt text is worse than no alt text. A missing attribute is a gap a listener recognises as a gap and a checker can flag. Confident, plausible, incorrect alt text is a lie delivered in a calm voice, and nobody downstream can detect it. The temptation with 266 empty fields is one UPDATE copying the file name into all of them. That produces text which is technically present and practically worthless, and it destroys the signal telling you which images still need a human.

The file name is not a description either. hook-head-lighthouse.jpg is a good file name and poor alt text, because it says what is in the frame and nothing about why the picture is here. An order that works:

  1. Take a database backup. Everything below writes to postmeta or post_content, both easy to get wrong at scale.
  2. Filter to images actually used somewhere. An orphan on no page needs no alt text, and cutting the list down is the difference between a finite job and an infinite one. Clearing out a media library covers how to establish which attachments are genuinely in use.
  3. Sort what remains by the traffic of the pages they appear on.
  4. Work down that list starting with functional images and images of text, which cost the most when missing.
  5. Mark decorative images with an explicit empty alt rather than leaving the field blank by accident, so the job can be finished.
  6. Then go back through the posts containing those images and replace the markup, because steps 2 to 5 only fixed the library.

Step 6 is the one everybody forgets. In the block editor, select the image block and retype the alt in the block sidebar, which writes straight into the markup. Across many posts a careful search and replace over post_content works, but match on the wp-image-NNN class so you target one specific attachment rather than a URL that appears in several sizes. Run that as a one-off WP-CLI command rather than something you leave installed: where custom code belongs is worth deciding deliberately.

For the library half at scale, WunderPaint’s media library manager collects everything with missing alt text into a smart folder and can generate titles, alt text and descriptions in bulk, which turns the impossible version of this job into an afternoon of editing rather than removing the editing step.

Whatever writes the first draft, remember what it cannot know. A tool can see a person at a desk. It cannot know the picture is there to show the desk.

Who is reading it

Three audiences, in order of how much they matter.

People using a screen reader. Someone navigating a page by ear, where every image either contributes a sentence to the story or interrupts it. This is the audience the field exists for.

Anyone whose image did not load. Slow connection, blocked host, a file that went missing in a migration. The alt text stands in for the picture.

Search engines. Real, but third. Alt text is one of several signals used to work out what an image shows, alongside the file name, the caption and the surrounding text. Writing for this audience first is how you end up with keyword-stuffed nonsense that helps nobody.

The question that makes it easy

Before writing anything, ask: if this image vanished, what would the reader lose?

Sometimes the answer is nothing, because the image is decorative. Sometimes it is a specific fact, because the image is a chart. Sometimes it is a mood, because the image sets a scene. Each answer produces a different kind of alt text, and getting the classification right is most of the work.

Decorative images get empty alt text

A background texture, a divider, a stock photograph of hands on a keyboard next to an article about productivity. These carry no information the text does not already carry.

The correct alt text is empty: alt="". Not missing, empty, and the difference is not pedantry. An empty attribute tells a screen reader that the author considered this image and decided it carries nothing, so skip it silently. A missing attribute says nothing, so the reader falls back on announcing the file name, and nobody needs to hear “d s c underscore zero four four seven one dot j p g”.

WordPress stores “deliberately empty” and “never filled in” identically, so core cannot tell decorative from not-done-yet. That is why the query above counts both as unusable, and why the decision has to be recorded by a person in the markup rather than inferred from the library.

Informative images get the information

The picture carries something the text does not. Describe that something, not the picture.

Compare “photo of a shoe” against “the sole of the running shoe, showing the two-density foam layers”. Both are true. Only one tells you why the picture was included. A product photograph should describe what a buyer is trying to see, usually the colour, the material and the angle.

Functional images get the function

If the image is a link or a button, the alt text is not a description, it is the destination. An icon of a house linking to the front page should be “home”, not “house icon”. A magnifier that opens search should be “search”. Describing the drawing rather than the action is the most common accessibility mistake in navigation, and no automated tool will catch it, because the attribute is present and the words are accurate.

Charts and diagrams get the finding

You cannot fit a chart into alt text, and you should not try. Give the conclusion in the alt attribute, then put the underlying data somewhere reachable: a caption, a table below, a link.

“Bar chart showing monthly signups” is nearly useless. “Signups roughly doubled between March and June, then held steady” is what a sighted reader takes away in one glance, so it is what the alt text should say.

Images of text get the text

If a picture contains words, the words go in the alt text, verbatim. A quotation card, a poster, a screenshot of an error message. This one has no nuance, is frequently skipped, and is the case where a missing alt removes information that exists nowhere else on the page.

Practical rules

  • Do not start with “image of” or “photo of”. Screen readers already announce that it is an image. You are wasting the listener’s first two words.
  • Keep it to about one sentence. There is no hard limit in the specification, but long alt text is read as an uninterrupted block with no way to skim. If you need more, you want a caption or body text.
  • Punctuate it. A full stop makes a screen reader pause. Without one, the alt runs into whatever follows.
  • Do not repeat the caption. Both get read out, and hearing the same sentence twice is worse than hearing it once. When the caption already says it, the alt can often be empty.
  • Context decides. The same lighthouse photograph might be empty on a homepage, “the lighthouse at Hook Head, seen from the cliff path” in a travel article, and “the black and white band pattern used to identify the tower by day” in a piece about navigation. There is no correct alt text for an image in isolation.

That last rule redeems the mechanism this article opened with. One attachment holds one alt text, but the same image can appear on five pages doing five different jobs. The insert-time copy, annoying as it is for bulk fixes, is what makes per-page alt text possible: the value in the markup can differ from the library value, deliberately. Treat the library value as the sensible default and the block value as the one that is published.

Alt text and SEO, honestly

Alt text is not a keyword field. It is an accessibility field that search engines happen to read, and that distinction decides every judgement call you will make about it.

Write the honest description and the keywords take care of themselves, because a genuine description of a picture on a page about a subject naturally contains the words of that subject. Stuffing has not worked for years. “Running shoes, best running shoes, cheap running shoes, buy running shoes online” is detected easily and is a downgrade rather than a neutral experiment. If your alt text would sound absurd read aloud, it is wrong, and being read aloud is literally its job.

Two things do help and cost nothing. Name the file properly before uploading, because hook-head-lighthouse-cliff-path.jpg beats IMG_4471.jpg in image search and in your own media library six months later. And put the image near text about the same subject, because the surrounding paragraph has always been a stronger signal than the alt attribute.

Symptom and cause

I fixed the alt text in the media library and the page still shows the old text. The image was inserted into the post before you made the edit, so the old value is baked into post_content. Edit the alt in the block sidebar on that post, not in the library.

The featured image picked up my change but the image in the article did not. Two code paths. The featured image goes through wp_get_attachment_image(), which reads the meta on every request. The in-content image is stored HTML.

A screen reader reads out a long file name where an image should be. The alt attribute is missing entirely, not empty. If the image is decorative, add alt="". Assistive technology treats the two completely differently.

A checker reports missing alt on a page where every image in the library has alt text. The images come from a template or a page builder rendering its own markup, so they never touched the library value. Find the source of the markup before editing anything in the media library.

Every image has alt text and it all reads like a file path. A bulk tool populated the field from the file name. Run the file-name query above. Alt text that is present and semantically empty passes every automated audit and fails every human one.

The read-aloud test

One test is worth more than every checker. Read the page aloud, substituting your alt text wherever an image appears. If it flows as a piece of writing, the alt text is right. If it stutters, repeats the caption, or announces things that do not matter, you now know which images to fix. No tool can do this, because the question is not what the picture shows but what the sentence around it needed.

The technical half is smaller than it looks. One meta key, _wp_attachment_image_alt, holds every alt text on the site. One copy at insert time explains every case where an edit appears not to have worked. A handful of read-only queries measure both halves, which here came to 274 unusable out of 684 images, a number that would otherwise have stayed a vague worry.

The writing half does not scale, and it should not. Alt text is the only place in a WordPress install where you have to state in one sentence why a picture is on the page. Plenty of images survive that question badly, and finding out which ones is worth more than filling 274 fields with text nobody wants to hear.

 

WordPress Alt Text: Where It Lives and Why Your Edits Do Not Show

Table of Contents

Learn it by building something

Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

One mail a week, and then it ends.
Unsubscribe in one click.

WordPress Images

Clean Up WordPress Media Library Without Breaking Pages

An attachment is a row in wp_posts; its files sit somewhere else, and deleting one does not delete the other. What to measure before you start, why "unused" is so hard to prove, and the order of operations that keeps the site up.

WordPress Images

How Big Should an Image Actually Be Before You Upload It

A 4032 pixel phone photograph in a 760 pixel column makes the browser download 12.2 million pixels to paint about 430,000 of them. What that costs in bytes and in memory, how to read the display width your theme actually uses, where the WordPress 2560 pixel safety net stops, and a browser tool that does the resizing before the upload.

Security & Privacy

What Your Photo Is Telling Everyone: An EXIF Viewer & Remover

Every photograph carries a block of tags nobody looks at: camera, lens, exposure, three timestamps, and often the coordinates of the room it was taken in. Read yours in the browser, then learn what each field means, who wrote it, and what quietly removes it.

CSS & Front-End

Type That Fits Every Screen Without a Single Media Query

One clamp() per level replaces a wall of media queries, but only if the middle term is solved rather than guessed. Here is how the slope and the intercept fall out of two viewport widths and two sizes, why a bare vw value fails the moment a reader zooms, and which three numbers your theme already knows.

Developer Tools

Cron Expression Generator: The Five Fields and the OR Rule

The five cron fields, the four special characters, and the rule that catches everyone: day of month and day of week are combined with OR, not AND. Plus why WP-Cron misses schedules, and the two lines that put WordPress on a real timer.

Security & Privacy

Stopping WordPress Hotlinking Without Breaking Image Search

Hotlinking is another site's HTML pointing at your image URL, so your server pays for their page. Prove it from the access log before you block anything, then write a referrer rule at the web server level that still lets crawlers, CDNs and link previews through.

Download the free WunderPaint Plugin for WordPress

The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

The Image Editor & Design Studio

Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

Free

Chaos Art

Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

Pro

Particle Strokes

Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

Pro

City Diorama

Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.