TucsonJS
August 2016
Keystone.js
The Node.js Answer to Wordpress?
Charlie King
Who are we?
Richard Key
Software Engineer @ Weedmaps
Creative Director @ Loot Storm
Agenda
- What is KeystoneJS?
- How it works
- Advantages / Disadvantages
- Questions / Comments
What is KeystoneJS?
The open source framework for developing database-driven websites, applications and APIs in Node.js. Built on Express and MongoDB.
CMS options
Gatsby
Content as a Service (CaaS)
Static site generators
Self hosted Content Management
Keystone Features
MVC - Customizable Express Routes, Mongoose Models and Views ( Jade, Swig, etc )
Out of the box - Authentication, Auto-generated Admin, Forms, email sending
Plugins - Google Maps, Google analytics, Mandrill, Azure, S3, etc.
How it works
Configuration
Views (Controllers)
Models + DB
Middleware (optional)
Routes
Templates
Configuration
require('dotenv').load();
// Require keystone
const keystone = require('keystone');
keystone.init({
'name': 'Your App',
'brand': 'Your App',
'less': 'public',
'static': 'public',
'favicon': 'public/favicon.ico',
'compress': true,
'views': 'templates/views',
'view engine': 'jade',
'emails': 'templates/emails',
'auto update': true,
'session': true,
'auth': true,
'user model': 'User',
'logger': "dev",
'cookie secret': process.env.COOKIE_SECRET || "sdfkjkjkjdf",
'mongo': process.env.MONGO_URI || "mongodb://localhost/your_app"
});
// Load your project's Models
keystone.import('models');
// Setup common locals for your templates. The following are required for the
// bundled templates and layouts. Any runtime locals (that should be set uniquely
// for each request) should be added to ./routes/middleware.js
keystone.set('locals', {
_: require('lodash'),
env: keystone.get('env'),
utils: keystone.utils,
editable: keystone.content.editable,
clientGaId: process.env.GA_TRACKING_ID
});
// Load your project's Routes
keystone.set('routes', require('./routes'));
// Configure the navigation bar in Keystone's Admin UI
keystone.set('nav', {
'posts': ['posts'],
'pages': ['pages'],
'enquiries': 'enquiries',
'users': 'users',
'metadata': ['tags', 'categories'],
'site configuration': ['configurations']
});
keystone.set('wysiwyg additional buttons', 'styleselect, blockquote, Figure');
keystone.set('wysiwyg additional plugins', 'visualblocks, image');
// Start Keystone to connect to your database and initialise the web server
keystone.start();
Models
const keystone = require('keystone');
const Types = keystone.Field.Types;
/**
* Post Model
* ==========
*/
const Post = new keystone.List('Post', {
map: { name: 'title' },
autokey: { path: 'slug', from: 'title', unique: true },
defaultSort: '-publishedDate'
});
Post.add({
title: { type: String, required: true },
slug: {
type: String,
required: true,
initial: false,
default: 'slug-goes-here',
note: "The slug is the unique URL for the item, e.g. videos/slug"
},
subtitle: { type: String, required: false },
state: {
type: Types.Select,
options: 'draft, published, archived',
default: 'draft',
index: true
},
author: { type: Types.Relationship, ref: 'User', index: true },
publishedDate: { type: Types.Datetime, index: true, default: Date.now },
image: { type: Types.CloudinaryImage, select: true},
content: {
extended: {
type: Types.Html,
wysiwyg: true,
height: 400,
note: "The content of the post"
}
},
tags: { type: Types.Relationship, ref: 'Tag', many: true },
category: { type: Types.Relationship, ref: 'Category', many: false }
});
Post.schema.virtual('content.full').get(function () {
return this.content.extended || this.content.brief;
});
// index for search
Post.schema.index({
tags: 'text',
title: "text",
subtitle: "text",
author: "text",
"content.extended": "text"
});
Post.defaultColumns = 'title, state|20%, author|20%, publishedDate|20%';
Post.register();
Middleware
/**
* This file contains the common middleware used by your routes.
*
* Extend or replace these functions as your application requires.
*
* This structure is not enforced, and just a starting point. If
* you have more middleware you may want to group it as separate
* modules in your project's /lib directory.
*/
const _ = require('lodash');
const keystone = require('keystone');
/**
Forces a redirect to SSL (HTTPS)
*/
exports.forceHTTPS = function requireHTTPS(req, res, next) {
if (process.env.NODE_ENV !== 'development' &&
req.headers['x-forwarded-proto'] !== 'https') {
return res.redirect(301,'https://' + req.get('host') + req.url);
}
next();
}
Routes
const keystone = require('keystone');
const middleware = require('./middleware');
const importRoutes = keystone.importer(__dirname);
// Common Middleware
keystone.pre('routes', middleware.forceHTTPS);
// Import Route Controllers
const routes = {
views: importRoutes('./views')
};
// Setup Route Bindings
exports = module.exports = function ( app ) {
// Views
app.get('/', routes.views.index);
app.get('/search', routes.views.search);
app.get('/articles/:category?', routes.views.blog);
app.get('/articles/post/:post', routes.views.post);
app.all('/contact', routes.views.contact);
app.get('*', routes.views.page);
// NOTE: To protect a route so that only admins can see it, use the requireUser middleware:
// app.get('/protected', middleware.requireUser, routes.views.protected);
};
Views
const keystone = require('keystone');
const async = require('async');
exports = module.exports = function( req, res ) {
const view = new keystone.View(req, res);
const locals = res.locals;
// Init locals
locals.section = 'blog';
locals.filters = {
category: req.params.category,
tag: req.query.tag
};
locals.data = {
posts: [],
categories: []
};
// Load the current category filter
view.on('init', function( next ) {
if ( req.params.category ) {
keystone.list('Category')
.model.findOne({key: locals.filters.category})
.exec(function( err, result ) {
locals.data.category = result;
next(err);
});
} else {
next();
}
});
// Load the current tags filter
view.on('init', function( next ) {
if ( locals.filters.tag ) {
keystone.list('Tag')
.model
.findOne({tag: locals.filters.tag})
.exec(function( err, result ) {
locals.data.tag = result;
next(err);
});
} else {
next();
}
});
// Load the posts
view.on('init', function( next ) {
const q = keystone.list('Post').paginate({
page: req.query.page || 1,
perPage: 12
})
.where('state', 'published')
.sort('-publishedDate')
.populate('author category');
if ( locals.data.category ) {
q.where('category').in([locals.data.category]);
}
if ( locals.filters.tag ) {
q.where('tags').in([locals.data.tag]);
}
q.exec(function( err, results ) {
locals.data.posts = results;
next(err);
});
});
// Load the featured posts
view.on('init', function( next ) {
const q = keystone.list('Post')
.model.find()
.where('state', 'published')
.where('featured', true)
.sort('-publishedDate')
.populate('author category');
if ( locals.data.category ) {
q.where('category').in([locals.data.category]);
}
q.exec(function( err, results ) {
if ( results.length ) {
locals.data.featuredPosts = results;
}
next(err);
});
});
// Render the view
view.render('blog');
};
Templates
extends ../layouts/default
mixin post(post)
.post-container
a(href='/articles/post/' + post.slug).post(data-ks-editable=editable(user, {list: 'Post', id: post.id}))
.post-heading
h2= post.title
if post.subtitle
h3=post.subtitle
if post.category
.post-block-category(title="#{post.category.name}",style="background-image:url(#{post.category._.image.src()")
if post.image.exists
.img-wrapper
img(src=post._.image.src())
mixin paginationBlock
.pagination-container
span.pagination-prev
if data.posts.previous
a(href="/articles#{filters.category?'/'+filters.category:''}?page=#{data.posts.previous}#{filters.tag?'&tag='+filters.tag:''}") Previous Page
span.text-weight-normal.paination-pages
strong #{data.posts.first}
| to
strong #{data.posts.last}
| of
strong #{data.posts.total}
| posts
span.pagination-next
if data.posts.next
a(href="/articles#{filters.category?'/'+filters.category:''}?page=#{data.posts.next}#{filters.tag?'&tag='+filters.tag:''}") Next Page
block content
.body-main.dirty-bg.pad-top
.container-fluid: .row
.col-lg-12
if filters.category && !data.category
h3.text-muted Invalid Category.
else
if data.posts.results.length
if !filters.tag && data.featuredPosts && data.featuredPosts.length
.mobile-featured.hide-on-desktop
h1.tag-header Featured Articles
.blog
each post in data.featuredPosts
+post(post)
if filters.tag && !filters.category
h1.tag-header
| Tagged:
em #{filters.tag}
if filters.category && !filters.tag
h1.tag-header
| Category:
em #{filters.category}
if filters.category && filters.tag
h1.tag-header
| Category:
em #{filters.category}
| and Tagged:
em #{filters.tag}
h1.tag-header.hide-on-desktop Articles
if data.posts.totalPages > 1
+paginationBlock()
.blog
each post in data.posts.results
+post(post)
if data.posts.totalPages > 1
+paginationBlock()
else
if data.category
h3.text-muted There are no posts in the category #{data.category.name}.
else
h3.text-muted There are no posts yet.
block js
script(src='/js/blog.min.js',defer=true)
Advantages (3.x)
ExpressJS
Familiar JS tools
Modern CSS for styling with integrated tooling
Performance
Go
Node.js
PHP
HHVM
Wordpress (PHP)
Wordpress (HHVM)
4542
3614
1773
3090
854
1259
Middleware
Middleware is standard ExpressJS middleware, so you can leverage the full Express ecosystem
Community
- Great community support, no corporate overlords
- Many new features in the 4.x roadmap (Themes, Plugins, Admin UI extensibility)
- Built with React and leverages that community, with solid core principles
Disadvantages (3.x)
Plugin system
- Very few compared to other (WP, Drupal)
- No way to create custom plugins!
Version
- Currently undergoing rewrite, so current branch feels stale.. No new features
Admin UI
- No way to manage the site using a UI. All configuration is done via the source code
Questions/Comments?
TucsonJS August 2016
By Charles King
TucsonJS August 2016
- 1,385