Giter Club home page Giter Club logo

locomotive-fundamentals's Introduction

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 [email protected], and if you need support from LocomotiveCMS team, ask [email protected].

Summary

  1. Foreword
  1. Overview
  1. Getting something running in 5 minutes
  2. Templating
  1. Models
  1. LocomotiveCMS Editor
  2. Using LocomotiveCMS in an existing Rails app
  3. Tips
  1. 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, and that protoype has been constantly improved ever since. I also spent a lot of time experimenting with many concepts and throughout the process, I learned a lot about what the perfect CMS should look like.

However, I stayed true to 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 require the help of a systems admin 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 single core concept: 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 one of the cases listed above applies to you, you should use LocomotiveCMS.

///

From a "business" point of view, LocomotiveCMS has several great selling points:

  • 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 Ruby and Rails is and you have a decent understanding of terms like Gem, Bundler, deployment
  • You know what a data model is, and ideally you understand document-oriented storage, like Mongo
  • You have basic knowledge of how to use a shell/command-line interface

Organization of this book

This guide is structured as follows:

First, the 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 is mounted within another Rails application. An engine can have its own models, views, controllers, generators and publicly served static files. ([more about engines](http://guides.rubyonrails.org/engines.html))

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 differs a bit from what you are used to, so it may be weird a first, but it's actually very simple.

In the classic 'Rails way', you have the following architecture, with page 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 master layout and the content of the index page. How do you re-use the layout without re-using the index page's 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 simple 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-uses all of its content, except the content inside the {% block %} tag which is overwritten. This tag is written using Liquid syntax which will be 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 has 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 inheritance can be applied to every page. When you create a page, it automatically inherits from index, but you can also make it inherit 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 page other than the parent

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 "second page" extend "first 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 you do it without putting the entire index in {% block %} tags? It's actually fairly simple: you are not forced to make a page inherit 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 extend 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 defined 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 index:

{% extends parent %}

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

Then the "alternate layout" page, which doesn't extend 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 the templating basics section, it's worth it to know that LocomotiveCMS gives you the ability to put some blocks of code in a separate folder called snippet, in the same way Rails does with partials. Snippets are very useful when you want to build a modular layout without repeating code.

Like Rails, you can pass a variable to the snippet, or simply include a static block 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, loops on the products model, and includes the snippet "product_information" for each product:

<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 coding 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:

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

Erb:

<%= @product.name %>

Liquid :

{{ product.name }}

Tag markup: matched pairs of curly 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 have access to a couple of basic objects, like the current site, page, logged in account, as well as collections, like your custom content types. These objects 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 options when you're creating 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 to the page you are creating, so 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, like this:

<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 use this to generate a RSS feed or build an simple API from your LocomotiveCMS site.

  • Templatized:

    Defines whether or not 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 on a page in a deployed site.

  • Listed:

    The Liquid {% nav %} generates a menu (doc) based on your page. Use this to determine whether or not this page appears in the generated menu.

  • Redirect:

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

    LocomotiveCMS will perform a 301 redirect. From an SEO perspective, this is a permanent redirection, so you should use it when your URLs have changed. When search engines encounter a 301 redirect, they update the URLs in their database.

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

  • Cache strategy:

    Define the cache strategy for this page here.

Recipe: Create an RSS feed

You can create a page which will return an RSS feed of 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
  • replace "example.com" with your real domain
<?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.created_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 and validate your feed here.

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, also known as 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 subchapter is about building relationships between your models. In the third subchapter we cover common use cases for rendering models with Liquid. In the fourth subchapter we will show the flexibility and functionality of the templating a model. Finally, we will cover the public submission of models, which will allow 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 look at an overview of each one. The rendering of these types will be reviewed later.

Nota bene : you will encounter some unexplained properties. This is because they are common to all field types and will be covered 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 will have a properties panel that appears when you click on the arrow on the right part of the line:

    type text 2

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

    type text tinymce

    And if you choose Text formatting: none, you will get 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. In the example we have "frontend", "backend" and "api". This type of attribute is handy, since it may allow you to avoid creating another model to store simple lists.

    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 can not be applied to this field type.

  • Date:

    A date field, with the following date selector built-in:

    type date

    The "updated at" and "created at" fields already exist by default, they can be 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 whether this field is required when the form is validated.

    Obviously, a model must have at least one field. The first field you 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 highlighted in yellow here matches the "Name" property below. 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 highlighted in yellow.

    It seems obvious, but if you change the name of the field (the one highlighted in yellow) and forgot to update the value of the field below to match it, 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. The text is 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 the following examples:

models advanced model

So let's say we have a 'Post' model 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's you customize the string displayed for each entry in the list of model entries. For example, let's say I don't display my posts grouped by category, but I would still like the category to appear beside the post title, I would enter the following:

    models advanced item template

    And my entries would display this way:

    models advanced item template1

Advanced options

And finally, the 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.

Model mappings

LocomotiveCMS lets you define the relationships between models using the same style you are used to in Rails, but the creation and usage of these mapped models can sometimes be difficult, so let's examine each mapping type in detail.

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 mappings.

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 best practice. (???)

Now we have our models defined, let's add some dummy entries. First, we'll 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 continue to use the same models from the previous section, and add that 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 then edit the books model. Now 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 then 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 whether 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">{{ item.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 %} {{ 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 locomotivecms/engine#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 : locomotivecms/engine#418

Recipe: Create models and content via YAML

Locomotive's ability to create models via the UI is really awesome. It's powerful and flexible. The only issue though is what if you maintain a Dev and Prod instance of your CMS (or say Dev, Staging and Prod)? Let's say further that this model doesn't even need a template. How do you keep your new models DRY and push whatever you created in Dev. Simple: you can create your model with a YAML file. You can even create initial content, either as a placeholder or as a default, first time content. Its really awesome.

Let's say you want to create a model that allows content editors make small simple entries for news about their company. Our model will be called "News."

First you need to create a directory inside your app directory called 'content_types'. Then create a .yml file named for your model. So, following our exmaple you would have this:

app/content_types/news.yml

Next you are going to represent your model in a similar way to how you do it in the UI. It helps to know YAML, but if your model is simple enough you can probably just follow this example.

Here is what we need in our model:

A title. A description. A URL to link to the actual news item. A tag (for the type of news items it is such as video, event or news), Whether the item should be featured on the home page. Whether the item should be published.

Here is what your file should look like:

name: News
slug: news
description: "stories and news about AliveCor"
order_by: pub_date
order_direction: desc
label_field_name: title
fields:
  - title:
      label: Title
      type: string
      hint: "Short title will display as a heading, keep it short! around 40 characters"
  - description:
      label: Description
      type: text
      hint: "Short description that will display as text below heading, around 90 characters"
  - url:
      label: URL
      type: string
      hint: "Type or paste the full URL of the news story we are linking to"
  - tag:
      label: Tag
      type: select
      hint: "Pick the type of news category this is"
      select_options:
       - news
       - event
       - video
  - featured:
      label: Featured
      type: boolean
      hint: "Will appear on homepage, you can only have two news stories there so watch it!"
  - pub_date:
      label: Date
      type: date
      hint: "The date you enter will be used for display and ordering of the news stories"
  - publish:
      label: Publish
      type: boolean
      hint: "Set this to yes when you are ready to publish it to the site"

You can push this to your Dev instance and see it in the UI now! Badass.

A few things to notice:

At this time the 'order_direction' doesn't seem to work. For me it always defaulted to 'asc' so for now I just manually do it in the UI. I'm sure this bug will be fixed soon.

The other thing to notice is how you create the choices that will appear in the select menu. They will appear in the order you list under 'select_options'.

Everything else is quite straighforward.

Now on to creating some default content!

For this you need to create a directory as a sibling of 'app'. Call this one 'data'. Inside 'data' you must create a .yml file with a name that corresponds to the one you created in the 'content_types' directory. So you will end up with this:

data/news.yml

Here is an example news story in that file:

'American Onion':
  title: 'American Onion'
  description: "Stoner Architect Drafts All-Foyer Mansion"
  url: 'http://www.theonion.com/articles/stoner-architect-drafts-allfoyer-mansion,1469/'
  tag: 'news'
  featured: true
  pub_date: 2000-08-09
  publish: true

Now you can push this to your Dev server and it will be visible in the UI and ready for you to reference in your templates. Huzzah!

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 protected]&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.

  1. 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.

  1. Precompile your assets : bundle exec rake assets:precompile
  2. 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.

  1. 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

locomotive-fundamentals's People

Contributors

bryant1410 avatar ericksoun avatar gabrielnau avatar jasongonzales23 avatar jdlich avatar mmartinson avatar scrooloose avatar

Watchers

 avatar  avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.