Introduction

Note
This documentation is still a WIP.
Robin CMS logo
Figure 1. Robin CMS logo

Robin CMS is a minimalist flat-file CMS built with Ruby and Sinatra. It is designed to be used by developers for creating custom built websites where the client needs to be able to update content themselves. It works with any Static Site Generator and can also be embedded in a dynamic Sinatra app. The idea is that you can just drop it into your project and it gives you a completely customised CMS for your website.

It is completely headless - it gives clients an admin interface where they can manage raw content, while giving the developer full control over the HTML and CSS.

You can define the content model of your website using a YAML file. That way you don’t have to wrangle all your data into a "blog" post. You can choose to store content either as HTML (predominantly for content with rich text), or YAML for structured key-value data.

Robin CMS is designed to keep things as simple as possible. It uses files to store data so you don’t have to worry about managing a database. The entire CMS can be installed with just two files - a two line config.ru file and a _cms.yml configuration file.

Getting started

Robin CMS is packaged as a Ruby gem. If you’re a Ruby developer, you probably know what to do already. Just use the usual incantation:

% gem install robin_cms

Or type bundle add robin_cms. Or manually add it to your Gemfile.

source "https://gem.coop"

gem "robin_cms"

If you’re new to Ruby, see the Ruby gems and Bundler documentation to see how it all works. The Jekyll Quickstart and Ruby 101 pages are also a great reference for getting started with Ruby gems.

Usage

You have a few options for using this gem in your project. You can use it as a standalone CMS for a Static Site Generator, you can embed it in a dynamic Sinatra app, or you can use it as a Jekyll plugin.

Option 1: Standalone CMS for an SSG

First install a Rack web server. A popular choice is Puma, but any Rack-based server will work.

% bundle add rackup puma

Create a file called config.ru in the root directory of your project.

require "robin_cms"
map "/admin" { run RobinCMS::CMS.new }

Make sure that your SSG ignores this file as you don’t want it ending up in your public directory. Now you should be able to run rackup, and go to http://localhost:9292/admin in your browser.

Option 2: Embed it in a Sinatra app

First install the required gems:

% bundle add sinatra rackup puma

Then create a config.ru file:

require "robin_cms"
require "sinatra"

map "/admin" do
  run RobinCMS::CMS.new
end

get "/" do
  "Hello, world!"
end

run Sinatra::Application.new

And run the server:

% rackup

If using the CMS as a Sinatra app, you get full access to the content API to use within your app. See API for a full description of the available API.

Option 3: Use it as a Jekyll plugin

Finally, you can use the CMS as a Jekyll plugin. This is the easiest option if you are using Jekyll as your SSG. Just add robin_cms to the :jekyll_plugins group of your Gemfile like so:

gem "jekyll"

group :jekyll_plugins do
  gem "robin_cms"
end

After running bundle exec jekyll serve, the CMS should be available on your website under /admin.

Configuration

The CMS is configured with a single YAML file called _cms.yml. This file defines the entire content model for the CMS. It also contains fields used to customise the user interface.

Note that if you are using it as a Jekyll plugin (see Usage), you can use your existing _config.yml file for configuration. In this case, all fields should be nested under cms in your config file, e.g.:

# Jekyll configuration...
cms:
	# Robin CMS configuration...

The following table lists the available top-level configuration options. Optional settings are marked with an asterix (*). If omitted, they will default to the value in the "Default value" column. Data types correspond to Ruby data types, as parsed by the YAML module.

Table 1. CMS configuration
Setting Type Default value Description

title*

STRING

""

The name of your website. Appears in the header of the admin page. If running as a Jekyll plugin, you can omit this field and it will use the value from your Jekyll config.

url

STRING

-

The URL where the website will be hosted. If running as a Jekyll plugin, you can omit this field and it will use the value from your Jekyll config.

build_command*

STRING

""

The command used to build the site (if using an SSG). This allows you to rebuild the site from within the admin portal using the "publish" button. The publish button will not be shown if this field is omitted.

accent_color*

STRING

"#fd8a13"

Used to highlight certain elements in the admin user interface. You can set this to match the theme of the website.

libraries

[LIBRARY]

A list of libraries, defining the content model of your CMS. See Libraries.

Libraries

This section lists the available library configuration options. There are two types of libraries: collection and data. For collection libraries, each item is stored in it’s own file. For data libraries, all items in the library are stored in a single file as an array of objects, in the format specified by the filetype attribute. These types map to Jekyll’s collections and data files respectively. This should work with most SSGs as most of them have a concept of data files and collections, though they might be named something different. Consult your SSG’s documentation for more information.

Note
At this stage, the only available filetypes are html for collection libraries, and yml for data libraries. There are plans to support Markdown for collection libraries and JSON, TSV, and CSV for data libraries in the future.
Table 2. Library configuration
Setting Type Default value Description

id

STRING

-

A unique identifier for this library. If type is data (see below), this will also be used as the name of the data file.

type*

collection | data

collection

The content type. Can be either collection or data. If set to data, saves all items in the library as a single YAML file. If set to collection, saves each item in the library as an individual YAML file.

label

STRING

-

A human-readable name for the library, used in the CMS user interface. It should be a plural.

label_singular

STRING

-

A singular version of the label.

location*

STRING

.

The location to store the content files for this library.

static_location*

STRING

assets

The location to store static files for this library.

filetype*

html | yml

html

The library filetype. If set to html, data will be saved in HTML format. If set to yml, data will be stored in YAML format. The default value for this field depends on type. If type is collection, defaults to html. If type is data, defaults to yml.

description*

STRING

""

Description for this library. Will be rendered in the CMS user interface.

can_delete*

BOOLEAN

true

Allow the user to delete library items.

can_create*

BOOLEAN

true

Allow the user to create new items.

pattern*

STRING

":title"

A pattern template to use for the file names. See Placeholders for a list of available parameters.

display_name_pattern*

STRING

":title"

A pattern template to use for the display name (rendered in the CMS user interface). Each word begining with a colon will be replaced with the corresponding field value (see Fields).

fields

[FIELD]

See Automatic fields.

A list of fields for the library.

Fields

This section lists the available library configuration options. Fields define a schema for what data is stored in your content files. For collection libraries, fields are stored in the content files as frontmatter. If the library contains a richtext field, it is stored in the body. For data libraries, fields correspond to the fields of each data object in the file.

Note
richtext fields can only be used with collection libraries, and only a single richtext field may be used in a library.
Table 3. Field configuration
Setting Type Default value Description

label

STRING

-

A human-readable label for the field.

id

STRING

-

A unique identifier for the field.

type*

text | richtext | date | hidden | number | color | email | url | image | select

text

The field type. These all map to the HTML input field types, with the exception of richtext which will store the data as formatted HTML using a simple WYSIWYG editor in the CMS user interface.

default*

STRING

""

Supply a default value for the field.

required*

BOOLEAN

false

Make the field required.

readonly*

BOOLEAN

false

Make the field readonly. Note that while you are not required to supply a default for readonly fields, it is recommended. Otherwise you will have an empty value which the user cannot edit.

order*

INTEGER

nil

The order to render the field relative to the other fields. Lower values are rendered first. If multiple fields have the same order, the resulting order is unspecified.

options*

[OPTION]

nil

Defines the options for select fields. It should be an array of hashes, each with a label and value attribute. This field is ignored for non-select fields.

dimensions*

STRING

nil

If set for an image field, uploaded images will automatically be resized to these dimensions using. This will be ingored if set on a non-image field. See Image assets for more details.

filetype*

STRING

png | jpg

If set for an image field, uploaded images will automatically be converted to the specified file type. This will be ingored if set on a non-image field. See Image assets for more details.

description*

STRING

nil

A description of the field. It will be rendered underneath the field label in the CMS user interface.

Automatic fields

Automatic field are fields that are automatically inserted into every library by default. If you define your own fields with the same id as any of the automatic fields, they will override the automatic fields.

Every library automatically gets the following fields:

- { label: Title, id: title, type: text, required: true, order: 1 }
- { label: Published date, id: created_at, type: hidden }
- { label: Last edited, id: updated_at, type: hidden }

Image fields

If you define an image field in your library, the following automatic fields fields will be created:

- { id: image_src, type: hidden }
- { id: image_alt, type: text, label: Alt text }

Note that the image_src field is hidden. Users can’t upload an image by URL directly. This field will automatically be populated with the path of the static asset that is uploaded by the image input. If you want to allow users to upload an image from a URL, instead of an image field, create a url field:

- { id: image_from_the_web, type: url, label: Paste link to your image here! }
Note
You may only define a single image field on a library.

Drafts

For collection libraries, the following automatic field will be created:

- id: published
  type: select
  label: Published
  default: false
  options: [{ label: Draft, value: false }, { label: Published, value: true }]

This allows the user to save drafts of content which won’t be rendered on the site when publishing. This works by default in Jekyll as any collection items with published: false in the frontmatter won’t be rendered. For other Static Site Generators, you may have to explicitly skip rendering of draft content. Check the documentation for your SSG. This feature is only enabled for collection libraries by default. If you want to enable drafts on data libraries, you can explicitly add this field.

Note
Jekyll ignores published: false for data files, so if you use this on data files, you’ll need to explicitly skip rendering of draft items.

Placeholders

For configuration options which take a pattern string, the following placeholders are available:

:title

replaced with the sluggified item title

:year

replaced with the created year

:month

replaced with the created month

:day

replaced with the created day

Warning
Put strings containing placeholders in quotes to prevent Ruby from interpreting them as a symbols.

Setting a password

The admin username and password needs to be set in a .htpasswd file in the root directory of the project. Obviously make sure you .gitignore that file. Also make sure your static site generator is ignoring it because you don’t want it in your public directory! Each line of the .htpasswd file should follow the format <username>:<password>, but note that only a single username/password is supported for now. The password needs to be encrypted with bcrypt. You can do this in Ruby with the bcrypt gem:

% ruby -r bcrypt -e "puts BCrypt::Password.create('mypassword')"

Another thing to note is that if no .htpasswd file is found, it will automatically create one with username “admin” and password “admin”. This lets you play around with it locally without configuring a password. So make sure you create a .htpasswd file before running it in production!

Setting a session secret

You’ll also need to expose a SESSION_SECRET environment variable. If you don’t, it will create one for you, but it creates a new secret each time the server starts, meaning you will have to log in again whenever you restart the server. It is recommended to create one via Ruby’s SecureRandom package.

% ruby -r securerandom -e "puts SecureRandom.hex(64)"

Image assets

TODO: Document available options for automatically formatting images using ImageMagick.

API

TODO: Document the API for Item, DataLibrary, and CollectionLibrary.

Deployment

This guide assumes:

  • The app is running on port 3001

  • The app is running as user www

  • The domain name is example.com

  • The CMS is running under example.com/admin

  • The source code is in /var/www/example.com

Sample nginx.conf:

server {
    listen 80;
    server_name example.com;

    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        root /var/www/example.com/_site;
        index index.html;
    }

    location ~ ^/admin(/.*)? {
        proxy_pass http://127.0.0.1:3001/admin$1$is_args$args;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
    }
}

Note that the $is_args$args part is important - without it, query string parameters won’t be passed on.

Sample systemd service file:

[Unit]
Description=Example admin
Requires=network.target

[Service]
Type=simple
User=www
Group=www
WorkingDirectory=/var/www/example.com
ExecStart=/bin/bash -lc "bundle exec jekyll serve --port=3001 --skip-initial-build --no-watch"
TimeoutSec=30
RestartSec=15s
Restart=always

[Install]
WantedBy=multi-user.target