Introduction
|
Note
|
This documentation is still a WIP. |
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.
| Setting | Type | Default value | Description |
|---|---|---|---|
|
|
|
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. |
|
|
- |
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. |
|
|
|
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. |
|
|
|
Used to highlight certain elements in the admin user interface. You can set this to match the theme of the website. |
|
|
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.
|
| Setting | Type | Default value | Description |
|---|---|---|---|
|
|
- |
A unique identifier for this library. If |
|
|
|
The content type. Can be either |
|
|
- |
A human-readable name for the library, used in the CMS user interface. It should be a plural. |
|
|
- |
A singular version of the |
|
|
|
The location to store the content files for this library. |
|
|
|
The location to store static files for this library. |
|
|
|
The library filetype. If set to |
|
|
"" |
Description for this library. Will be rendered in the CMS user interface. |
|
|
|
Allow the user to delete library items. |
|
|
|
Allow the user to create new items. |
|
|
|
A pattern template to use for the file names. See Placeholders for a list of available parameters. |
|
|
|
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). |
|
|
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.
|
| Setting | Type | Default value | Description |
|---|---|---|---|
|
|
- |
A human-readable label for the field. |
|
|
- |
A unique identifier for the field. |
|
|
|
The field type. These all map to the HTML input field types,
with the exception of |
|
|
|
Supply a default value for the field. |
|
|
|
Make the field required. |
|
|
|
Make the field readonly. Note that while you are not required to supply a
|
|
|
|
The order to render the field relative to the other fields. Lower values are
rendered first. If multiple fields have the same |
|
|
|
Defines the options for |
|
|
|
If set for an |
|
|
|
If set for an |
|
|
|
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