GlintCMS is a modular and flexible CMS that basically has no backend, but slick inline ediiting capabilities. It is built with node.js
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.
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.
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:
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.
node.js 0.12.7 and 4.1
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.
The GlintCMS core modules are mainly built with:
node.js |
|
express.js |
|
npm |
|
browserify |
|
JavaScript ES5 |
... and many more great open source npm modules ...
(no GlintCMS Requirement):
jQuery |
|
Bootstrap |
|
HTML 5 |
|
CSS 3 |
... and many more great open source npm modules ...
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 with blocks is fun!
+--------------+ +--------------+ +--------------+
| | | | | |
| wrap | +----> | container | +----> | block |
| | | | | |
+--------------+ +--------------+ +--------------+
+ +
| |
v v
+--------------+ +--------------+ + + + + + + + +
| | | |
| widget | +----> | adapter | +----> + cache +
| | | |
+--------------+ +--------------+ + + + + + + + +
created with asciiflow
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()
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)
Is optional and is not yet implemented.
The container
(glint-container
) holds the different blocks and orchestrates nifty details, like
edit
and back (save
or load
on cancel)adapter
and the block
s.block
s.If you don't have any editable content on your page, you don't need a container.
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.
The wrap
(glint-wrap
) wraps it all up.
container
s, widget
s and wrap
s.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());
Have a look at the extend documentation.
The following api
document describes how to "use" the building blocks
, (not how to extend them).
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());
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.
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);
})
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
.
Getter and Setter emit an event:
emit(name, value)
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 (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):
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
A wrap
can hold several container
s and widget
s as well as other wrap
s.
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 place
s.
the api
property must not be overwritten.
Wrap.api === 'wrap'
You can get/set these properties on the wrap
.
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.
// 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'))
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);
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
method calls, are executed after the previous method has finished.
It has got the same signature as the parallel
method.
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.
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);
});
}
With a widget, you can render/display noneditable content on the server and/or in the browser depending on the defined place
.
the api
property must not be overwritten.
Widget.api === 'widget'
You can get/set these properties on the widget
.
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.
// 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);
})
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);
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);
})
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.
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:
the api
property must not be overwritten.
Container.api === 'container'
You can get/set these properties on the container
.
// 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)
(it runs either on server or browser depending on the place
)
Internal sequence:
adapter
s load
method,block
s load
methods(runs only in the browser)
Internal sequence:
load
method on this containeredit
method on all of the block
s.(runs only in the browser)
Internal sequence:
block
s save
methodsadapter
s save
method,load
method on this container to finish the command.(runs only in the browser)
Internal sequence:
load
on this container.(runs only in the browser)
Internal sequence:
adapter
s delete
method,load
on this container, to finish things up.The blocks
are the heart of everything that is editable in GlintCMS.
the api
property must not be overwritten.
Block.api === 'block'
You can get/set these properties on the block
.
// 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:
You can extend block
s with plugins with the use
method, as well as with the mixin method
.
Consult the [extend] documentation or the code.
Consult the [extend] documentation or the code.
// Example:
var block.use(Style());
The adapter
is the interface to the storage.
the api
property must not be overwritten.
Adapter.api === 'adapter'
You can get/set these properties on the adapter
.
// 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
.
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.
You can extend adapter
s with plugins with the use
method, as well as with the mixin method
.
Consult the [extend] documentation or the code.
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());
A trigger
consumes the container
s api methods and exposes them to the user in a usable form.
Therefore trigger
s 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);
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.
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
.
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:
and search npm for glint-plugin
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;
};
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;
};
Plugins get a lot of power from the events.
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:
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.require('...')
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 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).
And don't forget to share your extensions! Just name the modules starting with
glint-
and publish them on npm.