How to Implement Custom 404 and 500 Error Pages in SXO

SXO automatically discovers and renders custom error pages when you create 404.jsx or 500.tsx files in your pages directory, using the resolveErrorPage utility and loadErrorPages loader to safely import and execute your components.

The gc-victor/sxo repository provides a lightweight framework that treats root-level 404 and 500 files as special error pages loaded by the server only when needed. When you implement custom 404 and 500 error pages in SXO, you follow a file-based convention that integrates with the server's request pipeline through specific resolution utilities and safe module loaders.

Understanding the Error Page Resolution System

SXO determines which error page to render through three core utilities that work together to locate, validate, and import your custom modules.

The Resolution Utilities

The framework provides specific functions to locate error pages on disk:

  • resolveErrorPage(name) – Implemented in src/js/server/utils/error-pages.js (lines 24-42), this function searches PAGES_DIR for files named 404 or 500 with extensions .tsx or .jsx in that precedence order. It returns the absolute path or null if no match exists.

  • resolveErrorPages() – A convenience wrapper in src/js/server/utils/error-pages.js (lines 65-70) that returns both paths in a single object.

  • loadErrorPages(options) – Located in src/js/server/shared/error-pages-loader.js (lines 54-95), this function imports the resolved modules, validates their exports, and returns the render functions (render404, render500) while catching and logging any import errors.

Server Integration

Both development and production adapters import these resolvers from the barrel file src/js/server/utils/index.js. When handling requests, the server calls loadErrorPages to obtain the render functions:

// Example pattern from src/js/server/prod/node.js
import {
  resolve404Page,
  resolve500Page,
} from "../utils/index.js";

const { render404, render500 } = await loadErrorPages({
  resolve404Page,
  resolve500Page,
  loadJsxModule,
  logger,
});

If a custom page is found, the server invokes its exported function to produce a full HTML document. If the resolver returns null or the module fails to load, SXO falls back to the minimal built-in response defined in renderErrorHtml within src/js/server/utils/html-utils.js.

File System Convention and Export Requirements

Creating custom error pages requires following specific naming and location rules.

The resolution system checks for files directly under PAGES_DIR with the supported filenames 404.tsx, 404.jsx, 500.tsx, or 500.jsx. The resolveErrorPage utility uses a precedence order of [".tsx", ".jsx"], checking each extension via fs.accessSync and stopping at the first match to guarantee deterministic behavior.

Your error page module must export either a default function or a named jsx function that returns a complete HTML string including the <html> and <head> tags. The function receives no arguments and should output a full document ready for the response stream.

Step-by-Step Implementation

Follow these patterns to create working custom error pages in your SXO project.

Create a 404 Not Found Page

Create a file named 404.jsx (or 404.tsx) in your pages directory:

// src/pages/404.jsx
export default function NotFound() {
  return `
    <html>
      <head><title>Page not found</title></head>
      <body>
        <h1>404 – Not Found</h1>
        <p>Sorry, the page you're looking for doesn't exist.</p>
        <a href="/">← Back to home</a>
      </body>
    </html>
  `;
}

The resolveErrorPage("404") function automatically discovers this file because it resides directly under the pages directory with a supported extension.

Create a 500 Server Error Page

Similarly, create 500.tsx (or 500.jsx) for server errors:

// src/pages/500.tsx
export default function ServerError() {
  return `
    <html>
      <head><title>Server error</title></head>
      <body>
        <h1>500 – Internal Server Error</h1>
        <p>Something went wrong on our end. Please try again later.</p>
        <a href="/">← Back to home</a>
      </body>
    </html>
  `;
}

Optional: Add Error Handling Hooks

You can hook into the loading process for additional logging or monitoring. The loadErrorPages function accepts an onError callback that receives the error and page name:

// src/js/server/dev/node.js (excerpt)
import { loadErrorPages } from "../shared/error-pages-loader.js";

const { render404, render500 } = await loadErrorPages({
  resolve404Page,
  resolve500Page,
  loadJsxModule,
  logger,
  onError: (err, page) => {
    console.error(`Failed to load custom ${page} page:`, err);
  },
});

This ensures your server remains stable even if a custom error page contains syntax errors or fails to import.

Testing Your Implementation

Verify your custom pages work by running the development server and requesting non-existent routes:

curl -i http://localhost:3000/does-not-exist

You should see the HTML from your src/pages/404.jsx file in the response body. The server automatically sets a Cache-Control: public, max-age=0, must-revalidate header for special error pages as handled in src/js/server/utils/html-utils.js.

Summary

  • Place 404.jsx, 404.tsx, 500.jsx, or 500.tsx files directly in your PAGES_DIR root.
  • Export a default function or named jsx function that returns a complete HTML document string.
  • The resolveErrorPage utility locates files with .tsx taking precedence over .jsx.
  • The loadErrorPages function safely imports modules and provides graceful fallbacks to built-in responses.
  • Both development and production adapters use the same resolution pipeline via src/js/server/utils/index.js.

Frequently Asked Questions

What file extensions does SXO support for error pages?

SXO checks for .tsx first, then .jsx when resolving error pages in src/js/server/utils/error-pages.js. This precedence order ensures TypeScript files take priority over JavaScript when both exist.

Where should custom error pages be located?

Custom error pages must reside directly under the PAGES_DIR directory (typically src/pages/) with the exact filenames 404 or 500 plus the supported extensions. The resolveErrorPage function uses fs.accessSync to verify these locations.

What happens if my custom error page fails to load?

If the module fails to import or the file doesn't exist, loadErrorPages catches the exception, logs the error through the provided logger, and returns null for that specific render function. The server then falls back to the minimal built-in error response defined in src/js/server/utils/html-utils.js.

Can I use server-side data fetching in error pages?

The error page exports must be synchronous functions that return a complete HTML string. According to the implementation in src/js/server/shared/error-pages-loader.js, the loader imports the module and expects either a default export or named jsx export that can be called immediately to generate the response, without awaiting async data fetching.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →