In the process of porting my blog to Octopress/Jekyll, I wanted to implement a custom 404 error page. Blogs around the internet suggested that I drop a 404.html into my source directory.

Here’s what I use:

---
layout: page
title: Error 404
footer: false
menu: false
---
<p>The page you have requested cannot be found. Why not read some of my latest entries instead?</p>
<div id="blog-archives" class="missing">

<article>
  <span class="post-meta">Feb 10, 2021</span>

<h2>
  
  <a class="post-link" href="/blog/2021/02/10/ipv6-docker-docker-compose-and-shorewall6-ip6tables/">IPv6, Docker(-compose), and Shorewall6/ip6tables</a>
  
</h2>
</article>

<article>
  <span class="post-meta">Dec 27, 2015</span>

<h2>
  
  <a class="post-link" href="/blog/2015/12/27/jekyll-3-post-excerpts/">Jekyll 3 Post Excerpts</a>
  
</h2>
</article>

<article>
  <span class="post-meta">Dec 24, 2015</span>

<h2>
  
  <a class="post-link" href="/blog/2015/12/24/why-i-gym-religiously/">Why I Gym Religiously</a>
  
</h2>
</article>

<article>
  <span class="post-meta">Dec 23, 2015</span>

<h2>
  
  <a class="post-link" href="/blog/2015/12/23/s3-custom-redirection-rules/">S3 Custom Redirection Rules</a>
  
</h2>
</article>

<article>
  <span class="post-meta">Dec 9, 2015</span>

<h2>
  
  <a class="post-link" href="/blog/2015/12/09/getting-f-lux-onto-your-ios-device-with-xcode/">Getting f.lux onto your iOS Device with Xcode</a>
  
</h2>
</article>

<article>
  <span class="post-meta">Sep 10, 2015</span>

<h2>
  
  <a class="post-link" href="/blog/2015/09/10/hey-siri-give-us-a-hint/">Hey Siri, Give Us a Hint</a>
  
</h2>
</article>

<article>
  <span class="post-meta">Jun 28, 2015</span>

<h2>
  
  <a class="post-link" href="/blog/2015/06/28/investment-portfolio-tracking/">Investment Portfolio Tracking</a>
  
</h2>
</article>

<article>
  <span class="post-meta">Jun 28, 2015</span>

<h2>
  
  <a class="post-link" href="/blog/2015/06/28/love-wins/">Love Wins</a>
  
</h2>
</article>

<article>
  <span class="post-meta">Jun 11, 2015</span>

<h2>
  
  <a class="post-link" href="/blog/2015/06/11/the-jekyll-reboot/">The Jekyll Reboot</a>
  
</h2>
</article>

<article>
  <span class="post-meta">Sep 17, 2013</span>

<h2>
  
  <a class="post-link" href="/blog/2013/09/17/forward-thinking/">Forward-Thinking</a>
  
</h2>
</article>

</div>

However, I noticed another problem. Sinatra was not returning HTTP status error 404 for this. A regular status 200 (OK) was sent, leading Google to start indexing these error pages.

But first, let me show you the default config.ru Rack-adaptor that ships with Octopress:

require 'bundler/setup'
require 'sinatra/base'

# The project root directory
$root = ::File.dirname(__FILE__)

class SinatraStaticServer < Sinatra::Base

  get(/.+/) do
    send_sinatra_file(request.path) {404}
  end

  not_found do
    send_sinatra_file('404.html') {"Sorry, I cannot find #{request.path}"}
  end

  def send_sinatra_file(path, &missing_file_block)
    file_path = File.join(File.dirname(__FILE__), 'public',  path)
    file_path = File.join(file_path, 'index.html') unless file_path =~ /\.[a-z]+$/i
    File.exist?(file_path) ? send_file(file_path) : missing_file_block.call
  end

end

run SinatraStaticServer

To ensure that the 404.html is served with a status 404, I had to add a :status option to send_file. Here’s how my file now looks like:

require 'bundler/setup'
require 'sinatra/base'

# The project root directory
$root = ::File.dirname(__FILE__)

class SinatraStaticServer < Sinatra::Base

  configure do
    enable :static_cache_control
  end

  get(/.+/) do
    send_sinatra_file(request.path)
  end

  not_found do
    send_file(File.join(File.dirname(__FILE__), 'public', '404.html'), {:status => 404})
  end

  def send_sinatra_file(path)
    file_path = File.join(File.dirname(__FILE__), 'public',  path)
    file_path = File.join(file_path, 'index.html') unless file_path =~ /\.[a-z]+$/i and !File.directory?(file_path)
    File.exist?(file_path) ? send_file(file_path) : not_found
  end

end

run SinatraStaticServer

I’ve rewritten the not_found method and also added another condition !File.directory?(file_path) within the send_sinatra_file method. This is to teach some web servers to serve the index.html when the path is a directory.

However, that’s not quite it. Support for sending status codes within send_file was only recently added and is only available in version 1.4.0 of Sinatra.

Not to worry, we will make use of trusty Bundler to help us fetch the latest version of Sinatra from source. Update the following in your Gemfile:

gem 'sinatra', "~> 1.4.0"

While at it, the gems defined in the Gemfile are ancient. I’ve cleaned up my Gemfile and here’s how it looks like:

source "https://rubygems.org"

group :development do
  gem 'rake'
  gem 'rack'
  gem 'jekyll'
  gem 'rdiscount'
  gem 'pygments.rb'
  gem 'RedCloth'
  gem 'haml', '>= 3.1'
  gem 'compass', '>= 0.11'
  gem 'rubypants'
  gem 'rb-fsevent'
  gem 'stringex'
  gem 'liquid'
end

gem 'sinatra', "~> 1.4.0"
gem 'thin'

Once you have updated the Gemfile, run the following commands:

bundle update
bundle install

With that, Octopress (and its Rack-compatible web server) will now serve the custom error 404 file not found page with the correct HTTP status code.