SEO

Soft 404s for a Small Website - Make the Status Match the Page

Soft 404s for a Small Website - Make the Status Match the Page

A visitor requests a URL that has never existed. The page clearly says “not found,” offers a search box, and links back to useful sections. Yet the response line says 200 OK. To a person, the page looks like an error. To an HTTP client, the request succeeded. Which message should be trusted?

This mismatch is commonly called a soft 404 in Google Search documentation. It is not a hidden HTTP status code, and it is not proof that a site has been penalized. It is a diagnosis: the returned content appears to describe a missing resource, while the protocol response does not clearly say that the resource is missing.

For a small CMS, the repair is usually less about “doing SEO” and more about telling the truth consistently. The route, status code, visible page, redirect, sitemap, and internal links should describe the same outcome. That helps browsers, crawlers, monitoring tools, caches, and people make sense of the response.

A Soft 404 Is a Classification, Not a Protocol Feature

HTTP already has a status for a resource that cannot be found: 404 Not Found. RFC 9110 defines 404 without saying whether the absence is temporary or permanent. If the server knows that access is gone and the condition is likely permanent, the specification provides 410 Gone.

“Soft 404” belongs to a different layer. Google documents that a successful 2xx response can be reported as a soft 404 when its content looks like an error, is empty, or otherwise suggests that no useful page exists. The exact classification is Google's judgment about the response; another crawler may use different terminology or heuristics.

This distinction prevents two common misunderstandings. First, there is no soft-404 header to add. Second, returning 200 does not force a page into a search index. Google explicitly says that a 2xx response allows content to be processed but does not guarantee indexing. A success code is a statement about the HTTP request, not a request for ranking.

Choose the Response from What Happened

A status should follow the resource's real state, not whichever option appears most favorable in a dashboard. A small decision table is often enough:

  • The URL has no matching resource: return 404 and render a helpful not-found page.
  • The resource was deliberately removed and is unlikely to return: 410 can express that stronger fact. If the history is uncertain, 404 remains honest.
  • The resource moved and has a genuine replacement: redirect to that replacement. Use a permanent redirect only when the move is intended to persist.
  • The resource still exists but the server cannot provide it because of a temporary failure: use an appropriate 5xx response. Calling an outage “not found” hides an operational problem.
  • The resource exists and the response represents it: a 2xx status is appropriate, even if the page has little text.

The difference between 404 and 410 is meaningful HTTP vocabulary, but it should not be turned into a deindexing trick. Google's current crawler documentation says it treats ordinary 4xx responses other than 429 alike for its indexing pipeline. Choose 410 because permanent removal is known, not because it is expected to buy a particular timeline.

A Friendly Error Page May Still Be a Real Error

The body and the status answer different questions. The status tells software what happened. The HTML helps a person decide what to do next. A real 404 response can still use the normal site navigation, explain the problem in plain language, offer search, and suggest relevant sections.

Conversely, a large polished page does not make 200 correct. A CMS often produces soft 404s when its front controller loads the site shell first, fails to find the requested record, renders an error template, and never changes PHP's default success status. The result looks intentional in a browser because browsers render response bodies for 404s and 200s alike.

Redirecting every unknown URL to the home page creates a different mismatch. The home page is rarely a replacement for /articles/a-title-that-does-not-exist. The visitor loses the broken URL's context, and automated clients see many unrelated addresses collapse into one destination. Google's redirect guidance frames redirects around content that has actually moved or has a new location. A redirect should be a map to a relevant successor, not a way to avoid admitting that no successor exists.

Set the Status Where the Route Becomes Known

In a native-PHP application, the controller should decide that a resource is absent before it renders the shared error view. The PHP manual documents http_response_code() as the way to get or set the response status and notes that the web-server default is 200 when no code has been set.

$post = findPublishedPostBySlug($slug);

if ($post === null) {
    http_response_code(404);
    require __DIR__ . '/views/errors/404.php';
    exit;
}

renderPost($post);

The exit matters because the application should not continue into a success template after rendering the error. The status must also be set before output has committed the response headers. Frameworks may provide a response object or exception for the same purpose; the principle is more important than this particular PHP shape.

Route families deserve separate tests. A CMS may correctly reject an unknown English slug while its /id/ or /de/ fallback silently returns a translated home page with 200. Category pages, author archives, pagination beyond the last page, deleted media, and malformed identifiers can each take different controller paths. One correct global 404 template does not prove that every missing route reaches it.

Preserve the Status Through Nginx

A web server can replace an error body without changing the original status. The official Nginx error_page documentation shows the basic pattern:

error_page 404 /404.html;

location = /404.html {
    internal;
}

That internal error document can provide a consistent page while the client still receives 404. Nginx also supports explicit status replacement. For example, error_page 404 =200 /empty.gif; deliberately turns the response into 200. That capability has legitimate specialized uses, but applying the same pattern to missing HTML pages creates exactly the semantic mismatch under discussion.

Applications behind FastCGI or a reverse proxy need an end-to-end check. It is possible for PHP to choose one status and for a proxy, cache rule, custom error handler, or application fallback to replace it later. Reading only the template code cannot reveal the final response seen by a client.

Client-Side Routing Has a Harder Boundary

A single-page application may receive its HTML shell with 200 before JavaScript discovers that the requested object does not exist. Changing the text in the document does not rewrite the status of the network response that already completed. This is why a client-rendered “not found” view can remain a soft 404.

Google's JavaScript troubleshooting guide describes redirecting to a URL that returns 404 or adding noindex when an SPA discovers missing content. For a public content site, resolving known routes on the server and returning the correct status in the initial response is usually the clearer design. A noindex directive can affect indexing, but it does not turn a successful HTTP response into a 404 and should not be presented as equivalent protocol semantics.

Test the Unknown, Not Only the Designed Page

Opening /404 in a browser may only test a demonstration route. A stronger check requests an arbitrary path that cannot have been deliberately configured:

curl -sS -o /dev/null -w '%{http_code}\n' \
  https://example.com/definitely-missing-7f3a9c/

curl -sS -I https://example.com/definitely-missing-7f3a9c/

The first command exposes the final status after redirects. The second makes the response headers easy to inspect, although a robust test should also perform a normal GET because some applications handle HEAD differently. Use a fresh random suffix to avoid testing a cached or specially handled URL.

A practical audit samples outcomes rather than chasing a perfect count:

  1. Request unknown article, category, author, locale, pagination, and asset URLs.
  2. Confirm each missing route returns 404 or a deliberately justified alternative.
  3. Check that the rendered body still explains the error and offers useful navigation.
  4. Test a moved page and verify that its redirect ends at the specific replacement, not a generic home page.
  5. Remove obsolete internal links and sitemap entries; a correct 404 does not make a broken internal link desirable.
  6. Test through the public proxy or CDN as well as the origin, because either layer can change a response.

For Google-specific reports, Search Console recommends inspecting examples and using live URL inspection. The report is diagnostic evidence, not a score that must reach zero. Random mistyped or external URLs can legitimately return 404. The useful question is whether an important existing page was misclassified, or whether the site's own routing returns a success response for resources it does not have.

Let the Status and the Page Tell the Same Story

A soft 404 is usually a symptom of an unclear boundary: the application knows that no record exists, but that knowledge never reaches the HTTP response. The durable repair is to make the decision explicit. Return 404 for an unknown resource, 410 only for known permanent removal, a redirect for a real successor, and 5xx when an existing service has failed.

None of this guarantees indexing or ranking, and not every 404 needs to be “fixed.” Some URLs simply do not exist. The goal is smaller and more defensible: people should receive a useful explanation, while software receives a status that describes the same reality.

References