Building a Customizable Content Management System

Exclusive offer: get 50% off this eBook here
Rails 4 Application Development HOTSHOT

Rails 4 Application Development HOTSHOT — Save 50%

Build simple to advanced applications in Rails 4 through 10 exciting projects with this book and ebook

£18.99    £9.50
by Saurabh Bhatia | April 2014 | Open Source

This article by Saurabh Bhatia, author of the book Rails 4 Application Development Hotshot, will teach you how to create a Content Management System. Content is the backbone of the Internet. A Content Management System (CMS) is essentially a software that helps you to easily and effectively manage the content of a website or a web application. There are several perspectives on CMS, with Drupal, Joomla!, and WordPress being the really popular ones. However, people still build tailor-made CMSes, because they want something that fits their needs exactly.

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

Rails 4 Application Development HOTSHOT Build simple to advanced applications in Rails 4 through 10 exciting projects with this book and ebook
Published: April 2014
eBook Price: £18.99
Book Price: £30.99
See more
Select your format and quantity:

Managing page parts

This task deals with the creation and management of page parts. Page parts are snippets of code, which we will use to render in the page. These parts can be banners, YouTube video channels, photos, polls, and so on. We will create a model for page parts and this will effectively manage content for different parts of our page.

Engage thrusters

We will begin by adding page parts to our CMS system:

  1. Generate the page parts model:

    heliouscms$rails g model part title:string content:string
    meta:string part_type_id:string
    invoke mongoid
    create app/models/part.rb
    invoke test_unit
    create test/models/part_

  2. We will now generate the model for part_types:

    :~/helioscms$ rails g model part_type name:string
    invoke mongoid
    create app/models/part_type.rb
    invoke test_unit
    create test/models/part_type_test.rb
    create test/fixtures/part_types.yml

  3. We will now associate the parts and part_types fields:

    app/models/part_type.rb
    class PartType
    include Mongoid::Document
    field :name, type: String
    has_many :parts
    end
    app/models/part.rb
    class Part
    include Mongoid::Document
    field :title, type: String
    field :part_type_id, type: String
    field :content, type: String
    field :meta, type: String
    field :user_id, type: String
    belongs_to :page
    belongs_to :part_type
    end

  4. Let's add some parts by firing up the Rails console:

    helioscms$ rails c
    Loading development environment (Rails 4.0.0)
    1.9.3-p327 :001 > part = Part.new
    => #<Part _id: a833e8207277751d1a000000, title: nil, part_type_
    id: nil, content: nil, meta: nil, user_id: nil,>
    1.9.3-p327 :002 > part.title = "YouTube Channel"
    => "YouTube Channel"
    1.9.3-p327 :003 > part.save!
    MOPED: 127.0.0.1:27017 COMMAND database=admin
    command={:ismaster=>1} runtime: 3.5448ms
    MOPED: 127.0.0.1:27017 INSERT database=project5_
    development collection=parts documents=[{"_id"=>BSON::ObjectId('a8
    33e8207277751d1a000000'), "title"=>"YouTube Channel"}] flags=[]
    COMMAND database=project5_
    development command={:getlasterror=>1, :w=>1} runtime: 1.3837ms
    => true

  5. We will now add part types to the part form so that we can save it during their creation:

    app/views/parts/_form.html.haml
    .field
    = f.label :part_type_id
    = f.select(:part_type_id, options_from_collection_for_
    select(PartType.all, :id, :name), {:prompt => 'Please Choose'},
    :class => "form-control")

  6. The following code shows what the full form looks like:

    views/plans/_form.html.haml
    = form_for @part do |f|
    - if @part.errors.any?
    #error_explanation
    %h2= "#{pluralize(@part.errors.count, "error")} prohibited this
    part from being saved:"
    %ul
    - @part.errors.full_messages.each do |msg|
    %li= msg
    .field
    = f.label :title
    = f.text_field :title, :class=>"form-control"
    .field
    = f.label :part_type_id
    = f.select(:part_type_id, options_from_collection_for_
    select(PartType.all, :id,
    :name), {:prompt => 'Please Choose'}, :class => "formcontrol")
    .field
    = f.label :content
    = f.cktext_area :content, :rows => 20, :ckeditor =>
    {:uiColor => "#AADC6E", :toolbar => "mini"}
    .field
    = f.label :meta
    = f.text_field :meta, :class=>"form-control"
    .field
    = f.hidden_field :user_id, :value=>current_user.id
    %br/
    .actions
    = f.submit 'Save',:class=>"btn btn-default"
    = link_to 'Cancel', parts_path,:class=>"btn btndanger

  7. In order to call the page_parts extension, we will use the association between page and page_parts.

  8. To see this, we will make a call on page and then call the parts related to that page. As a result, you will see the following screenshot:

Objective complete – mini debriefing

We created a page parts model in this task. We also created page part types in order to classify and arrange them. We have also created an association between page and page parts. Hence, we can now assign a single page part to multiple pages. Also, we can see all the parts associated with a page.

At the end of this task, our page part creation page should look like the following screenshot:

Creating a Haml- and Sass-based template

Now that we have the content defined for our pages, we will start building the frontend. We will keep the frontend as simple as possible and just render the information we have created using our CMS. This task deals with the creation of the frontend and how to separate it from the backend.

Engage thrusters

Let's get started with the process of frontend creation:

  1. Inside your app/assets folder, create a file called front.css. In Rails we have an advantage of asset pipeline. We can use this to separate the frontend assets. We will create a manifest file called front.css and define front end-related stylesheets under it:

    app/assets/stylesheets/front.css
    /*
    * This is a manifest file that'll be compiled into front.css,
    which will include all the files
    * listed below.
    *
    * indicates any CSS and SCSS file within the lib/assets/
    stylesheets/front_end, vendor/assets/stylesheets/front_end
    directory.
    * or vendor/assets/stylesheets of plugins, if any, can be
    referenced here using a relative path.
    *
    * You're free to add application-wide styles to this file and
    they'll appear at the top of the
    * compiled file, but it's generally better to create a new file
    per style scope.
    *
    *= require_self
    */

  2. We will also place these files under the folder called front_end and create a blank SCSS file under it:

    helioscms/app/assets/stylesheets$ mkdir front_end
    helioscms/app/assets/stylesheets/front_end$ touch structure.scss

  3. We will now load the structure.scss file from our manifest file:

    app/assets/stylesheets/front.css
    /*
    * This is a manifest file that'll be compiled into front.css,
    which will include all the files
    * listed below.
    *
    * Any CSS and SCSS file within this directory, lib/assets/
    stylesheets, vendor/assets/stylesheets,
    * or vendor/assets/stylesheets of plugins, if any, can be
    referenced here using a relative path.
    *
    * You're free to add application-wide styles to this file and
    they'll appear at the top of the
    * compiled file, but it's generally better to create a new file
    per style scope.
    *
    *= require_self
    *= require font-awesome
    *= require front_end/structure
    */

  4. We will follow the same procedure to create a front.js manifest file in assets/javascripts.

  5. Let's quickly define a simple three column layout with a header, footer, and a container. We will do this inside our structure.scss file:

    app/assets/stylesheets/front_end/structure.scss
    #primary, #content, #secondary {
    height: 300px;
    padding: 50px 0;
    }
    #container {
    width: 1200px;
    margin: 0 auto;
    }
    #primary {
    float: left;
    width: 150px;
    background: #eee;
    padding-right: 10px;
    padding-left: 10px;
    Building a Customizable Content Management System
    }
    #content {
    float: left;
    width: 800px;
    height: 300px;
    background: #ccc;
    padding-left: 10px;
    padding-right: 10px;
    }
    #secondary {
    float: left;
    width: 150px;
    background: #ddd;
    padding-left: 10px;
    padding-right: 10px;
    }
    #footer {
    clear: both;
    padding-top:3px;
    }
    #header {
    background: #fff;
    height: 100px;
    }

  6. We will also add a basic horizontal menu to the application:

    app/assets/stylesheets/front_end/structure.scss
    #menu ul
    {
    margin: 0px;
    padding: 0px;
    list-style-type: none;
    }
    #menu a
    {
    display: block;
    width: 8em;
    color: white;
    background-color: #000099;
    text-decoration: none;
    text-align: center;
    }
    #menu a:hover
    {
    background-color: #6666AA;
    }
    #menu li
    {
    float: left;
    margin-right: 0.5em;
    }

  7. Our front end layout is currently different. We will make a call on the manifest CSS and JS files in our header and define sections in the page. Also, we should change our extension's layout to Haml because the first layout created is html.erb by default:

    app/views/layouts/page_layout.html.haml
    !!!
    %html
    %head
    %title
    = stylesheet_link_tag "front"
    = javascript_include_tag "front"
    = csrf_meta_tags
    %body
    #container
    #header
    #menu
    #primary
    %p Primary Sidebar
    #content
    #secondary
    %p Secondary Sidebar
    #footer

  8. Finally, we will add some fonts before we start rendering the content into our page. We will use Google Fonts for simple usage:

    views/layouts/page_layout.html.haml
    %link{href: "http://fonts.googleapis.com/
    css?family=Cherry+Swash", rel: "stylesheet", type: "text/css"}/
    %link{href: "http://fonts.googleapis.com/css?family=Flamenco",
    rel: "stylesheet", type: "text/css"}/

  9. We need to apply this layout only to the limited actions in our controller:

    app/controllers/pages_controllers.rb layout 'page_layout', only: [:home_page, :show]

Objective complete – mini debriefing

In this task, we concentrated on creating a frontend and separated it completely from the backend in terms of look and feel. The advantage of this approach is that we can use any CSS and JS framework we want in the frontend without interfering with the backend. Zurb Foundation, Bootstrap, Less, or any other HTML5 and CSS3 framework can be used for the frontend.

= stylesheet_link_tag "front" = javascript_include_tag "front"
= javascript_include_tag "front"

Finally, we had to apply this layout to some select actions in our controller. This is because our page controller's actions—index, new, edit, create, and update—are administrator's actions. The actions show and home_page are supposed to display the page. Hence, the layout is applied to only these actions:

layout 'page_layout', only: [:home_page, :show]

The final output of our work in creating the frontend page is as seen in the following screenshot:

Generating the content and pages

We have already created the backend and also set the base for the frontend. However, we need to start rendering the content in the front end. We also want a dynamically generated menu from the pages we have created. We want the backend to play well with the front end page we just created. In this task, we will add site-related information that renders all the content on the front end page.

Engage thrusters

The following steps are used to render the content and also add some general site details:

  1. We will first create a scaffold for the site details:

    $ rails g scaffold site_detail title:string organization:string
    address:string facebook:string twitter:string google_
    plus:string skype:string linkedin:string google_analytics:string
    telephone:string
    invoke mongoid
    create app/models/site_detail.rb
    invoke test_unit
    create test/models/site_detail_test.rb
    create test/fixtures/site_details.yml
    invoke resource_route
    route resources :site_details
    invoke inherited_resources_controller
    create app/controllers/site_details_controller.rb
    invoke erb
    create app/views/site_details
    create app/views/site_details/index.html.erb
    create app/views/site_details/edit.html.erb
    create app/views/site_details/show.html.erb
    create app/views/site_details/new.html.erb
    create app/views/site_details/_form.html.erb
    invoke test_unit
    create test/controllers/site_details_controller_test.rb
    invoke helper
    create app/helpers/site_details_helper.rb
    invoke test_unit
    create test/helpers/site_details_helper_test.rb
    invoke jbuilder
    create app/views/site_details/index.json.jbuilder
    create app/views/site_details/show.json.jbuilder
    invoke assets
    invoke coffee
    create app/assets/javascripts/site_details.js.coffee
    invoke scss
    create app/assets/stylesheets/site_details.css.scss
    invoke scss
    identical app/assets/stylesheets/scaffolds.css.scss

  2. Be sure to remove the scaffolds.css.scss file, otherwise it will conflict with our default CSS.

  3. First generate the carrierwave uploader and call the uploaded file:

    helioscms$ rails g uploader file

  4. We will then add a field to the SiteDetail model:

    app/models/site_detail.rb
    class SiteDetail
    include Mongoid::Document
    field :title, type: String
    field :organization, type: String
    field :address, type: String
    field :facebook, type: String
    field :twitter, type: String
    field :google_plus, type: String
    field :skype, type: String
    field :linkedin, type: String
    field :google_analytics, type: String
    field :telephone, type: String
    mount_uploader :logo, FileUploader
    end

  5. The form to save the site details looks as follows:

  6. Now, in order to display these values in our site frontend, we will first make a call on SiteDetail and Page. We will call the SiteDetail and page value:

    app/controllers/pages_controllers.rb
    before_action :set_site, only: [:home_page, :show]
    layout 'page_layout', only: [:home_page, :show]
    def home_page
    @page = Page.find_by(page_type: "Home") rescue nil
    @pages = Page.all
    end
    # GET /pages/1
    # GET /pages/1.json
    def show
    @pages = Page.all
    end
    private
    def set_site
    @site = SiteDetail.first
    end

  7. We will then add these values to the page. First add the page and site value in the title bar:

    "app/" before the path "views/layouts/page_layout.html.haml"
    views/layouts/page_layout.html.haml
    !!!
    %html
    %head
    %title
    = @page.title
    | #{@site.title}

  8. We will then add site values to the footer, site title, and logo to the header.

  9. We will also call all the pages and loop them in line to generate our page navigation. We will lastly call the body inside the content tag so that the content is rendered there:

    app/views/layouts/page_layout.html.haml
    %body
    #container
    #header
    %p= image_tag @site.logo_url.to_s,
    :alt=>"#{@site.title}"
    #menu
    %ul
    - @pages.each do |page|
    %li= link_to page.title, page
    #primary
    %p Primary Sidebar
    #content
    %p= @page.body.html_safe
    #secondary
    %p Secondary Sidebar
    #footer
    %h3= @site.organization
    %p
    = link_to '<i class="fa fa-facebook"></i>'.html_safe,
    @site.facebook, :target=>"blank"
    | #{link_to '<i class="fa fa-twitter"></i>'.html_safe,
    @site.twitter, :target=>"blank"} | #{link_to '<i
    class="fa fa-linkedin"></i>'.html_safe,
    @site.linkedin, :target=>"blank"} | #{link_to '<i
    class="fa fa-skype"></i>'.html_safe, @site.skype,
    :target=>"blank"}
    %p
    = @site.address
    %br/
    = @site.telephone

Objective complete – mini debriefing

At the end of this task, we created a model for storing the site details. In our controller, we called the site details and assigned the instance variable to actions where we need our site object:

before_action :set_site, only: [:home_page, :show] private def set_site @site = SiteDetail.first end

We called the values of site title, address, and contact details in the footer, and content in the content tag. The advantage of using Haml and Sass is clean markup, with very good indentation and code readability. Sass is like an extension of CSS, which compiles to CSS code. One of the main advantages of using Sass is the usage of a variable to make some of the code reusable. Values such as font sizes, colors, and font-family can easily be made dry using Sass variables. We can do a quick refactor of our Sass using a variable for defining the font-family as follows:

app/assets/stylesheets/front_end/structure.scss
$primary-font: 'Cherry Swash', cursive;
#footer {
clear: both;
padding-top:3px;
font-family: $primary-font;
}
#header {
background: #fff;
height: 100px;
font-family: $primary-font;
}

The other option to keep the CSS code clean is using Less CSS (http://lesscss.org/). This extends the CSS to use features such as functions and mixins too. We can see in the following screenshot how Sass is compiled into and is rendered with the site details also displayed:

 

Rails 4 Application Development HOTSHOT Build simple to advanced applications in Rails 4 through 10 exciting projects with this book and ebook
Published: April 2014
eBook Price: £18.99
Book Price: £30.99
See more
Select your format and quantity:

Implementing asset caching

In our CMS there are several kinds of assets. As we build themes we will beautify them with varied JavaScript, CSS, and images. In order to keep the speed of our sites fast in the frontend, we will use asset caching in Rails.

Engage thrusters

The steps to follow will be to cache the content and speed up our site, as follows:

  1. We will first make sure we have the right asset-related gems in Gemfile:

    gem 'sass-rails', '~> 4.0.0'
    gem 'uglifier', '>= 1.3.0'
    gem 'coffee-rails', '~> 4.0.0'

    This will enable all kinds of assets and make it ready for production.

  2. We will first enable asset compression inside our production.rb file:

    config/environments/production.rb.
    config.assets.compress = true

  3. We will continue to edit the same file:

    config.assets.compress = true
    # Compress JavaScripts and CSS.
    config.assets.js_compressor = :uglifier
    config.assets.css_compressor = :sass

  4. We will now fix the asset folder to tmp/cache/assets:

    config/environments/production.rb
    config.assets.cache =
    ActiveSupport::Cache::FileStore.new("tmp/cache/assets")

  5. We need to make sure that we run the following step before deployment:

    helioscms$rake assets:precompile

  6. In order to make use of memcached in our application, we will add a gem called dalli. The dalli gem is a replacement of the memcache client:

    gem 'dalli'

  7. We will configure the cache in our production.rb file:

    config/environments/production.rb
    config.cache_store = :dalli_store
    config.action_controller.perform_caching = true

  8. We will also add a simple action cache so that we always cache the layout along with the cache. In our pages_controller.rb file, we will add an action caching method:

    app/controllers/pages_controller.rb
    def home_page
    expires_in 5.minutes
    sleep 15
    @page = Page.home.first
    cache_client = Dalli::Client.new('localhost:11211')
    @pages = Page.all
    end

  9. Lastly, we will initiate a cache client and store a cached object in it:

    app/controllers/pages_controller.rb
    @page = Page.find_by(page_type: "Home")
    cache_client = Dalli::Client.new('localhost:11211')
    cache_client.set('Home', @page)
    value = cache_client.get('Home')

  10. So, finally our method for home page looks as follows:

    def home_page
    expires_in 5.minutes
    sleep 15
    @page = Page.home.first
    cache_client = Dalli::Client.new('localhost:11211')
    cache_client.set('Home', @page)
    value = cache_client.get('Home')
    @pages = Page.all
    end

Objective complete – mini debriefing

This is an extremely basic caching technique that comes as an extension to Rails. We added memcached for caching the page beforehand. This will help us to speed up our site's frontend. We looked at how to enable memcached for the application using the dalli gem. Memcached is a distributed key-value store for storing memory objects like objects, sessions, strings, API, and data calls. In a way it's a technique to store and retrieve temporary data. This data is stored in the form of an array and is quickly retrieved as soon as the page loads, instead of going back to the database and calling the page again. This avoids unnecessary queries and thus reduces the database load. This technique also saves API calls because for applications such as Twitter clients, the number of requests is a very important criterion. Hence, we first installed memcached on our local system. We then enabled Dalli in our production.rb file:

Dalli::Client.new('localhost:11211')

This will directly access the memcached at its port number and initiate a new client object. We first defined the time for cache expiry:

expires_in 5.minutes

In order to keep the transaction fast, the application pre-fills the cache with a value. This, however, can lead to another problem. It is quite possible that the same key is being accessed by multiple clients. Also, if the cache is empty, it could be filled with multiple keys. Hence, memcached gives an option called sleep, which provides a lock for the time defined in sleep:

sleep 15

If two processes are accessing the same key at the same time, then sleep will tell the other process that the cache is empty, while waiting for the sleep time to finish. Once done, the lock is released and autoassigned to the next value in the queue. In order to store a value in memcached, we used the following set method:

cache_client.set('Home', @page)

The set method includes the key ('Home') and the value (@page). For retrieving the value of the page, we used a simple get method:

value = cache_client.get('Home')

This value is retrieved using a key called Home. We must note that action caching is deprecated in Rails 4. We will have to use a third-party caching technique such as memcached to perform action caching. Fragment caching is another strategy where we cache certain parts of a page instead of the entire page, which is also a very commonly used technique and works nicely out of the box in Rails.

Mission accomplished

We laid down the foundation for a dynamic CMS in this article. We built a backend admin with a functionality that could create pages and parts. We also created a frontend and did the markup using Haml and SCSS. SCSS is fast to load and easy to manage. It also fits neatly in the Rails asset pipeline. Hence, it is a recommended form of markup with Rails. Some of the ideas we looked at in this article were as follows:

  • We replaced ActiveRecord with Mongoid for our database and model

  • We created an admin area and made devise function only for the admin

  • We saw how namespaces in controllers and routes work

  • We created a self referential association, a parent and a child association on the same model

  • We integrated CKEditor with our application

  • We then created different layouts and manifests for our frontend and backend

  • We looked at how Haml and Sass markup are done and their probable advantages

  • Lastly, we looked at a memcached-based caching strategy for caching our actions

Hotshot challenges

We need to take our CMS to the next level. The exercise contains a few ideas worth trying out:

  • Using nested attributes assign parts to a page.

  • Adding a responsive HTML5 layout to the frontend.

  • Adding validation for the home page. There should always be only one page called home.

  • Adding tests to test content created with CKEditor.

Summary

In this article, we learned how to create a customizable content management system to power simple content-driven websites. We effectively created a system where designers will have the freedom to choose the frontend they want and end users can easily manage the content for that frontend.

Resources for Article:


Further resources on this subject:


About the Author :


Saurabh Bhatia

Saurabh Bhatia has been developing professional software since 2005. However, his programming interests date back to his school days. Starting with Java, he quickly moved to Ruby on Rails in 2006, and it has been his primary choice of development framework since then. He built a Ruby on Rails consulting company and ran it for five years. He has worked with several companies in the tech industry, from getting two-person startups off the ground to developing software for large corporates. He is currently the CTO of Ruling Digital Inc., a software company that develops software for universities.

He has been an open source enthusiast and has helped Ubuntu penetrate the Indian market since 2007. He was a part of the open source promotion society called Twincling Society for Open Source in Hyderabad. He started and moderated Bangalore Ruby Users Group and also moderates the Mumbai Ruby Users Group. He is also a part of the RailsBridge initiative for mentoring new Rails developers.

Over the years, he has written several articles online and in print for different publications, such as Linux User and Developer, Linux For You, Rails Magazine, Developer.com (http://www.developer.com/), and SitePoint Ruby (http://www.sitepoint.com/ruby/). He currently resides in Taiwan. He wishes to continue writing and share his knowledge as much as possible with budding developers.

Books From Packt


 Learning Devise for Rails
Learning Devise for Rails

RubyMotion iOS Development Essentials
RubyMotion iOS Development Essentials

  CoffeeScript Programming with jQuery, Rails, and Node.js
CoffeeScript Programming with jQuery, Rails, and Node.js

Ruby on Rails Enterprise Application Development: Plan, Program, Extend
Ruby on Rails Enterprise Application Development: Plan, Program, Extend

 Building Dynamic Web 2.0 Websites with Ruby on Rails
Building Dynamic Web 2.0 Websites with Ruby on Rails

Instant RubyMine Assimilation [Instant]
Instant RubyMine Assimilation [Instant]

 Ruby on Rails Web Mashup Projects
Ruby on Rails Web Mashup Projects

Ruby and MongoDB Web Development Beginner's Guide
Ruby and MongoDB Web Development Beginner's Guide


Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software