How to add a Contact Form to your Ghost blog using Mailgun

Ghost is a simple, beautiful blogging platform. Over it's competitor Wordpress it has the style, simplicity and elegance. Unfortunately though, if you want to make use of the CMS beyond the basics that Ghost provides you may struggle. As a blogging platform it's still in it's infancy and it doesn't yet support plugins or apps fully.

For this blog I had need of a contact form and I couldn't find anything anywhere near the quality that I desired. Top Google search reveals a Ghost Support answer recommending the use of 123ContactForm.com - my initial thought? “What the hell are they thinking?” I saw that and glazed over the rest - time to crank out some code I guess...

Avoid proprietary software lock-in at all costs

Using a proprietary vendor locks you into their workflow and creates a horrible mess of a platform. You don't want to end up with a Frankenstein monster-style mess of a blog so don't use proprietary unless it's absolutely a no-brainer. You're much better investing some time yourself in building your contact form and platform (and blog) out. That's exactly what I decided to do.

Mailgunner - A lightweight Mail server

I found Mailgun API hosted by Rackspace which provides a really nice, clean and slick API for generating emails. There are a bunch of handy clients written in all sorts of different languages too so it's fairly easy to integrate with. Due to the fact that Ghost is written in node.js I opted for the node.js client as it was the most well supported client. It was pretty seamless when I tried it.

I opted for a really simple Express app to act as my REST framework. Take a look at the Mailgunner source on my Github repository for more information. First there's a simple bin/www.js file that acts a wrapper for the starting the app.js file:

var app = require('../app'); //Require our app

app.set('port', process.env.PORT || 3030);

var server = app.listen(app.get('port'), function() {  
  console.log('Express server listening on port ' + server.address().port);
});

Then I create the app.js file which pulls in express and the various libraries required to enable use of the API such as specifying it supports json and the default routes are all located under /api:

var express = require('express'),  
    bodyParser = require('body-parser'),
    mailgunner = require('./routes/mailgunner');

var app = express();

//configure body-parser
app.use(bodyParser.json());  
app.use(bodyParser.urlencoded({ extended: false }));  
app.use('/api', mailgunner); //This is our route middleware

module.exports = app;  

The crux of the logic is all located within the routes/mailgunner.js file. Really though, there's not much to it. All it does it grabs config information for Mailgun from a specific location in the app (in our case config/default.json), builds the data block to be sent to mailgun from the API data provided and processes the response reporting whether or not it errored.

var express = require('express'),  
    config = require('config'),
    Mailgun = require('mailgun-js');


var router = express.Router(),  
    api_key = config.get('Mailgun.apiKey'),
    domain  = config.get('Mailgun.domain'),
    sender  = config.get('Mailgun.sender');

router.route('/submit/:mail').post(function(req,res) {  
    var mailgun = new Mailgun({apiKey: api_key, domain: domain});
    var name = req.body.name,
        email = req.body.email,
        subject = req.body.subject,
        htmlBody = req.body.htmlBody;

    var sender;
    if (name !== undefined) {
      sender = name + '<' + email + '>';
    } else {
      sender = email;
    }

    var data = {
      from: sender,
      to: req.params.mail,
      subject: subject,
      html: htmlBody
    }

    mailgun.messages().send(data, function (err, body) {
        if (err) {
            res.send('error', { error : err});
            console.log("Error trying to send email: ", err);
        }
        else {
            res.send("OK");
        }
    });
});

module.exports = router;  

This just leaves our config/default.json that we mentioned that contains our Mailgun API keys etc:

{
  "Mailgun": {
    "apiKey":  "YOUR_API_KEY",
    "domain":   "yourdomain.com",
    "sender": "someemail@gmail.com"
  }
}

Contact Form

Now that we know we can make a call to our local Mailgunner app that's co-hosted on the same box as our Ghost blog, we're free to finish up and create our contact form. Now, I went a bit over board with my form and started implementing lots of the fancy features around Twitter Bootstrap 3 making use of the interactive progress bars and interactive status updates. I also added form validation which you probably need. Your contact form doesn't have to be as feature-rich as this and could work perfectly well implemented in a more simple manner but I decided to include it here for brevity.

Create a page called page-contact.hbs in the root of your Ghost blog theme and place the following html:

{{!< default}}
<form id="contact-us-form" data-toggle="validator" role="form">  
  <div class="form-group">
    <div class="input-group input-group-lg name">
      <span class="input-group-addon" id="sizing-addon2"><span class="glyphicon glyphicon-user"></span></span>
      <input type="text" id="contact-name" class="form-control" placeholder="Name" data-error="Please enter your name." aria-describedby="sizing-addon2" required>
    </div>
    <div class="help-block with-errors"></div>
  </div>
  <div class="form-group">
    <div class="input-group input-group-lg email">
      <span class="input-group-addon" id="sizing-addon2"><span class="glyphicon glyphicon-envelope"></span></span>
      <input type="email" id="contact-email" class="form-control" placeholder="Email" data-error="Email address is invalid." aria-describedby="sizing-addon2" required>
    </div>
    <div class="help-block with-errors"></div>
  </div>
  <div class="form-group">
    <div class="input-group input-group-lg website">
      <span class="input-group-addon" id="sizing-addon2"><span class="glyphicon glyphicon-globe"></span></span>
      <input type="url" id="contact-website" class="form-control" placeholder="Website" data-error="Please enter a valid URL (must start http://*)." aria-describedby="sizing-addon2">
    </div>
    <div class="help-block with-errors"></div>
  </div>
  <div class="form-group">
    <div class="input-group-lg title">
      <input type="text" id="contact-subject" class="form-control" placeholder="Subject" data-error="Please enter a subject." aria-describedby="sizing-addon2" required>
    </div>
    <div class="help-block with-errors"></div>
  </div>
  <div class="form-group">
    <div id="contact-us-message" class="input-group-lg message">
      <textarea id="contact-message" class="form-control" placeholder="Enter a message" data-error="Please enter a message" rows="4" required></textarea>
    </div>
    <div class="help-block with-errors"></div>
  </div>
  <button id="contact-us-send-message" type="submit" onsubmit="return false;" class="btn btn-custom orange-button">
    <span class="glyphicon glyphicon-send" aria-hidden="true"></span>
    <span class="no-pointer-events contact-us-button-text">SEND MESSAGE</span>
  </button>
  <div class="email-progress-wrapper">
    <p id="email-progress-text">Email Status - READY</p>
    <div class="progress email-progress">
      <div class="progress-bar progress-bar-info progress-bar-striped active" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
        <span class="sr-only">0% Complete</span>
      </div>
    </div>
  </div>
</form>  

If all of this markup confuses, don't worry about it. I used @1000Hz Twitter Bootstrap Validator plugin for my form validation. Basically because it's feature rich and makes things fairly easy.

You'll need to download and include the dist files from 1000Hz project from Github. Save the validator.min.js in the js/ folder and include it in your html markup to be pulled in. I did this in my themes default.hbs file.

I did mention it was fairly easy to get the validation working didn't I? Well, it wasn't without it's issues. I always tend to try and make more work for myself and the interactive progress bars definitely did because there were issues with trapping the onSubmit events fired by the validator. As a consequence I had to disable the onSubmit event for the button. You need to declare the button of type="submit" in the form because otherwise the Validator plugin won't pick it up.

So without further ado, here's the contact.js file I wrote for performing the Contact form processing, email and validation:

/*!
 * Ghost email validator v1.0.0, by @jmurphyuk
 * Copyright 2015 James Murphy
 * Uses Validator by Cina Saffary (see https://github.com/1000hz/bootstrap-validator)
 * Licensed under http://opensource.org/licenses/MIT
 */
$(document).ready(function() {

  var formSuccess = true;

  $('*').on('invalid.bs.validator', function(e) {
    formSuccess = false;
  });

  $('#contact-us-send-message').click(function(e) {
    formSuccess = true;
    $('#contact-us-form').submit(false);
    $('.progress-bar').addClass('active');
    $('.progress-bar').addClass('progress-bar-info');
    $('.progress-bar').removeClass('progress-bar-danger');
    $('.progress-bar').removeClass('progress-bar-success');
    $('.email-progress').css('visibility', 'visible');

    var name = $('#contact-name').val(),
        email = $('#contact-email').val(),
        website = $('#contact-website').val(),
        subject = $('#contact-subject').val(),
        message = $('#contact-message').val(),
        postBody,
        emailTo = "youremail@gmail.com";

    var processDetails = function(cb) {
      setProgress(20, 'Analysing details...', 800,
        function() {
          if (formSuccess === false) {
            $('.progress-bar').removeClass('progress-bar-info');
            $('.progress-bar').removeClass('progress-bar-success');
            $('.progress-bar').addClass('progress-bar-danger');
            $('.progress-bar').removeClass('active');
            $('#email-progress-text').html('Form details invalid - please review errors');
          } else {
            cb();
          }
        }
      );
    };

    var buildPostBody = function(cb) {
      postBody = {
        "name": name,
        "email": email,
        "subject": subject,
        "htmlBody":
          "<html><h1>[DevAngst.com] - Contact Form</h1>" +
            "<p>Name: " + name + "</p>" +
            "<p>Email: " + email + "</p>" +
            "<p>Website: " + website + "</p>" +
            "<p>Message: " + message + "</p>" +
          "</html>"
      }

      setProgress(40, 'Generating payload...', 800, cb);
    };

    var processSuccess = function(cb) {
      setProgress(100, 'Email launched - SUCCESS!', 0,
      function() {
        $('.progress-bar').removeClass('progress-bar-info');
        $('.progress-bar').addClass('progress-bar-success');
        $('.progress-bar').removeClass('active');
      });
    };

    var sendEmail = function(cb) {
      setProgress(60, 'Launching email...', 1000,
        function() {
          $.ajax({
            type: "POST",
            url: 'http://mailgunner-server.com:3034/api/submit/' + emailTo,
            data: postBody,
            success: processSuccess(cb),
            dataType: 'json'
          });
        }
      );
    };

    setProgress(0, 'Engaging thrusters...', 1000,
      function() {
        processDetails(
          function() {
            if (formSuccess === false) {
            } else {
              buildPostBody(sendEmail);
            }
          }
        );
      }
    );
  });
});

var setProgress = function(progress, text, delay, cb) {  
    $('.email-progress .sr-only').html(progress + '% Complete');
    $('.progress-bar').css('width', progress + '%');
    $('#email-progress-text').html(text);
    setTimeout(cb, delay);
};

Again, probably a lot of fluff and fancy stuff you don't need but I liked the fact that it gave the person submitting the impression my site is working in the background.

Unfortunately, none of this stuff works with javascript turned off but if I'm honest if you have javascript turned off none of my site will work anyway. You could just opt to serve up your email address instead and different page using <noscript> rather than try and get fancy for the 0.0001% of users without js.

Making Mailgunner available

You're missing one more critical piece of the puzzle to finish it all off. You need to set up Mailgunner so that it's accessible from the contact form. Although, they'll both be located on the same box effectively the javascript will be executed from the browser and not from the Ghost server so you'll still need to make Mailgunner outwardly accessible to the internet.

To do this you need to open up the port so it's accessible to the internet. I'm on AWS so I configured my Security Group for the blog to allow access. I also configured a subdomain CNAME record for mail.devangst.com on port 3032 so that it was accessible to the internet. Still this isn't the last piece of the puzzle because you then need to configure your host that you're using for Ghost. I'm using nginx so I configured my nginx to include the sub-domain mail.devangst.com, again on port 3032. After a restart of nginx we're there. Mailgunner should be ready to use.

One final thing to make your Contact Form accessible in Ghost. You need to add a page called “Contact” post and mark it static in the Ghost editor. Ghost will then automatically pick up the page-contact.hbs file and use the mark-up you've defined in that page. Make sure you give it a pretty url in the settings as well to make it SEO friendly.

You can see the results on the contact form on my blog.

If you enjoyed these posts don't forget to subscribe to my RSS feed so that you never miss an updated and follow me on Twitter @developerangst to get other updates. You can also follow my personal Twitter @jmurphyuk if you'd like to hear me ramble on about random topics!

So that's it. Thanks for reading and I hope you enjoyed it.

James Murphy

Java dev by day, entrepreneur by night. James has 10+ years experience working with some of the largest UK businesses ranging from the BBC, to The Hut Group finally finding a home at Rentalcars.

Manchester, UK
comments powered by Disqus