This repo is an ebook about the locomotive cms that is currently in work. Feel free to Fork !!
Pull request Compare This branch is 20 commits behind geraudmathe:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
images
.gitignore
README.md
todo.md

README.md

LocomotiveCMS Guide

This book is incomplete and should evolve in the future. Any contribution is very welcomed !

For any questions or advices about this book, ask mail@geraudmathe.com, and if you need support from LocomotiveCMS team, ask support@locomotivecms.com.

Summary

  1. Foreword
  2. Overview
  3. Getting something running in 5 minutes
  4. Templating
  5. Models
  6. LocomotiveCMS Editor
  7. Using LocomotiveCMS in an existing Rails app
  8. Tips
  9. Appendix

Foreword

Why this guide ?

There is already an official documentation reference, which lists almost everything. Still, a pragmatic guide to LocomotiveCMS is missing, especially for beginners.

What's more, since there is a lot of goodness in the LocomotiveCMS's Google Group, it seems relevant to gather good practices & hacks in one place.

TBR: This guide isn't the official one, even if some members of the LocomotiveCMS core team have reviewed some parts of it.

Philosophy

Let's start first with a little bit of history.

When I was a developer at a Chicago web agency, I built numerous custom and unique content management system applications for each of our clients. Even though I enjoyed crafting unique back-offices for our clients, it was clear to me that I should spend less time on those kinds of projects so that I may tackle larger ones. At this time, there were no open source Rails CMSes fitting both my needs and my vision of ideal CMS.

Upon returning to France, I kept thinking about what the perfect CMS should be. My thoughts were also enhanced by my experiences as a Rails developer in other companies. I began coding a prototype which have constantly improved ever since. I also spent a lot of time experimenting with many concepts and through process learned a lot about what the perfect CMS should look like.

However, I stayed true to what were the basic requirements of my dream CMS and never moved away from them. These requirements are:

  • A single instance of LocomotiveCMS should host many sites. Once LocomotiveCMS is installed, setting up a new site has to be quick and will not have to require the help of an admin system guy.
  • It should be effortless for the content editors to edit the site without ruining the layout or, worse, crashing the site.
  • Developing a LocomotiveCMS site should not require Ruby on Rails knowledge.
  • It should be possible and easy to extend and customize LocomotiveCMS in elegant ways.
  • The back-office should be sexy as hell.
  • The code of LocomotiveCMS should not "smell". Thus, refactoring the code is a continuous process during all the development. That also means using standard components like for instance "Devise" for the authentication part.

Why should you use LocomotiveCMS ?

LocomotiveCMS is a CMS that has been created with a main guideline: keep it simple !

  • Keep it simple, for the developer who shouldn't have to go deep in architecture, and should be able to edit a website quickly.
  • Keep it simple, for the author who needs to be focused on content, and shouldn't have to go through several pages to edit.

If you recognize yourself in on of the case listed above, you should use LocomotiveCMS.

///

From a "business" point of view, LocomotiveCMS have several benefits to sell :

  • Front-end editing of static texts, using Aloha editor
  • Hosting on Heroku / AWS very cheap, almost free
  • Finally, a great looking back-office !

Assumptions

During this reading, it is assumed that:

  • You know what is Ruby and Rails and you've a good feeling with terms like Gem, Bundler, deployment
  • You know what is a data model, and ideally what is a document-oriented storage like Mongo
  • You have basic knowledge about shell and command-line interface

Organization of this book

This guide is structured as follow :

First, an Overview of the CMS aims to introduce the environment and the main things to know about.

Getting something running in 5 minutes may help LocomotiveCMS's beginners walking through the

Overview

Anatomy of a LocomotiveCMS app

LocomotiveCMS is crafted as an engine.

A Rails engine is an application packaged as a ruby gem that is able to be run or mounted within another Rails application. An engine can have its own models, views, controllers, generators and publicly served static files. (more about engines)

What's inside ?

Key features

You have out of the box :

  • Multi sites : manage multiple websites with one application instance
  • Flexible content types
  • Front-end inline editing (Aloha editor)
  • Content localization
  • Restful API
  • Haml / Sass support
  • Liquid templating langage
  • A very nice User Interface

Getting something running in 5 minutes

TODO: définir avec géraud et didier ce que l'on fait dans cette app, liste des choses à voir : editable texts, models, templates (héritage), tags liquid de base, … ?

il n'y a pas de template de base, sauf si on achète loco editor là il y en a mais sinon non, pas dans la version 2.0

Templating

Templating Logic

Basics of inheritance

The logic in LocomotiveCMS is differs a bit from what you are certainly used to, it may be weird a first, but it's actually very simple.

In the classic, Rails way, you have the following architecture, with page's content integrated in the application layout using the yield statement :

+- Views
    +- layout
        +- application
    +- mysite
        +- index
        +- first page
        +- second page

In Locomotive, it's a bit different :

+- Pages
    +- index
        +- first page
        +- second page

All pages inherit from index. This way, the index contains the application's layout and the index page content. How do you re-use the layout without re-using the index page content ? By introducing {% block 'block_name' %} ... {% endblock %} : since all pages inherit from index, you declare blocks of content inside the layout (index), which will be overwritten in child pages. Here is a the simplest example :

Index page :

<html>
  <head>
    <title>My index page</title>
  </head>
  <body>
    <header>
      layout header
    </header>
    <div id="content">
      {% block content %}
        the content of the index page
      {% endblock %}
    </div>
    <footer>
      layout footer
    </footer>
  </body>
</html>

A page, which inherits from index :

{% extends parent %}

{% block content %}
  the content of this page
{% endblock %}

By extending index, 'a-page' re-use all of its content, except the content inside the {% block %} tag which is overwritten. This tag is written with the Liquid syntax which will is explained later.

You can have as many {% block %} tags as you want, everywhere in the layout, as long as the name of each block is unique. For a basic application which only have one layout, that's all you need to know.

src: http://doc.locomotivecms.com/templates/tags#block-section

Going further

Several levels of inheritance

The principle of page's inheritance can be applied to every page. When you create a page, it automatically inherits from index, but you can also make it inherits from another page, by specifying it's parent :

Specifying parent

By doing so, you can define as many levels as you want :

+- Pages
    +- index
        +- first page
            +- child of first page
                +- child of first page's child
        +- second page

Inherit from an other page than the parent one

When you extend the parent's layout, you use the tag {% extends parent %}, but what if you would like to extends a page which isn't a direct parent ?

For example, how would you make "first page" extends "second page" ?

+- Pages
    +- index
        +- first page
        +- second page

It's simple : {% extends first_page %} ! You specify the page you want to extend with its slug.

src: http://doc.locomotivecms.com/templates/tags#extends-section

What about several layouts ?

Let's say your website needs two layouts, how do it without putting the entire index in {% block %} tags ? It's actually fairly simple : you are not forced to make a page inherits its content from another.

Remember the previous page we created which inherited from index :

{% extends parent %}

{% block content %}
  the content of this page
{% endblock %}

Well, actually the tag {% extends parent %} can be removed, so the page doesn't extends any other page, letting you define a brand new layout if needed.

Let's illustrate this with an example:

  • the main layout of the site will be define in index
  • a second layout will be defined in a page called "alternate_layout"
  • we will have a page called "normal" which will use the main layout
  • and finally an other page called "alternate_page" which will use the alternate layout

The skeleton will look like that :

+- Pages
    +- index
        +- normal
        +- alternate_layout
            +- alternate_page

Here we go :

First, the index page :

 <html>
  <head>
    <title>The Main Layout</title>
  </head>
  <body>
    <header>
      Main header
    </header>
    <div id="content">
      {% block main_content %}
        the content of the index page
      {% endblock %}
    </div>
    <footer>
      Main footer
    </footer>
  </body>
</html>

The "normal" page, which inherits from it index :

{% extends parent %}

{% block main_content %}
  the content of the normal page
{% endblock %}

Then the "alternate layout" page, which doesn't extends its parent, index :

<html>
  <head>
    <title>The Alternate Layout</title>
  </head>
  <body>
    <header>
      Alternate header
    </header>
    <div id="content">
      {% block alternate_content %}
        the content of the alternate layout page, it can be empty if you just want to define an empty layout
      {% endblock %}
    </div>
    <footer>
      Alternate footer
    </footer>
  </body>
</html>

And finally, the "alternate page", which inherits from "alternate layout". You may notice the {% extends alternate_layout %} instead of {% extends parent %}, as explained in the previous part.

{% extends alternate_layout %}

{% block alternate_content %}
  the content of alternate page, using the layout defined in alternate_layout.liquid.html
{% endblock %}

Snippets

To conclude with templating basics, it's worth to know that LocomotiveCMS gives you the ability to put some blocks of code in a separate file called snippet, in the same way Rails does with partials. That's very useful when you have to build a modular layout without repeating code.

Like Rails, you can pass a variable to the snippet, or simply include a static bloc of code. The following example will cover both cases, don't bother with the liquid syntax which will be explained in the next part.

+- Pages
    +- index
+- Snippets
    +- sidebar
    +- product_information

Here is the index, which includes the sidebar, and also loops on products model and include the snippet "product_information" at each product item :

<html>
  <head>
    <title>Snippet example</title>
  </head>
  <body>
    <header>
    </header>
    <div id="content">
      <!-- Loop on products  -->
      {% for product in contents.products %}
        <!-- Include "product_information" snippet with the current product -->
        {% include 'product_information' with product %}
      {% endfor %}
    </div>
    {% include 'sidebar' %}
    <footer>
    </footer>
  </body>
</html>

Then the sidebar :

<div id="sidebar">
  the sidebar
</div>

And finally the product_information snippet which uses the context "product" :

<div class="product">
{{ product.name }} :  {{ product.price }}$
</div>

src: http://doc.locomotivecms.com/templates/tags#include-section

Liquid syntax

Liquid is a templating library extracted from Shopify, the project is hosted at http://liquidmarkup.org. LocomotiveCMS reuses a lot of the original library.

Everything in 2 markups

The liquid syntax is a templating engine based on a set of functions that allow the developer (or the designer, since you don't need strong code skills to write it) to keep focus on the rendering of the data, not on the way it could render it. Liquid defines 2 types of markup, pretty close to what you are used to with Erb :

Ouput markup : matched pairs of curly brackets output the value of an object :

Erb :

<%= @product.name %>

Liquid :

{{ product.name }}

Tag markup : matched pairs of culry brackets and percent, not resolved to text :

Erb :

<% name = @product.name %>

Liquid :

{% assign name with product.name %}

Liquid is extracted from http://www.shopify.com, but LocomotiveCMS extends it. To cover all, we will distinguish 3 cases :

original Liquid doc: https://github.com/Shopify/liquid/wiki/Liquid-for-Designers

Objects

When writing a liquid template, you will access to a couple of objects representing for instance the current site, page, logged in account as well as collections such as your custom content types. They are also called 'drops'.

Available objects and their attributes are listed here : http://doc.locomotivecms.com/templates/objects

SEO purpose

You can either use the object site and have the same meta all over your website :

<html>
  <head>
    <title>{{ site.seo_title }}</title>
    <meta name='description' content='{{ site.meta_description }}' />
    <meta name='keyword' content='{{ site.meta_keywords }}' />
  </head>
  <body>
  </body>
</html>

Or you can define SEO meta for each page :

<html>
  <head>
    <title>{{ page.seo_title }}</title>
    <meta name='description' content='{{ page.meta_description }}' />
    <meta name='keyword' content='{{ page.meta_keywords }}' />
  </head>
  <body>
  </body>
</html>

Filters

img magick http://markevans.github.com/dragonfly/file.ImageMagick.html

Tags

editable file => https://groups.google.com/forum/#!topic/locomotivecms/hOaqFUcZCm8 only in backoffice for 2.0

example: promotion

Creating a page

You have several option when you create a page. Let's take a look.

General information

General Information

Nothing complex, just specify the name of the page. The slug field will be updated automatically.

Be aware the slug will reference the url linked with the page you are creating, if you change it later, it could break links in the website.

Set the parent page, as explained in Templating Logic.

SEO settings

SEO settings

Edit the meta title, keyword, description for the page, or leave it empty if you want use the global meta.

These meta values will then be available for use in the template with Liquid tags, this way :

<title>{{ page.seo_title }}</title>
<meta name="keywords" content="{{ page.keywords }}"/>
<meta name="description" content="{{ page.description }}"/>

Advanced options

Advanced options

  • Handle :

    Used when you integrate LocomotiveCMS with a Rails app, see this chapter.

  • Response type :

    You can choose between HTML, RSS, XML or JSON. You may this way generate a RSS feed or build an simple API from your LocomotiveCMS site.

  • Templatized :

    Defines wether this page should be a template for a model instance, see this chapter.

  • Published :

    Since only authenticated accounts can view unpublished pages, this allows debugging a page on a deployed site.

  • Listed :

    The Liquid {% nav %} generates a menu (doc) based on your page. Control here wether this page appears in the generated menu.

  • Redirect :

    If you check this, you can redirect the page to a url.

    It then will be a 301 redirection, which from a SEO point of view, is a permanent redirection. You should use it when you have changed your urls. The search engine will then forget the previous page and update the url in its database.

    source: [https://groups.google.com/d/topic/locomotivecms/UoNFhChvpOQ/discussion](https://groups.google.com/d/topic/locomotivecms/UoNFhChvpOQ/discussion)

  • Cache strategy :

    Define here the cache strategy for this page.

Recipe : Create a RSS feed

You can create a page which will be used as a RSS feed for your blog. In the list of pages, it is tagged with the "RSS" label.

RSS page

Assumptions:

  • a "articles" page was created as well as a templatized page for an article.
  • an "article" model was also created.

In order to create that kind of page, follow these steps:

  • click on the "new page" button.
  • select "RSS" as the response type in the "Advanced options".
  • fill the template
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:wfw="http://wellformedweb.org/CommentAPI/"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
  xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
  >
  <channel>
    <title>{{ site.seo_title }}</title>
    <description>{{ site.meta_description }}</description>
    <link>http://www.example.com</link>
    <language>en</language>
    <copyright>Example</copyright>
    <ttl>30</ttl>
    <atom:link href="http://www.example.com/articles/rss.xml" rel="self" type="application/rss+xml" />
    {% for article in contents.articles %}
      <item>
        <title>{{ article.title }}</title>
        <description><![CDATA[{{ article.excerpt }}]]></description>
        <content:encoded><![CDATA[{{ article.body }}
        ]]></content:encoded>
        <link>http://www.example.com/articles/{{ article._permalink }}</link>
        <guid isPermaLink="true">http://www.example.com/articles/{{ article._permalink }}</guid>
        <pubDate>{{ article.posted_at | localized_date: '%a, %d %b %Y %H:%M:%S %z' }}</pubDate>
        <source url="http://www.example.com/">example.com</source>
      </item>
    {% endfor %}
  </channel>
</rss>
  • click on the "create" button at the bottom of the screen

Note: do not forget to set the "Published" flag to true.

If you want the browsers and news readers to auto-detect your RSS feed, add the following statement within the "head" tag of your template.

{{ '/articles/rss.xml' | auto_discovery_link_tag }}

Models

This chapter covers models, or the custom content, LocomotiveCMS lets you build in the UI. Here we use the word model as it's what we are used to, but in the LocomotiveCMS reference you will see content type for model, and content entry for an instance of a model.

The first subchapter aims to introduce the very basic creation and usage of models, the second one is about building relationships between your models. We cover then the rendering of models using Liquid, where we try to show the common use cases, and after that a subchapter is dedicated to the functionality of templating a model. Finally, we will cover the public submission of models, which allows frontend users to create instances.

Basics

First step of model creation, specify the name of the model :

Create model

As mentioned in the hint, you will reference your model in Liquid logic by its slug. Then, define the fields (attributes) of your model in the following section :

Create fields

Fields types

The following types of attributes (fields) are available :

Types list

Let's detail each one of them. The rendering of these types will be detailed later.

Nota bene : you will encounter some properties not explained, it's because they are common to all field types and will be detailed later.

  • Simple input :

    A string, max 255 chars (?)

  • Text :

    Text field, but you can choose the format. When you add a field :

    type text 1

    you then have a properties panel which appears by clicking on the arrow on the right part of the line :

    type text 2

    If you choose Text formatting : HTML, you will have a WYSIWYG editor, TinyMCE :

    type text tinymce

    And if you choose Text formatting : none, you will have a simple textarea :

    type text textarea

  • Select :

    Displays a select list of options for the field.

    type select

    You have to put the options of the list in the property of the field, here we have "frontend" "backend" and "api" for the example. This type of attribute is handy, since it may avoids you the creation of a third-party model for simple lists like this one.

    The options are both editable from the model editing page and the model instance creating/editing pages.

  • Checkbox :

    A simple boolean field. The "Required" attribute will not applied to that kind of field.

  • Date :

    A date field which is editable by this :

    type date

    But if you need a field for "updated at" or "created at", be aware this attributes already exist by default, they are rendered with entry.created_at and entry.updated_at.

  • File :

    A field of this type supports the upload of any kind of file.

The other fields specifying a relationship with an other model (belongs_to, has_many and many_to_many) will be explained in the next section, Models mapping.

Common fields properties :

When you define an attribute (or a field) for your model, you have some properties which are specific for each kind of attribute (detailed previously), and some which are common to every one.

type properties

  • Required / Optional :

    Defines wether this field is required or not for the validation.

    A model must have at least one field, that's obvious, and the first field you will define will be considered as the mandatory one, and will be automatically saved as Required. There is one exception though: you can't have a mandatory field whose type defines a relationship with an other model.

  • Name :

    Make sure the name of the field highlighten in yellow here match the "Name" property bellow. As the tip explains "Name of the property for liquid templates", it will be this value you will have to use in the liquid template, and not the value highlighten in yellow.

    It seems obvious, but if you change the name of the field (the one highlighten in yellow) and forgot the update accordingly the value of the field bellow, you will not be able to retrieve the object in Liquid and may wonder why...

  • Hint :

    Hint for the end user of the back-office, displayed in the model form just below the field.

  • Localized :

    Used for internationalization, detailed here.

Presentation and Advanced options

When the attributes of the model are defined, click on "Create" to edit advanced options of the model :

models advanced props

For the purpose of the example, the following model will be used in what's follow :

models advanced model

So let's say we have a model of posts, with a title (string), some text (text), a category (select with options "frontend" and "backend") and a publishing_date (date).

Presentation

Theses options let you customize how entries of your model are displayed in the back-office page.

  • Label field :

    Choose the field of the model displayed for each entry.

    If you choose the field title, you have :

    models advanced label 1

    And if you choose publishing_date, you end up with :

    models advanced label 2

  • Group by field :

    Group entries by a common field value. This is available only for fields which have the type select. So here we can group by category :

    models advanced group by

  • Item template :

    Let you really customize the string displayed for each entry in the list of model's entries. For example, let's say I don't display my posts grouped by category, but still I would like the category appears aside the post title, I would do so :

    models advanced item template

    And my entries would display this way :

    models advanced item template1

Advanced options

And finally, last properties of your model:

  • Order by, Order direction : The ordering of items in your model, both in frontend and in backend.

  • Public Submission : Let frontend users create entries for the model, so typically you would use that option in a model "messages" for a contact form. This is detailed here.

Models mapping

LocomotiveCMS lets you define the relationships between models you are used to in Rails, but the creation and usage of these mapped models can sometimes be uneasy, so let's see for each one of them of to deal with.

This part is dedicated to the models creation and mapping in the admin UI, and the template code shown here will be very concise and simple. For more about displaying models, read the next part.

We will see the belongs to, has many and many to many relationships and finally look at more complex mapping

Belongs to

We have the model books belongs_to authors.

First, we create the model authors in its simplest form :

authors

The mapping with the model books will be defined in books. Let's create books:

books step 1

We give him a title field, and a writer field which defines the belongs_to relationship.

But wait, we haven't mapped books to authors yet, so click on the "add" button and then on the down arrow to specify more options concerning this field :

books arrow

You then have the option panel where you choose the Class name of the model targeted by the belongs_to relationship :

books step 2

We are done, so click on the "Create" button to save the model.

Nota Bene : the name of the field defining the belongs_to relationship (here 'writer') can be named as you want, you don't have to name it the singular of the targeted model (here it would be 'author'), even if it may be a good practice. (???)

Now we have our models defined, let's add some dummy entries. We will create a new book entry :

books entrie empty

We can give him a name, but the writer list is empty, right, because authors model hasn't any entries yet.

So create an author :

author add

And then go back in the book creation page, the author appears in the writer list :

book entrie valid

Great, save the entry and we will check if it works.

In a dummy page, we loop on books entries, and for each one (here the only one), we display the title of the book and its writer :

{% for book in contents.books %}
  {{ book.title }} written by {{ book.writer.name }} is a great lecture.
{% endfor %}

and it displays : Responsive Web design written by Ethan Marcotte is a great lecture..

Has many

Let's keep the previous models, and let's say books have reviews. A review is basically a piece of text, and it is often published in a media. What's more, a book has many reviews, but a review refers to one and only one book. So we are indeed in the relationship books has_many reviews.

First, we create the reviews model, which has a string field journal (in which the review is published) and a text field content, and also a belongs_to field book :

book reviews

The belongs_to field targeting the parent model class name, books, is required !

book reviews belongs to field

Save the model, and now go editing books. We will add a has_many field named reviews, targeting the Class name reviews, and Inverse of itself, so books :

book editing

Nota Bene : here again, the name of the field defining the has_many relationship (here 'reviews') can be named as you want.

Save the updated books model, and let's edit the previous books entry :

book entrie

Let's add a review to this book, click on "Add a new entry" and fill it with dummy text :

book entrie reviewed

Click on "Save" to close the modal window and create the reviews entry related to this books entry. Click again on "Save" to update the book.

In the previous dummy page where we tested the belongs_to relationship, we add a loop on the reviews of a book :

{% for book in contents.books %}
  {{ book.title }} written by {{ book.writer.name }} is a great lecture.
  <br>
  Reviews :
  <br>
  {% for review in book.reviews %}
    Published in {{ review.journal }} : {{ review.content }}
  {% endfor %}
{% endfor %}

and it displays :

Responsive Web design written by Ethan Marcotte is a great lecture.
Published in Web design monthly :
Awesome book, blablabla ...
UI enabled

When you added review to your book writer, it was possible because of the property "Ui enabled" of your has many field. This property is set to true by default :

tips ui enable

This property sets either you can edit and create a child model entry from a parent model entry, or not.

Many to many

Finally, we will add the ability to associate tags to a book. Here, the model tags have many books and books have many tags.

Let's create the tags model.

tags creation 1 tags creation 2

We have the books field referring to books via a many_to_many relationship. Like previously, we specify the Class name of the targeted model, which is books. We also have to specify Inverse of (itself) tags here, but we can't, the select list is empty.

For now, save the tags model, we will get back here soon. Go edit the books model and add, as you may guessed, the tags field referring to tags via a many_to_many relationship. The Class name of the targeted model is tags. Yes I'm repeating myself a little, just in case. But here you can define the Inverse of (itself) which is books, so do it please :

books m2m update

Then save the books model and go back editing tags model : magic, the Inverse of attribute of the many_to_many field books is field with the appropriate value tags :

tags inverse of update

Here it is, your many to many is settled.

Now we will add some tags to our book, but unlike the previous cases, you can't create a tag entry in the book entry page :

m2m enable ui problem

We have to create a new tag entry separately, and then add it when editing the book entry. So we do :

m2m tag available in book

You don't have to select here the book entry you want to connect the tag. And when we go back to the book entry we were editing, the tag is available in the select list :

m2m tag available in book

So let's (finally !) add our tag to our book, save, and check if everything is okay back in frontend :

{% for book in contents.books %}
  {{ book.title }} written by {{ book.writer.name }} is a great lecture.
  <br>
  Tags :
  <br>
  {% for tag in book.tags %}
    "{{ tag.text }}"
  {% endfor %}
{% endfor %}

displays :

Responsive Web design written by Ethan Marcotte is a great lecture.
Tags :
"responsive"

And if we do the opposite, it's okay too (as expected after so much pain) :

{% for tag in contents.tags %}
  Tag : "{{ tag.text }}" is related to the following books :
  <br>
  {% for book in tag.books %}
    {{ book.title }} written by {{ book.writer.name }}
  {% endfor %}
{% endfor %}

displays :

Tag : "responsive" is related to the following books :
Responsive Web design written by Ethan Marcotte

More complex mapping

TODO: considerations about nested relationships and performance of associated mongo queries

Rendering models

In this subchapter, we will try to show the most common cases of rendering a model entries. It would be tedious to list every possible cases, the aim is only to give an overview of what's possible.

First we will see the very basics of iterating over a collection of entries and the available logic you can add, then the pagination of results and finally the scoping the query of results.

Basics

The simplest loop is an iteration over your model entries. We loop here on the model posts, the one from the previous Models basics subchapter:

{% for item in contents.posts %}

{{ item.title }}

{% endfor %}

Loop over contents.slug_of_your_model, and for each entry you have access to the custom fields of your model, and also to a list of attributes :

entries attributes

Reference

Adding logic

Logic liquid tags let you put some logic inside your loops.

What about empty fields ?

You will certainly have some required fields, and some not, so how to not display an attribute if it's empty ? An empty field will actually return nil, so you can test it in a if statement :

{% for item in contents.posts %}
  {% if item.category %}
    <div class="category">{{ itme.category }}</div>
  {% endif %}
{% endfor %}

Rendering model's attributes

We will see here how to render each type of attribute.

  • Simple input :

    Nothing tricky here, title is our simple input field :

    {% for item in contents.posts %}
    <p>Title of the post : {{ item.title }}</p>
    {% endfor %}
  • Text :

    This field has an property Text formatting HTML or none. In the first case, you can edit the content of it with a WYSIWYG editor, in the other case it's just a textarea. It doesn't change anything for the rendering, in one case the output will be HTML formatted and in the other it will be a simple long string.

    main_text is our text field :

    {% for item in contents.posts %}
    <p>Content of the post : {{ item.main_text }}</p>
    {% endfor %}
  • Select :

    Display the current value of an entry's select field. You will not be able to list all the select options of a model, just the current value.

    category is our select field :

    {% for item in contents.posts %}
    <ul>
      <li>Category of the post : {{ item.category }}</li>
    </ul>
    {% endfor %
  • Checkbox :

    This field is a simple boolean one. So you may use it mainly for templating logic, but you can also display it's raw values ('true' or 'false').

    Here the checkbox field is `is_a_report, let's first use it for logic :

    {% for item in contents.posts %}
      {% if item.is_a_report %}
        This item is a report.
      {% else %}
        This item is not a report
      {% endif %}
    {% endfor %}

    And let's display it's raw value :

    {% for item in contents.posts %}
      Is it a report ? {{  item.is_a_report }}
    {% endfor %}
  • Date :

    Every entry has by default the properties created_at and updated_at. But if you need an additional date field, like publishing in the above example, here is how to render it :

    {% for item in contents.posts %}
      Publishing date : {{  item.publishing }}
    {% endfor %}

    It will render a raw ISO date, if you need to format it, have a look at the next part "Don't forget Liquid filters".

  • File :

    The only thing you can do with a file field is display its url. You must specify the property url when you display it, like in the above example with an attached field :

    {% for item in contents.posts %}
      Attached file url : {{  item.attached.url }}
    {% endfor %}

    One of the usual case is displaying images. In this case, you would simply have to encapsulate the url in an image HTML tag, and if you need to resize it have a look at the next section.

    {% for item in contents.posts %}
      <img src="{{  item.attached.url }}">
    {% endfor %}

Usage of capture and assigns

  • Capture :

    Combine a number of strings into a single string and save it to a variable.

    May be useful to clean your Liquid template if you have a lot of logic and formatting, or simply to keep it DRY.

    {% for item in contents.posts %}
      {% capture full_titile %}{{ item.title }} - {{ item.category }}{% endcapture %}
      {{ full_title }}
    {% endfor %}

    Reference

  • Assigns :

    Can be used to assign a value to a variable.

    Reference

Don't forget Liquid filters

Some Liquid filters will allow you to format your entries attributes.

  • Resize image :

    An image rendered from a file's field is done this way :

    <img src="{{  item.attached.url }}">

    But the the DragonFly gem can resize any image on the fly behind the scene. The url to the dynamically resized image is returned. The processing relies on ImageMagick. Here is an example :

    <img src="{{  item.attached.url | resize: '100x100' }}">

    Just add the "resize" filter, which takes the ImageMagick geometry string for argument. Here is a list of arguments examples taken from the Dragonfly doc:

    '400x300'            # resize, maintain aspect ratio
    '400x300!'           # force resize, don't maintain aspect ratio
    '400x'               # resize width, maintain aspect ratio
    'x300'               # resize height, maintain aspect ratio
    '400x300>'           # resize only if the image is larger than this
    '400x300<'           # resize only if the image is smaller than this
    '50x50%'             # resize width and height to 50%
    '400x300^'           # resize width, height to minimum 400,300, maintain aspect ratio
    '2000@'              # resize so max area in pixels is 2000
    '400x300#'           # resize, crop if necessary to maintain aspect ratio (centre gravity)
    '400x300#ne'         # as above, north-east gravity
    '400x300se'          # crop, with south-east gravity
    '400x300+50+100'     # crop from the point 50,100 with width, height 400,300
    

    Resized images are cached by default by the Rack::Cache middleware. If you host your LocomotiveCMS on Heroku, Varnish is used instead. For more information, please visit this page.

    Reference

  • Format / Localize a date :

    If you have a date field, or if you simply use the properties created_at, updated_at of an entry, you may wanna format it. Here it is :

    {{ item.created_at | localized_date: '%d %B' }}

    The localized_date takes as argument the format string which depends on the current locale. There is also an optional argument 'locale', if you need to force an other locale than the current one :

    {{ item.created_at | localized_date: '%d %B', 'fr' }}

    Reference

  • Format text :

    There is several filters allowing text formatting,

    • Underscore : Makes an underscored, lowercase form from the expression in the string. Reference
    • Dasherize : Replaces underscores with dashes in the string. Reference
    • Multi_line : Inserts a
      tag in front of every \n linebreak character. Reference
    • Concat : Append strings passed in args to the input Reference
    • Textile : Convert a Markdown-formatted string into HTML. Reference
  • Embed Flash tag : Embed a flash movie into a page given an url Reference

Displaying Grouped By

In the Presentation and Advanced options of the Basics subchapter, we saw how customize the displaying of a model's entries. One of the options is to group entries by a field, at the condition the field you want group_by entries is a select type.

In frontend, you also have this feature. Here is an example, using the posts model, the one from the previous Basics subchapter.

{% for cat in contents.posts.group_by_category %}
{{ cat.name }} : {% for entrie in cat.entries %}
{{ entrie.title }} {% endfor %} {% endfor %}

The syntax is the following : contents.slug_of_your_model.group_by_field_name, with the field name corresponding to the select type field you group by entries.

As explained in the documentation : The method returns an ordered Array of Hash. Each Hash stores 2 keys, name which is the name of the option and entries which is the list of the ordered entries for the option. The Array is ordered based on the order of the options set in the back-office.

So we first loop on each value of the select type field, which is "cat", and then for each "cat" we loop on each entries of this category.

Paginate entries

LocomotiveCMS comes with a paginate tag.

{% paginate contents.posts by 3 %} {% for post in paginate.collection %}

{{ post.title }}

{% endfor %} {% endpaginate %}

It creates a paginate objects with the following attributes :

paginate attributes

There are all pretty straightforward, but let's have a look at the parts attributes, it seems there is everything here to build the navigation of your paginated pages.

Here is an example of how we would to it :

{% paginate contents.posts by 1 %} {% for post in paginate.collection %}

{{ post.title }}

{% endfor %}

<!-- pagination links -->
{% for page in paginate.parts %}
  {% if page.is_link %}
    <a href="{{ page.url }}" >{{ page.title }}</a>
  {% else %}
    {{ page.title }}
  {% endif %}
{% endfor %}

{% endpaginate %}

Well, that's fine if you want have the control of your markup, but if you don't, there is the filter default_pagination for rendering a clean pagination navigation without pain :

{% paginate contents.posts by 1 %} {% for post in paginate.collection %}

{{ post.title }}

{% endfor %} <!-- pagination links --> {{ paginate | default_pagination, next: 'Next', previous: 'Previous' }} {% endpaginate %}

Which takes 2 arguments :

paginate filter

And which renders this kind of markup :

<div class="pagination ">
  <span class="disabled prev_page">« Previous</span>
    <span class="current">1</span>
    <a href="/posts?page=2">2</a>
    <a href="/posts?page=3">3</a>
  <a href="/posts?page=2" class="next_page">Next »</a>
</div>

Reference : paginate

Reference : default paginate

Scope results

http://doc.locomotivecms.com/templates/tags#with-scope-section

{% with_scope _slug: params.section %} {% assigns section = contents.sections.first %} {% endwith_scope %}

{% for article in section.articles %} ... {% endfor %}

with_scope: replace _permalink by _slug in the query https://github.com/locomotivecms/engine/issues/449

Templatize a model

The idea of a templatized page is that's a view of one instance of a model you specify in the templatized option of a page.

Here is how it works : let's say you have the model posts, the one from the previous Basics subchapter. You need to have the following pages structure :

templatized page archi

With :

  • posts page :

    The templatized mechanism expects to have "models" under a parent folder which makes more sense for SEO purpose. This page can be used for instance to list the products, or could be a redirect page.

    So for the example, here are the parameters of the page, but there isn't anything specific here :

    templatized page posts

    templatized page posts 2

    Again for the example, we could list in this page the entries of posts model, and display the link of each entry. You have to build the relative url according this page's slug, and using the _permalink attribute of a model instance.

    templatized page posts liquid

  • template page :

    The template of the templatized model is defined as follow :

    templatized page 1

    You set the page posts as the parent of this one. A templatized page must have a parent, other than index.

    templatized page 2

    Set the parameter Templatized as true, and select bellow the model you want to templatize.

    To follow the example, we will display a full post, using directly the posts instance (notice the singular) :

    templatized page template liquid

Note: in LocomotiveCMS 2.0, you can't have multiple nested levels of templatized pages. It will be possible with the 2.1 version, if you need this feature now, have a look at this branch and this discussion.

Recipe : Public Submission

https://github.com/locomotivecms/engine/blob/master/features/public/contact_form.feature

//// Very big form, session problem hack : https://github.com/locomotivecms/engine/issues/418

LocomotiveCMS Editor

lien: Carrier Wave seems to be locking down ThemeAsset to only allow images

https://groups.google.com/forum/#!topic/locomotivecms/r7f-54gSg0U

Using LocomotiveCMS in an existing Rails app

sources :

https://groups.google.com/d/topic/locomotivecms/suBZggHJ0OI/discussion

https://groups.google.com/d/topic/locomotivecms/ZMhKPe78pZM/discussion

Tips

Using multi-sites

notes :

Well, each site is fully independent form the others: they have different pages, domains, content types, ...etc. The ONLY part they have in common is that they have a "administration" access point based on a common domain name as explained in the guide (http://doc.locomotivecms.com/guides/multisites) but since we can use domain aliases, it's not a problem at all.

dev locally : https://groups.google.com/d/topic/locomotivecms/nmgDaCdb7Ts/discussion

Export site

This tip was given here

In LocomotiveCMS 1.0, there was an export feature, which allowed to export the site (template, models, entries) in a zip. This is no longer the case in LocomotiveCMS 2.0, since it now uses a REST API for push & pull commands.

Pushing a site (build with LocomotiveCMS Editor) is described in this section and here.

For now, there isn't any pulling script, but still it's possible, following these steps :

  • Get an auth token

    curl -d 'email=me@mysite.com&password=secret' 'http://mysite.com/locomotive/api/tokens.json'
    

    Obviously change the email and password to be valid credientials.

    Response will be something like

    {"token":"dtsjkqs1TJrWiSiJt2gg"}
    
  • Use the token to retrieve needed data.

    Pages :

    curl 'http://mysite.com/locomotive/api/pages.json?auth_token=dtsjkqs1TJrWiSiJt2gg'
    

    Content Types :

    curl 'http://mysite.com/locomotive/api/content_types.json?auth_token=dtsjkqs1TJrWiSiJt2gg'
    

    Content Entries ( assume we have a "Projects" model ) :

    curl 'http://mysite.com/locomotive/api/content_types/projects/entries.json?auth_token=dtsjkqs1TJrWiSiJt2gg'
    

Internationalization

LocomotiveCMS handles internationalization easily. We will see how set up several languages, deal with localized pages and models, and add a custom language to an app.

Setup

First thing first, choose the languages you want support in the Settings panel :

internationalization panel

Select your locales and save. If you go back in the 'Contents' tab, you will see a locale switcher at the right part of the menu :

internationalization locale switcher

There is several kind of content you may wanna translate :

  • HTML template of pages
  • editable contents in pages
  • content entries (your models entries)

Page translation

A page has as many HTML templates as locales in the app. You can

Let's try it : create a page named lambda with the following template :

That's it, a page in english
The editable custom text in english.

At test.herokuapp.com/lambda we have :

That's it, a page in english
The editable custom text in english.

Go back to the admin, and switch to the French locale. You will see that LocomotiveCMS indicates pages which are not yet translated :

internationalizationeng page

Notice : Pages which are not translated are not available in front, they generates a 404.

Edit the lambda page in French this time, we will edit both the template and the custom content (editable_short_text) :

Et voila, une page en francais.
{% editable_short_text 'custom-content' %}
Le contenu editable en francais.
{% endeditable_short_text %}

And now at test.herokuapp.com/fr/lambda (notice the locale prefix) we have the translated page :

Et voila, une page en francais.
Le contenu editable en francais.

Since the default locale of this app is English, the lambda page url is test.herokuapp.com/lambda but is also test.herokuapp.com/en/lambda. It's your choice to redirect all url to the prefixed one, which may make sense for SEO purpose.

Models translation

Let's create a dummy model with a string field :

internationalization model

See the localized checkbox ? It's all it takes to localize a model field ! You can localize the fields you want, but not necessarily all custom fields.

Let's create an entry in english, save it, and switch to French, and translate this entry.

Notice : LocomotiveCMS indicates which entries are not translated, as it does with pages. But unlike pages, a model entry which isn't translated yet will still appears in front.

Then we go back to our lambda page and display the dummy models entries. Don't forget to update the template in both locales :

{% for item in contents.dummy %}
{{ item.title }}
{% endfor %}

As expected, test.herokuapp.com/en/lambda displays

My dummy entry in english.

and test.herokuapp.com/fr/lambda displays

Mon entree en francais.

Internationalize the template

Locale switcher in front

The first thing you may need is a locale switcher, allowing front users to choose their language. There is a Liquid tag for that : {% locale_switcher %}. The params of the tag are explained here.

Variables

There is also global variables relating to locales (reference) :

  • locale gives is the current locale, which you may use in logical statements :

    {% if locale == 'en' %}
    English content
    {% else %}
    Contenu francais
    {% endif %}
  • locales is an array of the site's locales :

    {% for loc in locales %}
    {{ loc }}
    {% endfor %}
  • default_locale is the default site's locale.

Duplicated templates

LocomotiveCMS's behavior allows a different page's template for each locale. So let's say you have developed your site in English only, everything is developed, and then you add a locale : pages will be duplicated in the new locale.

It's not possible to specify one template for every locales, so if you don't want to duplicate every page template at each code update, it could be easier to validate the site in one locale, and then at the end, adding the other locales.

Localized links

Your template will have relative links to the app page, but if you don't localize them, they will point to the default localized page, and not the current localized one.

Making them localized is pretty simple, just add the locale prefix :

<a href="/{{ locale }}/target-page">click here</a>

This way, each link will point to the current localized page.

Add a language

LocomotiveCMS comes with the following available locales :

internationalization locales

What if you need for example to have a Chinese locale ? There is 2 things to consider with locales :

  • back-office language
  • url prefixes (for SEO purpose)

There is a way to add custom locale without pain.

  1. Install a LocomotiveCMS engine as described in the reference
  2. Once you have run bundle exec rails g locomotive:install, go in config/initializers/locomotive.rb

    # default locale (for now, only en, de, fr, pt-BR, it and nb are supported)
    #config.default_locale = :en
    
    # available locales suggested to "localize" a site. You will have to pick up at least one among that list.
    #config.site_locales = %w{en tw}
    

    The first line specifies the default locale, but as mentioned, you can't specify a locale which doesn't exists in LocomotiveCMS defaults locales.

    The second one specifies the list of available locales for the site. That's where you will add your custom locale. You can also remove all unwanted locales, so that they don't appear in the admin panel.

  3. When your custom locale is added in the LocomotiveCMS config, you need to add the .png image of the locale flag in the application assets, which will appears in the admin panel (if you don't, it will raises an error) :

    internationalization locales

    The .png image is 24x24px. Put this file in the main Rails app, in the folder /app/assets/images/locomotive/icons/flags/. The name of the image must be your locale's name, for example Taiwanese flag must be named tw.png.

  4. Precompile your assets : bundle exec rake assets:precompile

  5. Add translations.

    LocomotiveCMS backoffice have the following translation files (with LOCALE being the shortname of each locale) :

    admin_ui.LOCALE.yml
    carrierwave.LOCALE.yml
    default.LOCALE.yml
    devise.LOCALE.yml
    flash.LOCALE.yml
    formtastic.LOCALE.yml
    

    The first thing to know is i18n will look for default_locale translations if there isn't any translation for the current locale. In the case you want for example a backoffice in English for every locale, you don't need to add any custom translation, since it will fallback to the default_locale.

    The second thing is, in every case, you need to add the translation of your custom locale name in the admin_ui.LOCALE.yml above the others :

    locales:
        en: English
        de: German
        fr: French
        pt-BR: "Brazilian Portuguese"
        it: Italian
        nl: Dutch
        nb: Norwegian
        es: Spanish
        ru: Russian
    

    Add here your custom locale, in order to display it in the backoffice panel.

    To summarize :

    If your custom locales doesn't have to be translated in the admin, define your default_locale and just add your custom locale name translation in admin_ui.DEFAULT_LOCALE.yml.

    If you want translate the backoffice in your custom locale, copy these files :

    admin_ui.LOCALE.yml
    carrierwave.LOCALE.yml
    default.LOCALE.yml
    devise.LOCALE.yml
    flash.LOCALE.yml
    formtastic.LOCALE.yml
    

    and rename them with your custom locale shortname. Translate their content, and don't forget to add your custom locale name above in admin_ui.LOCALE.yml :

    locales:
        en: English
        de: German
        fr: French
        pt-BR: "Brazilian Portuguese"
        it: Italian
        nl: Dutch
        nb: Norwegian
        es: Spanish
        ru: Russian
    

Customize TinyMCE

The <head> section of the admin layout is structured as follow (full code here) :

%meta
  …
%script
  …
= yield :head
= render 'locomotive/shared/main_app_head'

There is an empty partial meant to be overridden for additional css or js : _main_app_head.html.haml located in app/views/locomotive/shared/ of the engine directory. Since it's the last one called in the <head>, every css or js added here will override the other ones.

How override this partial ? Well, LocomotiveCMS is build as a Rails engine, so you just need to create it in your main Rails app, because when a view is called, Rails first look for it in the main app, and then in the matching engine. We will add here a javascript tag referring to our customized TinyMCE configuration.

Let's proceed :

  1. Within your main Rails app, create a partial at the app/views/locomotive/shared/_main_app_head.html.haml location (create the folders if they don't exist).

  2. Edit the _main_app_head.html.haml file and insert this : = javascript_include_tag 'locomotive_misc'

  3. Create a javascript file at the app/assets/javascripts/locomotive_misc.js.coffee and fill in with window.LocomotiveCMS.tinyMCE.defaultSettings.valid_elements = "<your option>"

    You can modify all the default TinyMCE settings for LocomotiveCMS. Take a look at this file.

  4. Tells Rails this asset have to be precompiled, add config.assets.precompile += %w( locomotive_misc.js ) in config/application.rb

Writing custom Liquid tags

https://groups.google.com/d/topic/locomotivecms/vfxun5pOvEY/discussion

Appendix

LocomotiveCMS API

notes récupérées du google group :

I looked at the api source and it appears currently you can only query the entire model using it's slug which returns all entries in the model

https://groups.google.com/d/topic/locomotivecms/_sPLTseOnJs/discussion