Soft 404 — Page Looks Empty to Google

Google flagged your page as "Soft 404" — content too thin to be a real page.

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

Tags: #SEO #Google #Search Console #Indexing