Your homepage loads. Admin works. But every post returns a 404. That’s not “WordPress being WordPress.” That’s your routing layer—WordPress rewrite rules, your web server config, caches, or redirects—arguing about what a URL means.
And yes, it’s a production incident. Posts are revenue, leads, and credibility. Fixing it fast is easy. Fixing it correctly—without torching SEO signals and leaving a trail of broken URLs—is the part that separates a quick hack from an outage postmortem.
Fast diagnosis playbook
If you only have 15 minutes before someone “just switches permalinks back” in panic, do this in order. You’re looking for the shortest path to certainty.
First: confirm what kind of 404 you’re getting
- Server 404 (Nginx/Apache): WordPress never ran. Fix server routing.
- WordPress 404 (theme 404 page): PHP ran, WordPress couldn’t resolve the request to a post. Fix rewrite rules, permalink structure, query vars, or database slugs.
- CDN/WAF cached 404: Origin may be fixed but users still see 404. Purge caches and adjust caching rules.
Second: test one known post URL end-to-end
Pick a post that definitely exists in the database. Don’t test “maybe” URLs. Test reality.
Third: verify rewrite plumbing, then flush rules once (not 37 times)
If you’re on Apache, check mod_rewrite and .htaccess permissions. If you’re on Nginx, check the try_files stanza. Then flush rewrite rules in a controlled way.
Fourth: if the site moved or permalinks changed, design redirects before you touch anything else
Don’t “fix” 404s by changing permalink structure blindly. That’s how you turn a contained routing issue into an SEO reindexing event.
What a WordPress post 404 really means
“Post 404” is one phrase that hides three different failures:
- The web server can’t map the URL to WordPress (pretty permalinks require rewrite rules). This is usually Nginx/Apache config or missing
.htaccess. - WordPress receives the request but can’t resolve it (rewrite rules out of date, wrong permalink structure, missing rewrite tags, plugin conflicts, messed up siteurl/home, wrong language prefixes, etc.).
- WordPress resolves it, but something else intercepts (redirect loops, canonical redirects to dead URLs, caching layers serving stale responses, WAF blocking, or a proxy sending requests to the wrong backend).
The fix depends on which one you have. The mistake is treating all 404s like “flush permalinks.” Sometimes flushing works because it regenerates rewrite rules. Sometimes it does nothing because the server never hands the request to WordPress. Sometimes it makes things worse because it masks an underlying mismatch between old and new URL structures.
One quote worth keeping in your head while you debug: Hope is not a strategy.
— Vince Lombardi
It’s not a reliability engineer’s quote, but the SRE brain recognizes it instantly: measure, verify, then change.
Joke #1: Flushing permalinks is like rebooting a printer: it works often enough to become superstition, but not enough to be a plan.
Interesting facts and historical context (because this problem has a backstory)
- Pretty permalinks weren’t “default” forever. Early blogging engines commonly used query-string URLs like
?p=123. Clean URLs required server rewrite support. - WordPress relies on the web server to “lie” for it. With permalinks enabled, your web server pretends a file doesn’t exist and routes requests to
index.php, where WordPress resolves the post. - Apache’s
.htaccessmade shared hosting cheap—and fragile. WordPress became ubiquitous partly because you could control rewrites without root access. That flexibility also made it easy to break things with permissions or missing modules. - Nginx never supported
.htaccesson principle. That design is faster and safer, but it means a migration from Apache to Nginx often surfaces permalink issues immediately. - Google’s handling of 404s evolved. Search engines got better at dropping dead URLs quickly and treating endless redirect chains as low quality. That raises the cost of sloppy URL changes.
- 301 vs 302 became a practical SEO concern. “Permanent” redirects (301) typically pass signals more reliably than temporary ones (302), though modern engines interpret intent. Still: use 301 for moved content.
- The WordPress rewrite system is database-backed. Rewrite rules are generated and stored; they can go stale if a plugin modifies routes and later gets removed.
- Canonical URLs matter. WordPress will try to redirect “almost correct” URLs to canonical ones; if your canonical logic points to a non-existent path, you can create self-inflicted 404 loops.
- CDNs normalized caching of negative responses. Caching a 404 can be useful, but when the 404 is accidental, you’ve now distributed your bug globally.
Hands-on tasks: commands, outputs, what they mean, and what to do next
These tasks are written like an on-call runbook: you run a command, you interpret the output, you make a decision. Use them in order, or jump to the section matching your stack.
Task 1: Check whether the 404 is from the origin or a CDN
cr0x@server:~$ curl -sI https://example.com/2025/hello-world/ | sed -n '1,12p'
HTTP/2 404
date: Fri, 26 Dec 2025 10:22:11 GMT
content-type: text/html; charset=UTF-8
server: nginx
x-cache: HIT
cf-cache-status: HIT
What it means: You’re seeing a 404 that is likely cached (HIT). If you fix the origin but don’t purge, users keep seeing the failure.
Decision: Purge the CDN cache for a representative URL once you’ve verified the origin is correct. If you can’t purge, set a short TTL for 404s temporarily.
Task 2: Confirm if WordPress executed (WordPress 404 vs server 404)
cr0x@server:~$ curl -s https://example.com/2025/hello-world/ | grep -iE 'wp-content|wp-includes|rel="shortlink"|wp-json' | head
<link rel='shortlink' href='https://example.com/?p=123' />
<script src='https://example.com/wp-includes/js/jquery/jquery.min.js' id='jquery-core-js'></script>
What it means: WordPress generated the page (even if it’s a 404 template). This points to rewrite rules, permalink mismatch, missing post, or canonical redirects—not static file routing.
Decision: Move on to rewrite rules inspection and WordPress-level checks.
Task 3: Validate the post exists via WP-CLI
cr0x@server:~$ cd /var/www/html
cr0x@server:~$ wp post list --post_type=post --name=hello-world --fields=ID,post_title,post_status,post_name --format=table
+-----+-------------+------------+------------+
| ID | post_title | post_status| post_name |
+-----+-------------+------------+------------+
| 123 | Hello World | publish | hello-world|
+-----+-------------+------------+------------+
What it means: The post exists and is published. The 404 is routing, not content deletion.
Decision: Focus on permalinks/rewrite/canonicalization and the web server config.
Task 4: Check the current permalink structure in WordPress
cr0x@server:~$ wp option get permalink_structure
/%year%/%postname%/
What it means: WordPress expects URLs like /2025/hello-world/.
Decision: If your URLs in production are different (e.g., /blog/hello-world/), don’t guess—restore the structure or redirect cleanly.
Task 5: Inspect rewrite rules size and whether they look populated
cr0x@server:~$ wp rewrite list --format=table | head -n 12
+-------------------------------+------------------------------------------+----------------+
| match | query | source |
+-------------------------------+------------------------------------------+----------------+
| ^wp-json/?$ | index.php?rest_route=/ | rest-api |
| ^wp-json/(.*)? | index.php?rest_route=/$matches[1] | rest-api |
| ^([0-9]{4})/([^/]+)/?$ | index.php?year=$matches[1]&name=$matches[2] | post |
| ^([0-9]{4})/page/([0-9]{1,})/?$ | index.php?year=$matches[1]&paged=$matches[2] | post |
What it means: Rewrite rules exist and include patterns matching your permalink structure.
Decision: If rules are empty or missing the expected patterns, flush rewrite rules (next task) and look for plugin conflicts or failed writes.
Task 6: Flush rewrite rules safely (CLI, once)
cr0x@server:~$ wp rewrite flush --hard
Success: Rewrite rules flushed.
What it means: WordPress regenerated rules and wrote them (or updated its internal cache). On Apache, “hard” flush attempts to update .htaccess too.
Decision: Retest a failing URL. If it still 404s, the problem is likely server routing (.htaccess ignored, Nginx config wrong) or something like canonical redirect logic.
Task 7: On Apache, confirm mod_rewrite is enabled
cr0x@server:~$ apachectl -M 2>/dev/null | grep rewrite
rewrite_module (shared)
What it means: Rewrite module is loaded.
Decision: If it’s missing, enable it (package-specific) and reload Apache; without it, pretty permalinks will 404.
Task 8: On Apache, confirm the vhost allows overrides (so .htaccess works)
cr0x@server:~$ sudo apachectl -S 2>/dev/null | sed -n '1,40p'
VirtualHost configuration:
*:80 example.com (/etc/apache2/sites-enabled/example.conf:1)
ServerRoot: "/etc/apache2"
cr0x@server:~$ sudo grep -R "AllowOverride" -n /etc/apache2/sites-enabled/example.conf
15: AllowOverride None
What it means: AllowOverride None disables .htaccess. WordPress can regenerate rules all day and Apache will ignore them.
Decision: Set AllowOverride All (or at least FileInfo) for the document root, reload Apache, and retest. Or better: move rewrites into the vhost config for performance and clarity.
Task 9: On Apache, verify .htaccess exists and is readable
cr0x@server:~$ ls -la /var/www/html/.htaccess
-rw-r--r-- 1 www-data www-data 612 Dec 26 10:10 /var/www/html/.htaccess
What it means: File exists and permissions look sane.
Decision: If it’s missing or owned by root with restrictive perms, fix ownership/perms and re-flush rewrite rules.
Task 10: On Nginx, confirm you’re using a WordPress-compatible try_files
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -n "try_files" | head
147: try_files $uri $uri/ /index.php?$args;
What it means: This is the canonical WordPress routing line. If it’s missing, permalinks often 404 because Nginx tries to find a file that doesn’t exist and returns 404.
Decision: If you see something like try_files $uri =404;, fix the location block and reload Nginx.
Task 11: Confirm PHP-FPM is reachable (a “404” can be an upstream failure masked by error handling)
cr0x@server:~$ systemctl status php8.2-fpm --no-pager
● php8.2-fpm.service - The PHP 8.2 FastCGI Process Manager
Loaded: loaded (/lib/systemd/system/php8.2-fpm.service; enabled)
Active: active (running)
What it means: PHP-FPM is running; good.
Decision: If it’s down or flapping, fix that first. Routing bugs don’t matter if the app can’t execute reliably.
Task 12: Check WordPress “home” and “siteurl” values after migrations
cr0x@server:~$ wp option get home
https://example.com
cr0x@server:~$ wp option get siteurl
https://example.com
What it means: These control how WordPress builds canonical URLs and internal links. Wrong values can create redirects to dead hosts or paths, which looks like 404s for users.
Decision: If they point to an old domain, fix them and purge caches. Then verify canonical redirects behave.
Task 13: Identify if canonical redirects are interfering
cr0x@server:~$ curl -sI https://example.com/2025/hello-world | sed -n '1,12p'
HTTP/2 301
date: Fri, 26 Dec 2025 10:23:01 GMT
location: https://example.com/2025/hello-world/
What it means: WordPress (or web server) is normalizing the trailing slash. That’s fine.
Decision: If the Location points somewhere wrong (old domain, wrong base path like /blog/), fix home/siteurl, permalink structure, or server redirects.
Task 14: Verify a post resolves by ID even if permalinks fail
cr0x@server:~$ curl -sI "https://example.com/?p=123" | sed -n '1,12p'
HTTP/2 200
date: Fri, 26 Dec 2025 10:23:22 GMT
content-type: text/html; charset=UTF-8
server: nginx
What it means: WordPress can serve the post. Your content is fine. Your “pretty” routing is broken.
Decision: Keep the site online by temporarily switching permalinks to “Plain” if you must (short term), but you’ll need redirects and a careful plan to avoid SEO damage. Better: fix rewrites properly.
Task 15: On Nginx, spot accidental “static-only” caching that returns 404
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -n "location ~ \\\\.php" -n | head
162: location ~ \.php$ {
cr0x@server:~$ sudo nginx -T 2>/dev/null | sed -n '150,190p'
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
What it means: Basic routing is correct. If your config instead had a location block for / returning 404 for unknown paths, you’d break permalinks.
Decision: If configs are complex (multiple location blocks), simplify until permalinks work, then reintroduce optimizations carefully.
Task 16: Check for redirect rules that accidentally eat post URLs
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -n "return 301\|rewrite " | head -n 20
88: return 301 https://example.com$request_uri;
205: rewrite ^/blog/(.*)$ /$1 permanent;
What it means: You have rewrite/redirect rules. The /blog/ rewrite could be correct—or it could strip a needed prefix and create 404s if WordPress is configured to include it.
Decision: Validate redirect intent against actual permalink structure and historical URLs. Don’t “clean up” rewrite rules during an outage unless you like surprises.
Task 17: Inspect WordPress-generated .htaccess rules (Apache)
cr0x@server:~$ sed -n '1,120p' /var/www/html/.htaccess
# BEGIN WordPress
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
# END WordPress
What it means: This is the standard WordPress block. If it’s missing or malformed, permalinks will 404.
Decision: Restore the WordPress block (carefully, preserving other directives), then flush rewrite rules and retest.
Task 18: Check server logs for the failing URL
cr0x@server:~$ sudo tail -n 50 /var/log/nginx/access.log
203.0.113.10 - - [26/Dec/2025:10:24:01 +0000] "GET /2025/hello-world/ HTTP/2.0" 404 548 "-" "Mozilla/5.0"
cr0x@server:~$ sudo tail -n 80 /var/log/nginx/error.log
2025/12/26 10:24:01 [error] 12345#12345: *910 open() "/var/www/html/2025/hello-world/index.html" failed (2: No such file or directory), client: 203.0.113.10, server: example.com, request: "GET /2025/hello-world/ HTTP/2.0", host: "example.com"
What it means: Nginx attempted to open a file path (static) and failed, suggesting it didn’t route to index.php. That’s classic misconfigured try_files or a conflicting location.
Decision: Fix the Nginx location / block routing and reload.
Root causes by stack: WordPress, Apache, Nginx, CDN
WordPress-level causes (WordPress runs, still 404s)
- Stale rewrite rules after plugin/theme changes or migrations. Fix: flush rewrite rules via WP-CLI; confirm the permalink structure matches expected URLs.
- Permalink structure mismatch (site expects
/%postname%/but links are/%year%/%postname%/). Fix: restore the previous structure or implement redirects that preserve the old URLs. - Wrong
home/siteurlleading to canonical redirects that point to dead paths/domains. Fix: correct options, then verify redirect behavior. - Custom post types and rewrite slugs changed. Fix: re-register post types with consistent rewrite args; flush rules; add redirects for old slugs.
- Conflicting plugins that hook into rewrites, redirects, or canonicalization. Fix: disable the suspect plugin temporarily, confirm routing, then adjust configuration.
- Multisite / language plugins adding prefixes (
/en/) that the server doesn’t route properly. Fix: ensure server rewrite passes prefixed paths to WordPress.
Apache causes (pretty permalinks rely on rewrite + overrides)
mod_rewritenot loaded. Symptom: all pretty permalinks 404, but?p=IDworks.AllowOverride Noneprevents.htaccessfrom applying. WordPress “writes” rules but Apache ignores them.- Missing or corrupted
.htaccess. Can happen during deployments, permission hardening, or container builds. - Wrong DocumentRoot (vhost points to the wrong directory) so requests never hit the WordPress install that owns the database.
Nginx causes (no .htaccess safety net)
- Missing
try_files ... /index.php?$args;. This is the #1 reason “posts 404” after migrating to Nginx. - Overzealous location blocks that short-circuit routing (
location / { try_files $uri =404; }), or regex locations that win precedence unexpectedly. - Incorrect fastcgi params causing PHP execution weirdness; sometimes manifests as “works for admin, not for posts” depending on routes.
CDN / reverse proxy causes
- Cached 404 responses (including negative caching). You fix origin, but the world keeps seeing the old answer.
- Redirect rules at the edge that rewrite paths (
/blog/stripping or adding) without WordPress being configured to match. - HTTPS termination mismatch where WordPress thinks it’s on HTTP; canonical redirects bounce and can land users on dead variants.
Fix permalinks without breaking SEO
Here’s the operational truth: most “WordPress permalink fixes” accidentally change URLs. Changing URLs is not inherently bad. Changing URLs without a redirect plan is malpractice.
Decide what you’re actually trying to do
There are two different goals that people confuse:
- Restore working routing without changing public URLs. This is an outage fix. SEO-friendly.
- Change URL structure intentionally. This is a migration/change management task. Needs mapping, redirects, and validation.
If you just want posts to stop 404ing, you likely want goal #1.
Rule #1: restore the old permalink structure first
If posts 404 because someone changed permalinks, the safest SEO move is to revert to the structure that search engines already know. Then you can plan any future URL improvements later, when nobody is paging you.
Operationally: this reduces blast radius. SEO-wise: it preserves continuity of indexed URLs, inbound links, and historical engagement metrics.
Rule #2: when you must change URLs, implement 301 redirects with a one-hop policy
A clean redirect strategy looks like this:
- Old URL → New URL (301), exactly one hop.
- No old URL should redirect to an intermediate URL that then redirects again.
- No redirect should land on a 404 or a soft-404 page.
Redirect chains waste crawl budget and make debugging miserable. Also they tend to accumulate like dust bunnies: quietly, until someone checks behind the couch.
Rule #3: keep the content identity stable
Search engines treat a URL as a “content identity pointer.” If you change URLs, keep everything else stable:
- Same content body (don’t mass-edit during URL changes).
- Same title and metadata unless there’s a reason.
- Canonical tags pointing to the final URL, not to legacy ones.
Rule #4: decide where redirects live (and be consistent)
You can implement redirects in several places. Pick one primary layer to avoid duplication and loops.
- At the web server (Nginx/Apache): Fast, reliable, good for simple patterns (e.g.,
/index.php/removal, old prefix changes). - In WordPress (plugin or functions.php): Flexible, but adds app overhead and can fail if WordPress doesn’t run.
- At CDN/edge: Very fast and global, but easy to misconfigure at scale. Also can mask origin behavior.
My opinionated default: pattern-based redirects at the web server, content-aware redirects inside WordPress only when you truly need them.
Rule #5: verify with sampling and with logs
Do not verify a URL migration by clicking three links and declaring victory. Verify with:
- A sample set of old URLs (top traffic posts, plus random long-tail posts).
- Status code checks (expect 301 then 200).
- Access logs: ensure bots and users are landing on 200s, not ping-ponging through redirects.
Joke #2: SEO is the only discipline where people panic over a 302 like it’s a security breach.
Common mistakes (symptom → root cause → fix)
1) “Only posts 404, but pages work”
Symptom: /about/ works, /2025/my-post/ 404s.
Root cause: Permalink structure includes date segments, but rewrite rules were generated for a different structure; or server rewrites are too narrow (e.g., only matching top-level slugs).
Fix: Confirm permalink_structure; flush rewrite rules; ensure Nginx routes all paths through index.php when no file exists.
2) “Admin works, front-end posts 404”
Symptom: /wp-admin/ works, most public URLs fail.
Root cause: Server is configured to allow explicit PHP endpoints but not “pretty” paths; missing try_files or Apache rewrite rules.
Fix: Fix Nginx location / or Apache .htaccess support; retest with ?p=ID to isolate.
3) “Everything 404s after migration from Apache to Nginx”
Symptom: Site loads partially, but clean URLs fail.
Root cause: Expecting .htaccess to carry over. Nginx ignores it.
Fix: Add WordPress-friendly Nginx config with try_files $uri $uri/ /index.php?$args; and correct PHP location block.
4) “Fix worked for me but not for users”
Symptom: You get 200; others get 404.
Root cause: CDN caching a 404; split-brain between multiple origins; or old cache nodes still serving stale content.
Fix: Purge edge cache for the affected paths; verify origin directly (bypass CDN if possible); ensure all origins have identical config and rewrite rules.
5) “Switching permalinks to Plain fixes it”
Symptom: /?p=123 works; pretty URLs don’t.
Root cause: Rewrite not happening at the server layer.
Fix: Don’t leave it on Plain as a “solution.” Fix the rewrites and switch back, with redirects if you changed public URLs in the meantime.
6) “After changing permalink structure, traffic drops and Search Console lights up”
Symptom: Many 404 reports for old URLs.
Root cause: URL structure changed without redirects; or redirects exist but chain/loop; or canonical tags point wrong.
Fix: Implement 301 mapping old → new; remove chains; confirm canonical tags and sitemap reflect the new URLs; verify with log sampling.
Three corporate mini-stories (anonymized, technically real)
Story 1: The incident caused by a wrong assumption
A mid-sized company migrated a WordPress marketing site from a legacy VM running Apache to a managed container platform fronted by Nginx. The migration checklist included database export/import, media sync, and “make sure wp-admin works.” It did. The team called it done.
Monday morning: sales reports “every blog post is broken.” The homepage loaded, category pages were inconsistent, and individual posts returned 404. The on-call engineer did the classic move: flushed permalinks in the admin UI. No change. Then flushed again. Still no change. Panic begins.
The wrong assumption was subtle and common: they assumed WordPress “owns permalinks.” In Apache-land, WordPress appears to own permalinks because it can write .htaccess. In Nginx-land, WordPress owns nothing unless the server is configured to hand unknown paths to index.php.
The fix was a one-line try_files change in the correct server block, plus a reload. Posts immediately served 200. The postmortem action item was even more important: add a synthetic test for a known post URL (not the homepage) to deployment gates.
They didn’t need heroics. They needed a better definition of “works.”
Story 2: The optimization that backfired
An enterprise team had a WordPress site behind a CDN and a reverse proxy tier. Someone noticed a high cache miss rate and decided to “optimize” by caching 404 responses at the edge for a long TTL. The argument sounded rational: 404s are common from bots, and caching them reduces origin load.
Then a content editor published a major campaign post and shared it on social. Within minutes, the first wave of traffic hit a brief window where rewrites were broken on one origin node (a config deploy lag). The CDN cached the 404 for that URL. Not for seconds. For hours.
They fixed the origin quickly, but the internet kept seeing the cached 404. Support tickets rolled in. The marketing team believed “the fix didn’t work,” because from their perspective it hadn’t. The SRE on duty had to trace headers to realize the 404 was a distributed artifact, not a current origin response.
The lesson: caching 404s can be fine, but only with short TTLs and careful scope. If you cache negatives too aggressively, you’re essentially turning temporary routing hiccups into durable outages.
They ended up with a nuanced policy: short TTL for 404s, no caching for known content patterns like /%year%/%postname%/, and a purge-on-publish hook for high-value paths.
Story 3: The boring but correct practice that saved the day
A large org ran multiple WordPress properties and had been burned by permalink changes years prior. So they implemented a boring practice: every deployment included a small suite of curl-based synthetic checks from outside the network.
Not “is the site up.” Specific URLs: a known post, a known page, a known category, and one known legacy URL expected to 301 to a canonical destination. The checks ran against both the CDN endpoint and the origin endpoint.
During a routine update, a new Nginx config template accidentally removed the WordPress try_files line for one environment. The deployment succeeded, health checks stayed green (they only tested /), but the synthetic checks failed immediately on the known post URL. Rollback happened before anyone noticed publicly.
No drama. No emergency meeting. Just a failed gate and a quiet fix. That’s what “reliability” looks like when it’s working: it’s almost disappointingly uninteresting.
Checklists / step-by-step plan
Plan A: Restore working permalinks with minimal SEO risk (recommended for incidents)
- Identify a known-good post ID using WP-CLI (
wp post list). Write it down. - Test
?p=ID. If that returns 200, content is fine and rewrites are the problem. - Check permalink structure (
wp option get permalink_structure). Confirm it matches your historic URLs. - Flush rewrite rules once (
wp rewrite flush --hard). - Fix server routing:
- Nginx: confirm
try_files $uri $uri/ /index.php?$args; - Apache: confirm
mod_rewrite,AllowOverride, and WordPress block in.htaccess
- Nginx: confirm
- Verify a representative URL set (top 10 posts, plus 10 random older posts). Use curl headers, not just browser clicks.
- Purge CDN caches for affected paths once origin is correct.
- Monitor logs for 404 rate and redirect rate; ensure bots are seeing 200 for canonical URLs.
Plan B: You intentionally changed permalinks (migration/change request)
- Freeze the target permalink structure. Decide it once. Don’t “tune it” mid-migration.
- Export a URL map:
- Old URL patterns (previous permalink structure)
- New URL patterns (new permalink structure)
- Implement 301 redirects:
- Prefer pattern-based rules at Nginx/Apache where possible
- Use WordPress-level redirects only for exceptions
- Update internal links if needed (WordPress will usually generate new permalinks automatically; watch for hard-coded links in content).
- Validate canonical tags point to new URLs and that old URLs redirect to the same canonical destination.
- Validate at scale with sampling and logs; look for redirect chains and 404 clusters.
- Keep redirects for a long time. “Long time” means long enough for old links and bookmarks to die out. In corporate terms: longer than anyone wants, shorter than forever.
Plan C: Temporary containment when you can’t fix rewrites immediately
- Switch permalinks to Plain (
/?p=ID) only as a last resort and only temporarily. - Put a banner in incident notes: “Plain permalinks enabled; SEO risk; revert after rewrite fix.”
- Implement redirects from old pretty URLs to
?p=IDonly if you have a reliable mapping (often you don’t). Otherwise you’ll just create wrong redirects. - Fix rewrites properly, restore permalink structure, then remove containment hacks.
FAQ
1) Why do posts 404 but the homepage loads?
The homepage is often / which can resolve even when rewrites are broken. Posts rely on rewrites to route /year/postname/ to index.php.
2) Is “Settings → Permalinks → Save” safe?
Usually, yes—it flushes rewrite rules. But if your server ignores .htaccess (Apache overrides disabled) or you’re on Nginx, it won’t fix routing. Treat it as a diagnostic step, not a solution.
3) What’s the quickest test to confirm it’s a rewrite problem?
Request /?p=POST_ID. If that returns 200 but the pretty URL 404s, your rewrites are broken.
4) Should I switch to “Plain” permalinks to stop the outage?
Only as a temporary containment measure. It changes public URLs and can create a messy SEO situation unless you redirect correctly. Fix the rewrite plumbing instead.
5) How do I fix permalinks on Nginx?
Ensure the site’s location / includes try_files $uri $uri/ /index.php?$args; and PHP requests are passed to PHP-FPM. Then reload Nginx and retest.
6) How do I fix permalinks on Apache?
Ensure mod_rewrite is enabled, your vhost allows overrides (AllowOverride), and the WordPress rewrite block exists in .htaccess. Then flush rewrite rules once.
7) Do redirects hurt SEO?
Redirects are normal. Broken URLs are worse. Use 301 redirects for permanent moves, avoid chains, and make sure the final URL returns 200 with the expected content.
8) Why do I still see 404 after fixing the server config?
Most often: CDN cached the 404, browser cache, or you fixed one origin but not all of them. Check response headers for cache hits and compare origin vs CDN behavior.
9) Can a plugin cause post 404s?
Yes. SEO plugins, multilingual plugins, membership plugins, and custom post type plugins can register rewrite rules or redirects. Temporarily disable the suspected plugin, flush rewrites, and retest.
10) How do I avoid this in future deployments?
Add synthetic checks for a known post URL and a known legacy redirect. Gate deployments on those checks. Also keep web server config under version control and review rewrites like application code.
Conclusion: next steps you can ship today
If WordPress posts are 404ing, don’t start with superstition. Start with classification: server 404 vs WordPress 404 vs cached 404. Then prove content exists via ?p=ID. From there, fix the routing layer—Apache rewrite support or Nginx try_files—and flush rewrite rules once.
If permalinks changed intentionally or accidentally, treat it like a URL migration. Restore the old structure if possible. If not, implement clean one-hop 301 redirects and validate with sampling plus logs. Then purge caches so the fix actually reaches users.
Practical next steps:
- Pick one broken post URL and run the header checks (Task 1 + Task 13).
- Confirm the post exists and
?p=IDreturns 200 (Task 3 + Task 14). - Verify server routing (
try_fileson Nginx or.htaccess/AllowOverrideon Apache). - Flush rewrite rules once, retest, then purge CDN cache.
- Add a deployment gate: one known post URL must return 200, always.