It has been a long time goal of mine to move my blog off of Ghost and host it myself here. I loved using Ghost and could never bring myself to make the switch, but finally decided to take the plunge.

I originally got the inspiration for using Prember from Chris Manson (@mansona) from Stone Circle, who was actively working on improving several of the Ember learning sites, like the official Ember guides and getting them to run as static sites with Prember. Having been looking for a way to use markdown to statically render my blog, I was intrigued by this possibility, and set out to implement it for the Ship Shape blog.

I needed a few things to make this seamless. Let’s break down the steps.

  1. Markdown Support
  2. Displaying Formatted Markdown and Code Syntax Highlighting
  3. Prember Route Generation

Markdown Support

I knew Chris had been working on broccoli-static-site-json, and his own out of the box Ghost replacement ember-casper-template, which I intend to explore further, and potentially switch to, but wanted to document my initial approach first.

I ended up using ember-cli-markdown-resolver to pull in my markdown, from my app/blog folder. The installation was a simple ember install.

ember install ember-cli-markdown-resolver

I then had to simply tell it the folder my markdown was in.

// config/environment.js

ENV['ember-cli-markdown-resolver'] = {
  folders: {
    blog: 'app/blog'

Then I needed routes for both the blog index, to show the links to the posts, and a post route to display the posts themselves.

ember g route blog/index
ember g route blog/post

I then configured these routes in router.js.

// router.js

this.route('blog', function () {
  this.route('post', { path: '/*path/' });

I then had to set up model hook to load the list of posts in the index.

// routes/blog/index.js

import Route from '@ember/routing/route';
import RSVP from 'rsvp';
import { inject as service } from '@ember/service';

export default Route.extend({
  markdownResolver: service(),

  model() {
    return this.markdownResolver.tree('blog').then((tree) => {
      return new RSVP.Promise((resolve) => {
        const sortedPosts = tree.files.sortBy('').reverse();

Once we had the list of posts, we needed to make sure each post route would load the content for that individual post.

// routes/blog/post.js

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

export default Route.extend({
  markdownResolver: service(),

  model({ path }) {
    // We had to handle removing the slash that we need for pulling static html from most servers
    const withoutSlash = !path.endsWith('/') ? path : path.slice(0, -1);
    return this.markdownResolver.file('blog', withoutSlash);

You might notice, we have some extra logic to remove slashes from the path. We want to enforce routes having a trailing slash because most static servers like having the slash to pull the index.html for each route automatically. Without the slash you will get a redirect to the slash most times. This is a common thing we need to handle in several places.

Displaying Formatted Markdown and Code Syntax Highlighting

With this simple setup, we are now pulling in the data from the markdown, but we need a way to display this in a meaningful way. For this we will be using ember-cli-showdown and ember-prism.

ember install ember-cli-showdown
ember install ember-prism

I use the markdown front matter to define a slug for the posts, which is the same as the name of the markdown file, so we can link to it. I also supply titles, authors, and any other data that makes sense. For example the front matter for this post is:

authorId: Robert Wagner
authorId: RobbieTheWagner
date: 2018-04-04
slug: static-blogs-with-prember-and-markdown
tags: ember, fastboot, static, prember, prerender, blog, ghost, markdown
title: Static Blogs with Prember and Markdown

To display the list of blog posts we will simply each through the model provided in the blog index route.

// templates/blog/index.hbs or a component for the menu etc

{{#each posts as |post|}}
  <box fit class='blog-post'>

    <div class='attribution'>
      {{moment-format 'LL'}}

In the blog post route, we simply want to pass the content to showdown. I personally do some displaying of the author, and author image, the date of the post, etc. but the only requirement is to pass the markdown to showdown.

{{markdown-to-html content}}

We should now have the markdown for the post displayed! ember-prism should be automatically doing syntax highlighting of code blocks placed in markdown, but it may require some additional language config. You will also likely want to copy over some styles to make things look nice. The markup will be unstyled from showdown, but you can borrow styles from your favorite markdown rendering site, like Ghost, GitHub, etc.

Prember Route Generation

The final step is to install Prember and make sure it knows about these routes we have created.

ember install prember

I have a simple function I use to generate the route paths to pass to Prember from our markdown.

function buildPremberUrls() {
  // Build prember urls
  const urls = [

  const { extname } = require('path');
  const walkSync = require('walk-sync');

  const paths = walkSync('app/blog');

  const mdFiles = paths
    .filter((path) => extname(path) === '.md')
    .map((path) => {
      const stripMD = path.replace(/\.md/, '');
      return `/blog/${stripMD}/`;

  mdFiles.forEach((file) => {

  return urls;

This defines our routes that we are always sure will remain the same, and then looks at our markdown files to generate the other routes. This ensures, when we add new markdown files, the new routes are automatically pulled into Prember. Prember only runs in production or when you run PREMBER=true ember s. You may need to also install prember-middleware and ember-cli-fastboot, but please refer to the most up to date instructions in the Prember README.

Next Steps

This was just the initial implementation, but I will follow up with further posts to improve upon this functionality, by doing things like:

Thanks for reading! I hope this helps, and stay tuned for the followup posts!