How we integrated the Ghost blogging platform

By Michael Argentini
Managing Partner, Technology and Design

We recently upgraded the Fynydd blog with Ghost, a blogging platform based on Node.js with Handlebars as a theme mechanism. The catch? Our site is running PHP on Windows Server 2012 with IIS 7.5. Node expects to serve its own content. And though it runs just fine on a Windows server, getting it to run alongside a web server like IIS can be challenging.

I must hate myself. Well, that's how it felt at first.

But ultimately, I couldn't think of a better first Node.js project! Hopefully the following review of how it went, tips, and resources, will help you in your next (or first) Node.js on Windows project as well.

So why did we decide to make the move? Well, we relaunched our site for 2014, and though I wrote a neat little multi-markdown blog, it was woefully inadequate for the long-term. Our lead user interface designer, Dane Troup, had recommended Ghost as a possible option, and as we looked at it, we decided it was worth a shot.

Some of the things we were looking for in a blog included:

  1. It had to be mobile optimized; even the authoring experience.
  2. It had to be fast!
  3. Creating a theme for it had to be really flexible, allowing it to be skinned with our custom responsive site framework.
  4. We wanted it to run in a subdirectory to ease the transition and sharing of CSS, and to leverage the existing Google Analytics configuration. But a subdomain could work as well.
  5. It should allow customization completely through themes; we wanted to leave the core codebase untouched for upgradeability and manageability.
  6. Authoring with markdown was a must.
  7. It would be nice to have live previews of markdown content when editing posts.
  8. We expected attention to detail in the UI; it should be as good as something we would write ourselves.
  9. Users should have easy content navigation through tags, a robust search, and author filters. Ghost didn't have a search feature, but we had ideas on how to write one for it.
  10. Great direct or indirect metadata support (e.g. Open Graph, microdata) for social sharing was not optional.
  11. It had to run on Windows Server 2012 through IIS.
  12. We needed a really approachable, modern authoring environment to encourage contributions from everyone at the company.


After some investigation, I found that we could accomplish just about everything we needed to with the core Ghost platform and in Beatles parlance, "with a little help from our friends".

In short, here's how the initial installation went down. For the most part I followed the recommended installation steps from the Ignite Technologies site.

  1. Install Node.js (64 bit), version 10.x from the download page. If you get the error “Error: ENOENT”, you need to manually create the folder “C:\Users\%USERNAME%\AppData\Roaming\npm".
  2. Install iisnode from the download page on Github to allow IIS to host Node.js web applications.
  3. Download Ghost 0.53 from the download page on Github. We recommend installing Ghost 0.55 as it fixes a few bugs for running it out of a subdirectory, and fixes a memory leak. We originally installed 0.53.
  4. Unzip Ghost it into its final resting place. Open up a Node.js Command Prompt as an Administrator (via right-click). Change the directory to the Ghost folder you created and use the following command to install Node for the production instance of Ghost. It should create a node_modules folder within the Ghost folder.

    npm install --production

  5. Add read permissions for the Ghost folder, and include write permissions in the Ghost /content/data/ path. The user account that you should assign these permissions to is the group MACHINE NAME\IIS_IUSRS.
  6. Add the site (or virtual application) to IIS.
  7. In the IIS site settings, choose "Modules", and remove the WebDAV module (or nothing will be writable in the Ghost admin). I found this nugget after a ton of searching. It was on a Russian web page that required translation. YOU'RE WELCOME. ;)
  8. Use the template web.config (see below) as a base.
  9. Use the template config.js (see below). Note the port setting "process.env.PORT", which allows Node.js to respond on the IIS port via iisnode.

Sample web.config file:

<?xml version="1.0" encoding="utf-8"?>
            <add name="iisnode" path="*.js" verb="*" modules="iisnode" />
                <rule name="LogFile" patternSyntax="ECMAScript" stopProcessing="true">
                    <match url="iisnode" />
                <rule name="DynamicContent">
                        <add input="{{REQUEST_FILENAME}}" matchType="IsFile" negate="True" />
                    <action type="Rewrite" url="index.js" />
      <iisnode node_env="production" nodeProcessCommandLine="&quot;%programfiles%\nodejs\node.exe&quot;" interceptor="&quot;%programfiles%\iisnode\interceptor.js&quot;" />
            <remove name="WebDAVModule" />

Sample Ghost config file (just the top and production portions):

var path = require('path'), config;

config = {
    // When running Ghost in the wild, use the production environment
    // Configure your URL and mail settings here
    production: {
        url: '',
        mail: {
            transport: 'SMTP',
            options: {
                service: 'Gmail',
                auth: {
                    user: '',
                    pass: 'password'
        database: {
            client: 'sqlite3',
            connection: {
                filename: path.join(__dirname, '/content/data/ghost.db')
            debug: false

        server: {
            host: '',
            port: process.env.PORT

First Attempt: Subdirectory

Initially we wanted to run the blog out of the same subdirectory as the previous one (e.g. /blog). To do this, instead of setting up an explicit site in IIS, I set up a virtual application (folder) within the existing site. In this case, the config.js had one difference from the example above. The url value was ...//

It worked pretty well. The original blog article URLs hadn't changed. And it was easy to reference the core site assets (e.g. CSS, images, Javascript) from the blog theme this way.

Here are some of the problems I ran into:

  1. Only 3 or fewer tags would appear at the bottom of an article. We had 4 or more on some posts, so this was a problem.
  2. URLs were case sensistive no matter what configuration settings I used. This only affected one article, so I simply re-posted the article to our various social accounts. But this could have required URL rewriting to fix (or further troubleshooting).
  3. The images were stored in the database with full paths, which made Open Graph image URLs duplicate the subdirectory (breaking them). This required a modification to the core Ghost codebase, which we didn't want to do but had no other choice (this was fixed in Ghost 0.5.4).
  4. Default author avatars pulled from Gravatar had invalid URLs in JSON microdata (this was fixed in Ghost 0.5.4 as well). (e.g. a sample bad URL looks something like ...//
  5. It was difficult to set up a local test environment on non-Windows machines (like our Macs).

Second Attempt: Subdomain

So although it was working pretty well, I decided to convert it to an explicit site running on a subdomain ( I did this primarily to address the fact that we were forced to modify the core Ghost codebase, creating a complexity for upgrades and maintenance.

I had to create URL rewrite rules within the web.config file to 301 redirect old blog articles to the new location. By doing this conversion we gained some reliability, though we had to address some other differences as well:

  1. All tags now appear on posts.
  2. Post images now have correct paths in Open Graph metadata.
  3. I decided to create a new Google Analytics site profile and track blog traffic separately, as opposed to creating filters.
  4. I had to copy the main site CSS to the blog theme when updates to it affect the blog theme. The site logo files were also copied.
  5. We can now update the core platform as new versions are released. In fact, we're currently running 0.55.
  6. It's easier to run on a local machine for development.
  7. The default avatars pulled from Gravatar were still not correctly linked in JSON microdata (this was fixed in Ghost 0.5.4).

Remaining Issues

So where does this leave us? We're pretty happy so far. The platform is really new, and lacks some basic features you'd find elsewhere, but we're interested in seeing where this goes.

Here are some of the more pressing issues we still have, many of which are (thankfully) on the Ghost roadmap for resolution.

  1. We cannot publish articles with a future date.
  2. Previews of posts using the site theme are not possible until the post is published (live).
  3. We had to write our own search in PHP by tapping into the SQLite database and using FTS4 for determining relevance. I'll post a separate article for this piece.
  4. The Ghost Handlebars API limitations (like the lack of conditional blocks) required the use of Javascript to do things like add icons to tag page headers.
  5. No general purpose media management is available, so leveraging the blog architecture to store and present things like cover images is not possible because we're using the post image feature to store the social sharing image, as that's more important to us.
  6. We didn't want a feedback or commenting mechanism, so the fact that Ghost is lacking this feature wasn't a big deal. But it could be for you.

Well, you're looking at the results. Again, I'm pretty happy with it so far. And I'm anxious for the platform to mature. But I'd still recommend that you try it out if you're looking for a blog with its feature set.

Visit the Ghost site for more information.

Article last updated on 4/21/2018