hckr.fyi // thoughts

Creating a Newsletter Sign-Up Form for Ghost and Mailgun

by Michael Szul on

I've been enjoying a few email newsletters recently, which says a lot about how far the Internet has come. I guess we survived the initial newsletters-as-information phase, which quickly trickled into the newsletters-as-marketing phase that eventually ended with newsletters-as-…spam. Newsletters are making a comeback, however, and it has largely to do with the indie scene. Indie content publishers--or even indie product makers--have given new life to the email newsletter, and are delivering quality content, devoid of spam, once or twice a week (sometimes less).

When I first started blogging again, I contemplated building my own blogging system because that's what I do. To put it into perspective, back in 2004 I built an HttpHandler on top of Mono that executed XSLT transformations to produce HTML content. This was inspired by an XSLT proprietary extension that was developed at the company for which I was working, but their language was only supported on Windows, and I wanted to run things on a Linux server. This XSLT technology that I developed made database calls, managed sessions, and accepted form and query string input. The HttpHandler created consumable XML from the server variables, query string variables, database results, etc. and passed that XML to an XSLT file with an *.xsp extension. This file was used to transform the data into the appropriate HTML.

With my latest attempt to blog on a regular basis, however, I needed to really soul search, and ask myself if creating my own blogging software was necessary. What did it accomplish? Did I need the programmer cred? No. Were there missing features? Maybe. Did those features really matter that much? Probably not. I made the decision to go with a pre-built solution, and at the time I was making this decision, I was exploring the Express framework in nodeJS. I happened to read a few articles on nodeJS and blogging, and was directed to Ghost as a blogging platform--even better, Ghost is available as an Azure Websites gallery image, which I was using at the time. I gave the blogging platform a try, and despite any missing features or quirks, it does what I need, and I'm quite happy with it.

After launching Codepunk, I decided I wanted to jump on the newsletter bandwagon. Ghost recommends Mailgun for your internal email usage, so I already had an account. I just needed to wire up a form to it. The problem was that Mailgun doesn't offer direct posting ability to whatever mailing list you create. You have to use the API. This meant interfacing with the API directly, or using an npm library.

I opted for the latter and installed mailgun-js, which seems to have an adequately fleshed out interface to the APIs. I then had to modify the index.js file of the Ghost installation to include a new route. I put the following code in it:

parentApp.get('/subscribe/', function (req, res) {
        var api_key = "YOUR_API_KEY";
        var domain = "YOUR_DOMAIN";
        var mailgun = require('mailgun-js')({ apiKey: api_key, domain: domain });
    
        var data = {
        subscribed: true
        ,address: req.query.EMAIL
        ,upsert: "yes"
        };
    
        var list = mailgun.lists('YOUR_MAILING_LIST');
        list.members().create(data, function (err, data) {
        });
        res.json({ message: "done" });
    });
    

The mailing list code is mostly from the mailgun-js examples.

Some important things to note that require further investigation: For one, this probably isn't the most appropriate place to put a custom route. Ghost documentation on the matter is sparse, and although you can put it with the rest of the routes in the frontend.js file, I feel that it's better to put it in the index.js file, and use the Express routing rather than the Ghost extensions. In reality, one would want to create an extensible package for placing custom routes in Ghost. It's just not at the top of my list.

The second thing to note is the res.json call. This will return a simple message of "done" whether the mailing list addition is successful or not. The reason? If placed within the function passed to the create the HTTP call will timeout. There seems to be a problem with the event handling for that particular method, and when a response is returned. I haven't had time to debug the npm library, so for now, I assume all additions are successful, even if it means a few failures that get unrecognized. The reality is that the call should only fail with an invalid email address or if Mailgun went down.

On the front-end, the HTML form looks like this (from the Aspire Ghost theme):

<aside class="widget">
        <h3 class="widget__title">Newsletter</h3>
        <p>Sign up for our newsletter--delivered every two weeks.</p>
        <div class="widget__body">
          <form action="/subscribe/" method="get">
            <input type="email" name="EMAIL" placeholder="Your email address">
            <input type="submit" class="button button--expand" value="SIGN UP">
          </form>
        </div>
      </aside>
    

Lastly, the JavaScript code (using jQuery) on the front-end for submitting the form, looks like this:

$("input[value='SIGN UP']").click(function(e) {
            e.preventDefault();
            $.ajax({
                    url: 'http://YOUR_DOMAIN/subscribe/'
                    , dataType: 'json'
                    , method: 'GET'
                    , data: { EMAIL: $("input[name='EMAIL']").val() }
                }).done(function (result) {
                    console.log(result);
                }).fail(function (jqxhr, textStatus, error) {
                    console.log(jqxhr);
                }).always(function () {
                    $("input[name='EMAIL']").val('');
                    $("input[name='EMAIL']").closest('.widget').append('<p>Thanks!</p>');
                });
        });
    

Absolutely nothing fancy, and plenty of room for improvement, but right now, it accepts valid emails, and adds those emails to the Codepunk newsletter list.

Some improvements that can be made for the future: