A contact form for Jekyll, powered by Sinatra

sinatra-contact-form-jekyll

Contents: Introduction The Sinatra app Rendering only the contact form in an iframe Rendering the whole contact page with Sinatra/Liquid The full contact form template Captcha verification Validating form data Sending the email Config.ru and dependencies Keeping the contact form… alive Conclusions

Introduction

The previous post was a quite detailed guide on building static sites with Jekyll, including tips on how to migrate data from an existing WordPress blog – among many other things. 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.

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 app

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:

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:

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:

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:

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:

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:

a) to make sure both Sinatra and Jekyll can find the layouts since liquid expects layouts to have file names with the .liquid extension;

b) 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:

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:

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

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 _:

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

would become

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:

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:

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, 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:

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:

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:

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:

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

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:

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:

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:

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:

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)




Have your say!

Please see my comment policy if this is your first time here or if you have any questions regarding comments.