← back to the writings

Customising KeystoneJS: Adding Post Types [Part 2]

Posted on March 30th, 2015 in Tutorials, Node.js by Graeme

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 :)

Have any thoughts?