15 min read

(For more resources related to this topic, see here.)

Mission briefing

This article deals with the creation of a Content Management System. This system will consist of two parts:

  • A backend that helps to manage content, page parts, and page structure

  • A frontend that displays the settings and content we just entered

We will start this by creating an admin area and then create page parts with types. Page parts, which are like widgets, are fragments of content that can be moved around the page. Page parts also have types; for example, we can display videos in our left column or display news. So, the same content can be represented in multiple ways. For example, news can be a separate page as well as a page part if it needs to be displayed on the front page. These parts need to be enabled for the frontend. If enabled, then the frontend makes a call on the page part ID and renders it in the part where it is supposed to be displayed. We will do a frontend markup in Haml and Sass.

The following screenshot shows what we aim to do in this article:

Why is it awesome?

Everyone loves to get a CMS built from scratch that is meant to suit their needs really closely. We will try to build a system that is extremely simple as well as covers several different types of content. This system is also meant to be extensible, and we will lay the foundation stone for a highly configurable CMS. We will also spice up our proceedings in this article by using MongoDB instead of a relational database such as MySQL.

At the end of this article, we will be able to build a skeleton for a very dynamic CMS.

Your Hotshot objectives

While building this application, we will have to go through the following tasks:

  • Creating a separate admin area

  • Creating a CMS with the ability of handling different types of content pages

  • Managing page parts

  • Creating a Haml- and Sass-based template

  • Generating the content and pages

  • Implementing asset caching

Mission checklist

We need to install the following software on the system before we start with our mission:

  • Ruby 1.9.3 / Ruby 2.0.0

  • Rails 4.0.0

  • MongoDB

  • Bootstrap 3.0

  • Haml

  • Sass

  • Devise

  • Git

  • A tool for mockups

  • jQuery

  • ImageMagick and RMagick

  • Memcached

Creating a separate admin area

We have used devise for all our projects and we will be using the same strategy in this article. The only difference is that we will use it to log in to the admin account and manage the site’s data. This needs to be done when we navigate to the URL/admin. We will do this by creating a namespace and routing our controller through the namespace. We will use our default application layout and assets for the admin area, whereas we will create a different set of layout and assets altogether for our frontend. Also, before starting with this first step, create an admin role using CanCan and rolify and associate it with the user model. We are going to use memcached for caching, hence we need to add it to our development stack. We will do this by installing it through our favorite package manager, for example, apt on Ubuntu:

sudo apt-get install memcached

Prepare for lift off

In order to start working on this article, we will have to first add the mongoid gem to Gemfile:

Gemfile
gem 'mongoid'4', github: 'mongoid/mongoid'

Bundle the application and run the mongoid generator:

rails g mongoid:config

You can edit config/mongoid.yml to suit your local system’s settings as shown in the following code:

config/mongoid.yml development: database: helioscms_development hosts: - localhost:27017 options: test: sessions: default: database: helioscms_test hosts: - localhost:27017 options: read: primary max_retries: 1 retry_interval: 0

We did this because ActiveRecord is the default Object Relationship Mapper (ORM). We will override it with the mongoid Object Document Mapper (ODM) in our application. Mongoid’s configuration file is slightly different from the database.yml file for ActiveRecord. The session’s rule in mongoid.yml opens a session from the Rails application to MongoDB. It will keep the session open as long as the server is up. It will also open the connection automatically if the server is down and it restarts after some time. Also, as a part of the installation, we need to add Haml to Gemfile and bundle it:

Gemfile gem 'haml' gem "haml-rails"

Engage thrusters

Let’s get cracking to create our admin area now:

  1. We will first generate our dashboard controller:

    rails g controller dashboard index
    create app/controllers/dashboard_controller.rb
    route get "dashboard/index"
    invoke erb
    create app/views/dashboard
    create app/views/dashboard/index.html.erb
    invoke test_unit
    create test/controllers/dashboard_controller_test.rb
    invoke helper
    create app/helpers/dashboard_helper.rb
    invoke test_unit
    create test/helpers/dashboard_helper_test.rb
    invoke assets
    invoke coffee
    create app/assets/javascripts/dashboard.js.coffee
    invoke scss
    create app/assets/stylesheets/dashboard.css.scss

  2. We will then create a namespace called admin in our routes.rb file:

    config/routes.rb
    namespace :admin do
    get '', to: 'dashboard#index', as: '/'
    end

  3. We have also modified our dashboard route such that it is set as the root page in the admin namespace.

  4. Our dashboard controller will not work anymore now. In order for it to work, we will have to create a folder called admin inside our controllers and modify our DashboardController to Admin::DashboardController. This is to match the admin namespace we created in the routes.rb file:

    app/controllers/admin/dashboard_controller.rb
    class Admin::DashboardController < ApplicationController
    before_filter :authenticate_user!
    def index
    end
    end

  5. In order to make the login specific to the admin dashboard, we will copy our devise/sessions_controller.rb file to the controllers/admin path and edit it. We will add the admin namespace and allow only the admin role to log in:

    app/controllers/admin/sessions_controller.rb
    class Admin::SessionsController < ::Devise::SessionsController
    def create
    user = User.find_by_email(params[:email])
    if user && user.authenticate(params[:password]) &&
    user.has_role? "admin"
    session[:user_id] = user.id
    redirect_to admin_url, notice: "Logged in!"
    else
    flash.now.alert = "Email or password is invalid /
    Only Admin is allowed "
    end
    end
    end redirect_to admin_url, notice: "Logged in!" else
    flash.now.alert = "Email or password is invalid / Only Admin is allowed "
    end end end

Objective complete – mini debriefing

In the preceding task, after setting up devise and CanCan in our application, we went ahead and created a namespace for the admin.

In Rails, the namespace is a concept used to separate a set of controllers into a completely different functionality. In our case, we used this to separate out the login for the admin dashboard and a dashboard page as soon as the login happens. We did this by first creating the admin folder in our controllers. We then copied our Devise sessions controller into the admin folder. For Rails to identify the namespace, we need to add it before the controller name as follows:

class Admin::SessionsController < ::Devise::SessionsController

In our route, we defined a namespace to read the controllers under the admin folder:

namespace :admin do
end

We then created a controller to handle dashboards and placed it within the admin namespace:

namnamespace :admin do
get '', to: 'dashboard#index', as: '/'
end

We made the dashboard the root page after login. The route generated from the preceding definition is localhost:3000/admin. We ensured that if someone tries to log in by clicking on the admin dashboard URL, our application checks whether the user has a role of admin or not. In order to do so, we used has_role from rolify along with user.authenticate from devise:

if user && user.authenticate(params[:password]) && user.has_role? "admin"

This will make devise function as part of the admin dashboard. If a user tries to log in, they will be presented with the devise login page as shown in the following screenshot:

After logging in successfully, the user is redirected to the link for the admin dashboard:

Creating a CMS with the ability to create different types of pages

A website has a variety of types of pages, and each page serves a different purpose. Some are limited to contact details, while some contain detailed information about the team. Each of these pages has a title and body. Also, there will be subpages within each navigation; for example, the About page can have Team, Company, and Careers as subpages. Hence, we need to create a parent-child self-referential association. So, pages will be associated with themselves and be treated as parent and child.

Engage thrusters

In the following steps, we will create page management for our application. This will be the backbone of our application.

  1. Create a model, view, and controller for page. We will have a very simple page structure for now. We will create a page with title, body, and page type:

    app/models/page.rb
    class Page
    include Mongoid::Document
    field :title, type: String
    field :body, type: String
    field :page_type, type: String
    validates :title, :presence => true
    validates :body, :presence => true
    PAGE_TYPE= %w(Home News Video Contact Team Careers)
    end

  2. We need a home page for our main site. So, in order to set a home page, we will have to assign it the type home. However, we need two things from the home page: it should be the root of our main site and the layout should be different from the admin. In order to do this, we will start by creating an action called home_page in pages_controller:

    app/models/page.rb scope :home, ->where(page_type: "Home")} app/controllers/pages_controller.rb def home_page @page = Page.home.first rescue nil render :layout => 'page_layout' end

  3. We will find a page with the home type and render a custom layout called page_layout, which is different from our application layout. We will do the same for the show action as well, as we are only going to use show to display the pages in the frontend:

    app/controllers/pages_controller.rb
    def show
    render :layout => 'page_layout'
    end
  4. Now, in order to effectively manage the content, we need an editor. This will make things easier as the user will be able to style the content easily using it. We will use ckeditor in order to style the content in our application:

    Gemfile
    gem "ckeditor", :github => "galetahub/ckeditor"
    gem 'carrierwave', :github => "jnicklas/carrierwave"
    gem 'carrierwave-mongoid', :require => 'carrierwave/mongoid'
    gem 'mongoid-grid_fs', github: 'ahoward/mongoid-grid_fs'

  5. Add the ckeditor gem to Gemfile and run bundle install:

    helioscms$ rails generate ckeditor:install --orm=mongoid
    --backend=carrierwave
    create config/initializers/ckeditor.rb
    route mount Ckeditor::Engine => '/ckeditor'
    create app/models/ckeditor/asset.rb
    create app/models/ckeditor/picture.rb
    create app/models/ckeditor/attachment_file.rb
    create app/uploaders/ckeditor_attachment_file_uploader.
    rb

  6. This will generate a carrierwave uploader for CKEditor, which is compatible with mongoid.

  7. In order to finish the configuration, we need to add a line to application.js to load the ckeditor JavaScript:

    app/assets/application.js
    //= require ckeditor/init

  8. We will display the editor in the body as that’s what we need to style:

    views/pages/_form.html.haml
    .field
    = f.label :body
    %br/
    = f.cktext_area :body, :rows => 20, :ckeditor => {:uiColor =>
    "#AADC6E", :toolbar => "mini"}

  9. We also need to mount the ckeditor in our routes.rb file:

    config/routes.rb
    mount Ckeditor::Engine => '/ckeditor'

  10. The editor toolbar and text area will be generated as seen in the following screenshot:

  11. In order to display the content on the index page in a formatted manner, we will add the html_safe escape method to our body:

    views/pages/index.html.haml
    %td= page.body.html_safe

  12. The following screenshot shows the index page after the preceding step:

  13. At this point, we can manage the content using pages. However, in order to add nesting, we will have to create a parent-child structure for our pages. In order to do so, we will have to first generate a model to define this relationship:

    helioscms$ rails g model page_relationship

  14. Inside the page_relationship model, we will define a two-way association with the page model:

    app/models/page_relationship.rb
    class PageRelationship
    include Mongoid::Document
    field :parent_idd, type: Integer
    field :child_id, type: Integer
    belongs_to :parent, :class_name => "Page"
    belongs_to :child, :class_name => "Page"
    end

  15. In our page model, we will add inverse association. This is to check for both parent and child and span the tree both ways:

    has_many :child_page, :class_name => 'Page',
    :inverse_of => :parent_page
    belongs_to :parent_page, :class_name => 'Page',
    :inverse_of => :child_page

  16. We can now add a page to the form as a parent. Also, this method will create a tree structure and a parent-child relationship between the two pages:

    app/views/pages/_form.html.haml
    .field
    = f.label "Parent"
    %br/
    = f.collection_select(:parent_page_id, Page.all, :id,
    :title, :class => "form-control")
    .field
    = f.label :body
    %br/
    = f.cktext_area :body, :rows => 20, :ckeditor =>
    {:uiColor => "#AADC6E", :toolbar => "mini"}
    %br/
    .actions
    = f.submit :class=>"btn btn-default"
    =link_to 'Cancel', pages_path, :class=>"btn btn-danger"

  17. We can see the the drop-down list with names of existing pages, as shown in the following screenshot:

  18. Finally, we will display the parent page:

    views/pages/_form.html.haml
    .field
    = f.label "Parent"
    %br/
    = f.collection_select(:parent_page_id, Page.all, :id,
    :title, :class => "form-control")

  19. In order to display the parent, we will call it using the association we created:

    app/views/pages/index.html.haml
    - @pages.each do |page|
    %tr
    %td= page.title
    %td= page.body.html_safe
    %td= page.parent_page.title if page.parent_page

Objective complete – mini debriefing

Mongoid is an ODM that provides an ActiveRecord type interface to access and use MongoDB. MongoDB is a document-oriented database, which follows a no-schema and dynamic-querying approach. In order to include Mongoid, we need to make sure we have the following module included in our model:

include Mongoid::Document

Mongoid does not rely on migrations such as ActiveRecord because we do not need to create tables but documents. It also comes with a very different set of datatypes. It does not have a datatype called text; it relies on the string datatype for all such interactions. Some of the different datatypes are as follows:

  • Regular expressions: This can be used as a query string, and matching strings are returned as a result

  • Numbers: This includes integer, big integer, and float

  • Arrays: MongoDB allows the storage of arrays and hashes in a document field

  • Embedded documents: This has the same datatype as the parent document

We also used Haml as our markup language for our views. The main goal of Haml is to provide a clean and readable markup. Not only that, Haml significantly reduces the effort of templating due to its approach.

In this task, we created a page model and a controller. We added a field called page_type to our page. In order to set a home page, we created a scope to find the documents with the page type home:

scope :home, ->where(page_type: "Home")}

We then called this scope in our controller, and we also set a specific layout to our show page and home page. This is to separate the layout of our admin and pages.

The website structure can contain multiple levels of nesting, which means we could have a page structure like the following: About Us | Team | Careers | Work Culture | Job Openings

In the preceding structure, we were dealing with a page model to generate different pages. However, our CMS should know that About Us has a child page called Careers and in turn has another child page called Work Culture. In order to create a parent-child structure, we need to create a self-referential association. In order to achieve this, we created a new model that holds a reference on the same model page.

We first created an association in the page model with itself. The line inverse_of allows us to trace back in case we need to span our tree according to the parent or child:

has_many :child_page, :class_name => 'Page', :inverse_of => :parent_
page
belongs_to :parent_page, :class_name => 'Page', :inverse_of =>
:child_page

We created a page relationship to handle this relationship in order to map the parent ID and child ID. Again, we mapped it to the class page:

belongs_to :parent, :class_name => "Page"
belongs_to :child, :class_name => "Page"

This allowed us to directly find parent and child pages using associations.

In order to manage the content of the page, we added CKEditor, which provides a feature rich toolbar to format the content of the page. We used the CKEditor gem and generated the configuration, including carrierwave. For carrierwave to work with mongoid, we need to add dependencies to Gemfile:

gem 'carrierwave', :github => "jnicklas/carrierwave" gem 'carrierwave-mongoid', :require => 'carrierwave/mongoid' gem 'mongoid-grid_fs', github: 'ahoward/mongoid-grid_fs'

MongoDB comes with its own filesystem called GridFs. When we extend carrierwave, we have an option of using a filesystem and GridFs, but the gem is required nonetheless. carrierwave and CKEditor are used to insert and manage pictures in the content wherever required.

We then added a route to mount the CKEditor as an engine in our routes file. Finally, we called it in a form:

= f.cktext_area :body, :rows => 20, :ckeditor => {:uiColor =>
"#AADC6E", :toolbar => "mini"}

CKEditor generates and saves the content as HTML. Rails sanitizes HTML by default and hence our HTML is safe to be saved.

The admin page to manage the content of pages looks like the following screenshot:

LEAVE A REPLY

Please enter your comment!
Please enter your name here