Begin building your own CMS in just minutes! (code-along)

As I was building LuckyTube, I learned how in general it can be very straight forward to build a simple CMS using Sinatra. So you too can can start creating a Content Management System (CMS) with users managing a single resource in less than an hour!

Sinatra is a Ruby DSL for quickly creating web applications in Ruby with minimal effort” — Sinatra Readme.

Take Off (30 seconds)

The fastest way to start is by using the Corneal Sinatra structure generator. Corneal is a Ruby gem so you can get started as easy as:

gem install corneal
corneal new your_new_awesome_app_name

Now in terminal, access your directory and do:

bundle install

Note: If at any point in this code-along you get lost just access the Starter Github CMS Code with the final code.

What just happened? (skip if you want)

Corneal just did a bunch of things for us in the background. While most of this was creating our file structure, it also generated some very important files.

A Gemfile is auto-generated when you use Corneal. Gemfile is used by bundler so we can keep track of all our gems in a single place. Here’s how how my Gemfile looks (don’t worry if you don’t know all the gems included):

source 'https://rubygems.org'gem 'activerecord', '5.2'
gem 'rspec'
gem 'sinatra-activerecord'
gem 'rake'
gem 'database_cleaner'
gem 'require_all'
gem 'thin'
gem 'sinatra'
gem 'shotgun'
gem 'pry'
gem 'tux'
gem 'sqlite3', '~> 1.3.6'

Next. Sinatra uses Rack, a webserver interface that makes it easy to handle HTTP requests and responses with Ruby. So in order for your Sinatra CMS to run we need that config.ru file. This file was also auto-generated by corneal. This file can be as simple as mine:

require './config/environment'
run ApplicationController

Now, since we are building a CMS, we need some data persistence (database). Corneal already included sqlite3 in our Gemfile in the first step for us to use. Sqlite3 is a database which is a flexible, light and easy to setup. The environment.rb file that lives under config/environment takes care of setting up sqlite3. Corneal also auto-generated our environment file. You can check how my environment looks at this point too:

ENV['SINATRA_ENV'] ||= "development"
require 'bundler/setup'
require "sinatra/activerecord"
Bundler.require(:default, ENV['SINATRA_ENV'])#establish database connection
configure :development do
set :database, 'sqlite3:db/my_database_name.db'
end
require_all './app'

There’s a good video walk-through that explains some of this files in detail and overall file-structure (which we’ll talk more about next).

Our new CMS file-structure (2 minutes)

If you skipped straight through here don’t worry because Corneal made it easy for you and did all the set up, created folders, files and even installed most of the gems you’ll need to develop your CMS!

Our new CMS will mostly live in the following three folders:

├── app
│ ├── controllers
│ │ └── application_controller.rb
│ ├── models
│ └── views
│ ├── welcome.erb
│ └── layout.erb
├── db
│ ├── migrate
│ └── seeds.rb
├── public
│ └── css
│ └── style.css

app folder

Here’s where most of our coding will go. As you see, these folders follow the Model, View, Controller pattern. Our Model will handle the database actors, our View will display our CMS and resources to our Users and the Controller will be the go-in-between and handler of what to show-or-not from Views and what to retrieve-or-not from the Model(s). Since we’ll be doing most of the coding here, we’ll be going back to our app folder later.

public folder

This will hold your Stylesheets (CSS). Corneal already included a style.css for you. That’s nice right! Should you want additional CSS files or Frameworks (I used Bootstrap for LuckyTube) they will need to be included in the layout.erb file under the views folder.

db folder

This folder holds your database! The migrate folder will hold your database migrations or actions. We’ll be making some database migrations and decisions next, so hang on more to come!

Let’s decide upon our main resource to manage and build or database (5 minutes)

Before we do that, let’s take a quick step back and actually remember what a CMS is:

A content management system, often abbreviated as CMS, is software that helps users create, manage, and modify content on a website without the need for specialized technical knowledge. — What Is a Content Management System (CMS)?

So we want to guide you in creating an easy to use interface that any user can use to manage their content as needed. Here we will replace the word content for resource, since I believe it will help us decide better what content to manage by thinking in terms of a resource that belongs-to a particular user.

Now, take a minute or two to think of resource you’d like to track. Let me give you some ideas: recipes, calories, money spent, movies watched, books read, donations and even LuckyTube videos are examples of resources we can manage in our CMS! Now is your turn. Be creative!

By now you might have a great resource that your user will manage. This doesn’t represent just a single resource of its kind but many resources of its kind so you might say how our user will have-many, in my case many Luckyvideos.

Let’s write our two models (1 minute)

Before we build our database we need to write our Ruby model classes that our database will use to persist the data we pass to. We will write these two files inside the app/models directory.

Sinatra uses ActiveRecord (AR) which uses an Object-Relational Mapping (ORM) system so we can use Ruby to define, create, edit and delete all data that will live in our database. The process of building these classes is the same as defining any other class. The only extra thing we do is inherit from ActiveRecord::Base so we ensure all the magic is passed down. Here’s how my user AR model class file looks to start:

class User < ActiveRecord::Base
has_many :luckyvideos
end

My resource model class inside app/models/ looks like this:

class Luckyvideo < ActiveRecord::Base
belongs_to :user
end

Your will only have a different name for where resource is referred since you will be managing a different resource.

Build our db (5 minutes)

The process for building our database is as simple as creating some migration files that will build our database tables.

Sinatra uses Rake tasks to accomplish building our database. No worries you don’t have to define any db tasks since Sinatra already defined them for you. All you have to do is call them on the terminal with the particular resource table and user table. For example to create my user table I typed the following rake task command:

rake db:create_migration NAME=create_users_table

And to create my resource (luckyvideos) table, this command:

rake db:create_migration NAME=create_luckyvideos_table

You noticed that these two Rake task commands just magically created two files for us that now live under db/migrate folder. These two files uses AR methods to create our tables. We need to define our tables before we build our database.

To do this let’s use a create_table method inside the change method definition. This method takes a table_name as symbol and then passes a block that defines the different column names and types of our table. This is how my users table inside our migration folder looks to begin:

class CreateUsersTable < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :name
t.string :email
end
end
end

Here our users table has only two columns; a name column and an email column. We can always add columns but that is all we need for now.

As far as our resource, here’s where we need to take a minute or two to define our columns as attributes that our resource will have. Notice that sqlite3 supports different datatypes but for our purposes we’ll only be concerned about whether our resource attribute is a text or integer. Take a minute or two to think about this. I’ll wait for you.

Great! FYI This how my luckyvideos table looks now:

class CreateLuckyvideosTable < ActiveRecord::Migration[5.2]
def change
create_table :luckyvideos do |t|
t.string :title
t.string :url
t.integer :user_id
end
end
end

Note: I’m using [5.2] in my table definition because I’m using ActiveRecord 5.2. So if you run into any issues doing migrations just define your AR version as I did in between the brackets.

Note 2: Notice how the last column refers to the Users table. This is what ActiveRecord will use to set relationships. So we could say a luckyvideo belongs to an user. We only set this column parent_id on Luckyvideos (children) and will immediately affect Users so they can now have many luckyvideos.

Now..

Let’s build our db! There’s another rake command to execute the table definitions in our migration. We can just type:

rake db:migrate

Voila! You’ll see that a .sqlite database file and schema.rb file have been created. We have a database and now to the fun part!

Enter the app (5 minutes)

Yay! we did all the heavy lifting so let’s have some fun with Sinatra!

Our Models part is mostly good. Now, we’ll enter the app folder again to work on our controller and then have a stab at a simple view.

Let’s go ahead and access the application_controller.rb file inside app/controllers folder. You might see that our controller already has some configurations and a basic route defined. The configure method is defining where our views and CSS live. The basic route is defining our home (‘/’).

Let’s go ahead and start up our server by writing shotgun on the terminal. You’ll see a localhost URL shown so go ahead and copy and paste that URL on your browser. You’ll see something like this:

Note: Shotgun is a Ruby gem that allows you to make changes to the code and see the change instantly, by only refreshing the URL you’re working on. If you ever need to exit the server or access the terminal again just type CTR + C.

So all seems to be working! Let us take a look a the route after the get method inside our controller file in app/controllers.

get "/" do
erb :welcome
end

The ‘get’ is an HTTP Request method that Sinatra wraps as a ruby method that takes a route (in this case ‘/’ or root view) and a block (erb :welcome). Just as Sinatra had the get method it could’ve also been a post, delete, or patch request, you name it. The route could’ve been anything also. Give a try and change the route to “/test” then go to yoururl/test and see how you get the same view. Why? Because the route you defined now points to /test, what happens if you go back to main page? Your program breaks. Why? because you no longer have that route for it! Cool, right?

Now, what about the block passed? Go ahead and comment out “erb :welcome” and instead type “My CMS App”. Refresh your browser. You should’ve gotten this:

So whatever we passed to the route is what is rendered (aka the Views). In the case of “erb :welcome” the route is going inside the views folder and calling out an ERB file named welcome and displaying it in our browser. Let’s go back to our route and leave it as it was in the beginning.

Now, let’s go ahead and open the file welcome.erb under the views folder. So this is where all that mumbo jumbo came from after we started up our server. Let’s delete everything and just have a single HTML tag that says:

<h1> Welcome To My CMS User! </h1>

Refresh your browser and you’ll probably get this:

You might ask, how come we still have styling since our file is actually a 1 line file? Take a look a the file called layout.erb. This file is actually defining a basic layout that all the Views will share. Let’s go ahead and change some things in the layout file (like the title tag) and see how it instantly reflects our welcome.erb file.

Which route to take? (15 minutes)

Now that we have a basic understanding of how our Controller interacts with our View, let us now do our own route and view to better reflect our own CMS.

We’ll be creating a couple of views: Login, Sign Up, Home along with their corresponding routes. Should be a smooth ride. Let’s roll!

In our welcome let’s build up some links to our Sign Up and Login. So our welcome should look something like this:

<h1> Welcome To My CMS </h1>
<ul>
<li>
<a href="/login"><p><strong>Login </strong></p></a>
</li>
<li>
<a href="/signup"><p><strong>Sign Up </strong></p></a>
</li>

</ul>

You notice that if you click the links the program will break. Why? Because our controller doesn’t have routes for it! Let’s go ahead and quickly create the controller routes and views. Our login view will be named login.erb and our route will look like this inside our Controller:

get "/login" do 
erb :login
end

Same for signup, we’ll have signup.erb and a erb :signup in the “/signup” route. Now do a similar h1 tag we had before but name it accordingly. If you access it again you should see the views match the controllers we defined!

We made good progress. Let’s take deep breath now!

Moving forward.

In order for our Controller routes to communicate with our Model, we need to make use of HTML forms.

I will go ahead and provide you with the login/signup form. Sign Up/Login will be basically the same, just your controller action will be different!

Pay attention to the form action and HTTP method. Our login form could look like this:

<h1> Login</h1>
<h5><%= @error if @error %></h5><br/>
<form method="POST" action="/login">
<p>Email <input type="email" name="email" required></p>
<input type="submit" value="Login">
</form>

Our signup might look like this:

<h1> Sign Up</h1>
<h5><%= @error if @error %></h5><br/>
<form method="POST" action="/signup">
<p>Your Name: <input type="text" name="name" required></p>
<p>Email <input type="email" name="email" required></p>
<input type="submit" value="Sign Up">
</form>

Note: Notice the h5 tag? It has a Ruby variable! How come so? Because we are using ERB files which pretty much stand for “Embedded Ruby”. Just know Ruby code inside an ERB file is written inside this tags <% #ruby_code_here %>. If you want your Ruby output to display in the browser just add an = so <%= “this will be displayed in the browser” %>. Try it yourself!

Note 2: Notice the required after the input tag and input type=“email”? That will add some HTML5 validation to ensure users fill out all and the correct fields before submitting!

Notice how our program breaks if we try to either to either Login or Sign Up. Yes you know we need the routes in our Controller that matches the form method (“post”).

You’ll notice very soon that in order to ensure we can Login and Signup our users we’ll be using some ActiveRecord Methods, that will feel like magic. But they are not, don’t forget we inherited some AR methods when we defined our Models!

So we’ll be instantiating our Model classes and then using ActiveRecord methods to save and retrieve our user. You are free to play around with methods or look ahead for the code. With that being said, is time to level up!

So let’s take a look at our two post Sinatra routes that will handle either logging in or signing up.

Here’s our post ‘/signup’ route:

post '/signup' do
if User.find_by(email: params[:email])
@error = <<-HTML
User with that email already exists. Please <a href='/login'>Login</a>.
HTML
erb :'/signup'
else
@user = User.new(params)
@user.save
redirect to "/home/#{@user.id}"
end
end

Now our post ‘/login’ route:

post '/login' do
@user = User.find_by(email: params[:email])
if @user
redirect to "/home/#{@user.id}"
else
@error = <<-HTML
Your email was not recognized. Please try again or <a href='/signup'>Sign Up</a>.
HTML
erb :'/login'
end

end

Let’s break up the magic:

  • All our values are passed by Sinatra hashed called params. How does params know about email and name? Because it catches our name= in our inputs!
  • Methods like find_by, and .save are ActiveRecord methods that we inherited to talk to our database. Rock on!
  • What is user.id attribute? The id attribute gets added after we persist to our database so after .save! It is the unique identifier or primary key of our table.
  • When we redirect to “home/#{@user.id}” we are passing a dynamic route, with the id of the user that params will then catch and generate the particular home view specific to the user!
  • <<-HTML and what follows is what is called a Heredoc, in this case we are defining our error variable in the else statement that will be then displayed to the user that gives the wrong email or tries to sign up with an email that already exists.

If you thought that we are missing our route for /home, yes you’re right this will throw an error if we don’t have our view nor route for it. So let’s create a home.erb file and specific route for it inside our application_controller file:

get "/home/:id" do 
@user = User.find_by_id(params[:id])
erb :home
end

home.erb

<h1> Welcome Home <%=@user.name%>!</h1>

I know we went through a lot! But go ahead and try it yourself. Run shotgun and login, signup and play with it. Take a minute or two. Pretty neat right!

The final destination (12 minutes)

Our final destination (for this code-along at least) will be to have our CMS ensure that an User can add one or more its resources. So we will be building our resource route and implement it in a view. Are you ready?

Before we begin coding let’s write up what we want to accomplish so to have a map of where we’ll go:

  1. User will be able to add a resource
  2. The User resource will be displayed when logging in.

Cool we have a plan. OK do you remember our home.erb? It seems like a great place to actually accomplish both adding and displaying our resource!

So let’s add another simple form to our home.erb. Let’s stick to a couple of resource attributes for starters then scale up. Take a minute or two to build-up your form.

Here’s how my home.erb looks at this point:

<h1> Welcome Home <%=@user.name%>!</h1>
</br>
<h1> Your Luckyvideos</h1>
<% @luckyvideos.each do |luckyvideo| %>
<ul>
<li class "display-4"><a href="<%= luckyvideo.url %>" target="_blank"><p class="h4"><%= luckyvideo.title %></p></a></li>
</ul>
<% end %>
<h3><em><%= @empty if @empty %></em></h3><br/>
</br>
<h1> Add New Luckyvideo</h1>
<form method="POST" action="/luckyvideo/<%=@user.id%>">
<p>Title <input type="text" name="title" required></p>
<p>URL <input type="text" name="url" required></p>
<input type="submit" value="Add LuckyVideo!">
</form>

Take a minute to understand what is going on since we basically coded the form and the view of our resource in the same place!

Did you notice the empty instance variable? What about the luckyvideo iteration?

We kind of went backwards here (our CMS will break if we try to run it since we don’t have a luckyvideos variable defined). But that is fine. Although we should definitely refactor our home route. Let’s go there!

Here’s our refactored home route:

get "/home/:id" do 
@user = User.find_by_id(params[:id])
@luckyvideos = @user.luckyvideos
if @luckyvideos.empty?
@empty = "You don't have any LuckyVideos! Add one below!"
@user = User.find_by_id(params[:id])
erb :home
else
erb :home
end
end

Let’s go ahead and try to log in again! If all goes well, after we login or signup our home view should look like this right now:

Remember: The user.luckyvideos was made possible by our ActiveRecord magic we defined at the beginning of our code-along. Since there are no luckyvideos associated with user, our if statement gets triggered and so our empty instance variable in ERB gets populated. Nice right!

Now if we try to fill out the form to add our resource, our code will break. Why? Because we don’t have a post route for it. Let’s make it! Take a minute or two to think how you’ll do it. I’ll take break in the meantime.

We’re almost there! Let’s look at my post route for adding the resource:

post '/luckyvideo/:id' do
@user = User.find_by_id(params[:id])
@luckyvideo = Luckyvideo.new(params)
@luckyvideo.save
@user.luckyvideos << @luckyvideo
redirect to "/home/#{@user.id}"
end

If everything goes as planned we now have a resource added to our user and immediately populated to our home view.

We did it. Bam!

Don’t Forget! If at any point in this code-along you got lost just access the Starter Github CMS Code with the final code!

Final Note: If you look at the published beta version of LuckyTube you’ll notice that users can add their own Luckyvideos just by doing a simple search and being logged in. This is possible because LuckyTube makes use of a custom Ruby API that directly perform searches on YouTube and returns a random YouTube video.

The Road Ahead ( ∞ )

Hope you enjoyed this code-along!

In order to make your CMS more robust, take a look at the following tips and resources. All of them I implemented them in LuckyTube:

  • Use a password and encrypt it with Bcrypt. Bcrypt is a ruby gem that you can include on your Gemfile. You will then be able to authenticate users with passwords.
  • Make use of Sinatra Sessions to ensure your routes are clean, using cookies and users can be better authenticated and can even log out. This will be essential so that only authenticated users see their own videos.
  • Use other ActiveRecord methods to ensure full CRUD (Create, Read, Update, Delete) functionality. These include .update and .destroy_all. Note you’ll have to include Method Override inside your config.ru file if you’re updating and deleting. Take a look at my LuckyTube Repo to see how I implemented this.
  • Take advantage of helper methods and include them in your controller. An useful helper method to think about is one that checks whether the user is logged in. Once again LuckyTube Repo has examples of implementations of this. You can see how I have helper methods to creating luckytube object instances from API calls (go into app/helpers folder).
  • Polish your views and use a CSS framework such as Bootstrap. You can also use CSS Grid or Flexbox.

By now you should have the power of Sinatra and ActiveRecord to continue building your own CMS. You’ve inherited the power! Make good use of it.

Social Entrepreneur and Software Engineer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store