(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:
We will first generate our dashboard controller:
rails g controller dashboard indexcreate app/controllers/dashboard_controller.rbroute get "dashboard/index"invoke erbcreate app/views/dashboardcreate app/views/dashboard/index.html.erbinvoke test_unitcreate test/controllers/dashboard_controller_test.rbinvoke helpercreate app/helpers/dashboard_helper.rbinvoke test_unitcreate test/helpers/dashboard_helper_test.rbinvoke assetsinvoke coffeecreate app/assets/javascripts/dashboard.js.coffeeinvoke scsscreate app/assets/stylesheets/dashboard.css.scss
We will then create a namespace called admin in our routes.rb file:
config/routes.rbnamespace :admin doget '', to: 'dashboard#index', as: '/'end
We have also modified our dashboard route such that it is set as the root page in the admin namespace.
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.rbclass Admin::DashboardController < ApplicationControllerbefore_filter :authenticate_user!def indexendend
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.rbclass Admin::SessionsController < ::Devise::SessionsControllerdef createuser = User.find_by_email(params[:email])if user && user.authenticate(params[:password]) &&user.has_role? "admin"session[:user_id] = user.idredirect_to admin_url, notice: "Logged in!"elseflash.now.alert = "Email or password is invalid /Only Admin is allowed "endendend 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 doend
We then created a controller to handle dashboards and placed it within the admin namespace:
namnamespace :admin doget '', 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.
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.rbclass Pageinclude Mongoid::Documentfield :title, type: Stringfield :body, type: Stringfield :page_type, type: Stringvalidates :title, :presence => truevalidates :body, :presence => truePAGE_TYPE= %w(Home News Video Contact Team Careers)end
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
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.rbdef showrender :layout => 'page_layout'end
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:
Gemfilegem "ckeditor", :github => "galetahub/ckeditor"gem 'carrierwave', :github => "jnicklas/carrierwave"gem 'carrierwave-mongoid', :require => 'carrierwave/mongoid'gem 'mongoid-grid_fs', github: 'ahoward/mongoid-grid_fs'
Add the ckeditor gem to Gemfile and run bundle install:
helioscms$ rails generate ckeditor:install --orm=mongoid--backend=carrierwavecreate config/initializers/ckeditor.rbroute mount Ckeditor::Engine => '/ckeditor'create app/models/ckeditor/asset.rbcreate app/models/ckeditor/picture.rbcreate app/models/ckeditor/attachment_file.rbcreate app/uploaders/ckeditor_attachment_file_uploader.rb
This will generate a carrierwave uploader for CKEditor, which is compatible with mongoid.
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
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"}
We also need to mount the ckeditor in our routes.rb file:
config/routes.rbmount Ckeditor::Engine => '/ckeditor'
The editor toolbar and text area will be generated as seen in the following screenshot:
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
The following screenshot shows the index page after the preceding step:
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
Inside the page_relationship model, we will define a two-way association with the page model:
app/models/page_relationship.rbclass PageRelationshipinclude Mongoid::Documentfield :parent_idd, type: Integerfield :child_id, type: Integerbelongs_to :parent, :class_name => "Page"belongs_to :child, :class_name => "Page"end
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_pagebelongs_to :parent_page, :class_name => 'Page',:inverse_of => :child_page
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"
We can see the the drop-down list with names of existing pages, as shown in the following screenshot:
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")
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_pagebelongs_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:
Read more