@k8ordo/static

Errors & redirects

error.tsx for when something throws, not-found.tsx for when nothing matched, and redirect.ts for a URL that moved. In this mode a Server Component that throws during the build stops it, the not-found page becomes 404.html, and a redirect is written as a page that sends the visitor on.

error.tsx

An error.tsx beside a layout.tsx (or a page.tsx) is what shows in place of what is below it when that throws — inside the layout, so the frame survives the failure. Catching a render error is something only the browser can do, so error.tsx is a client component.

// src/routes/error.tsx
'use client';

import type { ErrorProps } from '@k8ordo/router';

export default function RouteError({ error, reset }: ErrorProps) {
  return (
    <section role="alert">
      <p>{error instanceof Error ? error.message : 'something went wrong'}</p>
      <button onClick={reset} type="button">
        try again
      </button>
    </section>
  );
}

It receives error, whatever was thrown, and reset, which renders the subtree again in place — and no params. The type is ErrorProps from @k8ordo/router, and the generated table checks each error.tsx with satisfies ErrorComponent. Wording that depends on the URL reads the URL itself.

What it covers

The boundary of an error.tsx sits inside the layout of its own directory. It covers the page.tsx beside it and everything in the directories below, their layouts included; when the layout beside it throws, the error.tsx one level up answers. The nearest one above the throw is the one that shows.

src/routes/
  layout.tsx
  error.tsx
  page.tsx
  shop/
    layout.tsx
    error.tsx
    page.tsx
    [id]/
      page.tsx

In this tree shop/error.tsx catches a failure in shop/page.tsx or shop/[id]/page.tsx inside shop/layout.tsx, while a failure in shop/layout.tsx itself is caught by the root error.tsx, inside the root layout — as is a failure in the root page.tsx.

This site has two. src/routes/[locale]/error.tsx renders where the page was when a page throws, with the header and footer still around it; src/routes/error.tsx is the frameless full-screen last resort for when the [locale] layout itself throws.

reset renders the subtree again where it is. Navigating to another page clears the failure on its own, because the boundary is recreated for each navigation that puts a page on screen.

Try it

The button below mounts a client component that throws while rendering. This page's content is replaced by src/routes/[locale]/error.tsx while the header and footer stay; pressing "Retry" calls reset, and the page renders again in place.

When it throws during the build

A page whose Server Component throws while the build renders it is not written as a page: the build stops rather than writing an HTML whose error would only show once a visitor's browser rendered it. The error is logged with the page's pathname, as k8ordo: rendering /broken failed. When a Suspense boundary sits above it — an error.tsx is one — the build then ends with the line below; with none, it ends with React's production error (An error occurred in the Server Components render…) instead, so that log line is the only place the thrown message appears.

static build could not render /broken see the error above

A client component is different. When one throws while the build renders the HTML inside a Suspense boundary — an error.tsx is one — that part is left to the browser: the file is written, and the browser renders it again, showing the nearest error.tsx after hydration if it throws there too. With no Suspense boundary above it, the error stops the build.

So under this mode error.tsx is mostly for what fails in the browser: a client component that throws after hydration or after a client navigation.

When no error.tsx catches it

When a page that arrived by client navigation fails to render and no error.tsx catches it, the framework loads the same URL as a document instead. A network failure, or an answer that is not a payload — a file the host serves — becomes a document load the same way. A failure during hydration is not reloaded: asking again for HTML that was already rendered cannot make it better, and would only loop.

In this mode the document load gets the page's prerendered file from the host — there is no server to answer with a 500 or an error page of its own — and a URL the site does not have comes back as 404.html, which is HTML and so a document load too.

not-found.tsx

A not-found.tsx is the catch-all (/*) for any pathname below its directory that nothing else answered. It comes last in its branch, so every declared route is tried first. Like a page it receives params and pathname, and renders its own <title>.

// src/routes/not-found.tsx
export default function NotFoundPage() {
  return (
    <>
      <title>Not found</title>
      <h1>This page does not exist</h1>
    </>
  );
}

The parameters above a catch-all are not validated, so the params.locale a not-found.tsx at /:locale/* receives can be any string. Check it before using it.

In this mode not-found.tsx is rendered into one file, 404.html. A static host answers every unknown URL from one file, so only one not-found.tsx can be represented — under a locale segment is fine. Declaring two stops the build; declaring none writes no 404.html, and the host decides what an unknown URL gets.

a static host answers every unknown URL from one file, so only one not-found.tsx can be represented this table declares /docs/*, /*

How 404.html is rendered, and what its parameters hold, is covered here. Build & deploy

redirect.ts

A directory that has moved keeps a redirect.ts instead of a page.tsx. It default-exports the target: a string, or { to, permanent }.

// src/routes/old/redirect.ts
export default '/products';
// src/routes/[locale]/legacy/redirect.ts
export default { to: '/:locale/new', permanent: true };

The target is a pattern the matched params fill in, so /:locale/legacy can send to /:locale/new. A target naming a param its own pattern does not have fails when the redirect is answered.

A redirect is consulted before the table. A directory that redirects has no page to render, so one holding both page.tsx and redirect.ts fails the build — and since a redirect counts as a declared URL, another group putting a page at the same URL is refused too.

In this mode no server will ever send the status, so a redirect is written as a page that sends the visitor on — <meta http-equiv="refresh"> and a link. There is no index.rsc beside it, so a client navigation hands the URL to the browser, which loads that page and follows it. permanent changes nothing about the file written. Redirects are left out of sitemap.xml, and a redirect.ts under a parameter takes its values from paths.