The New

You probably didn’t notice that we migrated our whole website from Drupal to Keystone.JS, but we did, and it was a great experience.

Why Not Drupal?

Drupal produced a fine end result, but we were experiencing too many pains in daily maintenance from a clunky admin and an annoying deployment process. The markup it generated was poor, and most of us at Vokal are not fans of PHP, which is the language on which Drupal is built.

Why Keystone.JS

Keystone.JS is a newer CMS based on Node.js and MongoDB that strikes an excellent balance between simplicity and flexibility. It’s free, open source and MIT licensed. Because Node.js is all JavaScript, a single developer is now easily able to manage the full stack in a language they already know.

The Migration

We have lots of updates coming to our website now that it’s migrated, but the first thing we wanted to do was move the existing layout and content.

Static Content

We started by copying all the CSS out of the current site and breaking it out into LESS documents. As we copied views from Drupal to Keystone we stripped out unnecessary markup and simplified LESS selectors along the way. We copied existing images for static content pages into the site files, and updated paths as needed.

All of the Drupal content was managed through the CMS, but because we have full access to the site code we found it simpler to move some infrequently updated content out of the database and into static site files.

CMS Content

Keystone organizes everything into lists, where each item in the list is a document in MongoDB. MongoDB stores all data in documents, which are JSON-style data structures. Keystone uses the MongoDB ODM Mongoose for data validation. Creating a model in Keystone is therefore very similar to creating a JSON document or a Mongoose model. For example, this is the model we use for team members that appear on

var TeamMember = new keystone.List( "TeamMember", {
    autokey: { path: "slug", from: "name", unique: true }
} );

TeamMember.add( {
    name: { type: Types.Name, required: true, "default": { first: "", last: "" } },
    profileImage: { type: Types.S3File },
    listImage1: { type: Types.S3File },
    listImage2: { type: Types.S3File },
    title: { type: String },
    tagLine: { type: String },
    featured: { type: Boolean },
    featuredOrder: { type: Number },
    state: { type: Types.Select, options: "draft, published, archived", "default": "draft", index: true },
    publishedDate: { type: Types.Date, index: true, dependsOn: { state: "published" } },
    content: {
        extended: { type: Types.Html, wysiwyg: true, height: 400 }
} );

This defines all the fields that we keep track of for each team member. Looking at our Mongo database, you would see almost the exact same structure in each document.

By comparison, Drupal runs in a relational database like MySQL and essentially stores every one of those values in a different table that has a relationship to a parent node which is the root of the item. To give you some idea of how much less intuitive that structure is, this is the SQL that was run to export team members out of the Drupal database:

SELECT node.title as fullName,
fquote.field_team_member_quote_value as tagLine,
fbody.body_value as content
fpos.field_job_position_value as title,
firstN.field_first_name_value as firstName,
lastN.field_last_name_value as lastName
FROM `vokal-io`.node
left join field_data_field_team_member_quote as fquote on fquote.entity_id = node.nid
left join field_data_body as fbody on fbody.entity_id = node.nid
left join field_data_field_job_position as fpos on fpos.entity_id = node.nid
left join field_data_field_last_name as lastN on lastN.entity_id = node.nid
left join field_data_field_first_name as firstN on firstN.entity_id = node.nid
where node.type='team_member';

The above includes five joins despite not even including any of the image fields, which probably would have resulted in eight joins to visualize what is one simple document in Keystone.

Careers Widget

One of the changes we did make during the migration was to replace a RecruiterBox widget we were using on our careers page with pages that were integrated into the site. This gave us some additional layout and management options, as well as being more SEO-friendly on what are very important pages.

Behind the scenes, Keystone is now checking the RecruiterBox API for changes in the background and then updating the MongoDB accordingly. In the Keystone admin, we are able to associate those openings with pages on our website using a relationship field type.


URLs and SEO

We were able to maintain all of our existing routes and SEO configuration during the migration. Keystone uses the excellent routing from Express. SEO titles and descriptions were maintained simply by having the master layout look for a consistently named item which could be loaded from Mongo or added directly to the view.

You may have noticed in the TeamMember model above that there is an S3File type for each image field. For these image fields, we manually uploaded files into Keystone and the S3File type automatically handles storing the files to an S3 bucket we created on AWS. As part of our implementation we found the WYSIWYG editor didn’t support storing files to S3, but it only took a couple hours to fork Keystone, add the feature, and open a pull request back to the project.


Out of the box, Keystone only has a couple levels of user access, but we were able to create some middleware so we can grant users access to some features but restrict others. Our use case is that we want everyone at Vokal to be able to add blog posts, but not everyone should be able to edit our job listings.

Other than that small customization, the Keystone admin is clean and fast out of the box. The UI is built on React, the search works flawlessly, and it Just Works™.


We are now hosting on Elastic Beanstalk, except for images uploaded through the admin which are in an S3 bucket. The database is hosted at MongoLab, mostly because it is easy to use. I personally had a bit of a learning curve with Elastic Beanstalk, but since the first deployment got pushed, making updates has been a breeze.

What’s Next?

We are looking forward to making additional updates to our site, but also excited to use Keystone.JS on more projects in the near future. Send us a message if you have any questions or comments about our experience.