3.1 Adding content types

One essential feature of any CMS is the ability to manage structured content. Fortunately, this is one of LocomotiveCMS's strongest points.

Most any website has some sort of structured content. For example, imagine a dance studio's website. One page lists the various classes available and their details, including a description, time, price, difficulty level, instructor and other relevant information. How can we make this page content easily manageable for the studio's management?

We could add a big editable_text field on the page and let the studio's management piece the page together with the rich text editor. But adding and changing class listings would involve styling each one by hand with the rich text editor. For one thing, this approach greatly limits available options for styling and makes it difficult to ensure styles are uniformly applied.

But what if we structured this data? In LocomotiveCMS, we can do that using content types. For this situation, we would define a new type called dance_classes and assign it various properties, like title, description, price, schedule, instructor, and so on. This would allow the studio's staff to easily manage the content through the back-office, where dance classes could be added and edited with a custom form displaying fields for the necessary information. And since the content has been structured, we are able to do a lot more with the data: we can uniformly apply styles to the dance classes, paginate them, group them by difficulty, sort them time of day, and so much more.

posts

Getting back to our site, Wisdom for Wanderers, let's make it into an actual blog by adding our first content type: posts.

First of all, we need to think about the structure of posts on our site. What fields does a post need? We can always add more later, so let's just start with a few fields:

  • title: the title of the post
  • posted_at: the date and time of when this post was put online
  • tags: various tags that describe the content of the post
  • teaser: a short introduction to the post
  • featured_image: a photo associated with the post
  • body: a large, free form field holding the content of the post

Now that we have a general outline of what we want to do, let's move on to creating it.

Generating content types

As with pages, there is also a wagon generate content_type command. Run wagon help to get more information.

$ wagon help generate
Usage:
  wagon generate RESOURCE ARGUMENTS

Description:
  Generates a content_type, page, or snippet

  RESOURCE can be set to content_type, page, or snippet.

  Use wagon generate help [RESOURCE] for usage information and examples.

As you can see, the generate subcommand actually has its own help subcommand.

$ wagon generate help
Commands:
  wagon generate content_type SLUG FIELDS  # Creates a content type with the specified slug and fields.
  wagon generate help [COMMAND]            # Describe subcommands or one specific subcommand
  wagon generate page FULLPATH             # Create a page. No need to pass an extension to the FULLPATH arg
  wagon generate snippet SLUG              # Create a snippet

Options:
  -p, [--path=PATH]  # if your LocomotiveCMS site is not in the current path
                       # Default: .

It looks like we can pass the content_type command two arguments: SLUG and FIELDS. Let's use the help [COMMAND] subcommand mentioned above to get more more guidance regarding the SLUG and FIELDS arguments.

$ wagon generate help content_type
Usage:
  wagon content_type SLUG FIELDS

Options:
  -p, [--path=PATH]  # if your LocomotiveCMS site is not in the current path
                     # Default: .

Description:
  Creates a content type with the specified slug and fields.

  SLUG should be plural, lowercase, and underscored.

  FIELDS format: field_1[:TYPE][:REQUIRED] field_2[:TYPE][:REQUIRED] ...

  TYPE values: string, text, integer, float, boolean, email, date, date_time, file, tags, select, belongs_to, has_many, or many_to_many. Default is string.

  To require a field, set REQUIRED to true. The first field is required by default.

  Examples:

  * wagon generate content_type posts title published_at:date_time:true body:text

  * wagon generate content_type products title price:float photo:file category:belongs_to:true

Ok, lots of information here. SLUG is the lowercase, underscored, plural name of our content type. FIELDS is a space separated list of lowercase, underscored field names. These fields will be strings by default, but we can change their type by appending a colon and the field type. Finally, if we want this field to be required, we append another colon and the keyword true.

Let's examine the various field types.

string A sequence of characters. In the back-office, this field will be displayed as a single line, text type input. Use this for short bits of text, for example a title field or author field.
text This is longer, bigger block of text. In the back-office this field will be displayed as multi-line text area. You can control whether you would like the field to allow HTML, which will enable a rich text editor in the back office. Use this field type for larger bits of text, for example the body of a message or a summary of a video game.
integer A number without a decimal, like -2, -1, 0, 1, 2, 3, and so on.
float A number, optionally with a decimal. For example, -1.2, 0, 2.5, or 3.
boolean Can be set to true or false. In the back-office, this is displayed as a "Yes" or "No" switch. For example, imagine an online magazine where some articles are available to everyone while other are only available to premium subscribers. I would want to add a boolean premium field to my articles content type.
email An email address. Like string, it displays as a single line text input in the back-office. Input is validated to make sure it is an email address.
date A date, like August 19, 2009.
date_time A date and time, like 8:15 AM, August 6, 1945.
file A file. In the back-office this field will be displayed as a file picker where site editors can upload a file from their computer.
tags A list of so called tags, or descriptive words. For example a blog post about LocomotiveCMS might be tagged with CMS, Ruby, MongoDB, awesome, and so on.
select A list of several options that can be chosen between. In the back-office, this field is displayed as a drop down menu. In databases, this type of field is often called an enum. For example, a books content type might have a format field where editors can choose between Hardcover, Paperback, or eBook.
belongs_to, has_many, many_to_many These are relational types, which we will come back to later.

Considering the structure of our posts and the available field types, let's supply the generate command with the necessary information to create the type.

$ bundle exec wagon generate content_type posts title posted_at:date_time:true tags:tags teaser:text featured_image:file body:text
  exist
  create  app/content_types/posts.yml
  create  data/posts.yml

Great, it looks like that worked. Note that in the above command, I didn't specify a type for title, because string is the default. Also, the first field is always required, so even though I didn't specify it, title will be required.

Let's look at the first generated file, app/content_types/posts.yml.

# Human readable name of this type
name: Posts

# Lowercase, underscored handle used to access this type
slug: posts

# Explanatory text displayed in the back-office
description: A description of the content type for the editors

# Slug of field used to identify entries by default, such as the title
label_field_name: title

# Valid values: manually, created_at, updated_at, or the slug of any field
order_by: manually

# Valid values: asc (ascending) and desc (descending). Set to asc by default.
# order_direction: asc 

# Specify a field slug to group entries by that field in the back-office.
# group_by: <your field>

# Activate public 'create' API (e.g for a contact form)
# public_submission_enabled: false

# Array of emails to be notified of new entries made with the public API
# public_submission_accounts: ['john@example.com']

# A list describing each field
fields: 
- title: # The lowercase, underscored name of the field
    label: Title # Human readable name of the field
    type: string
    required: true
    hint: Explanatory text displayed in the back office
    localized: false

- posted_at: # The lowercase, underscored name of the field
    label: Posted at # Human readable name of the field
    type: date_time
    required: true
    hint: Explanatory text displayed in the back office
    localized: false

- tags: # The lowercase, underscored name of the field
    label: Tags # Human readable name of the field
    type: tags
    required: false
    hint: Explanatory text displayed in the back office
    localized: false

- teaser: # The lowercase, underscored name of the field
    label: Teaser # Human readable name of the field
    type: text
    required: false
    hint: Explanatory text displayed in the back office
    localized: false
    # text_formatting: html # html (uses rich text editor) or text (uses plain text editor)

- featured_image: # The lowercase, underscored name of the field
    label: Featured image # Human readable name of the field
    type: file
    required: false
    hint: Explanatory text displayed in the back office
    localized: false

- body: # The lowercase, underscored name of the field
    label: Body # Human readable name of the field
    type: text
    required: false
    hint: Explanatory text displayed in the back office
    localized: false
    # text_formatting: html # html (uses rich text editor) or text (uses plain text editor)

I'm glad we didn't have to type all of that. Most of the file should be fairly self-explanatory, thanks to the inline comments. I'm mostly happy with the file, but we need to customize a few things.

To start, change the description field to something more descriptive.

    description: Blog posts about travel

Like most blogs, these posts will be ordered by the date posted, so specify that.

    order_by: posted_at

And we will want the posts to go from newest to oldest.

    order_direction: desc 

We can leave the public submission and group by fields commented out. The field definitions basically look good, though we need to update or remove the hint option for each field.

fields: 
- title: # The lowercase, underscored name of the field
    label: Title # Human readable name of the field
    type: string
    required: true
    localized: false

- posted_at: # The lowercase, underscored name of the field
    label: Posted at # Human readable name of the field
    type: date_time
    required: true
    localized: false

- tags: # The lowercase, underscored name of the field
    label: Tags # Human readable name of the field
    type: tags
    required: false
    localized: false

- teaser: # The lowercase, underscored name of the field
    label: Teaser # Human readable name of the field
    type: text
    required: false
    hint: A short lead-in to the post displayed when listing posts
    localized: false
    text_formatting: html # html (uses rich text editor) or text (uses plain text editor)

- featured_image: # The lowercase, underscored name of the field
    label: Featured image # Human readable name of the field
    type: file
    required: false
    hint: An image displayed next to the teaser when listing posts
    localized: false

- body: # The lowercase, underscored name of the field
    label: Body # Human readable name of the field
    type: text
    required: false
    hint: The full post text
    localized: false
    text_formatting: html # html (uses rich text editor) or text (uses plain text editor)

I uncommented the text_formatting option for body and teaser. Since I've left the option at its default value, html, I could have also left the option out, but I think in this situation it's helpful to explicitly define it. Since we're not tackling localization yet, all of the localization options are set to false.

Let's move onto the second generated file, data/posts.yml. This file specifies content entries for the posts content type.

- "Sample 1":
    teaser: "Debitis et qui optio corporis enim labore. Et dolorem eum beatae officiis nam. Dignissimos iure adipisci est vel."
    featured_image: null # Path to a file in the public/samples folder or to a remote and external file.
    body: "Voluptatum nam eius cupiditate. Dignissimos fugit hic ut mollitia aut sunt. Iusto odio magnam sunt eum dolores ea. Ea sunt qui molestiae temporibus. Velit quia similique recusandae sunt qui voluptatem voluptates tempore."

- "Sample 2":
    teaser: "Ex et provident error ut. Nihil enim sapiente tempora error sequi et eligendi ea. Molestiae ut esse praesentium voluptates quas asperiores sapiente vel. Aut sequi ut ducimus. A voluptates nobis enim aut et quod labore."
    featured_image: null # Path to a file in the public/samples folder or to a remote and external file.
    body: "Temporibus rem provident aut ipsum perspiciatis. Corrupti alias nostrum cupiditate quia numquam ex. Consequuntur fuga debitis eligendi quibusdam eius placeat. Doloremque nesciunt officiis error aut. Non illo animi culpa doloremque."

...

Each entry is defined with the content type's label field. For our posts type, the title field is the label field, as was specified in app/content_types/posts.yml

    label_field_name: title

Under each title, you can specify other fields for the entry.

These are just sample entries that we will be displayed when using the Wagon web server. The real posts will be entered through the back-office by site editors, so leaving these with lorem ipsum text is just fine.

However, we should add in data for the fields which are missing or blank: featured_image, posted_at, and tags. Further, the body samples are a little short. And finally, it's always a good idea to include plenty of variety in testing data by having text of varying lengths and including some entries that are missing the optional fields.

So, I used text from Fillerama, placeholder images from placeholder.com, and a short Ruby script to generate a new ` file with high quality sample data. You can can download it here: new data/posts.yml.

Place the file the data diretory. Let's take a look at the first entry.

- Mr. F:
  posted_at: '2013-08-04 15:00:00 -0500'
  tags: [Camping, South America, Outdoor, Urban, Expensive]
  featured_image: samples/placeholders/640x480.png
  teaser: <p>That's what it said on 'Ask Jeeves.' Army had half a day.</p>
  body: <p>That's what it said on 'Ask Jeeves.' Army had half a day. Bad news....

I've added in the posted_at, tags, and featured image fields. I've also changed the content of the title, teaser, and body fields.

You'll notice the posted_at field uses a date and time in the YEAR-MONTH-DAY HOURS:MINUTES:SECONDS UTC_OFFSET format. But actually, there's a fair amount of flexibility since the dates will be parsed by Ruby. I like the unambiguous format used in the sample file above. Also, this data is just for testing purposes so you shouldn't worry too much about its accuracy, but if you don't specify a UTC offset, times will be in GMT by default.

You'll also notice that the featured_image fields all reference images of various formats and sizes in a samples/placeholders directory. You can this placeholders directory with the referenced files here: placeholders.zip.

Finishing up

In this chapter we created our first content type, though we haven't done much with it yet. I don't like ending this chapter with so little to show for our work, so I'll give you a little preview of what these files have setup.

The screenshot below is of the back-office interface for editing these content types, which is automatically setup for you when you create content types. Pretty cool, huh?

Adding a new post

There's still lots to learn about content types, but let's finish here for now. Add the new files to the git repository and commit the changes.

$ git add app/content_types/posts.yml data/posts.yml
$ git add public/samples/placeholders
$ git commit -m "Added posts content type."

When you're ready, move onto to the next lesson where we will cover how to list content type entries on your site.

Next: listing content entries
© 2024 LocomotiveCMS Terms of use Privacy Policy
Back to top