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)
<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
<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
<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
RJLCustom404({ imgFileNames: ["/img/404-a.png", "/img/404-b.png", "/img/404-c.png"] }); A numbered pattern
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.imgPatternneedsimgCount. If bothimgFileNamesand 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.
Netlify
Put 404.html at the root of your publish directory. Netlify serves it automatically. The explicit alternative, in _redirects:
/* /404.html 404 Vercel
For static deployments, put 404.html at the root of the output directory. Frameworks with their own 404 route (Next.js pages/404.js or app/not-found.js) should call the library from that route instead.
GitHub Pages
Put 404.html at the root of the publishing source (the repository root or /docs). Project sites live under /repo-name/, so use absolute image paths that include it.
Apache
In .htaccess at the site root:
ErrorDocument 404 /404.html nginx
Inside the server block:
error_page 404 /404.html;
location = /404.html { internal; } Drop the internal line if you want /404.html to be openable directly.
IIS
In web.config at the site root:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<httpErrors errorMode="Custom">
<remove statusCode="404" subStatusCode="-1" />
<error statusCode="404" path="/404.html" responseMode="ExecuteURL" />
</httpErrors>
</system.webServer>
</configuration> Express
const path = require("path");
// after all other routes and static middleware
app.use((req, res) => {
res.status(404).sendFile(path.join(__dirname, "public", "404.html"));
}); 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.
<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.
<!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.