slop audit
Blog post 33 / Technical SEO

Soft 404: how to find and fix it before launch

Learn how to find and fix a soft 404, return the right HTTP status, test custom 404 pages, and prevent crawl and monitoring errors before launch.

By Slop Audit · 9 min read

A soft 404 happens when a URL looks missing or empty to a visitor but the server returns a success status such as 200 OK. Fix the mismatch at the routing or hosting layer, keep the error page useful, and test the deployed response—not only the screen you see in a browser.

This guide shows developers how to choose the right response, catch SPA fallback mistakes, test a custom 404 page with curl and Playwright, and verify the result before launch.

Soft 404 checklist

Use this sequence when a missing URL behaves incorrectly or Google Search Console reports a soft 404 error:

  1. Request a unique URL that has never existed on the site.
  2. Record the final URL, redirect chain, HTTP status, content type, and response body.
  3. Decide whether the requested content is missing, permanently gone, moved, or still valid.
  4. Return 404 for a missing resource, 410 for an intentionally permanent removal, or 301 only when a relevant replacement exists.
  5. Serve a useful HTML error page without changing the error status to 200.
  6. Make sure SPA and static-host fallbacks do not treat every unknown path as a valid app route.
  7. Test both a top-level missing path and a missing dynamic route.
  8. Keep the check in CI and repeat it against the deployed environment.
  9. Inspect affected URLs in Search Console after the production fix is live.

The important evidence is the response and the page together. A large “Page not found” heading does not make a response an HTTP 404.

What a soft 404 is

Google defines a soft 404 as a URL that returns content suggesting the page does not exist while also returning a success status, commonly 200. Google may also classify nearly empty or error-like pages this way. Its crawl-error documentation recommends returning 404 or 410 when content is gone and has no suitable replacement.

Compare these responses:

HTTP/2 200 OK
content-type: text/html

<h1>Page not found</h1>
HTTP/2 404 Not Found
content-type: text/html

<h1>Page not found</h1>

The bodies can be identical. The first response says the request succeeded; the second says the resource was not found. MDN's 404 Not Found reference describes 404 as the status for a resource the server cannot find.

A normal 404 is not automatically an SEO defect. Missing URLs are expected when visitors mistype a path or when external sites link to an old address. The defect is returning an inaccurate response, leaving internal links broken, or sending every old URL to an unrelated destination.

Use the broken-link testing guide to find links that point to missing resources. Use this guide to verify how the server handles a URL that truly does not exist.

Choose 404, 410, 301, or 200

Do not choose a status just to make a report disappear. Choose it from the state of the resource:

  • Return 404 Not Found when the resource does not exist and you do not know that the absence is permanent.
  • Return 410 Gone when the resource was deliberately removed and the removal is expected to be permanent. MDN's 410 Gone reference recommends 404 when the server does not know whether the condition is permanent.
  • Return 301 Moved Permanently when there is a genuine replacement that satisfies the old URL's purpose.
  • Return 200 OK when the requested page exists and contains the useful content the URL promises.

Avoid redirecting every unknown URL to the homepage. A visitor following a link to a removed API guide did not ask for the marketing homepage, and Google warns that redirects to irrelevant destinations can be treated as soft 404s. The canonical URL and redirects guide covers URL moves and destination alignment in more detail.

If a valid page is being classified as a soft 404, do not force it to return 404. Check whether the main content failed to load, a server dependency returned an error, critical resources were blocked, or the rendered result is nearly empty. Repair the valid page and keep its success status.

Return a real 404 with useful HTML

A custom 404 page and an HTTP 404 status are compatible. The router should set the status, while the page body should help the visitor recover.

A minimal server fallback might look like this:

app.use((_request, response) => {
  response
    .status(404)
    .type("html")
    .send(`<!doctype html>
      <html lang="en">
        <head><title>Page not found</title></head>
        <body>
          <main>
            <h1>Page not found</h1>
            <p>The requested page does not exist.</p>
            <a href="/">Return to the homepage</a>
          </main>
        </body>
      </html>`);
});

Put the catch-all after valid routes. For a dynamic route such as /docs/:slug, return the same error response when the record lookup has no result. Do not render the normal document template with empty fields and a success status.

A useful custom 404 page should:

  • say clearly that the page was not found;
  • keep the site name and essential navigation recognizable;
  • provide a route back to the homepage or a relevant section;
  • avoid pretending that unrelated content is the requested resource; and
  • remain small enough to work when other parts of the application are failing.

The status is machine-readable evidence. The recovery links are for people. Test both.

Prevent soft 404 errors in single-page apps

Single-page applications often configure a host to rewrite every unknown path to index.html. That is useful for real client-side routes, but an unrestricted rewrite can also return the app shell with 200 OK for any invented URL.

Separate known application routes from missing ones at the server, edge, or hosting layer. When a dynamic record does not exist, let the route produce a not-found response instead of showing an empty component inside a successful shell.

Google's JavaScript SEO guidance describes two fallbacks when client-side routing makes status handling difficult: redirect to a server URL that returns 404, or add a noindex robots directive to the client-rendered error page. The second option may prevent indexing, but it does not turn the original response into a real HTTP 404. Prefer accurate server or edge behavior when the platform supports it.

Framework conventions also need deployed testing. A component named NotFound does not prove which status reached the network. Rewrites, streaming behavior, CDN rules, and platform adapters can change the final response.

Test the soft 404 response in production

Create a path that is unique enough to avoid colliding with a future route. Use GET, because a server or CDN can handle HEAD differently:

url="https://example.com/.well-known/release-404-probe-7f31c9"

curl -sS -D - -o /tmp/release-404.html "$url"
head -n 12 /tmp/release-404.html

Confirm all of the following:

  • the final response is 404, without a redirect to the homepage;
  • Content-Type identifies HTML;
  • the body is non-empty and explains what happened;
  • the page offers a usable recovery path;
  • the missing URL is not included in navigation or the XML sitemap; and
  • the same result appears at the production hostname and any deployed preview used for release review.

Then test representative route families. A root fallback may work while /products/missing-item or /docs/missing-slug still returns an empty 200 template. If old URLs moved, test their specific redirects separately from the generic missing-path behavior.

The website launch checklist helps keep this response check beside HTTPS, crawl controls, metadata, forms, and monitoring instead of treating it as an isolated SEO task.

Add soft 404 testing to Playwright

Playwright's page.goto() documentation notes that navigation does not throw merely because the server returns 404; the returned response exposes the status. Assert the network response and visible recovery UI in the same test:

import { expect, test } from "@playwright/test";

test("unknown routes return useful HTML with status 404", async ({ page }) => {
  const response = await page.goto(
    "https://preview.example.com/.well-known/release-404-probe-7f31c9",
  );

  expect(response).not.toBeNull();
  expect(response?.status()).toBe(404);
  expect(response?.headers()["content-type"]).toContain("text/html");
  await expect(
    page.getByRole("heading", { name: /page not found/i }),
  ).toBeVisible();
  await expect(
    page.getByRole("link", { name: /home/i }),
  ).toHaveAttribute("href", "/");
});

Use a preview URL created from the change under review, then repeat the check against production after release. Keep the probe path stable in a single environment but unlikely to become a real product route.

Add the test to the same release gate as the routes most likely to regress. The website audit with GitHub Actions guide shows how to run public-URL checks without mixing them with source-only tests, and the website regression testing checklist covers baseline and monitoring decisions.

Check soft 404 behavior with Slop Audit

Run a Slop Audit on the deployed URL after the missing-route response is live. Slop Audit requests a safe same-origin path at /.well-known/slop-detector-404-probe. Its custom 404 behavior check passes when that path returns HTTP 404, declares an HTML content type, and has a non-empty response body. The report retains the probed URL and observed status as evidence.

This is a bounded launch check. It does not reproduce Google's soft 404 classification, inspect every dynamic route, judge how helpful the error copy is, validate 410 decisions, find every broken link, or prove that Search Console has processed a fix. Pair the scan with route-specific tests and review affected URLs in Search Console.

Soft 404 release sign-off

  • A unique missing path returns 404, not 200 or a homepage redirect.
  • The response contains non-empty HTML and a clear not-found message.
  • Visitors can recover through a relevant navigation link.
  • Missing dynamic records use the same accurate response behavior.
  • Deliberately removed content uses 410 only when permanence is known.
  • Moved content redirects only to a relevant replacement.
  • SPA and hosting fallbacks distinguish known routes from invented paths.
  • CI checks the response status, content type, and visible error state.
  • The deployed URL is tested again after routing, CDN, or framework changes.

A soft 404 is easy to miss because the page can look correct. Treat the HTTP status and the rendered recovery page as one release contract, and keep evidence for both.