TL;DR: Search Console marks a URL “Soft 404” when the page looks like a “not found” page but the server returned HTTP 200 OK. Google fetched it, found no real content (or a “Not found” / “No results” message), and decided to treat it as a 404 and not index it. The fastest fix: for URLs that genuinely no longer exist, make the server return 404 (or 410) instead of 200; for real pages that only look empty to Googlebot, use URL Inspection → Test Live URL to see the rendered HTML and fix whatever is missing.
This is almost never a content-quality penalty. It is a status-code or rendering problem — the page either shouldn’t return 200, or it returns 200 but renders blank for Googlebot.
First: which bucket are you in?
The fix depends entirely on whether the URL should exist. Run URL Inspection on one flagged URL, click Test Live URL, then open View Tested Page and decide:
| What you see | Bucket | Fix |
|---|---|---|
| URL is for content that’s deleted / never existed | Should be a real 404 | Return 404 or 410 (Steps 2-3) |
| Empty tag / category / 0-result search page | Thin index page | noindex or 404 (Step 4) |
| Rendered HTML shows your real content fine | False positive on a thin page | Add substantive content, then validate |
| Rendered HTML / screenshot is blank or shows an error | JS render failure | Fix the render (Step 5) |
Every random URL returns the homepage or index.html | SPA / redirect misconfig | Fix the dispatcher (Step 6) |
The View Tested Page panel has three tabs — HTML, Screenshot, and More Info. The Screenshot tab shows what Googlebot actually rendered; More Info → JavaScript console messages lists every console error Googlebot hit during render. A single uncaught exception during hydration can blank out your main content. (Note: a live test does not update the index — it is a preview of what Google would see right now.)
Common causes
1. Empty category / tag / search-result pages return 200
The highest-frequency case. /tag/unused-tag/ has no articles but the template still renders header, footer, and “no content yet” — HTTP 200, body nearly empty.
How to confirm:
curl -sI "https://yourdomain.com/tag/empty-tag/" | head -1
# Want 404; if HTTP/2 200, that's a Soft 404 candidate
2. Dynamic page still 200s when content is missing
// Wrong
app.get('/article/:slug', async (req, res) => {
const article = await db.findOne({ slug: req.params.slug });
if (!article) {
res.render('not-found', { message: 'Article not found' }); // 200
return;
}
res.render('article', { article });
});
res.render('not-found') defaults to 200. It should be res.status(404).render('not-found').
3. SPA 404 route returns index.html + 200
If you deploy to Firebase / Netlify / Vercel with an SPA fallback:
{
"rewrites": [
{ "source": "**", "destination": "/index.html" }
]
}
Every URL returns index.html + 200, including completely nonexistent ones. The frontend router then shows “404 Not Found” — but the HTTP status is still 200.
4. Redirect to homepage instead of 404
// Wrong
app.get('*', (req, res) => res.redirect('/'));
Nonexistent URLs shouldn’t silently redirect home. Google sees a pile of “different URLs all serving the homepage” and flags Soft 404 or Duplicate.
5. Only navigation / footer / ads, no main content
The page HTML loads, the body has header, nav, sidebar, ads, footer — but <main> has only “loading” or is empty.
# Count chars inside main
curl -sL https://yourdomain.com/page | grep -oE '<main[^>]*>[\s\S]*</main>' | wc -c
# Under 500 chars is typically a Soft 404 candidate
6. JS renders fine in your browser but blank for Googlebot
If your site relies on client-side rendering or SSR, and the render errors out (or a critical script fails to load), Googlebot sees a blank page even though it loads fine for you. This is the bucket that confuses people most: the URL works in Chrome, but the Test Live URL screenshot is empty. Check More Info → JavaScript console messages for the failing script.
Shortest path to fix
Step 1: Export the Soft 404 list and categorize
Search Console → Indexing → Pages → under “Why pages aren’t indexed” click Soft 404 → export the URL list. Group by pattern:
/tag/* → empty tag pages
/search?q=* → 0-result searches
/article/* → content deleted but URL remains
/products/* → out-of-stock products
Each group needs a different fix.
Step 2: Real 404 for URLs that should 404
// Express / Node
app.get('/article/:slug', async (req, res) => {
const article = await db.findOne({ slug: req.params.slug });
if (!article) {
return res.status(404).render('not-found', { message: 'Article not found' });
}
res.render('article', { article });
});
// Next.js App Router
import { notFound } from 'next/navigation';
export default async function ArticlePage({ params }) {
const article = await getArticle(params.slug);
if (!article) notFound(); // serves the not-found UI with a 404 status
return <Article {...article} />;
}
// Astro
---
const article = await getArticle(Astro.params.slug);
if (!article) return new Response(null, { status: 404 });
---
Give the 404 page a little real content (a search box, links to popular pages). A bare “Not found” with a 200 status is the textbook soft 404; a helpful 404 page with the correct status is fine.
Step 3: Permanently deleted URLs → 410 Gone (clearer intent)
If a URL is permanently removed rather than “temporarily missing,” return 410:
res.status(410).send('This page has been permanently removed.');
A note on the common “410 de-indexes faster than 404” claim: Google’s own documentation says crawlers treat 404 and 410 the same — both tell the indexing pipeline the content is gone and both get removed from the index, with crawl frequency tapering off either way. So 410 won’t reliably speed anything up; its real value is communicating intent explicitly (“gone for good,” not “try again later”). Either status resolves the soft 404 — the important thing is that you stop returning 200.
Step 4: Empty tag / 0-result search → noindex or 404
// Empty tag page: noindex + remove from sitemap
export default function TagPage({ posts }) {
if (posts.length === 0) {
return (
<>
<Helmet><meta name="robots" content="noindex,follow" /></Helmet>
<main>No articles in this tag yet</main>
</>
);
}
// ...
}
Or more aggressive: just 404 it — empty tag pages aren’t useful to users either. Do not block these in robots.txt if you also want them de-indexed: a blocked URL can’t be crawled, so Google may never see the noindex tag. Let Google crawl them and read the noindex.
Step 5: Fix the render for the JS-blank bucket
If Test Live URL shows a blank screenshot for a page that should have content:
- Open More Info → JavaScript console messages and fix the first error in the list (often a failed hydration or a 4xx/5xx on a critical bundle).
- Make sure no critical JS/CSS file is blocked in
robots.txt— Googlebot needs to fetch them to render. - For content that must be indexed, prefer SSR / static rendering over pure client-side rendering so the HTML response already contains the content (you can confirm with
curl -sL <url> | grepfor a known phrase). - After fixing, re-run Test Live URL and confirm the rendered HTML now contains your content.
Step 6: SPA deployment — fix at the dispatcher layer
If using a Firebase Hosting / Netlify / Vercel SPA fallback:
// firebase.json
{
"hosting": {
"rewrites": [
{ "source": "/article/**", "function": "ssrArticle" },
{ "source": "**", "destination": "/index.html" }
]
}
}
Route /article/** (where a real 404 is needed) to a server function that can return 404. Keep the SPA 200 fallback for genuine client-side routes only.
For a pure client-side-rendered site you cannot change the HTTP status from the client — the server already sent 200 before your router runs. Server-side rendering (an SSR function or prerendering) is the only way to emit a real 404.
Step 7: Verify with curl + Search Console revalidation
# A URL that should 404
curl -sI "https://yourdomain.com/article/already-deleted" | head -1
# Want: HTTP/2 404
# A URL that should 200
curl -sI "https://yourdomain.com/article/exists" | head -1
# Want: HTTP/2 200
Then in Search Console → Indexing → Pages → Soft 404 row → Validate Fix. This starts a tracking cycle (you’ll see the status move from Started → Passed or Failed under “See details”), and Google emails you when it completes. As of June 2026 a full revalidation typically takes about 1-3 weeks; you don’t need to keep clicking — clicking again only restarts the clock.
How to confirm it’s fixed
curl -sIthe formerly-flagged URL returns the status you intended (404/410for gone pages,200for real ones).- URL Inspection → Test Live URL on a real page shows your content in the rendered HTML and screenshot — no blank or error screen.
- The Soft 404 validation in Search Console reaches Passed, and the affected count drops on the Pages report over the following weeks.
Prevention
- Any “resource not found” code path must
res.status(404)(or410) — never default to200. - Give the 404 page real, helpful content (search box, popular links); a bare “Not found” with a 200 is the classic soft 404.
- Empty category / tag / search pages are either
noindexor404— never a200empty shell, and don’trobots.txt-block ones you want de-indexed. - SPA deployments must distinguish a frontend “404 screen” from a server 404; the latter requires an SSR function returning a real
404. - CI smoke test: hit a randomly nonexistent URL and assert the status is
404, not200.
FAQ
Is a Soft 404 hurting my rankings? Not directly — flagged URLs simply don’t get indexed, so they can’t rank. It’s a signal that those URLs are wasting crawl budget and may indicate a wider status-code bug. Fix the status code and the URLs either get indexed (if real) or cleanly removed (if gone).
My page loads fine in the browser — why does Google call it a Soft 404? Googlebot renders with its own headless Chromium and is stricter about errors. Run URL Inspection → Test Live URL and look at the screenshot: if it’s blank, a script failed during render. Check More Info → JavaScript console messages for the cause. Browser-vs-Googlebot mismatches almost always come down to a render error or a blocked resource.
Should I use 404 or 410 for a deleted page?
Either resolves the soft 404. Google treats them the same in the index, so use 410 when you want to clearly state the page is gone for good, and 404 for everything else. The key is that you stop returning 200.
How long does “Validate Fix” take? A tracking cycle, typically about 1-3 weeks as of June 2026. Google emails you when it finishes. Clicking Validate Fix again before it completes just restarts the cycle — let it run.
Can a client-side-rendered (CSR) site return a real 404?
No. The server returns 200 before your JavaScript router runs, so the status is already set. You need SSR, prerendering, or a server/edge function that can return 404/410 for the not-found paths.
Related
External references: Google: HTTP status codes and how Google handles them, Google: Page Indexing report.
Tags: #SEO #Google #Search Console #Indexing