Skip to content

fix(404): align default not-found copy with Next.js#1567

Open
james-elicx wants to merge 2 commits into
mainfrom
fix/default-404-copy
Open

fix(404): align default not-found copy with Next.js#1567
james-elicx wants to merge 2 commits into
mainfrom
fix/default-404-copy

Conversation

@james-elicx
Copy link
Copy Markdown
Member

Closes #1454

Summary

  • Pages Router and App Router default 404 now render This page could not be found. to match Next.js v16.
  • Pages Router: new buildDefaultPagesNotFoundResponse() helper renders the canonical Next.js _error.tsx HTML body (status + message + dark-mode theme CSS) instead of the previous <h1>404 - Page not found</h1> placeholder. Wired into the production worker entry, the dev-server fallback, and pages-page-data so all Pages Router 404 paths agree on the same body.
  • App Router: new DEFAULT_NOT_FOUND_MODULE mirrors Next.js's packaged not-found.tsx (HTTPAccessErrorFallback with status=404 / message="This page could not be found."). Threaded through app-fallback-renderer as the effectiveRootNotFoundModule fallback (same pattern as the existing effectiveGlobalErrorModule) so route-miss 404s render a real HTML page even when the app does not define app/not-found.tsx.

Test plan

  • Added tests/pages-default-404.test.ts asserting the canonical body, content-type, and removal of the old placeholder.
  • Added regression tests in tests/app-fallback-renderer.test.ts for both the new default and the user-override precedence.
  • Updated tests/pages-page-data.test.ts 404 assertion to the canonical copy.
  • pnpm run check passes (format + lint + types).
  • pnpm test tests/pages-default-404.test.ts tests/pages-page-data.test.ts tests/app-fallback-renderer.test.ts tests/http-error-responses.test.ts tests/nextjs-compat/not-found.test.ts tests/app-rsc-handler.test.ts tests/app-router.test.ts tests/app-page-request.test.ts tests/routing.test.ts tests/pages-router.test.ts passes.

Notes

  • The plain-text notFoundResponse() helper in http-error-responses.ts is intentionally unchanged: it mirrors Next.js's res.end('This page could not be found') plain-text fallback (no period) used by API-route and minimal-mode paths. Only the HTML-rendered defaults are touched.
  • Scope kept narrow per the issue: did not redesign error UI, did not touch basePath handling.

The Pages Router previously emitted `<h1>404 - Page not found</h1>` and the
App Router emitted plain-text `This page could not be found` (no period) when
no user-defined 404 boundary was present. Next.js renders
`This page could not be found.` (with trailing period) for both routers.

Closes #1454.
@james-elicx
Copy link
Copy Markdown
Member Author

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good change — the scope is well-chosen, the test coverage is solid, and the new code follows established patterns (mirrors the DEFAULT_GLOBAL_ERROR_MODULE approach cleanly). Two real issues to fix, both minor.

tl;dr: Fix charSetcharset in the raw HTML template and drop the stray line-height:49px on the Pages Router h1 to match Next.js's _error.tsx exactly. Everything else looks correct.


const CSS = `body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}`;

const HTML = `<!DOCTYPE html><html><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width"/><title>${STATUS}: ${MESSAGE}</title><meta name="next-head-count" content="2"/><style data-next-hide-fouc="true">body{display:none}</style><noscript data-next-hide-fouc="true"><style>body{display:block}</style></noscript></head><body><div id="__next"><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div style="line-height:48px"><style>${CSS}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding-right:23px;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">${STATUS}</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:28px">${MESSAGE}</h2></div></div></div></div></body></html>`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: charSet should be charset in raw HTML.

charSet is the React JSX prop name (camelCase), which React transforms to charset when emitting HTML. But this is a hand-authored HTML string literal, not JSX — the browser receives it as-is. The HTML spec attribute is charset (all lowercase).

Browsers are forgiving here (they handle it case-insensitively), so this won't cause user-visible breakage, but it's technically malformed HTML. Since the goal is to match Next.js's _error.tsx output exactly, it should be charset.


Bug: stray line-height:49px on h1 — not present in Next.js's Pages Router _error.tsx.

Next.js's pages/_error.tsx does NOT set lineHeight on the h1 style object — the h1 inherits line-height: 48px from the parent desc div. The 49px value comes from the App Router access-error-styles.ts, not the Pages Router.

For reference, Next.js's _error.tsx styles.h1 is:

h1: {
  display: 'inline-block',
  margin: '0 20px 0 0',
  paddingRight: 23,
  fontSize: 24,
  fontWeight: 500,
  verticalAlign: 'top',
  // NO lineHeight here
}

Fix both on this line: charSetcharset, and remove line-height:49px from the h1 inline style.

// When the app does not define `app/not-found.tsx` (and has not opted into
// `app/global-not-found.tsx`), fall back to vinext's built-in default
// not-found component so route-miss 404s render the canonical Next.js
// markup (status + "This page could not be found." message). Matches the
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean. Mirrors the effectiveGlobalErrorModule pattern perfectly. The as unknown as TModule cast is unavoidable given the generic signature — same trade-off as the global error fallback.

}
}

// No custom error page found — use plain text fallback
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (no action needed): The Response → Node.js ServerResponse bridge here works but is a little ceremonial. If you ever touch this again, buildDefaultPagesNotFoundResponse() could also expose the raw HTML + status directly (or the dev server could use DEFAULT_PAGES_NOT_FOUND_HTML with a manual writeHead). Not worth changing now — just noting it.

lineHeight: "49px",
},
h2: {
fontSize: 14,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

App Router component looks correct — styles match Next.js's access-error-styles.ts (h1.lineHeight: "49px", h2.lineHeight: "49px", padding: "0 23px 0 0"). Note this intentionally differs from the Pages Router template (which should use 48px / no h1 lineHeight per _error.tsx). Two different Next.js upstream sources, two different vinext implementations — correct.

});

// Mirrors Next.js 16 experimental.globalNotFound behavior.
// Ported from Next.js: test/e2e/app-dir/global-not-found/{basic,both-present,not-present}.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice behavioral change. This test previously asserted null (renderer gave up), and now it correctly asserts the built-in default renders. Callers of renderNotFound no longer need their own fallback path for the "no not-found module" case — a simplification.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 22, 2026

Review posted successfully. Here's a summary of the review:

PR #1567 Review Summary

The PR cleanly aligns vinext's default 404 copy with Next.js for both routers. Two issues found:

  1. charSet vs charset in raw HTML (pages-default-404.ts:26): The HTML template uses charSet (React's JSX prop name) in a raw HTML string literal. Since this isn't JSX, it should be charset (the actual HTML attribute). Browsers handle it fine, but it's technically malformed.

  2. Stray line-height:49px on the Pages Router h1 (pages-default-404.ts:26): Next.js's pages/_error.tsx does NOT set lineHeight on the h1 — it inherits 48px from the parent div. The 49px value was accidentally pulled from the App Router's access-error-styles.ts. Should be removed.

Everything else is solid — the App Router component correctly matches HTTPAccessErrorFallback, the effectiveRootNotFoundModule pattern mirrors the existing effectiveGlobalErrorModule approach, tests cover both the default and user-override cases, and the behavioral change in the "global-not-found absent" test (now renders the built-in default instead of returning null) is a nice simplification.

github run

- Use HTML attribute name 'charset' (not the JSX 'charSet') in the raw HTML template.
- Drop stray 'line-height:49px' on the Pages Router h1 — Next.js's _error.tsx inherits 48px from the parent div.
@james-elicx
Copy link
Copy Markdown
Member Author

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both issues from the previous review are fixed in the follow-up commit (9adff76):

  1. charSetcharset: pages-default-404.ts:26 now has <meta charset="utf-8"/> — correct HTML attribute name.
  2. Stray line-height:49px removed from Pages Router h1: The h1 inline style no longer includes line-height. The App Router component (default-not-found.tsx:39) correctly retains lineHeight: "49px" since that matches the upstream access-error-styles.ts.

LGTM — clean fix, good test coverage, review feedback addressed.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 22, 2026

Review posted. Both issues from the previous round are fixed:

  1. charSetcharset in the raw HTML template at pages-default-404.ts:26
  2. Stray line-height:49px removed from the Pages Router h1 style (App Router correctly keeps 49px)

Approved the PR.

github run

@james-elicx james-elicx marked this pull request as ready for review May 22, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Default 404 page copy differs from Next.js (This page could not be found.)

1 participant