Search Console flags pages as “Soft 404” when “the page looks like a 404 but the server returned 200.” Google fetched the page, found nothing (or only a “Not found” / “No content” message), but got HTTP 200 OK. Its response: treat as 404 (don’t index), and flag explicitly so you can fix.
This isn’t a content-quality verdict — it’s a site engineering problem. The server should be returning 404 / 410, not 200.
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 Soft 404
2. Dynamic page still 200s when content 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. Should be res.status(404).render('not-found').
3. SPA 404 route returns index.html + 200
If you deploy to Firebase / Netlify with 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 HTTP is 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
# < 500 chars is typically a Soft 404 candidate
6. JS render fails but HTTP is 200
If your site relies on SSR but the SSR errors and falls back to empty HTML, Google sees a blank page.
Shortest path to fix
Step 1: Export the Soft 404 list and categorize
Search Console → Pages → click “Soft 404” → export 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(); // returns 404
return <Article {...article} />;
}
// Astro
---
const article = await getArticle(Astro.params.slug);
if (!article) return new Response(null, { status: 404 });
---
Step 3: Permanently deleted URLs → 410 Gone (more explicit)
If a URL is permanently removed rather than “temporarily missing,” 410 is better:
res.status(410).send('This page has been permanently removed.');
Google removes 410s from the index faster than 404s.
Step 4: Empty tag / 0-result search → noindex or 410
// 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.
Step 5: SPA deployment — fix at the dispatcher layer
If using Firebase Hosting / Netlify / Vercel SPA fallback:
// firebase.json
{
"hosting": {
"rewrites": [
{ "source": "/article/**", "function": "ssrArticle" },
{ "source": "**", "destination": "/index.html" }
]
}
}
Route /article/** (where real 404 is needed) to a server function that can return 404. Keep the SPA 200 fallback for genuine client-side routes.
(For pure CSR sites you can’t change HTTP status from the client. SSR is the only fix.)
Step 6: 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
In Search Console → Pages → Soft 404 row → “Validate fix.” Google re-evaluates in 7-21 days.
Prevention
- Any “resource not found” code path must
res.status(404)— never default to 200 - Use 410 for permanent deletions (faster removal from index)
- Empty category / tag / search pages are either noindex or 404 — never 200 empty shells
- SPA deployments must distinguish frontend 404 from server 404; the latter requires an SSR function returning real 404
- CI smoke test: hit a randomly nonexistent URL, assert 404 not 200
Related
Tags: #SEO #Google #Search Console #Indexing