on Writings, Tutorials, Keystone, Node, CMS

Customising KeystoneJS: Adding Post Types [Part 2]

What we left off with in part 1 would give us a 404 error if we were to visit ‘http://localhost:3000/portfolio’. That's because we haven't yet configured the route to this path. In this second half of the tutorial, we're going to build the part that will let us access our portfolio posts from the front end.

Step 1: Configure the routes

We start by configuring the routes so we can access the our portfolio entries. Open up /routes/index.js, and add these two lines within the //Views section:

app.get('/portfolio/', routes.views.portfolio);  
app.get('/portfolio/post/:post', routes.views.portfolioitem);

Now, open /routes/middleware.js, and add our navigation links to locals.navLinks, the same way the others have already been added: 

{ label: 'Portfolio',        key: 'portfolio',       href: '/portfolio' }  

Step 2: Create the view middleware

We now need to create two new views inside /routes/views/: portfolio.js and portfolioitem.js to match the references we’ve just added into the views section of /routes/index.js (first bit of step 1). 

The first view, portfolio.js is simply a copy of /routes/blogs.js, but we can remove the loading of categories and the currentCategory filter. The final /routes/views/portfolio.js middleware should look like this:

var keystone = require('keystone'),  
    async = require('async');

exports = module.exports = function(req, res) {

    var view = new keystone.View(req, res),
        locals = res.locals;

    // Init locals
    locals.section = 'portfolio';

    locals.data = {
        portfolioitems: []

    // Load the posts
    view.on('init', function(next) {

        var q = keystone.list('Portfolio').paginate({
                page: req.query.page || 1,
                perPage: 10,
                maxPages: 10
            .where('state', 'published')
            .populate('author categories');

        q.exec(function(err, results) {
            locals.data.posts = results;


    // Render the view


 All I've done is simply replaced the instances of Post with Portfolio, and also removed anything related to categories.

We'll create the second view middleware (/routes/views/portfolioitem.js) in a similar fashion as the first, but by instead mimicking /routes/views/posts.jsHere’s the final portfolioitem.js:

var keystone = require('keystone');

exports = module.exports = function(req, res) {

    var view = new keystone.View(req, res),
        locals = res.locals;

    // Set locals
    locals.section = 'portfolio';
    locals.filters = {
        post: req.params.post
    locals.data = {
        posts: []

    // Load the current post
    view.on('init', function(next) {

        var q = keystone.list('Portfolio').model.findOne({
            state: 'published',
            slug: locals.filters.post
        }).populate('author categories');

        q.exec(function(err, result) {
            locals.data.post = result;


    // Load other posts
    view.on('init', function(next) {

        var q = keystone.list('Portfolio').model.find().where('state', 'published').sort('-publishedDate').populate('author').limit('4');

        q.exec(function(err, results) {
            locals.data.posts = results;


    // Render the view


Great work, we’re nearly there.You’ll have noticed that the final line in each of the view middleware files are rendering a view, with the same name as the current file. Why is this? Well, they’re rendering this file from our /view folder.  And at the moment they don't exist, so if we were to restart the server we'd get this 500 error:

 500 Error

 Clearly we’ve got two more files to create in step 3.

Step 3: Create the Jade views

We’re now going to add our public facing view templates for our portfolio post types. Go ahead and create the two new files: portfolio.jade and portfolioitem.jade - the first will show a list of our portfolio items, and the latter is used to display a single portfolio item. These are essentially the same as blog.jade and post.jade, but again we'll remove all references to the category, and replace Post with Portfolio where necessary.

These are my final files, starting with portfolio.jade:

extends ../layouts/default

mixin post(post)  
    .post(data-ks-editable=editable(user, { list: 'Portfolio', id: post.id }))


            p.lead.text-muted Posted 
                if post.publishedDate
                    | on #{post._.publishedDate.format('MMMM Do, YYYY')} 
                if post.author
                    | by #{post.author.name.first}

            //post image
            if post.image.exists
                div.imgcontainer.col-sm-12.nopad: a(href='/portfolio/post/' + post.slug)

            //post title
            h2: a(href='/portfolio/post/' + post.slug)= post.title

            //post content
            p!= post.content.brief

            if post.content.extended
                p.read-more: a(href='/portfolio/post/' + post.slug)

block intro  
        h1= 'Portfolio'

block content  
    .container: .row
                if data.posts.results.length
                    if data.posts.totalPages > 1
                        h4.text-weight-normal Showing 
                            strong #{data.posts.first}
                            |  to 
                            strong #{data.posts.last}
                            |  of 
                            strong #{data.posts.total}
                            |  posts.
                        h4.text-weight-normal Showing #{utils.plural(data.posts.results.length, '* post')}.
                        each post in data.posts.results

                    if data.posts.totalPages > 1
                            if data.posts.previous
                                li: a(href='?page=' + data.posts.previous): span.glyphicon.glyphicon-chevron-left
                                li.disabled: a(href='?page=' + 1): span.glyphicon.glyphicon-chevron-left
                            each p, i in data.posts.pages
                                li(class=data.posts.currentPage  p ? 'active' : null)
                                    a(href='?page=' + (p  '...' ? (i ? data.posts.totalPages : 1) : p ))= p
                            if data.posts.next
                                li: a(href='?page=' + data.posts.next): span.glyphicon.glyphicon-chevron-right
                                li.disabled: a(href='?page=' + data.posts.totalPages): span.entypo.glyphicon.glyphicon-chevron-right
                        h3.text-muted There are no posts yet.

                h2 Sidebar

 And here's portfolioitem.jade:

extends ../layouts/default

block content  
    .container: .row: .col-sm-10.col-sm-offset-1.col-md-8.col-md-offset-2
            p: a(href='/portfolio') ← back to the portfolio
            if data.post.image.exists
                .image-wrap: img(src=data.post._.image.fit(750,450)).img-responsive
            if !data.post
                h2 Invalid Post.
                    h1= data.post.title
                    h5 Posted 
                        if data.post.publishedDate
                            | on #{data.post._.publishedDate.format('MMMM Do, YYYY')} 
                        if data.post.author
                            | by #{data.post.author.name.first}
                    != data.post.content.full


Now the two Jade files are in place, your portfolio items will render nicely (although you'll probably want to edit the Jade templates). Well that's it until next time - I hope the tutorial has been beneficial to you. I'd like to improve it where possible, so feedback is welcome :)