logo

Menu
  • Home
  • Works with what
  • Building Blocks
  • Documentation
  • Impressions
  • Contact
  • Login

The invisible Content Management System

GlintCMS is a modular and flexible CMS that basically has no backend, but slick inline ediiting capabilities. It is built with node.js

TL;DR

GlintCMS is an in-page, overlay style WYSIWYG (What you see is what you get) CMS. What this means is that to edit a page you simply navigate to the page as you would when you browse your website. Once you arrive at a page you use the overlaid main menu to perform tasks or you simply hover over the main section of the page to edit content or change page settings.

The building blocks of GlintCMS are designed that you can build Web-Sites as well as Web-Applications.

Stability

GlintCMS is robust and is used e.g. on www.intesso.com and www.glintcms.com (the site you are looking at).

The API Stability is somewhere between experimental and stable.

Design Principles

  • it is a GlintApp implementation
  • runs in node.js and in the browser with browserify
  • universal aka (isomorphic) javascript
  • modular | collection of reusable, replacable modules | follows unix philosophy
  • fast, no magic for SEO (like screen scraping of own content with headless browser)
  • for WebSites as well as for WebApps
  • Clean Code over Convention over Configuration
  • mostly framework agnostic
  • easy to learn
  • flexible
  • reusable
  • inline editing
  • minimal management backend
  • minimal build tasks
  • minimal configuration
  • no dynamic require
  • no magic
  • only simple abstractions

Use Cases

From the very beginning GlintCMS was designed to run efficiently on both the server and in the browser. That's why it is a really good solution to many problems:

  • Web Application (Single Page Applications)
  • Web Sites / Platforms (Heavy server side rendering)
  • Hybrid variants of the above

Technology

It is written purely in JavaScript and runs on node.js and in the browser thanks to browserify and makes use of many great modules from npm.

  • Works with all the newer browsers
  • Uses Express on the server side
  • Runs on Linux and Mac OS, Windows is not yet supported (any help appreciated)
  • Tested with node.js 0.12.7 and 4.1

Getting Started

The best way to get started is to read along and install a starter project e.g. glintcms-starter-intesso.

(it should only take a few of minutes to get going)

You can use it out of the box, for small to medium sites.

Have a look at the code of the modules and shout out if you need help.

github

npm

Works With What

Built with

The GlintCMS core modules are mainly built with:

node.js, efficient web server with javascript

node.js

express.js, Fast, unopinionated and minimalist web framework for Node.js

express.js

npm, node package manager

npm

browserify, bundle node.js modules for the browser

browserify

javascript, ES5

JavaScript ES5

... and many more great open source npm modules ...

 

 

Additionally this site uses

(no GlintCMS Requirement):

jQuery

Bootstrap

html5

HTML 5

css 3

CSS 3

... and many more great open source npm modules ...

 

Compatible with

It is one of the strengths of GlintCMS, that it is not very obtrusive.

You can use your own preferred frameworks and modules and just add GlintCMS alongside it.

So GlintCMS is basically compatible with almost everything.

 

 

 

Building Blocks

building blocks

building with blocks is fun!

                                                                         
   +--------------+        +--------------+        +--------------+
   |              |        |              |        |              |
   |    wrap      | +----> |  container   | +----> |    block     |
   |              |        |              |        |              |
   +--------------+        +--------------+        +--------------+

          +                       +
          |                       |
          v                       v

   +--------------+        +--------------+        + + + + + + +  +
   |              |        |              |
   |    widget    | +----> |   adapter    | +----> +    cache     +
   |              |        |              |
   +--------------+        +--------------+        +  + + + + + + +

created with asciiflow

block

Blocks are the heart of everything that is editable in GlintCMS.

A block provider can be a plain text glint-block-text, or an image or a rich text e.g. glint-block-ckeditor etc.

The block itself (glint-block) is a unifying interface for the block providers.

It is the block providers responsibility to display its data and to turn it into something editable, when switching to edit mode.

They must basically implement the following sync functions:

load(content)
edit()
var content = save()

adapter

The adapter (glint-adapter) is the unifying interface for the adapter providers like e.g. glint-adapter-fs or glint-adapter-elasticsearch.

The adapter provider is responsible for storing and retrieving the data/content.

It only solves access to single types, entities, documents or tables (the terms very with the providers technology, let's stick with the term type').

It does not combine instances of different types (joins), that's up to the

An adapter provider must implement these async functions:

load(.., cb)
save(.., cb)
delete(.., cb)
find(.., cb)

cache

Is optional and is not yet implemented.

container

The container (glint-container) holds the different blocks and orchestrates nifty details, like

  • what should be loaded (rendered) on the server and what in the browser.
  • switching the blocks into the edit mode edit and back (save or load on cancel)
  • passing the data between the adapter and the blocks.
  • a container has got one adapter and most likely has several blocks.

If you don't have any editable content on your page, you don't need a container.

widget

A widget (glint-widget) can be useful for displaying content, that does not need to be editable in this place.

As an example, you could use it to display the three latest blog entries on the first page.

A widget implementation needs to implement the functions:

data(cb) // async, optional, if you don't need to load data in an async way
render(fn) // sync, returns the rendered content

The widget itself exposes the load function to integrate with the wrap loading mechanism.

wrap

The wrap (glint-wrap) wraps it all up.

  • it can have several containers, widgets and wraps.
  • it also is involved in what should be loaded (rendered) on the server and what in the browser.

Documentation

API

The design of the module's api was chosen to get a good balance between "easy to use" and "easy to extend".

The api of the modules is designed after what we call the associative-provider model.

You wouldn't use a TextBlock directly, but via a Block that holds the TextBlock

//e.g.: Block -> TextBlock
var block = Block(TextBlock()).use(Style());

general

extendability

Have a look at the extend documentation.

The following api document describes how to "use" the building blocks, (not how to extend them).

naming/require

The building blocks are called: wrap, container, etc. in this document. However, the module names to require them start always with glint-. Also in general the module names are hierarchically structured, separated with a dash -, where on the left hand side is the more generic part of the name.

So if you want to use the adapter with the ajax provider for example, you do it like this:

var Adapter = require('glint-adapter');
var AjaxAdapter = require('glint-adapter-ajax');

var adapter = Adapter(AjaxAdapter());

get/set

Getter and Setter can be called like this:

// set
var place = obj.place('server');

// get
console.log(obj.place());
// --> server

When you provide a value (set), the method returns this, so that you can chain other methods.

chainable methods

Most of the methods are chainable (they return this):

// example:
Wrap(o)
  .editable(req.userCan('edit'))
  .i18n(req.i18n)
  .cid(req.params.article)
  .place(o.place)
  .load(res.locals, function(err, result) {
     debug('route loaded', err, req.params.article, result);
     if (err) return next(err);
     res.send(result.page);
  })

events

Most of the building blocks inherit from EventEmitter and expose useful events:

// Example from the code:
var EventEmitter = require('events').EventEmitter;

/**
 * Expose Block element.
 */
exports = module.exports = Block;
inherits(Block, EventEmitter);

The Events can be especially helpful, when designing a plugin.

get/set

Getter and Setter emit an event:

  • emit(name, value)

methods

The building blocks methods emit events for every method with the given name:

  • emit(pre-<methodName>, arguments)
  • emit(<methodName>, arguments)
  • emit(post-<methodName>, arguments)

rendering/place

rendering (load) is done by default on the server. editing, saving and deleting is always initiated in the browser. blocks and widgets can be defined to render in the browser when needed. however you can also override where the components (blocks and widgets) are rendered all together. you can use this for example to let everything be rendered on the server, when the site is being called by a bot, search engine, crawler or the like.

place glint is designed to use on the server as well as in the browser (pick you buzzword for it... universal, polymorph, what ever you like). With the place get/set method, you can define, where you would like the control to be rendered.

These are the available options (strings):

  • server
  • browser
  • both
  • force:server
  • force:browser
  • force:both

priorities

(0:low priority ... 3:high priority)

 0 render on server by default

 1 Block.render('browser') or
   Widget.render('browser')
   -> render these items in the browser

 2 Wrap.render('server') or
   Container.render('server')
   -> render ALL items on the server, e.g. when requested by a search engine.

 3 SpecificBlock.render('force:both') or
   Widget.render('force:both)
   -> when a Specific Block has this flag, it will always be rendered on both sides (server and browser)

 4 Same as priority 3 but with 'force:server' or 'force:client'
   -> render always on the server respectively in the browser

wrap

A wrap can hold several containers and widgets as well as other wraps. Let's call them controls. With the wrap, you can define how the controls are loaded. First you can define the default object with the method defaults. With the methods parallel, series and eventually, you can define everything from a simple to a quite sophisticated loading execution. See flow-builder

The wrap runs on the server and/or in the browser depending on the defined places.

properties

api

the api property must not be overwritten.

Wrap.api === 'wrap'

get/set

You can get/set these properties on the wrap.

  • key
  • id
  • selector
  • prepend
  • append
  • el
  • place
  • editable
  • cid

When you set editable or place on the wrap, the value is also set on all of it`s controls.

cid gets/sets the id on the first container that the wrap holds.

methods

constructor


// the `wrap` has got the optional key, control arguments. 
// what's added in the constructor get's added with the `parallel` workflow.
var wrap = Wrap(key, control);

// but you can also create the Wrap fist, and then define your workflow.
// in this example the articles and projects `load in parallel` and after they are done,
// the resulting transfer object is handled over to the next steps:
//   1. in this exampe it's first the contentWidget,
//   2. and then the layoutWrap after the previous step is done.
var wrap = Wrap();
wrap
  .parallel(container)
  .parallel('articles', articles.selector('.js-articles'))
  .parallel('projects', projects.selector('.js-projects'))
  .series('content', contentWidget.place('force:server'))
  .series(LayoutWrap(o.layout).place('force:server'))

defaults

It let's you define a default object, that's the starting object in every load workflow. This object is the initial workflow transfer object

// set single key, value
wrap.defaults(key, value);
// set object
wrap.defaults(object);
// get value
wrap.defaults(key);

parallel

When you define several controls with the parallel method right after each other, they get executed in parallel, and only when all of them have finished (or one of them has an error), the next step is executed.

// adds a single control, the clone of the resulting object merged with the transfer object.
wrap.parallel(control;
// adds a sincle control, the clone of the resulting object is insterted into the transfer object with the given `key`.
wrap.parallel(key, control);

// examples
wrap.parallel(container);
wrap.parallel('news', widget1);
wrap.parallel('articles', widget2);

series

series method calls, are executed after the previous method has finished. It has got the same signature as the parallel method.

eventually

eventually method calls are started immediately, but are evaluated only at the very end when the whole workflow has finished. It has got the same signature as the parallel method.

load

Calling the load method, starts the defined wrap workflow.

// it has got an optional context object. This object is taken as the *initial workflow transfer object* when provided.
// the callback function `callback(err, result)` is called once everything is `load`ed.

wrap.load([context, ] callback);
// or
wrap.load(callback);


// Example:
function(req, res, next) {
  wrap.load(res.locals, function(err, result) {
    if (err) return next(err);
    res.send(result.page);
  });
}

widget

With a widget, you can render/display noneditable content on the server and/or in the browser depending on the defined place.

properties

api

the api property must not be overwritten.

Widget.api === 'widget'

get/set

You can get/set these properties on the widget.

  • key
  • id
  • selector
  • prepend
  • append
  • el
  • place
  • template
  • data
  • render

When you set editable or place on the wrap, the value is also set on all of it`s controls.

cid gets/sets the id on the first container that the wrap holds.

methods

constructor

// you can provide the `render` method in the constructor
var widget = Widget(renderFunction);

// or you can provide the `data` and `render` method on the widget instance. Example:
Widget()
  .data(function(fn) {
    adapter.findLatest(o.getLocale(), 3, fn);
  })
  .render(function(options) {
    return ejs.render(o.template || template, options);
  })

data

You can optionally provide a data method, if your widget need's to make asynchronous data calls. It returns this and takes a callback function with the parameters: callback(err, result).

widget.data(callback);

render

The render function is a synchronous function and must return the rendered content. The data object is provided in the first argument. It let's you choose what ever rendering engine you want to use (as long as it runs on the server as well as in the browser).

wrap..render(function(options) {
  return compiledDotTemplate(options);
})

load

The load method is called from the wrap that contains this widget during the load workflow execution. Most probably, you never have to call this method directly. Internally, the load method calls the data method (if it was provided), and then render with the resulting object.

container

Containers are only used when you use blocks. A container runs on the server and in the browser. On the server, only the load method is called (most likely from the surrounding wrap), In the browser, there is more methods:

  • load
  • edit
  • save
  • cancel
  • delete

properties

api

the api property must not be overwritten.

Container.api === 'container'

get/set

You can get/set these properties on the container.

  • key
  • id
  • place
  • template
  • editable
  • blocks
  • adapter

methods

constructor

// you can optionally provide the `blocks` as well as the `adapter` in the constructor

var blocks = {
  title: text().selector('body h1'),
  short: text().selector('[data-id=short]'),
  text: editor().selector('[data-id=text]'),
  meta: Block(MetaBlock())
};

var adapter = Adapter(AjaxAdapter())
  .db(db)
  .type(type)
  .use(Dates())
  .use(Id())

// constructor
var container = Container(blocks, adapter);


// or you can provide them afterwards. Example:
var container = Container();
container
  .blocks(blocks)
  .adapter(adapter)

load

(it runs either on server or browser depending on the place)

Internal sequence:

  1. it first calls the adapters load method,
  2. afterwards all of the blocks load methods

edit

(runs only in the browser)

Internal sequence:

  1. it first calls the load method on this container
  2. it then calls the edit method on all of the blocks.

save

(runs only in the browser)

Internal sequence:

  1. first it calls all of the blocks save methods
  2. it then calls the adapters save method,
  3. aaand finally it calls the load method on this container to finish the command.

cancel

(runs only in the browser)

Internal sequence:

  1. it basically just calls the load on this container.

delete

(runs only in the browser)

Internal sequence:

  1. it first calls the adapters delete method,
  2. and afterwards it calls the load on this container, to finish things up.

block

The blocks are the heart of everything that is editable in GlintCMS.

properties

api

the api property must not be overwritten.

Block.api === 'block'

get/set

You can get/set these properties on the block.

  • id
  • selector
  • el
  • place

methods

constructor/methods

// you normally instantiate the `block` with the specific `block-provider` that it should hold
var block = Block(blockProvider);

// Example:
var block = Block(TextBlock()).use(Style());

// however you can also add/remove the `block-provider` later with `delegate` and `undelegate`
var block = Block();
block.delegate(TextBlock());

// you can also undelegate a block
block.undelegate(textBlock);

The block basically delegates the method calls to the specific block-provider. Due to a runtime behaviour (missing el "HTMLElement"), the block has the ability to buffer method calls in a FIFO, and execute them later.

In addition to the 'getters and setters', it buffers and forwards the following methods:

  • load
  • edit
  • save
  • cancel
  • hasChanged
  • isValid

plugins

You can extend blocks with plugins with the use method, as well as with the mixin method.

use

Consult the [extend] documentation or the code.

mixin

Consult the [extend] documentation or the code.

// Example:
var block.use(Style());

adapter

The adapter is the interface to the storage.

properties

api

the api property must not be overwritten.

Adapter.api === 'adapter'

get/set

You can get/set these properties on the adapter.

  • db
  • type
  • fn

methods

constructor/methods

// you normally instantiate the `adapter` with the specific `adapter-provider` that it should delegate it's calls to.
var adapter = Adapter(adapter);

// Example:
var adapter = Adapter(AjaxAdapter());

// however you can also add/remove the `adapter-provider` later with `delegate` and `undelegate`
var adapter = Adapter();
adapter.delegate(AjaxAdapter());

// you can also undelegate a block
adapter.undelegate(ajaxAdapter);

The adapter basically delegates the method calls to the specific adapter-provider.

  • find(query, callback)
  • load(id, callback)
  • save(id, content, callback)
  • delete(id, callback)

The function has the form: callback(err, result)

the result object is the parsed javascript object for the given id, and an array with the matching objects on the find callback.

plugins

You can extend adapters with plugins with the use method, as well as with the mixin method.

use

Consult the [extend] documentation or the code.

mixin

the mixin is mainly used to extend the adapter's query capability. Since the adapter does not "magically unify the different storage query language", but exposes it directly, the adapter needs a way to handle the different provider's queries.

It does it with the mixin:

var mixin = {
  fs: {
    findWithLocale: function(locale, fn) {
      var query = 'this.id.indexOf("__template__") === -1 && this.locale === "' + locale + '"';
      this.find({$where: query}, fn);
    }
  },
  elasticsearch : {
    findWithLocale: function(locale, fn) {
      // pseudocode
      this.find(elasticSearchSpecificQuery, fn)
    }
 }
};

adapter.mixin(mixin);

As you can see in the example, the mixin must have an object, with the adapter-provider name as the key, and the functions to mixin as the value (nested object).

This way you can support more than just one adapter-plugin, and if you use some one else's module that does not have the queries for your provider, just add them yourself and send a pull request.

// Example:
var block.use(Style());

trigger

A trigger consumes the containers api methods and exposes them to the user in a usable form. Therefore triggers are only useful in the browser, not on the server side. Although theoretically, you could write a trigger to use on the server side as well, maybe for application integration.

// usage example with the trigger implementation: keyboard and sidenav.

var keyboard = require('glint-trigger-keyboard');
var sidenav = require('glint-trigger-sidenav');

wrap.containers.forEach(function(container) {
  keyboard().add(container);
  sidenav().add(container);
});

The trigger itself ('glint-trigger') is only a base class, the trigger implementors can inherit:

// Snippet from the code.

/**
 * Expose `Keyboard`
 */
exports = module.exports = Keyboard;
inherits(Keyboard, Trigger);

extend glintcms

It was important during the design of the module's api, to come up with something that's easy to extend. And I think you can say, it is a strength of GlintCMS, that it is very flexible and easy to extend.

providers

If you want to integrate third party modules for example for a new editing experience, you can just create a new block-provider, and implement the methods:

  • load
  • edit
  • save

Have a look at e.g.: glint-block-text

You don't have to worry, how the stuff is being saved, the adapter takes care of that.

À propos adapter, if you want store your data in another database, you can create a new adapter-provider and implement the methods:

  • find
  • load
  • save
  • delete

Check out e.g.: glint-adapter-fs

Then extend your find queries adapter.mixin() with the query language of your new provider.

plugins

Another great opportunity is, that you can create plugins for the different building-blocks either with the use or mixin method.

Actually, much of the functionality of GlintCMS is build with plugin modules:

Have a look at the examples:

  • block plugin
  • adapter plugin
  • wrap plugin

and search npm for glint-plugin

use

The use method takes a function as argument that's called with this, and returns this, to make it chainable.

It's probably easiest to look at the code, and the usage examples just mentioned before.

Block.prototype.use = function(plugin) {
  plugin(this);
  return this;
};

mixin

If the plugin is just a function, or a couple of functions, you can insert them with the mixin mechanism. It takes an object with the required functions as key, value pairs, and it returns this, to make it chainable.

Maybe the code is even more understandable than the explanation :-):

Block.prototype.mixin = function(mixins) {
  var self = this;
  Object.keys(mixins).forEach(function(key) {
    self[key] = mixins[key];
  });
  return this;
};

events

Plugins get a lot of power from the events.


module development

Developing modules for glint is not difficult. It does not force you using specific libraries, or patterns.

However there is a few things you should consider:

general considerations

  • make sure your module runs on the server as well as in the browser (if it is not intended otherwise).
  • The main glint building blocks right now don't use ES2015, ECMAScript 6 or ES6 or how ever you call the latest JavaScript version. This helps to keep things simple and avoid running into compatibility pitfalls especially with the different browsers.
  • it is often better to keep the modules simple for every one, we don't use anything that needs to be compiled like e.g. less, sass or coffeescript.
  • if you still use something that needs to be compiled, anything like e.g. less, sass or coffeescript, do it in your build, and provide the compiled js, css.
  • also your module should be minimal and not depend on other large modules.
  • know what you require('...')
  • substack wrote a really helpful guide, how you can develop components with browserify: browserify-handbook

structure

  • there is no restrictions on how to structure a glint module
  • the good thing about small modules is, you don't need to care about structure, but size
  • try to split it up into different modules, when it becomes too big
  • a flat folder structure will do most of the time

common modules

If you can, it makes sense to depend on a few common modules instead of many different ones. This list contains modules (dependencies) that are used in many of the modules:

{
 "clone": "^1.0.2",
 "debug": "^2.2.0",
 "defaults": "^1.0.2",
 "dot": "^1.0.3",
 "ejs": "^2.3.4",
 "is-browser": "^2.0.1",
 "page": "git://github.com/intesso/page.js.git",
 "utils-merge": "^1.0.0"
}

style

style is a very personal thing

  • the glint modules use 2 spaces indent, and yes they use semicolons
  • if your module does it differently, that's fine
  • the style should be consistent within a module (check before submitting a pull request).

care and share

And don't forget to share your extensions! Just name the modules starting with glint- and publish them on npm.

Impressions

 

Edit: Content

 

Edit: Image Upload

 

Develop: Add new Block Field

 

 

 

Contact

Send Email

initiated by intesso

shapebootstrap