Integrating Ghost with KeystoneJS

Ghost is an open-source blogging platform that runs on Node.js. It features a theming system , multi-user blogging capabilities and a markdown editor that lets you see a live preview of the content you are working on.

In this post, I will show you how you to integrate Ghost with KeystoneJS without the need for redirections to another domain or subdomain. By the end of this article, we should have a KeystoneJS sample app running on Heroku that uses Ghost as its blogging platform.

1. Prerequisites

My local environnment is a Linux machine installed with node, npm, git, mongoDB and the Heroku toolbet:

$ uname -a
Linux nic-Aspire-5251 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux  
$ node -v
v0.10.36  
$ npm -version
1.4.28  
$ heroku version
heroku-toolbelt/3.25.0 (x86_64-linux) ruby/1.9.3  
$ git version
git version 1.9.1  
$ mongod -version
db version v2.6.7  
2015-02-10T22:11:18.004-0500 git version: a7d57ad27c382de82e9cb93bf983a80fd9ac9899  

IMPORTANT! Ghost requires Node v0.10.x

Ghost also requires a relational database to be installed. I will use a mysql server, but you might want to use a PostgreSQL database if you deploy your app to Heroku and need to sync local and production databases.

~/sandbox$ sudo apt-get install mysql-server
... (will ask for a password..)
~/sandbox$ ps -aux | grep "mysql*"
mysql     1077  0.0  0.8 887508 24656 ?        Ssl  10:22   0:17 /usr/sbin/mysqld  

Let's create an empty database that Ghost will use for storing its data:

~/sandbox$ sudo mysql -p
Enter password:  
Welcome to the MySQL monitor.  Commands end with ; or \g.  
Your MySQL connection id is 886  
Server version: 5.5.41-0ubuntu0.14.04.1 (Ubuntu)

Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its  
affiliates. Other names may be trademarks of their respective  
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> create database ghostDB;  
Query OK, 1 row affected (0.02 sec)

mysql>  

2. Creating a KeystoneJS app

For this post, I will assume that you can install and successfully run a KeystoneJS sample app on your local dev machine using the Yeoman generator1:

~/sandbox$ yo keystone

Welcome to KeystoneJS.  
? What is the name of your project? keystone-ghost
? Would you like to use Jade, Swig, Nunjucks or Handlebars for templates? [jade | swig | nunjucks | hb? Would you like to use Jade, Swig, Nunjucks or Handlebars for templates? [jade | swig | nunjucks | hbs] jade
? Would you like to use LESS or SASS for stylesheets? [less | sass] less
? Would you like to include a Blog? No
...
------------------------------------------------

Your KeystoneJS project is ready to go!

~/sandbox/keystone-ghost$ node keystone.js

------------------------------------------------
KeystoneJS Started:  
keystone-ghost is ready on port 3000  
------------------------------------------------

We answered 'no' when asked to include a Blog to our KeystoneJS app. As a consequence, the generator will not include a Blog link in the top navigation bar of our welcome page.

Let's add the link back by editing middleware.js, located under the /routes folder:

exports.initLocals = function(req, res, next) {

    var locals = res.locals;

    locals.navLinks = [
        { label: 'Home',    key: 'home',    href: '/' },
    // add blog entry here
        { label: 'Blog',    key: 'blog',    href: '/blog' },
        { label: 'Gallery',    key: 'gallery', href: '/gallery' },
        { label: 'Contact', key: 'contact', href: '/contact' }
    ];

    locals.user = req.user;

    next();

};

3. Using Ghost as a node module

We want to run the Ghost blogging platform as a sub application to our KeystoneJS app. Let's install Ghost as a node module2:

~/sandbox/keystone-ghost$ npm install ghost --save
~/sandbox/keystone-ghost$ npm list ghost
keystone-ghost@0.0.0 /home/nic/sandbox/keystone-ghost  
└── ghost@0.5.8 

EDIT 2015.03.10

It's been reported (see comments below) that npm install ghost --save can install a module that corresponds to this github project: https://github.com/ecto/ghost. This is the wrong module. Hopefully, npm will resolve this issue quickly.

You should now have ghost in your package.json dependencies. Open the keystone.js file and make the following changes to it:

At the top of the keystone.js file

// Simulate config options from your production environment by
// customising the .env file in your project's root folder.
require('dotenv').load();

// Require keystone
var keystone = require('keystone'),  
    app = keystone.express(),    // create an app instance 
    path = require('path'),
    ghost = require('ghost');   

keystone.init({  
    'name': 'keystone-ghost',
    'brand': 'keystone-ghost',  
    'less': 'public',
    'static': 'public',
    'favicon': 'public/favicon.ico',
    'views': 'templates/views',
    'view engine': 'jade',
    'app': app,                    // add this option here
    'emails': 'templates/emails',
    'auto update': true,
    'session': true,
    'auth': true,
    'user model': 'User',
    'cookie secret': ')"36]MBV[ySg,TH%"i@lPbAu544,^dJmbi=8"|9x`q.@aB_V"GXp%pF_b4@{*%l%'
});

REMARK:

Instead of letting Keystone create an express instance for us, we explicitly create one and pass it to Keystone as an option property3.

At the bottom of the keystone.js file

// add this function call instead of keystone.start()
ghost({  
    config: path.join(__dirname, 'ghostConfig.js')
}).then(function(ghostServer) {
    app.use('/blog', ghostServer.rootApp);
    keystone.start();
});

// comment out the keystone.start line
//keystone.start();    

REMARK:

The Ghost module exports a function, declared as ghost in the above, that returns a Javascript promise of a GhostServer object. The callback takes a reference to this object. The first line of the callback mounts on '/blog' an express app instance that was created by Ghost. The ghostServer object property rootApp refers to what Ghost calls the blogApp. It's the app that will handle requests coming off your blog urls4.

We can pass an options object to the ghost function. Currently, the only property supported is config, which specifies the location of a ghost configuration file. Ghost uses the configuration file to understand how it should run for various environments. The Ghost module source code contains a template configuration file that we can edit for our current local environment:

~/sandbox/keystone-ghost$ cd node_modules/ghost
~/sandbox/keystone-ghost/node_modules/ghost$ ls -a
.   bower.json  config.example.js  core          index.js  LICENSE       .npmignore    PRIVACY.md
..  .bowerrc    content            Gruntfile.js  .jscsrc   node_modules  package.json  README.md
~/sandbox/keystone-ghost/node_modules/ghost$ cp config.example.js ~/sandbox/keystone-ghost/ghostConfig.js

Open the ghostConfig.js file and make the following changes to the development property of the config object:

    development: {
        // The url to use when providing links to the site, E.g. in RSS and email.
        // Change this to your Ghost blogs published URL.
        url: 'http://localhost:3000/blog',       // modify this line

        database: {
            client: 'mysql',                     // changed to mysql
            connection: {                        
                host     : '127.0.0.1',          // added host
                user     : 'root',                 // added user
                password : 'root',                 // added password
                database : 'ghostDB',             // added db name
                charset  : 'utf8'                 // added charset
            },
            debug: false
        },
        server: {
            // Host to be passed to node's `net.Server#listen()`
            host: '127.0.0.1',
            // Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT`
            port: '3000'                         // changed port number
        },
        paths: {                                // add paths property
            contentPath: path.join(__dirname, '/content/')
        }
    },

REMARK:

The contentPath property specifies the location of the content folder used by Ghost for apps, data, images and themes. You can specify an alternative location by changing its value.

The last step before starting the web server is to copy the content folder found under node_modules/ghost to our app's root directory:

~/sandbox/keystone-ghost/node_modules/ghost$ cp -rf content ~/sandbox/keystone-ghost/

~/sandbox/keystone-ghost$ ls -ah
.                  content        ghostConfig.js  .gitignore   models        Procfile  templates
..                 .editorconfig  ghostconfig.js  .jshintrc    node_modules  public    updates
config.example.js  .env           ghost.md        keystone.js  package.json  routes  

Let's start our keystone app:

~/sandbox/keystone-ghost$ node keystone.js
Migrations: Up to date at version 003

------------------------------------------------
KeystoneJS Started:  
keystone-ghost is ready on port 3000  
------------------------------------------------

Warning: Ghost no longer starts automatically when using it as an npm module.  
 If you're seeing this message, you may need to update your custom code. 
 Please see the docs at http://tinyurl.com/npm-upgrade for more information. 

REMARK:

The warning printed by Ghost is displayed automatically after 5 seconds unless Ghost starts the web server for us (using the ghostServer.start function). In our context, however, we do not want Ghost to start the web server5. The warning can be safely ignored.

If you click the Blog link on the welcome page, you should see Ghost's home page:

SUCCESS!!! You are now running Ghost as a sub-app to your KeystoneJS app.

4. Deploying to Heroku

Heroku offers various datastores as add-ons. We will use the PostgresSQL add-on, which has a free plan with a 10K row limit. First, we need to prepare the KeystoneJS app to be deployed to Heroku by adding the MongoLab add-on and setting some config vars1 :

~/sandbox/keystone-ghost$ git init .
Initialized empty Git repository in /home/nic/sandbox/keystone-ghost/.git/  
~/sandbox/keystone-ghost$ git add .
~/sandbox/keystone-ghost$ git commit -am "init commit"
[master (root-commit) ccf6f6d] init commit
 153 files changed, 36054 insertions(+)
...
~/sandbox/keystone-ghost$ heroku create
Creating salty-sierra-2843... done, stack is cedar-14  
https://salty-sierra-2843.herokuapp.com/ | https://git.heroku.com/salty-sierra-2843.git  
Git remote heroku added

~/sandbox/keystone-ghost$ heroku apps:rename keystone-ghost
Renaming salty-sierra-2843 to keystone-ghost... done  
https://keystone-ghost.herokuapp.com/ | https://git.heroku.com/keystone-ghost.git  
Git remote heroku updated

~/sandbox/keystone-ghost$ heroku config:set CLOUDINARY_URL=cloudinary://333779167276662:_8jbSi9FB3sWYrfimcl8VKh34rI@keystone-demo
Setting config vars and restarting keystone-ghost... done, v3  
CLOUDINARY_URL: cloudinary://333779167276662:_8jbSi9FB3sWYrfimcl8VKh34rI@keystone-demo

~/sandbox/keystone-ghost$ heroku config:set NODE_ENV=production
Setting config vars and restarting keystone-ghost... done, v14  
NODE_ENV: production

~/sandbox/keystone-ghost$ heroku addons:add mongolab
Adding mongolab on keystone-ghost... done, v4 (free)  
Welcome to MongoLab.  Your new subscription is being created and will be available shortly.  Please consult the MongoLab Add-on Admin UI to check on its progress.  
Use `heroku addons:docs mongolab` to view documentation.  

Once these steps are performed, we can add the PostgresSQL add-on to our Heroku app.

~/sandbox/keystone-ghost$ heroku addons:add heroku-postgresql
Adding heroku-postgresql on keystone-ghost... done, v6 (free)  
Attached as HEROKU_POSTGRESQL_RED_URL  
Database has been created and is available  
 ! This database is empty. If upgrading, you can transfer
 ! data from another database with pgbackups:restore.
Use `heroku addons:docs heroku-postgresql` to view documentation.

Open your ghostConfig.js file and edit the production object property:

    production: {
        url: 'http://keystone-ghost.herokuapp.com/blog',   // add your url
        mail: {},
        database: {
            client: 'postgres',
            connection: {
                host: process.env.POSTGRES_HOST,          // add host
                user: process.env.POSTGRES_USER,          // add user
                password: process.env.POSTGRES_PASSWORD,  // add password
                database: process.env.POSTGRES_DATABASE,  // add db name
                port: '5432'
            },
            debug: false
        },
        fileStorage: false,                            // turn off file storage
        server: {
            // Host to be passed to node's `net.Server#listen()`
            host: '0.0.0.0',                           // let heroku figure it out
            // Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT`
            port: process.env.PORT                     // use assigned port number
        },
        paths: {
            contentPath: path.join(__dirname, '/content/')
        }
    },

Obtain your POSTGRES database credentials using heroku pg:credentials and set the corresponding process environment variables using heroku config:set:

~/sandbox/keystone-ghost$ heroku pg:credentials DATABASE
Connection info string:  
   "dbname=dc63fs7hi45svb host=ec2-54-197-249-212.compute-1.amazonaws.com port=5432 user=lfeuordvsgaftr password=0ZS6YxRG9edAvx38mjlY9Qywaw sslmode=require"
Connection URL:  
    postgres://lfeuordvsgaftr:0ZS6YxRG9edAvx38mjlY9Qywaw@ec2-54-197-249-212.compute-1.amazonaws.com:5432/dc63fs7hi45svb

~/sandbox/keystone-ghost$ heroku config:set POSTGRES_HOST=ec2-54-197-249-212.compute-1.amazonaws.com
Setting config vars and restarting keystone-ghost... done, v7  
POSTGRES_HOST: ec2-54-197-249-212.compute-1.amazonaws.com

~/sandbox/keystone-ghost$ heroku config:set POSTGRES_USER=lfeuordvsgaftr
Setting config vars and restarting keystone-ghost... done, v8  
POSTGRES_USER: lfeuordvsgaftr

~/sandbox/keystone-ghost$ heroku config:set POSTGRES_PASSWORD=0ZS6YxRG9edAvx38mjlY9QywawSetting config vars and restarting keystone-ghost... done, v9
POSTGRES_PASSWORD: 0ZS6YxRG9edAvx38mjlY9Qywaw

~/sandbox/keystone-ghost$ heroku config:set POSTGRES_DATABASE=dc63fs7hi45svb
Setting config vars and restarting keystone-ghost... done, v10  
POSTGRES_DATABASE: dc63fs7hi45svb  

Before deploying our app to Heroku, we need to make sure Heroku uses a Node version that is 0.10.x. Open your package.json file and change the caret range for a tilde range for the node property:

{
  "name": "fred",
  "version": "0.0.0",
  "private": true,
  "dependencies": {
    "async": "~0.9.0",
    "dotenv": "~0.4.0",
    "ghost": "^0.5.8",
    "keystone": "~0.3.0",
    "underscore": "~1.7.0"
  },
  "devDependencies": {
    "gulp": "~3.7.0",
    "gulp-jshint": "~1.9.0",
    "jshint-stylish": "~0.1.3",
    "gulp-watch": "~0.6.5"
  },
  "engines": {
    "node": "~0.10.22",  
    "npm": ">=1.3.14"
  },
  "scripts": {
    "start": "node keystone.js"
  },
  "main": "keystone.js"
}

Commit your changes and deploy to Heroku:

~/sandbox/keystone-ghost$ git commit -am "fix node version and ghost production settings"
[master c4103de] fix node version and ghost production settings
 2 files changed, 15 insertions(+), 7 deletions(-)

~/sandbox/keystone-ghost$ git push heroku master
Counting objects: 187, done.  
Delta compression using up to 8 threads.  
Compressing objects: 100% (175/175), done.  
Writing objects: 100% (187/187), 392.51 KiB | 0 bytes/s, done.  
...

After Heroku is finished deploying your app, open your browser to keystone-ghost.herokuapp.com and you should see a welcome page with a link to your Ghost blog!
Clicking on the blog link should direct you to the blog's home page :

Voila! You now have a KeystoneJS app that runs Ghost for its blogging platform.


Combining KeystoneJS and Ghost can be done relatively quickly and gives you an alternative to running your blog using KeystoneJS, which might require a bit more tweaking to make it look as you intend it. Ghost's live-preview markdown editor is a killer feature that attracted me to this system. It really makes the process of writing posts more enjoyable. Maybe it's something Keystone can integrate down the line...

The source code for the keystone-ghost app is available
on github: (https://github.com/infocinc/keystone-ghost.git)

Hope you enjoyed this post,

Nic


Footnotes

  1. Newcomers to KeystoneJS: please see this post for step-by-step instructions on how to accomplish this task.

  2. Ghost is still at major version zero, which according to the semver spec, implies that the public API may still be unstable. Hence, I would avoid the use of semver ranges (tilde and caret) until Ghost evolves to major version 1.0.

  3. The old way of passing an app to Keystone was to use the connect function. This is now deprecated in version 0.3.0. See (http://localhost:8080/docs/configuration#options-project) for more information.

  4. This excludes urls of the form /blog/ghost/*. These urls are handled by the adminApp, created by Ghost and mounted on /blog/ghost.

  5. An alternative to starting the web server using Keystone.start is to let Ghost start the web server and listen for incoming connections by invoking the ghostServer.start() function.