Skip to content

/oil-field returns HTTP 404 while rendering correctly, so crawlers see the advertised URL as broken #76

Description

@wormeyman

https://oilfieldplanner.factorygamefan.com/oil-field renders the app perfectly in a browser and returns HTTP 404. That is the URL README advertises (since #75) and the only real content route the app has.

Measured 2026-08-10:

Path Status Renders
/ 200 app (redirects to /oil-field)
/oil-field 404 app - <title>Factorio Tools</title>
/definitely-not-a-route 404 the NotFound component - correct

The mechanism

It is deliberate, and inherited rather than accidental. src/vue/public/_worker.js falls back to the SPA shell for any path with no matching asset, and hard-codes the status:

const response = await env.ASSETS.fetch(request);
if (response.status === 404) {
  const shell = await env.ASSETS.fetch(new URL("/404.html", url.origin));
  return new Response(shell.body, { status: 404, headers: shell.headers });
}

The comment above it says it is "mirroring the pre-Worker behavior where Pages served 404.html" - which it does faithfully. 404.html is itself just a copy of index.html, made by the build script:

"build": "... && vite build && cd dist && shx cp index.html 404.html"

So the status was 404 before the Worker existed too. The Worker preserved a pre-existing quirk rather than introducing one.

Why it matters

Nothing is broken for a human with a browser. It matters for everything that reads the status line:

  • Link-unfurl scrapers routinely skip 404s. There is an open TODO to add a fuller Open Graph / Twitter tag set for proper unfurls. Those tags may do nothing on the deep link while it returns 404, so this plausibly blocks that work rather than being independent of it.
  • Crawlers treat it as a dead page, so the one content URL of the site is the one they will not index.
  • Link checkers report the README link as broken, which is a slow-burn source of "is the site down?" confusion.

What is already correct - do not "fix" this part

/definitely-not-a-route returning 404 is right, and a naive fix breaks it. The router has a catch-all:

{ path: `${__BASE_PATH__}:catchAll(.*)`, component: NotFound }

So the SPA renders a real NotFound page for unknown paths. The usual Cloudflare Pages remedy - a _redirects file with /* /index.html 200 - would turn every unknown URL into a soft 404: 200 status on a "not found" page, which is worse for SEO than the current state, not better.

Two further traps:

  1. _redirects cannot simply be added alongside the Worker. A root _worker.js puts Pages in advanced mode and takes precedence, so the file would be inert. The fix has to live in the Worker.
  2. The Worker also serves the factoriotools-5jg.pages.dev -> custom domain 301. Whatever changes must leave that intact.

Suggested fix

Return 200 from the Worker only for paths that are genuine client-side routes, and keep 404 for everything else. Today that is exactly one path (/oil-field); / already resolves to a real asset and returns 200.

Keeping the known-route list in sync with main.ts by hand is the obvious weakness of this approach - worth a comment in both places saying so, or generating it at build time from the same source.

Verification

Now cheap to check before merge, because #70 made the deploy workflow run on pull requests: add a status assertion next to the existing "Check the WASM bundle landed in the right shape" step, or check the deployed preview. The assertion worth making is both halves - /oil-field is 200 and an unknown path is still 404 - since the failure mode of a careless fix is the soft 404 above.

Done when

/oil-field returns 200 with the app, an unknown path still returns 404 with the NotFound page, and the pages.dev 301 still works.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions