The dev server now resolves /a/b to a/b.html, so links written without the extension stop 404ing locally while working fine in production. The 404 page also loads its own styles at any depth, and serve.base "/" no longer 404s every request.
The dev server resolves extensionless URLs. GitHub Pages serves /a/b
from a/b.html without touching the URL. The local server didn't, so a link
written as /changelog/v1.9.1 worked in production and 404'd on
localhost:4040 — the one place you'd have caught it. Both agree now:
/a/b → a/b.html 200, URL stays /a/b
/a/b → a/b/index.html 301 to /a/b/, then the index
The directory redirect was already there; the file fallback is the new part,
and it only fires when neither a file nor a directory matches. Relative
assets on those pages need nothing special — an extensionless URL sits at
the same depth as the file behind it, so ../css/styles.min.css resolves
the same for /changelog/v1.9.1 and /changelog/v1.9.1.html.
404.html loads its assets at any depth. The 404 page is the one file
served from a path it doesn't live at: it sits at your site root but answers
for /a/b/c/anything. Its relative asset paths — ./css/styles.min.css —
then resolved against /a/b/c/, so a miss at the root rendered fine and a
miss two levels down rendered unstyled. The server now pins them:
<head>
<base href="/" />
<meta charset="utf-8" />
</head>
Injected only when the page doesn't already declare its own <base>, and
only in the response — your built 404.html is untouched on disk, which
matters when you publish under a project path like /poops/.
serve.base: "/" no longer 404s the whole site. The server keeps every
request inside its base directory by resolving the path and checking it
still starts with that base. A base of / joins to <cwd>/ — with the
trailing separator — so the check compared against <cwd>// and nothing
ever matched. Every URL, including /, came back 404. The base is now
normalized before anything is joined to it, so a trailing separator means
what you'd expect. Traversal attempts are still rejected the same way.