Nothing spikes your blood pressure quite like opening WordPress Media Library and seeing… a blank grid. Not “a few missing thumbnails.” Not “some broken images.” Just emptiness. Meanwhile, you know the files exist on disk, or S3, or somewhere your invoice says they should.
This failure is usually boring. It’s also usually fixable—if you stop guessing and start verifying the exact URLs WordPress is generating, the exact paths it thinks “uploads” live at, and whether your web server can actually serve those bytes.
Fast diagnosis playbook
If you’re on-call, you don’t have time for a philosophical discussion about CMS design. Do this in order. Each step narrows the search space and avoids “fixes” that mutate production data blindly.
First: is it a UI illusion or is WordPress genuinely not finding attachments?
- Check attachment counts in the database. If the posts table still has attachments, the library isn’t “empty,” it’s failing to render thumbnails or queries are filtered.
- Check one attachment record. Confirm the stored file path in
_wp_attached_fileand the site URL used to build the final URL.
Second: are URLs being generated wrong?
- Confirm
homeandsiteurl. A mismatch (http vs https, www vs non-www, wrong domain) breaks media URLs and admin Ajax calls in creative ways. - Check
upload_pathandupload_url_path. These options can override WordPress defaults and survive migrations like a cursed heirloom.
Third: can the web server serve the files at those URLs?
- Probe a real file with
curl -I. If you get 403/404/500, you’re debugging Nginx/Apache/CDN/storage, not WordPress. - Check filesystem permissions and SELinux/AppArmor constraints. “But it’s 755” is not a complete sentence.
Fourth: are you offloading media (S3/Cloud storage/CDN) and the plugin is lying?
- Identify offload plugins and their settings. Many rewrite URLs dynamically; some store canonical URLs in postmeta.
- Check whether the offload bucket actually has the objects. WordPress can be correct and the storage can still be empty.
Only after these do you consider bulk search/replace in the database. That’s a chainsaw. Use it, but don’t juggle it.
How the Media Library actually works (and how it lies to you)
The WordPress Media Library is not a file browser. It’s a database view. Specifically: it’s a list of posts where post_type = 'attachment', plus metadata that tells WordPress where the file should be relative to an uploads base.
When you upload an image, WordPress typically stores:
- An attachment post in
wp_postswith a title, MIME type, and a GUID (historically used as a “unique identifier,” often abused as a URL field). - The relative file path in
wp_postmetawith meta key_wp_attached_file(example:2025/12/photo.jpg). - Optional attachment metadata in
_wp_attachment_metadata(sizes, dimensions, thumbnails).
Then WordPress generates a public URL by combining a base URL (usually derived from home/siteurl plus /wp-content/uploads) with the relative path from _wp_attached_file. If any one of those inputs is wrong, you end up with valid database records that point to dead URLs.
Two consequences that matter in production:
- Your files can exist and still “not show.” A wrong base URL yields 404s; the Media Library grid looks empty because thumbnails never load.
- Your database can be correct and still “not show.” If the web server can’t read
wp-content/uploads, you’ll get 403s and broken thumbnails. WordPress will dutifully display nothing and wait for you to figure out that the OS exists.
One quote to keep you honest when you’re tempted to “just restart the thing”: Hope is not a strategy.
—Chris Snook
Short joke #1: WordPress troubleshooting is like detective work, except the suspect is always DNS and it has an alibi.
Interesting facts and context (so you stop being surprised)
- WordPress attachments are posts. Media items live in
wp_postsaspost_type=attachment; this is why database corruption or filtering can “erase” a library without touching disk. - The GUID field is historically overloaded. WordPress originally used GUID as a unique ID (think RSS). Many themes/plugins treat it as the file URL, which turns migrations into a slow-motion mess.
- The uploads path became configurable early—and stayed sticky. The
upload_pathandupload_url_pathoptions can persist across upgrades and migrations, even if they no longer match reality. - Year/month folders were a performance and organization choice. The default
uploads/2025/12/structure reduces directory bloat; turning it off can create millions of files in one directory and make filesystems grumpy. - Regenerating thumbnails became a “thing” because metadata ages badly. Change theme, sizes, or image processing and your
_wp_attachment_metadatamay not match what’s on disk. - Media offload plugins changed the failure domain. Once you rewrite URLs to S3/CDN, your WordPress box can be healthy while your bucket policy quietly denies everything.
- Admin-ajax and REST failures can look like “empty library.” The grid loads data via JS calls; CSP, mixed content, or blocked endpoints can cause a blank UI with no database issue at all.
- CDNs can cache your mistakes. A brief period of wrong URLs can get cached as 404s/403s, and then “fixing WordPress” doesn’t fix the user experience until you purge.
The real failure modes: DB URLs, paths, rewrites, storage
1) The site URL is wrong (or inconsistently wrong)
Classic: you migrated from staging to production, toggled HTTPS, added www, or moved behind a reverse proxy. WordPress stores URLs in the database, but also derives them at runtime. If home and siteurl don’t match the reality users see, you get mixed-content, wrong media URLs, and admin screens that behave like a haunted vending machine.
What “empty” looks like here:
- Media Library loads but thumbnails are blank (network shows 404/301 loops/mixed content blocked).
- Clicking an attachment shows a broken image icon.
- Browser console shows blocked requests, CORS errors, or repeated redirects.
2) upload_path / upload_url_path overrides are poisoning you
Most sites don’t set these. The ones that do often forget they did. After a migration, WordPress may still think uploads live at /var/www/oldsite/uploads or that public URLs start with some retired domain. The files might be correctly located at wp-content/uploads, but WordPress is looking elsewhere.
3) The database records are present, but the files are missing (or vice versa)
Backups and restores are great at restoring one side of this pair. People restore the database but not wp-content/uploads. Or they rsync uploads but not the database. Either way, the Media Library becomes a museum of broken references or a pile of unreferenced files you’re paying to store.
4) Web server routing blocks /wp-content/uploads
Overzealous Nginx rules, an Apache .htaccess deny, a security plugin dropping 403s, or a WAF rule that thinks JPGs are suspicious. If you can’t fetch a known file with curl, you don’t have a WordPress problem. You have a delivery problem.
5) Permissions, ownership, SELinux/AppArmor
Uploads readable by root but not by the web server user. Or readable, but traversal blocked due to directory execute bits. Or SELinux labeling denies httpd from reading the path. These issues manifest as 403s (sometimes 404s if the server hides authorization failures).
6) Offload / CDN / object storage is misconfigured
Offload plugins either:
- rewrite URLs on the fly (so the DB looks “fine” but requests go to S3/CDN), or
- store cloud URLs in postmeta / GUID / custom tables.
Common failure: credentials rotated, bucket policy changed, region endpoint changed, or private objects served without signed URLs. The WordPress admin shows nothing because thumbnails are fetched from the CDN and fail.
7) The Media Library UI is failing, not the media
If the attachment list query is fine but JavaScript requests fail, the library grid can render blank. Causes include CSP headers, blocked admin-ajax.php, REST API authentication issues, or a plugin that breaks the admin with a fatal error only on the media screen.
Short joke #2: The Media Library didn’t disappear; it just went to a new domain without telling Finance.
Practical tasks: commands, outputs, and decisions
These are the tasks I actually run when someone pings “media library empty” in chat. Each includes (1) a command, (2) an example output, and (3) what decision you make from it.
Task 1: Count attachments in the database
cr0x@server:~$ mysql -N -e "SELECT COUNT(*) FROM wp_posts WHERE post_type='attachment';"
12487
What it means: Attachments exist in DB. If the UI is empty, you’re likely dealing with URL generation, thumbnail requests, admin JS failures, or a query filter.
Decision: Don’t restore backups yet. Move on to URL/path verification and browser/network checks.
Task 2: Fetch a sample attachment meta (file path)
cr0x@server:~$ mysql -N -e "SELECT p.ID, p.post_title, pm.meta_value FROM wp_posts p JOIN wp_postmeta pm ON pm.post_id=p.ID WHERE p.post_type='attachment' AND pm.meta_key='_wp_attached_file' ORDER BY p.ID DESC LIMIT 1;"
88211 hero-banner 2025/12/hero-banner.jpg
What it means: WordPress expects the file at uploads/2025/12/hero-banner.jpg under whatever it considers the uploads base directory.
Decision: Validate that file exists on disk or in offload storage, and validate the public URL built for it.
Task 3: Check home and siteurl options
cr0x@server:~$ mysql -N -e "SELECT option_name, option_value FROM wp_options WHERE option_name IN ('home','siteurl');"
home https://www.example.com
siteurl https://example.com
What it means: Mismatch. Sometimes it’s intentional (WordPress in a subdir), often it’s accidental. It can break admin calls and media URL generation.
Decision: Decide what the canonical domain is (with or without www, https). Align these values, or implement proper proxy headers and rewrite rules if there’s a deliberate split.
Task 4: Check uploads overrides in wp_options
cr0x@server:~$ mysql -N -e "SELECT option_name, option_value FROM wp_options WHERE option_name IN ('upload_path','upload_url_path','uploads_use_yearmonth_folders');"
upload_path /var/www/legacy/uploads
upload_url_path https://legacy.example.net/uploads
uploads_use_yearmonth_folders 1
What it means: WordPress is explicitly configured to use a legacy filesystem location and URL. That’s your “empty library” right there.
Decision: Remove these overrides (set to empty) or update them to the correct path/URL. Don’t do this blind—verify the real location of media first.
Task 5: Determine uploads directory from WordPress config (WP-CLI)
cr0x@server:~$ cd /var/www/html
cr0x@server:~$ wp option get upload_path
/var/www/legacy/uploads
What it means: Confirms the DB option from Task 4 using WordPress itself (helpful when table prefixes differ).
Decision: If upload_path is wrong, fix it and retest one image fetch before bulk operations.
Task 6: Verify a known file exists on disk
cr0x@server:~$ ls -lah /var/www/html/wp-content/uploads/2025/12/hero-banner.jpg
-rw-r--r-- 1 www-data www-data 482K Dec 26 09:13 /var/www/html/wp-content/uploads/2025/12/hero-banner.jpg
What it means: File exists, readable, owned by the web server user (or at least a readable group). Good.
Decision: If the file exists but media is blank, move to HTTP fetching and web server rules. If it doesn’t exist, you need to restore uploads or reconcile offload storage.
Task 7: Check directory permissions up the tree (the “execute bit” trap)
cr0x@server:~$ namei -l /var/www/html/wp-content/uploads/2025/12/hero-banner.jpg
f: /var/www/html/wp-content/uploads/2025/12/hero-banner.jpg
drwxr-xr-x root root /
drwxr-xr-x root root var
drwxr-xr-x root root www
drwxr-xr-x root root html
drwxr-xr-x www-data www-data wp-content
drwxr-x--- root root uploads
drwxr-xr-x www-data www-data 2025
drwxr-xr-x www-data www-data 12
-rw-r--r-- www-data www-data hero-banner.jpg
What it means: The uploads directory is drwxr-x--- owned by root, so www-data may not traverse it depending on server user and group. That’s a 403 waiting to happen.
Decision: Fix ownership/permissions on uploads so the web server can traverse and read. Don’t just chmod 777; that’s how you meet ransomware professionally.
Task 8: If SELinux exists, check enforcement and recent denials
cr0x@server:~$ getenforce
Enforcing
cr0x@server:~$ sudo ausearch -m avc -ts recent | tail -n 5
type=AVC msg=audit(1735142651.812:912): avc: denied { read } for pid=2143 comm="nginx" name="hero-banner.jpg" dev="sda1" ino=550912 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0
What it means: SELinux is blocking reads because the file context is wrong (default_t instead of a web-readable type).
Decision: Relabel uploads with the correct context and make it persistent. Otherwise you’ll “fix” it until the next restore.
Task 9: Probe the media URL via HTTP (bypass the browser)
cr0x@server:~$ curl -I https://www.example.com/wp-content/uploads/2025/12/hero-banner.jpg
HTTP/2 403
date: Sat, 27 Dec 2025 10:12:15 GMT
content-type: text/html
server: nginx
What it means: 403 at the edge. That’s not “WordPress forgot the image.” That’s policy, auth, WAF, permissions, or a rule denying /wp-content.
Decision: Inspect Nginx/Apache config and security layers. Don’t touch the DB until HTTP is serving known files.
Task 10: Check Nginx config for accidental denies on uploads
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -nE "wp-content|uploads|deny|location"
128: location ~* /(wp-content|wp-includes)/.*\.php$ { deny all; }
141: location ~* ^/wp-content/uploads/ { deny all; }
What it means: Someone blocked uploads. Often intended to block PHP execution, but implemented as “deny all” instead of “deny PHP.”
Decision: Change rules to allow static files while denying executable content. Retest with curl -I.
Task 11: Check Apache .htaccess or vhost rules affecting uploads
cr0x@server:~$ sudo grep -RIn "uploads\|wp-content" /var/www/html/.htaccess /etc/apache2/sites-enabled 2>/dev/null | head
/var/www/html/.htaccess:12:RewriteRule ^wp-content/uploads/ - [F,L]
What it means: Rewrite rule forbids access to uploads (returns 403). This is either a misunderstanding or a copy-paste security snippet applied too broadly.
Decision: Fix the rule to only block dangerous patterns (like PHP in uploads), not the entire directory.
Task 12: Check whether requests are being redirected to the wrong host
cr0x@server:~$ curl -I http://www.example.com/wp-content/uploads/2025/12/hero-banner.jpg
HTTP/1.1 301 Moved Permanently
Location: https://example.com/wp-content/uploads/2025/12/hero-banner.jpg
What it means: Canonical host is different. If WordPress thinks “www” but your redirect forces non-www (or vice versa), you can get mixed content or blocked admin calls depending on how cookies and CORS line up.
Decision: Pick one canonical host and align WordPress options, redirects, and CDN configuration to it.
Task 13: Confirm attachment GUID patterns (spot stale domains)
cr0x@server:~$ mysql -N -e "SELECT COUNT(*) FROM wp_posts WHERE post_type='attachment' AND guid LIKE '%legacy.example.net%';"
12487
What it means: Every attachment still has a legacy domain in GUID. This may or may not matter, depending on theme/plugin behavior.
Decision: If your front-end or plugin uses GUID for URLs, you need a controlled update. If not, you still might update it for consistency—but do it carefully and test feeds/integrations.
Task 14: Search for old domains in postmeta (actual file URL overrides)
cr0x@server:~$ mysql -N -e "SELECT meta_key, COUNT(*) FROM wp_postmeta WHERE meta_value LIKE '%legacy.example.net%' GROUP BY meta_key ORDER BY COUNT(*) DESC LIMIT 10;"
_wp_attached_file 0
amazonS3_info 12487
What it means: Offload metadata references the legacy domain/bucket configuration. That’s often the real driver of broken thumbnails when using offload plugins.
Decision: Fix offload plugin settings first; then consider metadata migration/rehydration tools specific to that plugin.
Task 15: Validate WP-CLI can list attachments (bypass the admin UI)
cr0x@server:~$ wp post list --post_type=attachment --fields=ID,post_title,guid --format=table | head
+------+------------------+--------------------------------------------------------------+
| ID | post_title | guid |
+------+------------------+--------------------------------------------------------------+
| 88199| hero-banner | https://legacy.example.net/wp-content/uploads/2025/12/hero... |
| 88185| team-headshot | https://legacy.example.net/wp-content/uploads/2025/12/team... |
+------+------------------+--------------------------------------------------------------+
What it means: WordPress sees the attachments fine at the application layer.
Decision: If WP-CLI lists attachments but the UI is empty, focus on admin JS, API calls, and thumbnail fetch failures, not “missing DB rows.”
Task 16: Check admin Ajax/REST endpoints quickly (server-side)
cr0x@server:~$ curl -I https://www.example.com/wp-admin/admin-ajax.php
HTTP/2 302
location: https://www.example.com/wp-login.php?redirect_to=https%3A%2F%2Fwww.example.com%2Fwp-admin%2Fadmin-ajax.php
What it means: Endpoint is reachable. 302 to login is normal unauthenticated. If you get 403/500 here, the Media Library UI can break.
Decision: If admin endpoints fail, check WAF rules, mod_security, caching layers, and PHP errors before you touch media URLs.
Task 17: Find PHP errors tied to media requests (quick grep)
cr0x@server:~$ sudo tail -n 80 /var/log/php8.2-fpm.log | grep -iE "fatal|wp-admin|media|imagick|gd" | tail -n 10
[27-Dec-2025 10:11:05] PHP Fatal error: Uncaught Error: Call to undefined function imagewebp() in /var/www/html/wp-includes/media.php:4212
What it means: A missing image library feature (here WebP support) can break thumbnail generation or metadata operations, sometimes cascading into UI weirdness.
Decision: Fix the PHP extension/library mismatch or disable the feature/plugin relying on it; then regenerate thumbnails if needed.
Task 18: Controlled DB search/replace for domain changes (dry run)
cr0x@server:~$ wp search-replace 'https://legacy.example.net' 'https://www.example.com' --dry-run --all-tables
Success: Made 0 replacements.
What it means: WP-CLI didn’t find matches (or you’re not scanning the right tables/prefix). Alternatively, the old domain is stored without scheme, or stored in serialized arrays in a different form.
Decision: Re-scope the search (http vs https, with/without www), or target specific tables. When you do run it for real, take a DB snapshot first.
Task 19: Check for mixed-content patterns (http media on https site)
cr0x@server:~$ mysql -N -e "SELECT COUNT(*) FROM wp_posts WHERE post_type='attachment' AND guid LIKE 'http://%';"
12487
What it means: Attachments still reference http. Browsers will often block or warn, and admin thumbnails can fail depending on policies.
Decision: Plan a careful migration to https URLs (or ensure WordPress generates https regardless of GUID usage).
Three corporate mini-stories from the trenches
Mini-story 1: The incident caused by a wrong assumption
A mid-sized org migrated a WordPress estate from a legacy VM fleet to containers. The migration plan looked clean: database dump/restore, rsync wp-content, update DNS, done. The Media Library went blank right after cutover.
The team assumed “blank library = missing uploads.” So they re-ran rsync, twice, during business hours. Then they started restoring older backups because surely the latest copy was corrupt. They burned a day and created a nice little backlog of inconsistent states: some pages referenced media that existed only in the first restore, some in the second.
The actual issue was mundane: upload_url_path in wp_options still pointed at the old domain. WordPress was generating thumbnail URLs to a hostname that now returned a 301 to an internal-only endpoint. From inside the office VPN it “worked,” from the internet it didn’t. The UI in the admin looked empty because every thumbnail request got redirected into a place the browser wouldn’t follow due to mixed cookies and blocked cross-origin.
Fixing it was anticlimactic: clear the option, align home/siteurl, purge the CDN, and suddenly the library “reappeared.” The files had been there the entire time. The outage came from a wrong assumption: that the Media Library is a direct view of the filesystem. It isn’t.
Mini-story 2: The optimization that backfired
A marketing team complained that image uploads were “slow,” so an engineer introduced an offload-to-object-storage plugin and a CDN in front. On paper: fewer disk writes, less load on the origin, faster page loads. In practice: a new production dependency with a very exciting blast radius.
A few weeks later, the Media Library started showing empty squares. Not broken icons—just blank, like it never heard of images. The front-end pages also lost images, but not consistently. Some browsers worked. Some didn’t. The issue was intermittent, which is the universe’s way of laughing.
Root cause: the CDN cached 403 responses from the object store for a subset of keys. A policy update had temporarily denied reads for objects without a specific prefix. The plugin continued to generate CDN URLs, and WordPress itself had no idea anything was wrong. The admin thumbnails were coming from the CDN, which was faithfully serving cached 403s.
The “optimization” had moved media availability from “can the origin serve files?” to “is the CDN policy correct, is the bucket policy correct, are credentials valid, is the plugin rewriting URLs correctly, and did we purge caches after policy changes?” They fixed it by correcting the policy and purging the CDN, but the real lesson was governance: treat media offload as a platform change, not a quick plugin install.
Mini-story 3: The boring but correct practice that saved the day
A different company had a ritual: every migration required two verifications before declaring success. First, confirm attachment counts and a sample of _wp_attached_file entries. Second, fetch ten random images with curl -I from outside the network (a real external probe, not “works on my laptop”). Nobody loved this checklist. That’s why it worked.
During a datacenter move, the Media Library looked empty in the admin for a subset of users. The engineers didn’t panic. They ran the routine checks. Database counts were fine. File existence on disk was fine. HTTP fetches from outside returned 403—but only for requests missing a certain header. That narrowed it to the edge layer.
It turned out the new WAF profile blocked requests to /wp-content/uploads/ unless they had a browser-like User-Agent. The WAF vendor called it “bot protection.” The company called it “breaking the website.”
Because they had a habit of doing the same tests every time, they had clean evidence: “origin can read files, WordPress generates the right path, edge returns 403.” They got the rule fixed quickly and avoided the classic mistake of rewriting database URLs to chase a non-database problem.
Common mistakes: symptom → root cause → fix
-
Symptom: Media Library grid is blank; attachment list count looks like zero in the UI.
Root cause: Admin JS requests failing (REST/Ajax blocked by WAF, CSP, caching plugin, or a fatal error on the media screen).
Fix: Check browser devtools network/console, then validate
admin-ajax.phpand REST endpoints. Fix the server/WAF rule or plugin conflict before touching uploads paths. -
Symptom: Attachments exist in DB, but thumbnails are broken and show 404.
Root cause: Wrong
home/siteurl, or old domain baked into URL overrides.Fix: Align
homeandsiteurlto canonical domain. Clear or correctupload_url_path. Purge CDN caches after. -
Symptom: Thumbnails return 403 from
/wp-content/uploads.Root cause: Web server deny rules, filesystem traversal permissions, or SELinux context blocking reads.
Fix: Adjust Nginx/Apache rules to allow static files; correct ownership/permissions; relabel SELinux contexts.
-
Symptom: Files exist on disk but WordPress uploads new media into an unexpected directory.
Root cause:
upload_pathoverride points elsewhere, or container volume mapping is wrong.Fix: Remove the override so WordPress uses
wp-content/uploads, or fix the mount path. Confirm with a test upload. -
Symptom: Media shows in admin but images broken on front-end only.
Root cause: CDN caching old 404s, mixed-content http/https issues, or theme building URLs from GUID differently than admin.
Fix: Purge CDN, enforce https, and audit theme/plugins for GUID usage. Fix canonical URL generation.
-
Symptom: After migration, some images work, newer ones don’t.
Root cause: Partial restore of uploads directory; missing year/month subtrees; or object storage sync incomplete.
Fix: Compare filesystem trees, restore missing directories, or re-run object storage sync with verification.
-
Symptom: Clicking an attachment shows metadata but no preview; resizing fails.
Root cause: Missing GD/Imagick features or PHP fatal errors in media processing.
Fix: Install/enable required PHP extensions and libraries; then regenerate thumbnails if metadata is stale.
-
Symptom: Media Library shows items but search/filtering yields nothing.
Root cause: Plugin altering queries (hooks) or DB table collation/index issues causing slow/failed queries.
Fix: Disable suspect plugins, check MySQL slow logs and errors, repair tables/indexes if needed.
Checklists / step-by-step plan
Checklist A: “Media Library looks empty” in production
- Confirm DB has attachments. If zero, you have data loss or a wrong database connection/table prefix.
- Check one attachment’s
_wp_attached_file. Validate it’s sane (relative path, not some weird absolute legacy path unless intentional). - Confirm
homeandsiteurl. Decide your canonical domain and scheme. - Check
upload_pathandupload_url_path. Clear legacy overrides unless you intentionally use them. - Fetch one known media file with
curl -I. Note 200 vs 301 vs 403 vs 404. - Verify filesystem existence and traversal permissions. Use
namei -lfor the whole path. - Check SELinux/AppArmor if applicable. Enforcing mode plus AVC denials equals silent misery.
- Check web server rules. Ensure you block PHP in uploads, not the uploads directory.
- Check CDN/WAF. Look for cached 403/404 responses and path-based blocks.
- Only then do DB search/replace. Take a snapshot first, do dry runs, and validate serialized data handling via WP-CLI.
Checklist B: Safe URL/path remediation without making it worse
- Backup database. Logical dump plus snapshot if available. If you can’t restore, you can’t “fix.”
- Decide canonical URL. https vs http, www vs non-www, and whether WordPress lives in a subdir.
- Fix
home/siteurlfirst. It influences everything else. - Clear upload overrides unless needed. Let WordPress default to
wp-content/uploadsunless you have a reason. - Validate a single attachment end-to-end. DB record → filesystem/object → HTTP 200.
- Run
wp search-replacewith--dry-run. Confirm scope and table selection. - Run the real replacement in off-hours. Measure duration, lock impact, and have rollback ready.
- Purge CDN caches. Otherwise your “fix” is correct but invisible.
- Regenerate thumbnails if sizes changed. Only if you’ve confirmed the originals are accessible.
Checklist C: Hardening so this doesn’t happen again
- Monitor HTTP for sample media objects. A synthetic check that fetches a few known uploads catches 403/404 early.
- Track attachment count and uploads tree size. Big divergences indicate partial restores or broken pipelines.
- Document whether you use offload/CDN. Treat it as critical infrastructure with change control.
- Standardize canonical domain handling. Proxy headers, redirects, and WordPress settings must agree.
- Ban ad-hoc SQL search/replace in prod. Use WP-CLI for serialized safety and auditability.
FAQ
Why is the Media Library empty but posts still show images?
Often the front-end is rendering cached HTML or using a different URL source (theme hardcoding, CDN URLs), while the admin grid relies on JS calls and thumbnail endpoints that are blocked.
Is it safe to update home and siteurl directly in the database?
Yes, if you know the correct canonical URL and you have a rollback. Do it deliberately; mismatches can break login cookies and admin access. Prefer WP-CLI when possible for consistent behavior.
What’s the difference between home and siteurl?
home is the public-facing site address. siteurl is where WordPress core files live. They’re often the same, except in subdirectory installs or unusual setups. Accidental differences cause weirdness.
Should I “fix” attachment GUIDs during a migration?
Sometimes. WordPress doesn’t rely on GUID for media URLs in the clean path, but plugins and themes might. If you see GUIDs used in templates, or feeds/integrations depend on them, plan an update carefully.
Why do I get 403 when requesting files in wp-content/uploads?
Either the web server config denies that path, filesystem permissions block traversal/read, or SELinux/AppArmor is denying access. Don’t assume it’s a WordPress bug; prove it with curl -I and logs.
After fixing URLs, why do I still see missing thumbnails?
CDNs and browsers cache failures. Also, thumbnails might not exist on disk if you restored originals only or changed image sizes. Purge caches, then regenerate thumbnails if the originals are accessible.
How can I tell if an offload plugin is involved?
Look for plugin settings related to S3/object storage, check postmeta for keys like amazonS3_info, and inspect the thumbnail URLs in the browser network panel. If they point to a CDN/bucket domain, you’re offloading.
Can a reverse proxy cause an “empty media library”?
Yes. If WordPress doesn’t see the correct scheme/host (missing proxy headers), it can generate http URLs on an https site, triggering mixed-content blocking or redirect loops that stop thumbnails from loading.
What’s the single most common migration mistake?
Restoring the database without restoring wp-content/uploads (or restoring uploads without the database). Media is a coupled system: metadata + bytes.
Conclusion: next steps that actually reduce risk
If your WordPress Media Library looks empty, resist the urge to “reinstall WordPress” or smash random search/replace queries into production. The fix is usually a mismatch between what WordPress thinks the uploads URL/path is and what your infrastructure actually serves.
Do this next:
- Prove attachments exist in the DB and grab a sample
_wp_attached_file. - Align
home/siteurland remove or correctupload_path/upload_url_path. - Fetch a known upload via
curl -Iand fix the edge/web server/perms until you get a clean 200. - If offloading is in play, validate bucket/CDN permissions and purge cached 403/404s.
- Only then run controlled WP-CLI search/replace and (if needed) regenerate thumbnails.
Make it boring: add a small synthetic check that fetches a handful of known uploads. The best time to discover your uploads are blocked is never.