cms

1.8.1 • Public • Published

CMS

An elegant and easy-to-use, file-based content management system for static pages

Build Status Coverage Status Dependency Status Dependency Status

cms provides great flexibility without having to handle complicated installation steps or fighting with databases. It provides you all essentials, so you can focus on your contents. Regardless of whether you want to publish articles, galleries, simple pages, or big sized pages.

Usage

npm install --save-dev cms
const path = require('path');
const cms = require('cms');
 
cms({
  paths: {
    output: path.resolve(__dirname, 'build')
  }
}).render().then(() => {
  console.log('Done.');
}).catch((error) => {
  console.error(error);
});

Programmatic API

cms(…).render()

Renders the page(s) based on your configuration

Returns: Promise

cms(…).get()

Returns the genesis page based on your configuration for further processing (e.g. headless use)

Returns: Page

cms(…).config()

Returns the merged configuration (= defaults + your custom configuration)

Returns: Object

CLI

npm install -g cms
cms

Content and structure

Content creation can be done very quick and easy. For each page, a folder containing a text file is placed within the content folder. While the folder names form the URLs, the name of the text file determines which template is used by the system. Using numbers as prefixes in front of the folder name, you’re able to order/sort the pages. In addition, those prefixes decide whether a page is 'visible' or 'invisible' – this may be used to control which sub pages are listed in menus, for example (see 'Visible and invisible pages' below for more details).

Managing content

All your site’s content is located in the content folder. The structure of your site will be identical to the structure inside this folder. So, if you have a projects folder inside your content folder, your site will automatically have a http://example.com/projects page.

The content text files are divisible by a YAML-ish syntax into any number of fields. Those fields allow you an unlimited use of the API in templates to display or to control the output of the content. The data structure is modifiable at any time. In addition, the flexibility of the data structure allows creating pages that require a variety of different content types and templates.

You can put as many subfolders inside of folders as you like to build the structure of your site.

Visible and invisible pages

You may recognize that some of the folders in the content folder have numbers prepended to their names…

  • 1-projects
  • 2-about-us
  • 3-contact

…while others don’t…

  • imprint
  • error

The idea behind this: folders with numbers are 'visible' pages, folders without numbers are 'invisible' pages. This may sound a bit weird at first glance, but the difference between those page types is pretty easy: only 'visible' pages will appear in your site’s menu later, while you can still link to 'invisible' pages (but they won’t appear in your menu).

In addition, those numbers in front of visible pages are used by cms to sort pages. This makes it easy to setup a site’s menu at the same time as setting up the general structure. All numbers are automatically stripped in URLs, thus the folder 1-projects will nevertheless have the URL http://example.com/projects in the end.

Adding content

Each folder inside the content folder has a text file in it, which holds all the content for that page. This file may be called page.md (or post.txt or …). Those text files are very easy to read/edit and still offer amazing possibilities to add content. Have a look at the following example (i.e. an example for a blog post).

Title: Hello world
-----
Text: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
 
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
-----
Date: 2017-04-01T13:37:00

As you can see, each field has a name (which needs to consist of characters from A-Z and 0-9 without whitespaces or other fancy characters as it will be casted to camelCase anyways) followed by its content. You have to add five dashes after each field and that’s it.

To structure things a bit more clear, you may want to use additional line breaks (any line breaks at the start and/or end will be trimmed automatically anyways).

Title: Hello world
 
----
 
Text:
 
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
 
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
 
----
 
Date: 2017-04-01T13:37:00

By the way: if you want to do so, you can change the default file’s name as well as the separator characters (colon, five dashes) in your site’s configuration. See 'Configuration' below for details.

The right charset

To cut a long story short: You should always make sure to enter your text as UTF-8. This makes everything a lot easier and every decent text editor has support for UTF-8.

Managing media

cms makes it super easy to add images, videos, sounds, or documents to your pages. Simply drop them into the folder for each page.

(You can sort files by prepending those numbers, just like pages.)

Adding meta data to your files and images

Just like page content files, you may also create file content files, containing stuff like titles or captions. Simply add a text file for each file matching the full name of the file, followed by your content file extension (i.e. test.jpg.md for the file test.jpg). Inside those text files you can define your own fields and content, just like regular content files.

Title: Very nice image
----
Caption: This is a very nice image with loads of colors and stuff.

(cms will automatically fetch this data from the matching text files and add them to your file object, which you can access in the templates later.)

Anatomy of a page object

The following object dump represents the properties that are passed into the template engine next to globals and addons for every page.

Page {
  file: '/Users/johndoe/Repositories/my-fancy-website/content/home.md',
  parent: undefined,
  genesis: Page,
  index: 0,
  visible: false,
  invisible: true,
  identifier: '',
  url: '/',
  output: '/Users/johndoe/Repositories/my-fancy-website/build/index.html',
  template: 'home',
  children: [
    Page,
    Page,
    …
  ],
  hasChildren: true,
  files: [
    File,
    File,
    …
  ],
  hasFiles: true,
  images: [
    Image,
    Image,
    …
  ],
  hasImages: true,
  videos: [
    File,
    File,
    …
  ],
  hasVideos: true,
  sounds: [
    File,
    File,
    …
  ],
  hasSounds: true,
  documents: [
    File,
    File,
    …
  ],
  hasDocuments: true,
  title: 'Lorem ipsum dolor sit amet',
  text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.'
}

parent may include a reference to the current page’s parent page. This may be useful for stuff like breadcrumb navigations. In case the page is a first level page, parent is simply undefined.

genesis may include a reference to the root page. This may be useful for stuff like navigations (i.e. show all first level pages). In case the page is the first level page itself, genesis is simply a reference to this (= the page instance itself).

children may contain any direct child pages of the current page. This may be useful for stuff like navigations. In case the page has no children, children will be an empty array.

images, videos, sounds, and documents may contain any matching files of the current page. files contains all files combined. In case the page does not contain any files, these properties will be empty arrays.

title and text are exemplary custom properties taken from the content file.

Virtual pages

Using virtual pages, you can combine the file-based architecture with content from any other data source like APIs or databases. Any page that doesn’t exist in the content folder is called a »virtual page«. You can add virtual pages to any existing page (even the genesis page) using the addVirtualPage method which accepts properties containing all necessarry page properties.

Properties

The following properties are expected by the system and can be extended using any amount of custom properties (e.g. title, text, …).

index (Default: 0)

While number prefixes in front of folder names are used for ordering/sorting pages and defining their visibility for file-based pages, you may want to define the index explicitly for virtual pages.

identifier (required)

While the nested folder names of its text file determine the identifier of a page (and hence its URL), you have to define the full identifier explicitly for virtual pages.

template (required)

While the name of its text file determines which template is used by the system for file-based pages, you have to define the template name explicitly for virtual pages.

Example

const instance = cms({
  …
});
const genesis = instance.get();
const projects = genesis.findPageByUrl('/projects');
 
const virtualProject = projects.addVirtualPage({
  identifier: 'projects/a',
  template: 'project',
  title: 'This is a virtual project',
  text: 'Lorem ipsum',
});
 
const somethingElse = virtualProject.addVirtualPage({
  identifier: 'projects/a/a',
  template: 'project',
  title: 'This is a virtual project within another virtual project',
  someWeirdFieldName: 'Test'
});

Helpers

basepath(url)

Callback function which prepends the base option (see below) to the specified url. This may be useful if you’re planning to run your static site within a sub directory.

Example:

<link href="<%= basepath('/css/style.css') %>" rel="stylesheet">

has(key)

Returns a boolean indicating whether the current page has the specified property. This may be useful if you’re working with dynamic page properties.

Example:

<% if (has('description')) { %>
<p><%= description %></p>
<% } %>

get(key, defaultValue)

Returns the specified property (or the specified default value if it does not exist) for the current page. This may be useful if you’re working with dynamic page properties.

Example:

<% ['title', 'description'].forEach((prop) => { %>
<p><%= get(prop, 'Something') %></p>
<% }) %>

findPageByUrl(url, context)

Searches the page tree (starting at context, which equals the genesis page by default) recursively for the the page that has the URL url. This may be useful if you need to use properties of other pages located somewhere else in the page tree.

Example:

<% const contactPage = findPageByUrl('/contact') %>
<a href="<%= contactPage.url %>"><%= contactPage.title %></a>

Shortcodes

Shortcodes let you do nifty things with very little effort by allowing you to create macros to be used in your page contents. A trivial shortcode example may look like this.

(youtube: jNQXAC9IVRw width: 480 height: 360)

The example above shows a basic shortcode to embed a YouTube video. The actual embedment is done by an appropiate handler, called everytime the shortcode is used.

{
  youtube: (attrs) => {
    return `<iframe src="https://www.youtube.com/embed/${attrs.youtube}"${attrs.width ? ` width="${attrs.width}"`: ''}${attrs.height ? ` height="${attrs.height}"`: ''}></iframe>`;
  }
}

In addition, the page object of the current page (which includes/invokes the shortcode) gets passed into the shortcode handler function as its (optional) second parameter. This allows shortcodes to interact with the page context.

{
  photo: (attrs, page) => {
    const photo = page.images.find((item) => item.identifier === attrs.photo);
 
    if (photo) {
      return `
        <figure>
          <img src="${photo.url}" alt="${photo.title}">
          ${photo.caption ? `<figcaption>${photo.caption}</figcaption>` : ''}
        </figure>
      `;  
    }
 
    return '';
  }
}

Configuration

CMS uses a sane configuration by default that should cover most use cases. However, if you would like to adjust/extend the configuration, you can either create a file called cms.js within the root directory (where you’re running cms in) which exports the configuration object or pass the configuration object into your cms function call. In both cases, cms expects to receive a proper JavaScript object containing some of the following properties.

template

Function which is called using the parameters file (i.e. the path of the template) and data (i.e. locals) and which is supposed to return a Promise that resolves with the compiled template. This is the place where you would implement your preferred/custom template engine.

(file, data) => Promise.resolve(template(fs.readFileSync(file, 'utf8'))(data))

You may want to use consolidate, a template engine consolidation library.

Default: A Promise-ified version of Lodash’s template function

permalink

Callback function which is called using the parameters permalink (i.e. the plain page permalink) and which is supposed to decorate and return the permalink for a page. This may be useful in case you’re not able to rewrite URLs.

Default:

(permalink) => `${permalink}`

Example:

(permalink) => `${permalink}/index.html`

base

Prefix that is prepended to all links (e.g. pages, files, …). This may be useful if you’re planning to run your static site within a sub directory.

Default: ∅

Example: /wiki

paths.content

Path to the directory containing all your content.

Default: path.resolve(process.cwd(), 'content')

paths.templates

Path to the directory containing all your templates.

Default: path.resolve(process.cwd(), 'templates')

paths.output

Path to the directory where cms is supposed to save the static build to.

Default: path.resolve(process.cwd(), 'output')

separators.line

Pattern that is used to separate lines/blocks within your content files.

Default: -----

separators.values

Pattern that is used to separate keys and values within your content files blocks.

Default: :

extensions.content

Array of extensions your content files may use.

Default:

[
  'md'
]

extensions.templates

Array of extensions your template files may use. In case you’re using a custom template engine, you most certainly will need to set the appropriate extensions here.

Default:

[
  'tpl'
]

extensions.images

Array of extensions, your images within page content directories may use. These extensions are used to find matching images for each page (i.e. the images property).

Default:

[
  'jpg',
  'jpeg',
  'gif',
  'png',
  'webp'
]

extensions.videos

Array of extensions, your videos within page content directories may use. These extensions are used to find matching videos for each page (i.e. the videos property).

Default:

[
  'mpg',
  'mpeg',
  'mp4',
  'mov',
  'avi',
  'flv',
  'ogv',
  'webm'
]

extensions.sounds

Array of extensions, your sounds within page content directories may use. These extensions are used to find matching sounds for each page (i.e. the sounds property).

Default:

[
  'mp3',
  'wav',
  'm4a',
  'ogg',
  'oga'
]

extensions.documents

Array of extensions, your documents within page content directories may use. These extensions are used to find matching documents for each page (i.e. the documents property).

Default:

[
  'pdf',
  'doc',
  'xls',
  'ppt',
  'docx',
  'xlsx',
  'pptx'
]

extensions.output

Extension of static output files.

Default: html

globals

Object containing globals that will be passed into the template function next to the regular page data. This may be useful if you need to make data available to all pages. The object is deep-merged into the regular page data.

Default: {}

addons

Object containing globals that will be passed into the template function next to the regular page data. This shall be used for all custom functions you would like to provide within your templates.

Default: {}

Example:

{
  markdown: (input) => marked(input),
  reverse: (input) => input.split('').reverse().join('')
}

shortcodes

Object containing shortcode handlers that will be applied to page data. This shall be used to register any custom shortcode you would like to provide within your content.

Default: {}

Example:

{
  youtube: (attrs) => {
    return `<iframe src="https://www.youtube.com/embed/${attrs.youtube}"${attrs.width ? ` width="${attrs.width}"`: ''}${attrs.height ? ` height="${attrs.height}"`: ''}></iframe>`;
  }
}

TODO / Roadmap

cms is quite stable now. Most changes are new features, minor bug fixes, or performance improvements. Currently I’m tinkering with the following features.

  • Support for incremental builds (i.e. to speed up builds)

Changelog

  • 1.8.1
    • Ensure proper sorting of virtual pages
  • 1.8.0
    • Implement support for virtual pages
    • Update dependencies
  • 1.7.1
    • Collect test coverage
    • Fix npm ignore list
  • 1.7.0
    • Implement tests
    • Implement new page methods has and get
    • Update dependencies
  • 1.6.0
    • Move programmatic rendering to render method and add get/config methods to allow headless use
    • Enforce proper errors for promise rejections
    • Update dependencies
  • 1.5.0
    • Implement optional context parameter for shortcode helper
    • Update dependencies
  • 1.4.0
    • Hand over pages into shortcode handlers
    • Update dependencies
  • 1.3.1
    • Fix shortcode examples
  • 1.3.0
    • Migrate to shortcodes for shortcode parsing
    • Update dependencies
  • 1.2.5
    • Fix parsing of index prefixes to prevent misplaced digits within URLs
  • 1.2.4
    • Fix parsing of index prefixes to prevent inaccurate digit replacements within URLs
  • 1.2.3
    • Implement proper natural sorting of pages
    • Implement more specific rejections in case a page is invalid (e.g. missing genesis page)
  • 1.2.2
    • Apply base prefix and permalink callback to findPageByUrl helper queries automatically
  • 1.2.1
    • Adjust basepath to only prepend the base path to an URL in case it isn’t there yet
    • Fix typos in documentation
  • 1.2.0
    • Implement new helper methods basepath and findPageByUrl
  • 1.1.0
    • In case a page is the first level page itself, genesis is now a reference to this (= the page instance itself) instead of undefined
  • 1.0.0
    • Initial version

Thanks

Special thanks to Thom Blake for handing over the cms package name on npm to me. Please check out thomblake/cms in case you’re looking for the code of versions <1.0.0.

License

Copyright (c) 2019 Thomas Rasshofer
Licensed under the MIT license.

See LICENSE for more info.

Readme

Keywords

Package Sidebar

Install

npm i cms

Weekly Downloads

65

Version

1.8.1

License

MIT

Unpacked Size

35.4 kB

Total Files

12

Last publish

Collaborators

  • rasshofer