WordPress Images

Image File Names: The Boring Detail That Breaks Things

WordPress renames every file you upload, using rules almost nobody has read. Here is what sanitize_file_name actually changes, why a name with an umlaut resolves on one host and 404s on another, and why core answers a collision by adding a number instead of overwriting. Clean your names in the browser first, because after the upload the name is a URL.

Image File Names: The Boring Detail That Breaks Things

A client sends a folder. Inside it: Ünsere Prüfung.JPG, IMG_20240517_142305.jpg, Bildschirmfoto 2024-05-17 um 14.23.05.png, and four files called final.jpg, final (1).jpg, final_neu.jpg and final_neu_v2.jpg. You drag the lot into the media library, because that is what the media library is for.

What comes back out is not what went in. WordPress renames every one of those files on the way to disk, quietly, using rules almost nobody has read. Ünsere Prüfung.JPG is stored as Unsere-Prufung.jpg on an English site and as Uensere-Pruefung.jpg on a German one. The four versions of “final” do not collide and nothing is overwritten: you end up with four full size images and four sets of thumbnails, all of the same picture.

None of that is fatal. It is just permanent. The file name is welded into the URL the second the upload finishes, and every later reference to that URL (post content, srcset, an OG tag, a link somebody else published) points at whatever name happened to be sitting on your desktop that afternoon.

So the useful moment is the one before the upload, while the names are still just text.

Clean the list first

Paste your file names into the box on the left, one per line, or drop the actual files onto the target on the right. Dropping files takes their names only: the contents are never opened. Every rule is a chip you can switch off, and you get a before and after table with a warning under any result that would collide with another name, come out empty, or still be too long. Copy the cleaned list, or download it as a text file, then rename on your own machine and upload with names that will survive.

It all runs in this browser tab. Nothing is uploaded, nothing is stored, and no request leaves the page.

File name cleaner

Paste a list of file names, or drop the files themselves, and get tidy web safe names back. Only the names are read: the files are never opened and nothing leaves this browser tab.

Or drop files here
Drop files here
or press Enter to pick some. Only the names are taken, the contents stay on your disk.
Characters
Strip
Length and numbering
Nothing to clean yet
BeforeAfter

The extension is held back from every rule except lower case. Warnings sit under the new name.

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.

What WordPress does to a file name on upload

Two core functions decide the name on disk. The first is sanitize_file_name() in wp-includes/formatting.php. It runs on every upload, and it does less than people assume.

In order, it folds accented Latin letters to their plain equivalents through remove_accents(), deletes a fixed list of 32 punctuation characters plus the null byte, squashes runs of dots into one, turns runs of spaces, tabs, newlines and hyphens into a single hyphen, and trims dots, hyphens and underscores off both ends. The character list is worth reading once, because it is a deletion list, not a translation list:

? [ ] /  = < > : ; , ' " & $ # * ( ) | ~ ` ! { } % + ’ « » ” “

Three consequences follow from that list. Characters not on it survive: e@mail^test.jpg goes through completely untouched, at sign, caret and all. Deletion happens with nothing put in its place, so the surrounding whitespace is what saves you: Fish & Chips.jpg becomes Fish-Chips.jpg because the two spaces left behind collapse into a hyphen, while Fish&Chips.jpg becomes FishChips.jpg, two words glued into one. And % is deleted before the line that looks for %20 ever runs, so a name that arrives already URL encoded, my%20photo.jpg, is stored as my20photo.jpg.

Two behaviours surprise people more than the rest. First, sanitize_file_name() does not lower case anything. Ünsere Prüfung.JPG comes out of it as Unsere-Prufung.JPG, capitals intact. The extension is lowered later, by the second function, and the base of the name never is. Second, if the name contains more than one dot, the middle parts get an underscore glued on when they look like an extension that is not allowed: my.file.name.txt is stored as my.file_.name_.txt. That is a deliberate defence against double extension tricks, and it is why inner dots are a bad idea in a file name you intend to link to.

Table comparing six uploaded file names with the names WordPress stores on disk, showing accent folding, the German locale map, untouched Cyrillic, underscores added to inner dots, surviving at sign and caret, and a deleted ampersand.

Why an umlaut works on one host and 404s on another

The accent folding is not a fixed table. remove_accents() checks the site locale, and a site running any German locale (the check is a prefix match on de, so de_DE, de_AT, de_CH and the formal variants all qualify) gets a different map: ä becomes ae, ö becomes oe, ü becomes ue and ß becomes ss. Danish and Catalan and Serbian have their own overrides. So the same photo uploaded to a staging site in English and a live site in German lands on two different URLs, and any path you hard coded during the build is now wrong on exactly one of them.

The bigger problem is what the map does not cover. It handles Latin script. A name in Cyrillic, Greek, Arabic, Hebrew, Japanese or Chinese passes through sanitize_file_name() completely unchanged, because none of those characters are accented Latin letters and none of them are on the deletion list. отчёт.png is stored as отчёт.png, and from that moment the file depends on a chain of things agreeing about encoding: the filesystem, the web server, PHP, the browser, the CDN and whatever tool you later use to copy the site.

That chain is where the intermittent 404 comes from. In a URL the file has to appear percent encoded, so the browser asks for %D0%BE%D1%82%D1%87%D1%91%D1%82.png. Most of the time it resolves. Then one of these happens:

  • The file is copied between macOS and Linux. macOS stores ü decomposed (a plain u followed by a combining diaeresis), Linux stores whatever it is given. Two byte sequences, one visible name, and only one of them matches the URL.
  • A migration, backup or FTP client normalises the name, or mangles it into the wrong encoding on the way through.
  • The site moves from a case insensitive filesystem (Windows, default macOS) to a case sensitive one (nearly every Linux host), and Unsere-Prufung.JPG stops matching a link written as unsere-prufung.jpg.

Each of those is individually fixable and none of them is worth debugging. Names limited to a-z, 0-9 and hyphens never enter the argument.

Collisions: core never overwrites

The second function is wp_unique_filename(), in wp-includes/functions.php. It sanitises the name again, then loops with file_exists() against the target directory, appending -1, -2, -3 and upwards until it finds a free slot. It never replaces an existing file. That is the correct choice, because the file already there might be in use on a live page, but it has a side effect that shows up in every media library over a certain age: uploading the same picture four times produces four full size files and four complete sets of generated sizes.

Note what the check compares against. The target directory is the month folder, /wp-content/uploads/2026/08/ by default. A file called hero.jpg uploaded in July and again in August does not collide at all: both keep the clean name, in different folders, and nothing anywhere records that they are the same image. This is a large part of why libraries fill with near duplicate images that no listing screen will show you, and why the clean up job is mostly detective work rather than deleting.

Flow diagram of a second upload of photo.jpg: the month folder is checked with file_exists, the overwrite step is never taken, photo-1.jpg is written instead, and a full set of sub sizes is generated, with stats showing four uploads, four files kept and zero overwritten.

There is one more rule in there that catches people out. Any name ending in something that looks like a generated size gets a number appended even when it is unique. The pattern is -(d+xd+|scaled|rotated)$, so banner-1200x630.jpg is stored as banner-1200x630-1.jpg and hero-scaled.png becomes hero-scaled-1.png on a completely empty folder. Core does this so a manually uploaded file can never be mistaken for, or later clobbered by, a thumbnail it generates itself. It also means the tidy dimension suffix you put in the name to be helpful is the one thing guaranteed to make the name untidy. Leave dimensions out.

The rules the cleaner applies, and the order

Order matters more than the individual rules, so the tool fixes it: transliterate, strip camera prefix, strip dates, lower case, separators, character filter, collapse repeats, then sequence numbers and truncation last. The character filter deletes rather than substitutes, exactly like core does, which is why the separator rule has to run before it. The spaces around an ampersand become hyphens first, then the ampersand is dropped, and words stay apart instead of gluing together.

Transliteration does the German pairs before anything else (ae, oe, ue, ss, plus the uppercase forms and the Nordic letters), then folds the remaining Latin accents. That ordering is the whole point: a naive fold turns ü into u, and Prüfung silently becomes Prufung, which is not a word. Doing the pairs first gives you pruefung, which matches what a German language site would have produced anyway.

Extension handling is its own small problem. With Keep the extension on, the last dot plus up to eight letters or digits is split off and held back from every rule except lower case, so photo.JPEG becomes photo.jpeg and never loses or reshuffles its suffix. Dots inside the name are ordinary characters, so my.file.name.txt comes out as myfilename.txt, which sidesteps the underscore behaviour core would otherwise apply. Switch the chip off and the extension is cleaned along with everything else.

The camera prefix rule knows IMG, IMAG, DSC, DSCN, DSCF, DCIM, DJI, GOPR, MVI, PXL and VID, plus the Panasonic P form when seven digits follow, and it only strips when digits follow, so a real word starting with those letters survives. Date stripping is off by default, on purpose, because a date is often the most useful part of the name. Turned on, it handles yyyy-mm-dd, yyyy_mm_dd, yyyy.mm.dd, bare yyyymmdd and dd.mm.yyyy, with a trailing time stamp only when a separator or a T sits between date and time.

Some names are nothing but a prefix and a date. Strip both from IMG_20240517_142305.jpg and there is genuinely nothing left, so the row says “Nothing left of the name” instead of inventing a word. That is the honest answer, and it is also the signal that this particular file needs a human to look at the picture and describe it.

Collisions are compared case insensitively, because Windows and macOS treat names that way, and every row in a colliding group is flagged rather than just the second one. Add a sequence number resolves collisions by construction, with a pad width that follows the largest number in the list and a minimum of two digits. Truncation reserves room for the extension and the sequence suffix, so the total never exceeds your maximum and the suffix is never the part that gets cut. Switch truncation off and an over length name is left alone and flagged instead.

What it deliberately does not do: rename anything on disk, read file contents, or understand folders, so a dropped folder contributes only its own name. The table draws at most 200 rows and says so, while the counter, Copy all and Download .txt cover the whole list, which keeps a thousand name paste responsive. Renaming files that are already in the library is a different job, and it belongs to something that works on attachments rather than text, like the media library manager.

The SEO claim, sized honestly

Google’s image SEO documentation says it uses the URL path and file name to understand images, so the name is a real signal. It is also a small one, well behind the alt text, the caption, the surrounding text and the page the image sits on. Anyone selling you a file name strategy as a ranking strategy is selling folklore, and there is a longer catalogue of that folklore in the piece on what actually works in WordPress image SEO.

The usability payoff is much larger than the ranking one, and it is the reason to bother. A descriptive name is what a visitor sees in their downloads folder after saving your image. It is what the media library search matches on: the admin screens switch on the wp_allow_query_attachment_by_filename filter that WP_Query leaves off by default, so typing part of a file name into the search box actually finds the attachment. In a library of 4,000 files with no folders to organise it, search is the only navigation you have. And it is what stops you, eighteen months later, from opening six thumbnails to find out which one was the product shot.

Descriptive means three to six words about what is in the picture, in the language the site is written in, hyphen separated, no dimensions, no dates unless the date is the content, no final and no v2. blue-ceramic-mug-on-oak-desk.jpg is right. IMG_20240517_142305.jpg is a serial number. photo1.jpg is worse than either, because it looks intentional.

Renaming a file that is already published

Here is the asymmetry that makes all of the above worth doing up front. Fixing a name before upload costs a few seconds. Fixing it afterwards means changing a URL that other things point at.

Rename an attachment file on the server and you break, at minimum: the src in every post that embeds it, every entry in that image’s srcset, all of the generated sizes (which carry the old base name plus their dimensions), the OG image tag on any page that used it, external links and pins, and any cached HTML or CDN copy that still refers to the old path. The attachment row in the database keeps a _wp_attached_file meta value pointing at a file that no longer exists, so the media library shows a broken thumbnail while the file itself is sitting right there under a new name.

There are three sane ways out, in ascending order of effort. Leave the name alone and fix the metadata instead: the title, the alt text and the caption are editable at any time and carry more weight than the file name does. Or upload a properly named copy, repoint the posts that use it, and delete the old attachment once nothing references it. Or, if the file itself is being updated rather than just renamed, replace the file while keeping the attachment and every link to it, which avoids the whole problem by not changing the URL at all.

Whichever route you take, do the audit before the delete. An orphan check and a usage report will tell you which posts point at the old file, and cleaning up a media library without breaking pages is entirely a matter of knowing that before you press delete rather than after.

Thirty seconds now, or an afternoon later

File names are a strange corner of web work. Nothing about them is difficult and no single bad name does visible damage, so the cost always arrives later: a broken image on a page you did not touch, six versions of one photo under names that differ by a digit, a URL that resolves on staging and 404s in production. The work is boring, the failures are not.

The mechanism underneath is simple enough to hold in your head. sanitize_file_name() folds Latin accents according to the site locale, deletes a fixed list of punctuation, turns whitespace runs into hyphens, and leaves everything else alone including capitals and non Latin scripts. wp_unique_filename() then adds a counter rather than overwriting, checking only the month folder. Both are reasonable rules. Neither is trying to give you a good name, and neither can: a good name describes the picture, and only the person who took it knows what is in it.

So do the naming while the files are still on your own machine, where a rename is free and reversible. Paste the list into the box above, switch off any rule you disagree with, read the warnings, and upload names that will still resolve after the next migration. It is the cheapest thirty seconds in the whole publishing workflow, and the only one that has to happen before the upload rather than after it.

Image File Names: The Boring Detail That Breaks Things

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 Development

functions.php Explained: What It Is and How Not to Break Your Site With It

functions.php is a normal PHP file that ships with your WordPress theme and runs on every page load, which makes it powerful, easy to misuse, and the wrong place for anything you want to keep. Here's what it actually does, and how to edit it without taking your site down.

WordPress Development

One Design, Two Hundred Names: Designing From a Spreadsheet

Somebody needs two hundred name badges by Thursday. The spreadsheet with all those names already exists, and typing them a second time is pure waste.

Photo Editing

Brighten dark photos in your browser with a 590 KB neural network

A brightness slider adds the same amount everywhere, which is why a dark photo goes pale instead of bright. A small trained network predicts a different correction for every pixel, plus one gamma and one colour matrix for the whole frame. It runs in your browser tab and your picture never leaves it.

WordPress Images

WordPress srcset: How Core Builds It, and Where It Goes Wrong

WordPress writes the srcset attribute for you and gets it right. Then it writes the sizes attribute, which is a guess about a layout core has never seen, and that is where the bandwidth goes.

Speed & Performance

A Web Performance Budget for Your WordPress Site

Pick a connection profile and a target load time, and arithmetic hands you a byte budget per resource class. A web performance budget is a decision tool: it tells you which image, which font and which plugin gets told no, before the page ever gets slow.

Troubleshooting

The WordPress White Screen of Death Explained

A white screen with no error message isn't a WordPress bug. It's PHP dying silently mid-request. Here's how to see the real error in minutes and the exact order to check plugins, theme, and memory limits.

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.