Home Blog How to Add a Custom 404 Page to Your Jekyll Site
Tutorial

How to Add a Custom 404 Page to Your Jekyll Site

Create a helpful custom 404 error page for your Jekyll site — with search, navigation suggestions, and platform-specific setup for GitHub Pages, Netlify, and Vercel.

How to Add a Custom 404 Page to Your Jekyll Site

Every website has broken links. Old posts get renamed, pages move, external sites link to URLs that no longer exist. When a visitor hits one of these dead ends, the default browser 404 page — a stark, styled-nothing error message — sends them straight back to Google. A custom 404 page keeps them on your site.

A well-designed 404 page acknowledges the problem, offers useful paths forward, and maintains your site’s branding so the visitor knows they are still in the right place. In Jekyll, creating one takes five minutes. Making it genuinely useful takes a little longer, but the improvement in visitor retention is worth it.


Step 1: Create the 404 file

Create 404.md in your site root:

---
layout: page
title: "Page Not Found"
permalink: /404.html
sitemap: false
---

Three things matter here. The filename can be 404.md or 404.html but the output file must be 404.html — most hosting platforms look for exactly this filename to serve as the error page. The permalink: /404.html ensures Jekyll outputs it at the right path regardless of your global permalink setting (which might be pretty, producing 404/index.html instead). The sitemap: false (or sitemap: exclude: true depending on your plugin) prevents the error page from appearing in your sitemap and being submitted to search engines.


Step 2: Write helpful content

A useful 404 page has three things: a clear acknowledgment, navigation options, and ideally a search box.

Here is a minimal but effective 404 page body:

The page you're looking for doesn't exist or has been moved.

**Try one of these instead:**

- [Home](/) — back to the homepage
- [Blog](/blog/) — read our latest posts
- [Themes](/themes/) — browse Jekyll themes
- [Search](/search/) — search the full site

If you followed a link from another site and landed here, [contact us](/contact/) 
and we will look into it.

Simple, friendly, and offers real options. The contact link for “broken link from another site” is a nice touch — it turns a frustrating experience into a way for readers to help you fix your site.


Step 3: Build a dedicated 404 layout

For a polished result, create a purpose-built layout rather than reusing the generic page layout:


<!-- _layouts/404.html -->
---
layout: default
---

<div class="error-page">

  <div class="error-page__visual">
    <span class="error-page__code" aria-hidden="true">404</span>
  </div>

  <div class="error-page__content">
    <h1 class="error-page__title">Page not found</h1>
    <p class="error-page__message">
      The page you're looking for doesn't exist or may have been moved.
    </p>

    <form class="error-page__search" action="/search/" method="get" role="search">
      <label for="error-search" class="sr-only">Search the site</label>
      <input
        type="search"
        id="error-search"
        name="q"
        placeholder="Search for something…"
        class="error-page__search-input"
        autofocus
      >
      <button type="submit" class="btn btn--primary">Search</button>
    </form>

    <nav class="error-page__links" aria-label="Suggested pages">
      <p class="error-page__links-label">Or jump to:</p>
      <ul>
        <li><a href="/">← Homepage</a></li>
        <li><a href="/themes/">Browse themes</a></li>
        <li><a href="/blog/">Blog</a></li>
        <li><a href="/contact/">Contact</a></li>
      </ul>
    </nav>
  </div>

  {% if site.posts.size > 0 %}
  <div class="error-page__popular">
    <h2 class="error-page__popular-title">Popular posts</h2>
    <ul>
      {% assign popular_posts = site.posts | where: "featured", true | limit: 3 %}
      {% if popular_posts.size == 0 %}
        {% assign popular_posts = site.posts | limit: 4 %}
      {% endif %}
      {% for post in popular_posts %}
        <li><a href="{{ post.url }}">{{ post.title }}</a></li>
      {% endfor %}
    </ul>
  </div>
  {% endif %}

</div>

{{ content }}

Update your 404.md to use this layout:

---
layout: 404
title: "Page Not Found"
permalink: /404.html
sitemap: false
---

Step 4: Style the 404 page

// _sass/layouts/_404.scss

.error-page {
  max-width: 540px;
  margin: 4rem auto;
  padding: 0 1.5rem;
  text-align: center;
}

.error-page__visual {
  margin-bottom: 1.5rem;
}

.error-page__code {
  display: block;
  font-size: clamp(5rem, 20vw, 9rem);
  font-weight: 900;
  line-height: 1;
  color: var(--border-color);
  letter-spacing: -0.04em;
  user-select: none;
}

.error-page__title {
  font-size: 1.75rem;
  margin-bottom: 0.75rem;
}

.error-page__message {
  color: var(--text-muted);
  margin-bottom: 2rem;
  font-size: 1.0625rem;
}

// Search form
.error-page__search {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 2rem;
  text-align: left;
}

.error-page__search-input {
  flex: 1;
  padding: 0.625rem 0.875rem;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  font-size: 1rem;
  background: var(--bg-color);
  color: var(--text-color);
  transition: border-color 0.15s, box-shadow 0.15s;

  &::placeholder {
    color: var(--text-muted);
  }

  &:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
  }
}

// Navigation links
.error-page__links {
  text-align: left;
  margin-bottom: 2.5rem;
}

.error-page__links-label {
  font-weight: 600;
  margin-bottom: 0.5rem;
  color: var(--text-muted);
  font-size: 0.875rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.error-page__links ul {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
}

// Popular posts
.error-page__popular {
  text-align: left;
  border-top: 1px solid var(--border-color);
  padding-top: 1.5rem;
}

.error-page__popular-title {
  font-size: 1rem;
  font-weight: 600;
  margin-bottom: 0.75rem;
}

.error-page__popular ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

.error-page__popular li {
  padding: 0.3rem 0;
  border-bottom: 1px solid var(--border-color);

  &:last-child { border-bottom: none; }
}

Step 5: Platform configuration

Different hosting platforms handle 404 pages slightly differently.

GitHub Pages serves 404.html automatically for any URL that does not match a file in the built site. No configuration needed — push the file and it works.

Netlify also serves 404.html automatically from your publish directory. You can also configure custom redirects in _redirects or netlify.toml, but the default 404.html detection just works.

Cloudflare Pages follows the same convention — 404.html in the output directory is served for all unmatched routes.

Vercel requires a small configuration. Add vercel.json to your repository root:

{
  "routes": [
    { "handle": "filesystem" },
    { "src": "/(.*)", "dest": "/404.html", "status": 404 }
  ]
}

AWS S3 / CloudFront requires configuring the error page in S3 bucket settings or CloudFront distribution settings. In S3, go to Properties → Static website hosting → Error document and set it to 404.html.

Testing your 404 page. After deploying, visit a URL that definitely does not exist on your site (such as /this-does-not-exist-abc123/). Two things should be true: the browser should display your custom 404 page with your site’s branding, and the HTTP status code should be 404 (not 200). You can verify the status code in Chrome DevTools → Network tab — look for Status: 404.


Step 6: Auto-suggest based on the URL

For extra polish, parse the URL that failed and pre-fill the search box with a cleaned version:

// In your 404 layout or main.js
document.addEventListener('DOMContentLoaded', function () {
  const searchInput = document.getElementById('error-search');
  if (!searchInput) return;

  // Extract keywords from the failed URL
  const path = window.location.pathname;
  const slug = path
    .replace(/\/$/, '')           // Remove trailing slash
    .split('/')                    // Split on slashes
    .pop()                         // Take the last segment
    .replace(/[-_]/g, ' ')         // Replace hyphens/underscores with spaces
    .replace(/\.(html|md)$/, ''); // Remove file extensions

  if (slug && slug.length > 2) {
    searchInput.value = slug;
    searchInput.setAttribute('placeholder', '');
  }
});

If a visitor follows a broken link to /blog/how-to-install-jekyll-theme/, the search box is pre-filled with “how to install jekyll theme” — the most likely search query they had in mind. This small improvement makes finding the right page almost effortless.


What makes a good 404 page

Keep the site header and footer. Visitors who hit a 404 should not feel like they have left your site. Maintain consistent navigation so they can easily browse elsewhere.

Maintain the right tone. A 404 is frustrating. Acknowledge it briefly (“The page you’re looking for doesn’t exist”) without being overly apologetic. Avoid technical jargon like “404 Not Found” as the primary heading — “Page not found” is clearer.

Do not auto-redirect. Automatically redirecting a 404 to the homepage confuses visitors and search engines alike. The visitor wanted a specific page; sending them to the homepage without explanation is unhelpful. Show the 404 page with options instead.

Show content relevant to the failed URL. If your site has many topics, use the URL parsing script above to pre-fill a search or to highlight the most relevant category. A visitor who hit /blog/jekyll-performance/ probably wants your performance article, not your theme reviews.

Log 404s. If you have analytics set up, segment 404 traffic to identify your most common broken URLs. These often reveal broken internal links you need to fix, or popular external links pointing to moved pages that need redirects.


Adding redirects for moved pages

A 404 page is a last resort. If you know a page has moved, add a redirect so visitors and search engines go directly to the right place.

For Netlify and Cloudflare Pages, create _redirects in your Jekyll root:

/old-blog-post/     /new-blog-post/     301
/tutorials/jekyll/  /blog/              302

The 301 status is a permanent redirect (tells search engines the page has permanently moved and to transfer link equity). Use 302 for temporary redirects.

For GitHub Pages without Actions (which cannot process _redirects), create a stub HTML file at the old URL with a meta refresh:

---
layout: none
permalink: /old-blog-post/
sitemap: false
---
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="refresh" content="0; url=/new-blog-post/">
  <link rel="canonical" href="https://yourdomain.com/new-blog-post/">
</head>
<body>
  <p>This page has moved. <a href="/new-blog-post/">Click here</a> if you are not redirected.</p>
</body>
</html>

A well-crafted 404 page is a small investment with a meaningful return. It recovers visitors who would otherwise bounce and gives you insight into broken links across your site. Browse Jekyll themes on JekyllHub — many include a pre-styled 404 page you can customise.

What makes a 404 page actually useful

Most custom 404 pages fail at their primary job: helping the visitor find what they were looking for. A branded 404 page with a witty message and a “go home” link is better than a server default, but it still leaves the visitor without a path forward if what they wanted is not on your homepage.

A useful 404 page provides multiple recovery paths because different visitors arrived via different paths and have different next steps. Someone who followed a broken link from an external site may not know your site structure and needs a site overview or popular content list. Someone who typed a URL manually may have a typo and needs a clear signal of what URLs actually work. Someone navigating from an old bookmark needs a redirect or a search to find the content’s new location.

Consider adding a search field to your 404 page. If your Jekyll site uses Pagefind or Lunr.js for search, embedding a search interface on the 404 page lets visitors immediately search for what they were looking for without navigating elsewhere. This single addition recovers more visitors than any other 404 page element, because it directly addresses the most common “visitor intent” of a 404 page visit: finding specific content that should exist but could not be located at the expected URL.

Tracking 404 errors with analytics

Your 404 page is a diagnostic tool as much as it is a user experience feature. Visitors who land on it are telling you which URLs are broken — which is actionable information if you capture it.

Google Analytics (GA4) tracks 404 page visits automatically if you have analytics installed on your 404 page, which you should. In GA4, filter your events by page title to find all sessions where the title was “404 Not Found” (or however you have titled your 404 page). The page paths in these sessions are the broken URLs. Sort by frequency to find the most commonly hit 404 pages — these are the highest-priority redirects to create.

For more targeted 404 monitoring, add a custom event to your 404 page’s JavaScript that fires when the page loads and captures the current URL and referrer. This creates a dedicated 404 stream you can filter and alert on separately from regular traffic analytics. If your 404 rate suddenly spikes (perhaps because an external site with high traffic linked to a URL that does not exist), you will see the spike in your dedicated 404 analytics before it shows up in a manual audit.

Google Search Console’s Coverage report also surfaces 404 errors — specifically 404 pages that have been discovered by Google’s crawler. These are particularly high-priority to fix because they represent URLs that Google knows about (from your sitemap, from inbound links, or from previous crawls) that are now returning 404. Fixing these either by restoring the content or setting up a redirect can recover lost rankings.

Dynamic 404 pages with Liquid

Jekyll’s 404 page is a static file like any other — it is rendered once at build time with the usual Liquid processing. This means you can make the 404 page genuinely helpful using Jekyll data available at build time.

A 404 page that lists your most recent posts or most popular content gives visitors something relevant to navigate to when their intended destination is unavailable. Liquid can access site.posts and render a short list: the five most recent posts sorted by date, or a curated list of evergreen posts flagged in their front matter with featured: true. This is more useful than a generic homepage link because it shows the visitor the kind of content on your site and gives them a starting point for exploration.

For sites with categories or tags, a 404 page that shows your main topic categories with post counts helps visitors orient themselves and find the section most relevant to their interest. A visitor looking for Jekyll theme tutorials who lands on a 404 page and sees “Tutorials (47 posts)” and “Theme Reviews (23 posts)” has a clear path forward.

Preventing 404 errors with good content management habits

The best 404 page is the one visitors never see. Prevention through good content management habits reduces the 404 rate more effectively than any 404 page design improvement.

The most important habit is redirect-before-delete: any time you remove a page, rename a post, or change a permalink, immediately create a redirect from the old URL to the new one. For Jekyll sites on Netlify or Cloudflare Pages, the _redirects file makes this a one-line addition. For GitHub Pages, the jekyll-redirect-from plugin creates an HTML meta refresh at the old URL. Neither approach requires rebuilding or touching existing content — just add the redirect and you are done.

Run an automated broken link check before each major content update. The html-proofer gem checks every link in your built _site/ output — both internal links and external links if you enable the external check. Internal broken links caused by template errors, incorrect relative paths, or missing pages appear immediately in the proofer output. External broken links appear in a separate check. Catching these before deployment means visitors never encounter them.

For older content, set up a quarterly link audit using a tool like Screaming Frog (free up to 500 URLs) or Ahrefs Site Audit. These tools crawl your site from the outside, finding broken internal links, redirect chains, and pages that return 404 errors — the same perspective a visitor has. The quarterly cadence catches link rot from external sites changing their URLs without your awareness, which automated internal checking cannot detect.

The 404 page as brand expression

Within the practical constraints of being helpful, the 404 page is a rare opportunity for personality. Unlike service pages, product listings, or blog posts — all of which must be primarily functional — the 404 page allows a light touch of humour or creativity that is appropriate because the visitor already knows something went wrong.

Well-crafted 404 pages from developer tools and software products often reference the tools’s domain with wit: a version control product might show a git branch not found error message; a theme marketplace might show a broken mockup with a “theme not installed” message. These are effective because they confirm you are on the right site, they acknowledge the error with humour rather than apologising flatly, and they signal a company culture that cares about small details.

Keep the personality light and secondary to function. A clever illustration with no navigation options is a worse 404 page than a plain page with good recovery links. The personality is the icing; the recovery paths are the cake. Get the utility right first, then add character within those constraints.

Share LinkedIn