has_many :codes

Sinatra contact form for Jekyll blogs

Published  

Update September 15, 2020: Hi! If you reached this page chances are that you are a blogger :) I would like to introduce DynaBlogger, a new blogging platform I just launched as a simple alternative to Wordpress and Ghost for people who find these platforms overkill for their needs. This very website is now hosted on DynaBlogger! Trying it is easy: there is a free plan - no credit card required - and with the coupon "EARLYBIRD" there's a 50% discount for the first three months if you upgrade to a paid plan. Also, if you choose a yearly plan 2 months are free. Check it out! :)


Introduction

In the previous two posts I explained why I switched from WordPress to Jekyll for my blog, and a quite detailed guide on how to perform the migration. If you haven’t heard of Jekyll before, or are planning to migrate your site to Jekyll or simply want to know more about it, I recommend you read that guide first. In this post I will show how to implement a Sinatra contact form for Jekyll.

As I mentioned in the guide, because Jekyll itself does not use any server side technology, it cannot provide -out of the box- any dynamic features that would require any interaction with the server. One of such features is the typical contact form that we see on almost any website, and that requires the server (usually) to send an email. In the guide I suggested that it is possible to add this feature to a Jekyll site in two ways, namely:

  • you can just outsource your contact form to third party services such as Foxyform, in which case all you’ll have to do is paste a little code snippet in your contact page; the advantage in this case clearly is that your site would remain totally static as it was without a contact form, plus you could still use any hosting service regardless of whether it uses a server side technology, and regardless of which one, if any;
  • or you can integrate your static Jekyll site with any server side technology, just to implement dynamic features such as the contact form but more; of course this requires more work and that the hosting supports a server side technology, but you would have more control on those dynamic features and, in particular, your contact form would remain private.

In the guide I suggested that Sinatra is a natural choice for the Ruby-based Jekyll, and since a few readers asked me an example, this post shows how I implemented a simple contact form powered by Sinatra, in this Jekyll blog.

The example shown in here is fully functional, and is exactly what I use for my contact page. It does the following things:

  • basic validations for the required fields and their format
  • checks if the domain name in the email address has MX records, so to ensure that that domain can receive emails correctly
  • implements a simple Captcha verification to prevent spam

The Sinatra contact form for Jekyll

If you are unfamiliar with Sinatra, I recommend you read The Sinatra Book first, since you’ll find in there a lot of useful information on building web apps with this cool tiny library.

The most simple Sinatra app is nothing more than a Ruby code file similar to this:

require 'sinatra'

get '/' do
  'This text will be rendered in the page'
end

with ‘routes’ defined with methods such as get and post, in most cases.

For a Jekyll site’s contact form we could have two ‘routes’, one to render the contact form when a GET request is made, and one that sends the email when the form is submitted with a POST request instead, and also shows validation errors – if any, when rendering the same contact form. So, for starters, create an empty blog.rb (or give it whichever name you prefer) in either your site’s root or in a lib subfolder; I prefer to keep additional Ruby code and Rake tasks that have little to do with Jekyll in the lib folder. If you do the same, remember to add the lib folder to the exclude setting in your Jekyll’s _config.yml configuration file, to prevent that folder’s content from being available for download by clients. For example, this is what I have in mine:

exclude: ["lib", "blog.rb", "Capfile", "config", "log", "Rakefile", "tmp"]

Rendering only the contact form in an iframe

In the blog.rb file, you can now add the code that will render the contact form when triggered by a GET request:

require 'sinatra'
require 'haml'

get '/contact-form/?' do
  haml :contact_form
end

The view named contact_form.haml is expected; I am using haml here but you can use erb or whichever other engine you prefer.

In my case, you can see that the Sinatra app renders the contact form when a GET request is made for /contact-form/, rather than /contact/ – which is the address of my actual contact page. The reason is that I let Jekyll generate the main contact page as a static page as usual (address: /contact/), but the contact form is then rendered by Sinatra in the same page through an iframe (address: /contact-form/).

So this is the current content of my contact page as seen by Jekyll:

---
layout: default
title: Contact – Vito’s journal
meta-description: Vito Botta is a passionate web developer living and working in London, UK. His roles as analyst, developer and technology enthusiast overlap here on his web log.
meta-robots: noindex, noarchive, noodp, noydir
---
<h1>
Contact</h1>
I am very open to meeting and getting in touch with new people who share my same interests, so please feel free to contact me by filling out the form below; I’ll get back to you shortly.

You may also <a href="http://twitter.com/vitobotta">follow me on twitter</a>.

<iframe class="contact-form" frameBorder="0" src="/contact-form/"></iframe>

Rendering the whole contact page with Sinatra/Liquid Since iframes aren’t pretty, you may be wondering why I am using an iframe here instead of just letting Sinatra render the whole contact page. There is actually a pretty good reason for this, at least in my case. If your Jekyll site uses a very simple layouts, it doesn’t include any widgets or any data that may only be available in Jekyll’s context while it is generating the static pages (think also of post data, tags, categories – among other things), then you could simply let Sinatra render the whole page, and forget about the iframe.

But if your site’s layout, like this blog’s layout, includes widgets stored in separate files (with the include directive) and also needs data that is only available during the generation of the static pages with Jekyll, letting Sinatra render the whole page could become a bit complicated. You could tell Sinatra to use liquid to render directly the same layouts that Jekyll uses to generate the static site, instead of erb or haml. So the previous example would become:

get '/contact-form/?' do
  liquid :contact_form
end

However, as said this may only work with very simple layouts. With more complex Jekyll sites whose layouts use the include directive for widgets and such, I bet you’d have at least a couple issues.

Firstly, you would need to tweak liquid with some monkey patching for the following reasons:

  • to make sure both Sinatra and Jekyll can find the layouts since liquid expects layouts to have file names with the .liquid extension;
  • to allow the use of the include directive correctly with includes/widgets stored in another folder; with Jekyll, includes are typically stored in /_includes while layouts live in /_layouts.

Here’s just for reference what I tried with some success while I was attempting to render the whole contact page with Sinatra, for this blog:

module liquid
  class LocalFileSystem
    def full_path(template_path)
      raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/

      full_path = if template_path.include?('/')
        File.join(root, File.dirname(template_path), "_#{File.basename(template_path)}.html")
      else
        File.join(root, "_#{template_path}.html")
      end

      raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(root)}/

      full_path
    end
  end
end

It’s just a small patch that ensures liquid will look for a layout with the extension .html (expected by Jekyll) when used in Sinatra instead of erb/haml.

Secondly, if you just switched from erb/haml to liquid with no other change, your includes would not be ehm.. included correctly in the layout, and you would very likely see an error such as:

Liquid error: This liquid context does not allow includes.

To fix this you’d need to render the layout in a slightly different way:

get '/contact-form/?' do
  layout_path = .... # the actual location of the layout for the contact page
  Liquid::Template.file_system = Liquid::LocalFileSystem.new(layout_path)
  Liquid::Template.parse( File.read(layout_path) ).render
end

This way the error should disappear and your includes should work, but you’d need to rename your includes: when used in the same way as erb or haml, in fact, liquid expects include files to follow the naming convention of partials, that is they must have names that start with the _. To fix this, you can make another small change to the patch shown above so to just remove the _:

full_path = if template_path.include?('/')
  File.join(root, File.dirname(template_path), "#{File.basename(template_path)}.html")
else
  File.join(root, "#{template_path}.html")
end

During my tests I also found that the syntax I’ve been using for includes so far (which works with Jekyll), doesn’t work when using liquid with Sinatra. To fix this, all I had to do was to slightly change the names of the includes/partials in each include directive (in both the layout and the includes themselves because of nesting). So for example this

...some html...

{% include header.html %}

...some html...

would become

...some html...

{% include 'header' %}

...some html...

Now, I’ve added these tips about liquid just to save you some time in case you want to attempt rendering the whole page with Sinatra, using the same layouts and includes that Jekyll uses to generate the static site. This stuff would work OK if you just have some includes, but the task could get a lot more complicated than that if you use in either the layouts or the widgets/includes any data that is only available with Jekyll during the generation of the static site, as mentioned earlier. Post data, categories, tags and whichever data that is available in Jekyll’s context, will not be available in Sinatra’s context, unless you replicate a significant portion of Jekyll’s functionality into your Sinatra app, which – frankly – would IMHO be a pure waste of time, and perhaps there would be no point anymore with using Jekyll in first place.

So if your Jekyll’s site also uses layouts with includes and data that is only available with Jekyll during the generation of the static site, my recommendation is that you still keep the super speedy and light static site, and just use an iframe to show a dynamic contact form powered by Sinatra. The worst that can happen is that if something goes wrong with your Sinatra app for whatever reason, only the contact form would be temporarily lost while the rest of the site would still work. Besides problems with the application itself, should your site experience a lot of traffic, your Sinatra app may give up after a while since it can only serve a much smaller number of requests compares to static pages; but even in that case, you could of course use page/fragment caching with Sinatra to reduce these issues.

The full contact form template

Now that it is hopefully clear why -depending on the site- using an iframe for the contact form may be easier, let’s move on.

Here’s the content of the haml view I am using with Sinatra to render the contact form:

!!!
%html

%head
%link{:href => "/stylesheets/contact-form.css", :media => "screen", :rel => "stylesheet", :type => "text/css"}/
%meta{:content => "noindex, noarchive, noodp, noydir", :name => "robots"}/
%body
- if @sent == true
.notice Your message has been sent. Thank you!
- else
- if [email protected]? || @failure
.notice.failure= @failure || "Please fill in all the required fields."
%form.contact-form{:enctype => "multipart/form-data", :method => "post"}
%p.form-field-row
%span.label
%label{:for => "name"} Your Name
%span.required-field (required)
%span.field
%input#name{:autocomplete => "off", :name => "name", :type => "text", :value => @values[:name]}
%span.error= @errors[:name]
%p.form-field-row
%span.label
%label{:for => "email"} Email
%span.required-field (valid email required)
%span.field
%input#email{:name => "email", :type => "text", :value => @values[:email]}
%span.error= @errors[:email]
%p.form-field-row
%span.label
%label{:for => "website"} Website
%span.field
%input#website{:name => "website", :type => "text", :value => @values[:website] || "http://"}
%span.error= @errors[:website]
%p.form-field-row
%span.label
%label{:for => "message"} Message
%span.required-field (required)
%span.field
%textarea#message{:cols => "30", :name => "message", :rows => "8"}= @values[:message]
%span.error= @errors[:message]
%p.form-field-row.captcha
%span.label
%label{:for => "captcha"} Prove you're human!
%span.required-field (required)
%span.field
%input#captcha{:name => "captcha", :size => "4", :type => "text", :value => ""}
%input#captcha_id{:name => "captcha_id", :size => "4", :type => "hidden", :value => @captcha_id}
%img{:src => "http://captchator.com/captcha/image/#{@captcha_id}"}/
%span.error= @errors[:captcha]
%p.form-field-row.send
%input#send-button{:name => "send-button", :onclick => "return cforms_validate('', false)", :type => "submit", :value => "Submit"}

As I said earlier, you can of course use erb if you prefer. In that case, if you want to just use the same contact form as mine, you can find an erb version in this gist.

On a side note, Sinatra by defaults expects views to be in the views sub folder, but if you like me want to keep all views/layouts in one place, you can tell Sinatra to find them in the _layouts folder instead:

APP_ROOT = File.join(File.dirname(__FILE__), '..')

set :root, APP_ROOT
set :views, File.join(APP_ROOT, "_layouts")

Captcha verification

In the markup you can see some placeholders to display some validation errors, and you can see that I am using a quick and dirty trick to add a Captcha check: rather than having to implement it in my Sinatra app, I am just using a simple free service, Captchator [no longer available], which just works and has also been very reliable this far; it’s also extremely simple to integrate. The GET route that renders the contact form becomes:

get '/contact-form/?' do
  @captcha_id, @errors, @values, @sent = ActiveSupport::SecureRandom.hex(16), {}, {}, false
  haml :contact_form
end

Captchator expects a unique ID which I generate using the ActiveSupport::SecureRandom.hex method. Other instance variables also used in the template are here simply declared.

Checking whether the code entered by the user is valid or not, is very simple:

def valid_captcha? answer
  open("http://captchator.com/captcha/check_answer/#{@captcha_id}/#{answer}").read.to_i.nonzero? rescue false
end

Basically it simply requires an HTTP request (here I am using open-uri) to a URL on Captchator’s site that includes both the unique ID generated for each request/page view, and the answer or code entered by the user. The service will return 0 if the code was wrong, 1 if the code was correct instead. The dirty rescue false thingy there could be replaced with proper error handling… but I am too lazy so if something goes wrong the user gets to try again 🙂

Validating form data

The POST route in the Sinatra app, that is triggered when the user submits the contact form, looks as follows:

post '/contact-form/?' do
  @captcha_id = (params[:captcha_id] || ActiveSupport::SecureRandom.hex(16))
  @errors = validate(params)
  @values = params

  if @errors.empty?
    begin
      send_email(params, @env['REMOTE_ADDR'])
      @sent = true

    rescue Exception => e
      puts e
      @failure = "Ooops, it looks like something went wrong while attempting to send your email. Mind trying again now or later? :)"
    end
  end

  haml :contact_form
end

So a validation routine is executed, and then if all is OK the email is sent, otherwise the contact form is rendered once again and the validation errors displayed. If something goes wrong in the process (an exception occurs) a failure message is also displayed to notify the user.

The validate method simply checks the data entered by the user and returns a hash containing validation errors, if any:

def validate params
  errors = {}

  [:name, :email, :website, :message, :captcha].each{|key| params[key] = (params[key] || "").strip }

  errors[:name] = "This field is required" unless given? params[:name]

  if given? params[:email]
    errors[:email] = "Please enter a valid email address" unless valid_email? params[:email]
  else
    errors[:email] = "This field is required"
  end

  errors[:website] = "Please enter a valid web address" unless valid_url? params[:website]
  errors[:message] = "This field is required" unless given? params[:message]

  if given? params[:captcha]
    errors[:captcha] = "The code you've entered is not valid" unless valid_captcha? params[:captcha]
  else
    errors[:captcha] = "Please enter the code as shown in the picture"
  end

  errors
end

And here’s a few other helper methods that are used in the validation, together with the valid_captcha? we’ve already seen:

def valid_email?(email)
  if email =~ /^[a-zA-Z][\w\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.[a-zA-Z][a-zA-Z\.]*[a-zA-Z]$/
    domain = email.match(/\@(.+)/)[1]
    Resolv::DNS.open do |dns|
      @mx = dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
    end
    @mx.size > 0 ? true : false
  else
    false
  end
end

def valid_url? url
  return true if url == "http://"
  !(url =~ /^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/ix).nil?
end

def given? field
  !field.empty?
end

I’m using typical regular expressions to check the format of both email address and website URL, and to improve a little the email validation I also check whether the domain name in the email address has MX records, and therefore can correctly receive emails.

Sending the email

Finally, if the validation is successful, then the email is sent to the destination email address. I am using one of my favourite gems for this, Pony:'

def send_email params, ip_address
email_template = <<-EOS

Sent via http://vitobotta.com/contact/
When:
IP address:

Your name:
Email:
Website:

Message:

EOS

body = Liquid::Template.parse(email_template).render "name" => params[:name],
"email" => params[:email],
"website" => params[:website],
"message" => params[:message],
"when" => Time.now.strftime("%b %e, %Y %H:%M:%S %Z"),
"ip_address" => ip_address

Pony.mail(:to => "destination email address", :from => params[:email], :subject => "A comment from #{params[:name]}", :body => body)
end

Here I am using liquid to render the email template with the data, but also in this case you can use either erb or haml instead. I just prefer liquid in these cases.

Config.ru and dependencies

Since the Sinatra is just a Rack-based app, you will likely need a config.ru file in the app’s root directory, for the deployment to production. Here’s mine, you can just use the same:

require "rubygems"
require 'sinatra'

APP_ROOT = File.dirname(__FILE__)

set :environment, :production
set :root, APP_ROOT

require File.join(APP_ROOT, 'lib/blog.rb')

disable :run

FileUtils.mkdir_p 'log' unless File.exists?('log')
log = File.new("log/sinatra.log", "a")

$stdout.reopen(log)
$stderr.reopen(log)

run Sinatra::Application

Only thing to note here is that, as you can see, if you use this config.ru file as it is Sinatra will log everything to the log/sinatra.log file.

Also remember to required all the dependencies before any code:

%w(sinatra liquid active_support/secure_random resolv open-uri pony haml).each {|g| require g }

Of course you may have to require rubygems if you aren’t using Ruby 1.9.

Keeping the contact form… alive

One last tip: depending on which application server you use to run the Sinatra app, the app may be in idle at times if no incoming requests are processed for a while. This may have a nasty side effect, in that the static contact page will show up instantly as usual, while it may take several seconds before the Sinatra contact form also shows up in the iframe. A user may think the form is just missing, and go elsewhere.

To avoid this, the quickest trick I could think of was to schedule a request to the contact form each minute with cron. Just run crontab -e and add this if you want to do the same:

* * * * * /usr/bin/wget -O - -q -t 1 http://vitobotta.com/contact-form/ > /dev/null

This will have wget, locally, make a request to the contact form every minute, which in turn prevents the Sinatra app from being idle. It’s not the most elegant solution, but it works: the contact form always shows up immediately together with the main contact page, as if the contact form were rendered directly in the main page rather than in an iframe.

Conclusions

That’s it, you should now have a simple, working contact form in your Jekyll site.

You can also find a copy of the whole app in this gist, and add it to your Jekyll site in no time. Hopefully this will help those readers who have switched to Jekyll -or are going to- but are not familiar with either Ruby or Sinatra. If you have any questions or suggestions, please let me know in the comments.

(p.s.: to those who asked me weeks ago to write this post… sorry for the delay! I have been pretty busy lately with no much free time for the blog)

© Vito Botta