RJLCustom404 logo RJLCustom404

Getting started

RJLCustom404 renders a complete 404 page into an otherwise empty HTML file. You supply the file, the images and the options. Below is the whole process.

What it renders

  • The page title (the browser tab).
  • An <h3> heading and an optional sub-heading.
  • A random image from your set, or a bracketed placeholder when none is configured.
  • A "Back to your-site.com" button, or a plain link if you prefer.
  • Body background and text color, and a layout that stacks on small screens.

1. Include the script

Hosted (zero setup)

html
<script src="https://rjl.codes/error/404/custom404.js?v=RJL_1.3.0"></script>

Do not add an integrity= attribute. The file is updated behind a stable URL; a pinned hash would silently block it and your 404 page would render blank. The ?v= query is a cache-buster - bump it when you want browsers to fetch a fresh copy.

Self-hosted

html
<script src="/js/custom404.min.js?v=1.3.0"></script>

Download custom404.min.js from the GitHub repository. Self-hosting removes the runtime dependency on rjl.codes and lets you serve it under your own Content Security Policy.

2. Call the function

html
<script>
  RJLCustom404({
    imgFileNames: ["/img/404-a.png", "/img/404-b.png"],
    subHeaderText: "The page you asked for is not here."
  });
</script>

Every option is optional. With no options at all you get the default text, a blue button and a [[ No 404 image configured ]] placeholder where the image would be. All 27 options are listed in the options reference.

3. Add your images

No images are bundled. You host them, like any other asset on your site.

A list of files

javascript
RJLCustom404({ imgFileNames: ["/img/404-a.png", "/img/404-b.png", "/img/404-c.png"] });

A numbered pattern

javascript
RJLCustom404({ imgPattern: "/img/404-{n}.png", imgCount: 6 });                    // 404-1.png ... 404-6.png
RJLCustom404({ imgPattern: "/img/404-{nn}.png", imgCount: 12 });                  // 404-01.png ... 404-12.png
RJLCustom404({ imgPattern: "/img/404-{n}.png", imgCount: 6, imgStartIndex: 0 }); // 404-0.png ... 404-5.png
  • {n} is the number; {nn} and {nnn} zero-pad to that width.
  • imgPattern needs imgCount. If both imgFileNames and a pattern are set, the list wins.
  • Images load like any <img>: same-origin paths, or an absolute URL from a host that serves them to your origin.
  • One image is chosen at random on every load. With two or more images the shuffle is remembered in localStorage, so a visitor sees every image before any repeat.

4. Ship it as 404.html

Put the file at your site root and tell the host to serve it for missing URLs. Most hosts need nothing more than the file name.

Cloudflare Pages

Put 404.html at the root of your output directory. Nothing to configure; Pages serves it with a 404 status for every unmatched path. It must be .html, not .htm.

Whatever the host, the page must still return HTTP 404. Do not redirect missing URLs to the home page - search engines treat that as a soft 404 and users lose the context of what they were looking for.

The recommended pattern: guarded call and static fallback

The library generates the entire visible body of the page. If the script is ever blocked - an ad blocker, a strict CSP, a network hiccup, a stale integrity hash - an unguarded call throws RJLCustom404 is not defined and the visitor sees a blank page that still returns 404, so nothing in your monitoring notices. Two lines fix that: a plain fallback block and a typeof guard.

html
<div id="fallback-404">
  <h1>Page not found</h1>
  <p>Sorry, that page does not exist.</p>
  <p><a href="/">Return to the home page</a></p>
</div>

<script>
  if (typeof RJLCustom404 === "function") {
    var fb = document.getElementById("fallback-404");
    if (fb) fb.parentNode.removeChild(fb);
    RJLCustom404({ /* your options */ });
  }
</script>

The library appends to <body> and never clears it, which is why the fallback removes itself. Keep the fallback plain: it exists for the failure case only.

A complete 404.html

Copy this file, change the image paths, done. The playground generates the same file with your own options filled in.

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="robots" content="noindex">
  <title>404 - Page not found</title>
  <!-- No integrity= attribute: the file changes behind a stable URL and a pinned hash would block it -->
  <script src="https://rjl.codes/error/404/custom404.js?v=RJL_1.3.0"></script>
</head>
<body>
  <!-- Static fallback: only visible if the script is blocked or unreachable -->
  <div id="fallback-404">
    <h1>Page not found</h1>
    <p>Sorry, that page does not exist.</p>
    <p><a href="/">Return to the home page</a></p>
  </div>

  <script>
    if (typeof RJLCustom404 === "function") {
      var fb = document.getElementById("fallback-404");
      if (fb) fb.parentNode.removeChild(fb);
      RJLCustom404({
        autoText: true,
        imgPattern: "/img/404-{n}.png",
        imgCount: 4,
        imgBorderRadius: "24px",
        btnColor: "#004494"
      });
    }
  </script>
</body>
</html>

noindex keeps the error page out of search results. If your site runs analytics, paste the same tags into this file so broken-link traffic lands in your dashboards.

Frameworks

Any framework that outputs a static 404.html (Astro, Eleventy, Hugo, Jekyll, Vite, a Next.js static export) works unchanged. Frameworks that own the 404 route (Next.js app/not-found.js, Nuxt error.vue, SvelteKit +error.svelte, a React Router catch-all) can load the script and call RJLCustom404() from the route's mount hook. The library appends to <body> and sets the body background and text color, so use it on a route with no app shell around it, or style the container with the CSS hooks.

Upgrading

From 1.1 or earlier

1.2.0 removed the bundled default images. A page that sets neither imgFileNames nor imgPattern now shows [[ No 404 image configured ]]. Add your own images to restore the picture. The full release history is in the changelog on GitHub.

Cache busting

The hosted file is updated in place. Bump ?v= in your script tag to make browsers refetch it. The current version is 1.3.0; the running copy reports it as RJLCustom404.version.

Next steps