Wir verwenden Cookies, um Inhalte und Anzeigen zu personalisieren, Funktionen für soziale Medien anbieten zu können und die Zugriffe auf unsere Webseite zu analysieren. Außerdem geben wir Informationen zu Ihrer Verwendung unserer Webseite an unsere Partner für soziale Medien, Webung und Analysen weiter. Unsere Partner führen diese Informationen möglicherweise mit weiteren Daten zusammen, die Sie ihnen bereitgestellt haben oder die sie im Rahmen Ihrer Nutzung der Dienste gesammelt haben. Sie akzeptieren unsere Cookies, wenn sie "Cookies zulassen" klicken und damit fortfahren diese Webseite zu nutzen.

Cookies zulassen Datenschutzerklärung

Tutorial: Setup a headless CMS with KeystoneJS

Learn how to setup a Headless CMS with KeystoneJS (4.0.0) as a starter for your next blog, website or other great one-million-dollar idea!

Coding

Why CMS exist and why less is more in some cases

Per definition, CMS is short for 'Content Management System', so a CMS could be the books on a shelf, where you keep them in a certain sorted way, maybe alphabetically, maybe with context like drama, fantasy and textbook. 

When we think about digital data and the term CMS, most people will get WordPress, Django, Typo3 or any other system in their mind, because even if you are not a fan, those are popular and most of them - not Typo3 - are easy to use for the consumer who wants to run private or semi-professional blog.

Over the last few years it became harder for these preconfigured CMS frameworks, with backend solutions, template systems, posts, pages, categories and many different pieces and features build around a core to satisfy the requirements of every single web page and while web pages evolved to web applications, more requirements approaching the developer and product owner.

Focus on the 'Why?'

At this point Headless CMS entering the scene and the moment when less becomes more. If you focus on the main purpose of a CMS, it is to store data and make it available, processable and usable. While in some cases you still have a backend for administrative tasks and content creation, one benefit is that you are not bound to a certain schema but free to create your model or data structure upon your needs when using a scalable Headless CMS.


In times of content-driven design, SPA (Single Page Applications) and rapid, agile development, the backend should and may grow with the frontend needs and while a Headless CMS is just more or less a data layer, an easy integration of middleware or an API with GraphQL is just an increment away.

You could argue that, you can also use e.g. Wordpress simply as an API or turn Typo3 into a JSON delivering monstrosity, but what for? Let's not open pandoras box and start a discussion about security and personal preferences or JavaScript vs. PHP, but to start we should just have look at some fresh and more or less established player at the Headless CMS scene and find out.

About KeystoneJS

KeystoneJS is based on Node.js build on Express.js and Mongoose. As an OpenSource Framework for applications, Keystone is used for building database driven websites, apps or as an API for your frontend needs. 

This KeystoneJS server tutorial is build on KeystoneJS 4.0.0.

Prequisites

Install MongoDB

brew update
brew install mongodb

While MongoDB is using a schema-based concept for data models, Keystone is enhancing those features and uses them for the underlaying 'Keystone.Lists'. This concept helps KeystoneJS to organize it's data-structures and helps us to build beautifully admin areas and it offers a structured way to request and access data.

You can control the MongoDB via brew services, it must be started when you want to initialize the keystone server:

brew services start mongodb
brew services stop mongodb

- While we are in the MacOS world brew is available on the command line, please install MongoDB for your OS and refer to the MongoDB docs, if you head into any problems.

Installing KeystoneJS

Installing via Yeoman

Quite simple, while you can use a Yeoman Generator for setup or write a simple start script from scratch. Yeoman is a scaffolding tool and has many 'recpies' for over 5600 different projects. It is not a must have, but for testing purposes or if you need to setup projects real quick for rapid prototyping, this is a good way to go. 

npm install -g yo
npm install -g generator-keystone

If you want to setup a KeystoneJS project, you simply call the generator via yo, it is a small setup script with some questions for your initial project and you will be good to go:

yo keystone

After this setup you can start your Keystone Server with node:

node keystone.js

...Piece of cake.

If you are amazed by Yeoman, you can discover more generators for almost 8,000 different tools: http://yeoman.io/generators/

Installing from scratch

While installing via yo is as easy as that, setting up a server from scratch isn't much harder. At first create your project directory and run npm init within your folder to setup a fresh project and add keystone to the project dependencies. 

npm init
npm install --save keystone

Information

You may run into some errors and npm may also tell you to fix some problems, so run npm audit fix and run npm install again. Most of those problems are known, be sure to check KeystoneJS' Issues page before raising a bug and maybe contribute, if you can fix an error.

The KeystoneJS community and their developers are really eager to fix those issues, most of the errors you can see in the npm log are from upstream packages, but there are also some devs looking to fix their errors with pull requests - a really vivid community.

Continue

While we are already at the console, let's install some more packages for later:

npm install --save twig lodash dotenv async

If you want to use KeystoneJS as a complete headless CMS, you don't need to install any template engines, but since we want to have an view where users can login and a landing page, we will install Twig. KeystoneJS' default engine is Pug, but I like Twig more, so... You can use any express.js template engine, like Handlebars, Mustach or what you want.

Create a .env file and add your cookie secret

COOKIE_SECRET=mysupersecurecookiestring

Create a keystone.js to write your start script. 

require('dotenv').config();
var keystone = require('keystone');
var Twig = require('twig');
keystone.init({
  'cookie secret': process.env.COOKIE_SECRET,
  'name': 'keystonejs-headless-cms-tutorial',
  'less': 'public',
  'static': 'public',
  'favicon': 'public/favicon.ico',
  'views': 'templates/views',
  'view engine': 'twig',
  'twig options': {
    method: 'fs'
  },
  'custom engine': Twig.render,
  'auto update': true,
   'session': true,
  'auth': true,
  'user model': 'User',
});
keystone.import('models');
keystone.set('locals', {
   _: require('lodash'),
  env: keystone.get('env'),
  utils: keystone.utils,
  editable: keystone.content.editable,
});
keystone.set('routes', require('./routes'));
keystone.set('nav', {
  users: 'users',
});
keystone.start();

Now, since we have defined some options, we need to provide the necessary structure by creating some folders. As seen in our keystone.js we need a public folder for our static assets and pages, a templates folder for our views, a models folder for our User model, a routes folder for our application routes and not seen in our config, but we need an update folder for - you can imagine - updates . Summarized, create the following directories: public, templates, models, updates, routes.

./public
./templates
./models
./updates
./routes

Creating a USER Model

models/User.js

var keystone = require('keystone');
var Types = keystone.Field.Types;
var User = new keystone.List('User');
User.add({
  name: {
    type: Types.Name, 
    required: true,
    index: true
  },
  email: {
    type: Types.Email,
    initial: true,
    required: true,
    unique: true,
    index: true },
  password: {
    type: Types.Password,
    initial: true,
    required: true },
  },
  'Permissions', {
    isAdmin: {
      type: Boolean,
      label: 'Can access Keystone',
      index: true,
    },
  }
);
User.schema.virtual('canAccessKeystone').get(function () {
  return this.isAdmin;
});
User.defaultColumns = 'name, email, isAdmin';
User.register();

The data structure of KeystoneJS heavily builds on Lists. Our user is a list model where we define our fields, defined by keystone.Field.Types. This is a simple user model for this tutorial and yours could be more distinct, but you can see we described the user with a name, an email and a password, where all fields are required and the email should be unique. Read more about field type definitions at the KeystoneJS documentation.

You can see the Permissions object, where we can identify a user with admin privileges. These options are handled via 'Virtuals'. Virtuals are document properties that you can set and get, but are not persisted to the MongoDB. Read more about Virtuals at the MongoDB documentation.

The next step would be adding a user to our database. We defined 'auto update' as true in our keystone.js, so when started Keystone automagicallyinserts our updates in the database. Therefore we create an update file in our updates directory, as the documentations states it should be using a semantic versioning followed by an optional key, we name our file: 0.0.1-inituser.js

updates/0.0.1-inituser.js

exports.create = {
  User: [{
    'name.first': 'Admin',
    'name.last': 'User',
    'email': 'mail@example.com',
    'password': '123456',
    'isAdmin': true },
  ],
};

Caveat / Warning

exports.create is a shorthand and should not be used in production or for models with relationships. You should add relationships by hand because the update framework saves all items it receives before connecting the relationships. The application updates part of the documentation gives an example for Post Catgories.

Create Routes

Our user model is defined, now we create our routes to make the application accessible. We just need to provide routes and templates for our landing page, while the AdminUI is already included in KeystoneJS.

In our routes directory create some files and a folder, we will add content in a second: create index.js and middleware.js in the routes directory, then create a new folder views with an index.js.

./routes
  /views
    index.js
  middleware.js
  index.js

Edit routes/index.js and let's add some content:

var keystone = require('keystone');
var middleware = require('./middleware');
var importRoutes = keystone.importer(__dirname);
keystone.pre('routes', middleware.initLocals);
keystone.pre('render', middleware.flashMessages);
var routes = {
  views: importRoutes('./views'),
};
exports = module.exports = function (app) {
  app.get('/', routes.views.index);
};

At first we call our middleware to setup some locals and flashMessages. The last part is also simple, we call the Keystone importer to import the routes to our views.

Next one routes/middleware.js

var _ = require('lodash');
exports.initLocals = function (req, res, next) {
  res.locals.navLinks = [{
    label: 'Home',
    key: 'home',
    href: '/' },
  ];    
  res.locals.user = req.user;    
  next();
};
exports.flashMessages = function (req, res, next) {
  var flashMessages = {
    info: req.flash('info'),
    success: req.flash('success'),
    warning: req.flash('warning'),
    error: req.flash('error'),    
  };    
  res.locals.messages = _.some(flashMessages, function (msgs) {
    return msgs.length; }) ? flashMessages : false;
  next();
};
exports.requireUser = function (req, res, next) {
  if (!req.user) {
    req.flash('error', 'Please sign in to access this page.');
    res.redirect('/keystone/signin');
  } else {
    next();
  }
};

As you can see, we define our local navigation and some flash messages, which have to be written yet and more important with the export.requireUser we define that the user must be logged in to access the backend or is redirected to the 'sign-in page'.

The routes to our public views need to be defined in routes/views/index.js

var keystone = require('keystone');
exports = module.exports = function (req, res) {
  var view = new keystone.View(req, res);
  var locals = res.locals;
  locals.section = 'home';
  view.render('index');
};

Since we just have a landing page, that's all. We create a new view and render it to the index. With local.selection we can define what should be selected in our navigation, in our case 'Home'.

Create templates

In our middleware we defined some flash-messages to give feedback, if the login fails or the user is not allowed at certain pages, so we start with our templates and create some files and folders in your template directory:

./templates
  /layouts
    default.twig
  /mixins
    flash-messages.twig
  /views
    /errors
      404.twig
      500.twig 
    index.twig

Start with the default page layout at templates/layouts/default.twig, where we define the main page structure.

{% import "../mixins/flash-messages.twig" as FlashMessages %}
{% spaceless %}
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>{{ title|default("My Keystone Project") }}</title>
    <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
    <link href="//fonts.googleapis.com/css?family=Oswald:300,400,700" rel="stylesheet">
    <link href="styles/site.css" rel="stylesheet">
    {% if user and user.canAccessKeystone %}
        <link href="/keystone/styles/content/editor.min.css" rel="stylesheet">
    {% endif %}
    <!--[if lt IE 9]>
        <script src="//cdn.jsdelivr.net/html5shiv/3.7.3/html5shiv.js"></script>
        <script src="//cdn.jsdelivr.net/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
    {% block css %}{% endblock %}
    {% block head %}{% endblock %}
  </head>
  <body>
    <nav role="navigation">
      <ul class="nav navbar-nav navbar-left">
        {% for link in navLinks %}
          {% set linkClass = '' %}
          {% if link.key == section %}
            {% set linkClass = ' class="active"' %}
          {% endif %}
          <li{{ linkClass | striptags }}>
            <a href="{{ link.href }}">{{ link.label }}</a>
          </li>
        {% endfor %}
        {% if user is defined %}
          {% if user.canAccessKeystone %}
            <li><a href="/keystone">Open AdminUI</a></li>
          {% endif %}
          <li><a href="/keystone/signout">Sign Out</a></li>
        {% else %}
          <li><a href="/keystone/signin">Sign In</a></li>
        {% endif %}
      </ul>
    </nav>
    <main>
      {% block intro %}{% endblock %}
      {{ FlashMessages.renderMessages(messages) }}
      {% block content %}{% endblock %}
    </main>
      {% if (user is defined) and (user.canAccessKeystone) %}
      <script src="/keystone/js/content/editor.js"></script>
    {% endif %}
    {% block js %}{% endblock %}
  </body>
</html>
{% endspaceless %}

In the first line we import our flash-messages from the mixins folder, so let's create them next in templates/mixins/flash-messages.twig

{% macro renderMessages(messages) %}
    {% if messages %}
        {% import _self as Self %}
        {% if debug %}
            <pre>{{ dump(messages) }}</pre>
        {% endif %}
        <div id="flash-messages" class="container">
            {% for message in messages.info %}
                {{ Self.renderMessage(message, "info") }}
            {% endfor %}
            {% for message in messages.success %}
                {{ Self.renderMessage(message, "success") }}            {% endfor %}            {% for message in messages.warning %}                {{ Self.renderMessage(message, "warning") }}            {% endfor %}            {% for message in messages.error %}                {{ Self.renderMessage(message, "danger") }}            {% endfor %}        </div>    {% endif %}
{% endmacro %}
{% macro renderMessage(message, type) %}
    <div class="alert alert-{{ type }}">
        {% if message is iterable %}
            {% if message.title %}
                <h4>{{ message.title }}</h4>
            {% endif %}
            {% if message.detail %}
                <p>{{ message.detail }}</p>
            {% endif %}
            {% if message.list %}
                <ul>
                    {% for item in message.list %}
                        <li>{{ item }}</li>
                    {% endfor %}
                </ul>
            {% endif %}
        {% else %}
            {{ message }}
        {% endif %}
    </div>
{% endmacro %}

Since this is tutorial is not about Twig just mind that we take the default keystoneJS Messages and render them in a bootstrap-like style. If you are not familiar with Twig, head over to their docs.

For server errors we create two content blocks within templates/views/errors/

templates/views/errors/404.twig

{% extends "../../layouts/default.twig" %}
{% block content %}
    <div class="container">
        <h1>404</h1>
        <p class="lead">Sorry, the page you requested can't be found.</p>
    </div>
{% endblock %}

templates/views/errors/500.twig

{% extends "../../layouts/default.twig" %}
{% block content %}
    <div class="container">
        <h1>Error (500)</h1>
        <p class="lead">Sorry, the site has encountered an error.</p>
    </div>
{% endblock %}

The last template is our landing page where we get when accessing the 'Home' view of our page in templates/views/index.twig

{% extends "../layouts/default.twig" %}
{% block content %}
  <div class="container">
    <div class="content">
      <h1>Welcome</h1>
      <p>This is your new <a href="http://keystonejs.com" target="_blank">KeystoneJS</a> website.</p>
      <hr>
      {% if (user is defined) and (user.canAccessKeystone) %}
        <p><a href="/keystone" class="btn btn-lg btn-primary">Open the Admin UI</a>                </p>            {% else %}                <p>                    <a href="/keystone/signin" style="margin-right: 10px" class="btn btn-lg btn-primary">Sign in</a> to use the Admin UI.</p>
      {% endif %}
    </div>
  </div>
{% endblock %}

Create styles

Our last action is to fill the public folder with the assets and styles. It's a common courtesy to have a favicon, so go to https://favicon.io/ and get, create, make or steal one, if you don't have one and put in your ./public folder. In our keystone.js we defined that we would be using less, so let's create the following files and folders for a quick base system:

./public
  /styles
    /includes
      default.less
      variables.less
    site.less

We import our system in our site.less to keep a better structure

@import "includes/variables.less";
@import "includes/default.less";

If you want to use bootstrap, you could import it above those lines. Our default.less contains just some minimal, minimal, minimal styles:

body {
  display: flex;
  font-family: 'Oswald';
  justify-content: center;
  align-items: flex-start;
  min-height: 100vh;
  flex-direction: column;
}
main {
  padding: 40px 20px;
  flex: 1 0 100%;
}
nav {
  display: flex;
  align-items: center;
  justify-content: center;
  ul {
    list-style: none;
  }
}

The variables.lessis even more minimal:

// Override variables in this file, e.g. from Bootstrap
// @font-size-base: 14px;

Fire up

Couldn't be easier as:

node keystone.js

Annotation

This tutorial is super close to what you get when running yo keystone because I wanted to make it also understandable if you start from the generator. As mentioned you should read the documentation regarding models and the automatic update system, because there are some caveats and obstacles to hurdle.

Conclusion

As of writing these lines (11-2018) KeystoneJS has a lot of issues and npm audit is reporting some minor and major errors in upstream packages so I am not sure, if I would use it for production. But generally speaking all new headless CMS systems like Keystone or Strapi are fighting with 'childhood diseases', so at the end decide for yourself. Nevertheless it was really easy and fun to setup and hopefully there's much more to come.

Github

The complete code can be cloned from our zauberware GitHub repository:

https://github.com/zauberware/keystonejs-headless-cms-tutorial


EN