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">
{% for post in site.posts limit: 10 %}
<article>
{% include archive_post.html %}
</article>
{% endfor %}
</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.