November 27, 2017
•Last updated November 5, 2023
Let's Build: With Ruby on Rails - A Blog with Comments
Building a blog with comments using Ruby on Rails is a foundational exercise I went through to learn more about the framework. Working together, both Ruby and Rails lend us a hand to generate a fairly simple MVC pattern built on top of a CRUD approach when working with dynamic data.
Kicking things off with a blog
To easily demonstrate the principles of working with Ruby on Rails I chose to build a basic blog. Each blog post will be able to be created, read, edited, and deleted. There will also be comments associated with each individual blog post. Comments will be able to be created and deleted.
With Ruby on Rails, the possibilities are pretty endless in terms of what you can build. I'm sure new features and improvements to our blog are easy to spot as I guide you through the process of building it. My goal was to ease newcomers into the conventions, methods, and code patterns that helped me best when I first dove in. I hope you too can benefit.
Ultimately, the point of this tutorial and video is to help anyone new to the framework understand how it operates as well as the necessary conventions required to create a blog using Ruby on Rails. I touch on things such as routing, controllers, views, models, migrations, relations, and more. For this project, we make use of a few gems
of which help make my life a bit easier as I build out applications. You can find out more about those below.
Gems used in the project
Better Errors - Easier on the eyes when it comes to errors.
Bulma - Most of the time I would roll my own CSS or just use bits of a framework. I'm a big fan of Bulma so we will be using it a lot throughout this series.
Guard - This gem is useful for live reloading our
scss
,js
,css
, anderb
files, although it's capable of much more! Guard is required for the Guard LiveReload gem to work
Add the following within the development space in the Gemfile
. Make sure to run bundle
and restart your server (covered in the video).
group :development do
# Guard is a command line tool to easily handle events on file system modifications.
gem 'guard', '~> 2.14', '>= 2.14.1'
end
- Guard LiveReload - This gem depends on the Guard gem. I use this to automatically reload the browser when Guard senses changes within the code base.
- Download the livereload browser extension for your browser.
- Add the following within the development space of the
Gemfile
. Make sure to runbundle
and restart your server.
group :development do
# reload the browser after changes to assets/helpers/tests
gem 'guard-livereload', '~> 2.5', '>= 2.5.2', require: false
end
Run
guard init livereload
Be sure to comment out the following block in the `Guardfile if it gets generated for your project.
# comment this whole block out as we won't be making use if minitest # guard :minitest do # .... # end
Restart your server once more for good measure. Run:
bash
bundle exec guard
to start the "watching" process within your project directory. We use
bundle exec
as a prefix here so guard has access to all of our dependences in the project. βMake sure your browser extension is active when navigating to your app. If your console reads back something similar to the following, then you are in good shape.
00:00:00 - INFO - LiveReload is waiting for a browser to connect. 00:00:00 - INFO - Guard is now watching at '/path/to/your/project/' [1] guard(main)> 00:00:00 - INFO - Browser connected.
- Simple Form - For simpler forms!
Our final post form partial is as follows. Here I add Bulma specific classes to get Simple Form to play nice with the CSS framework. If you use Simple Form with Bootstrap or Foundation you likely need even less markup than this.
<div class="section">
<%= simple_form_for @post do |f| %>
<div class="field">
<div class="control">
<%= f.input :title, input_html: { class: 'input' }, wrapper: false, label_html: { class: 'label' } %>
</div>
</div>
<div class="field">
<div class="control">
<%= f.input :content, input_html: { class: 'textarea' }, wrapper: false, label_html: { class: 'label' } %>
</div>
</div>
<%= f.button :submit, 'Create new post', class: "button is-primary" %>
<% end %>
</div>
The Post Controller
The Post controller helps handle our actions when we create, edit, show, update and delete posts on the blog.
Creating a Post Controller (see video)
Run
rails g controller posts
Update routes file in
app/config/routes.rb
to useresources :posts
and also addroot: "posts#index"
Create
index
action on yourposts_controller.rb
file andindex.html.erb
view insideapp/views/posts/
Repeat all CRUD actions with a final controller as follows (follow along on the video for a complete explanation)
class PostsController < ApplicationController
def index
@posts = Post.all.order("created_at DESC")
end
def new
@post = Post.new
end
def create
@post = Post.new(post_params)
if @post.save
redirect_to @post
else
render 'new'
end
end
def show
@post = Post.find(params[:id])
end
def update
@post = Post.find(params[:id])
if @post.update(post_params)
redirect_to @post
else
render 'edit'
end
end
def edit
@post = Post.find(params[:id])
end
def destroy
@post = Post.find(params[:id])
@post.destroy
redirect_to posts_path
end
private
def post_params
params.require(:post).permit(:title, :content)
end
end
The Post Model
- Our app depends on a
Post
model of which we can create by running
rails g model Post title:string content:text
The model is responsible for the data of our post and interacting with the database. I've declared two new record types which we will make use of. title
is of the type string
and content
is of the type text
. You can name these whatever you wish but it makes sense to describe what they are for the specific data model in mention. You can add more data types at any time with migrations
. It's best to start with the basics and later add new migrations
as you go.
- After generating the
Post
modal you will need to runrails db:migrate
to migrate those new records into the database. After doing so you should now be able to interact with those specific records.
The Comments Controller
Similar to our posts controller we need to generate one for comments.
rails g controller comments
We only want the ability to create and delete comments associated with a post. To do this we need our controller to interact with both the Post model and Comment model (still need to generate this).
class CommentsController < ApplicationController
def create
@post = Post.find(params[:post_id])
@comment = @post.comments.create(params[:comment].permit(:name, :comment))
redirect_to post_path(@post)
end
def destroy
@post = Post.find(params[:post_id])
@comment = @post.comments.find(params[:id])
@comment.destroy
redirect_to post_path(@post)
end
end
The Comment Model
To generate a comment model we can run:
rails g model Comment name:string comment:text
This creates a new Comment table with a named row as a string
datatype and a comment row as a text
datatype.
After generating the model you will need to run rails db:migrate
to migrate the new rows into the database table.
Data Relations
Our blog posts need a way to talk to our comments as comments should only belong to the blog post they were created on. To do this we need to set up some relations between both the Post
model and the Comment
model.
Modify the Post
model to include the following
# found in app/models/post.rb
class Post < ApplicationRecord
has_many :comments, dependent: :destroy
# dependent: :destroy means the comments related
# to the specific post in mention get deleted if the post does.
end
Modify the Comment
model to include the following:
# found in app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :post
end
With this relationship in place, our application is set up to communicate between the post and comment models.
Associating an id
For the comments to work, we need an id added to the database table that comes from the post. To do this we need to run a migration that added another row of which will tie the relationship together.
Run the following migration:
rails g migration AddPostIdToComments
Then navigate to app/db/migrate/XXXXXXXXXXXX_add_post_id_to_comments.rb
and add the following code:
class AddPostIdToComments < ActiveRecord::Migration[5.1]
def change
add_column :comments, :post_id, :integer
end
end
Here we are creating a new row on the comments column called post_id
of the type integer
. Run rails db:migrate
to migrate that new migration.
Now at this point, we can create comments of where the post_id
on the Comment
model matches the id
of the Post
model.
Views
The views are pretty straightforward in the sense of a blog. We have a basic layout file that acts as our "master" file of sorts. You can find the application.htm.erb
in app/views/layouts/
. Here I've added some markup that comes from the Bulma framework as well as some navigational links to help get around the blog easier.
The final application.html.erb
file is below:
<!DOCTYPE html>
<html>
<head>
<title>DemoBlog</title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<section class="hero is-primary is-medium">
<!-- Hero head: will stick at the top -->
<div class="hero-head">
<nav class="navbar">
<div class="container">
<div class="navbar-brand">
<%= link_to 'Demo Blog', root_path, class: "navbar-item" %>
<span class="navbar-burger burger" data-target="navbarMenuHeroA">
<span></span>
<span></span>
<span></span>
</span>
</div>
<div id="navbarMenuHeroA" class="navbar-menu">
<div class="navbar-end">
<%= link_to "Create New Post", new_post_path, class:"navbar-item" %>
</div>
</div>
</div>
</nav>
</div>
<!-- Hero content: will be in the middle -->
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title">
<%= yield :page_title %>
</h1>
</div>
</div>
</section>
<%= yield %>
</body>
</html>
Posts Views
Our posts views folder is also straightforward. We have a new file for each route within our application. Kicking things off is the index page:
Find this code in app/views/posts/index.html.erb
<% content_for :page_title, "Index" %>
<div class="section">
<div class="container">
<% @posts.each do |post| %>
<div class="card">
<div class="card-content">
<div class="media">
<div class="media-content">
<p class="title is-4"><%= link_to post.title, post %></p>
</div>
</div>
<div class="content">
<%= post.content %>
</div>
<div class="comment-count">
<span class="tag is-rounded"><%= post.comments.count %> comments</span>
</div>
</div>
</div>
<% end %>
</div>
</div>
We render a partial which gets used to but the edit.html.erb
and new.html.erb
pages within the posts
views folder.
The new.html.erb
page found in app/views/posts/new.html.erb
.
<% content_for :page_title, "Create a new post" %>
<%= render 'form' %>
The edit.html.erb
page found in app/views/posts/edit.html.erb
<% content_for :page_title, "Edit Post" %>
<%= render 'form' %>
And of course the _form.html.erb
partial found in app/views/posts/_form.html.erb
<div class="section">
<%= simple_form_for @post do |f| %>
<div class="field">
<div class="control">
<%= f.input :title, input_html: { class: 'input' }, wrapper: false, label_html: { class: 'label' } %>
</div>
</div>
<div class="field">
<div class="control">
<%= f.input :content, input_html: { class: 'textarea' }, wrapper: false, label_html: { class: 'label' } %>
</div>
</div>
<%= f.button :submit, 'Create new post', class: "button is-primary" %>
<% end %>
</div>
The and finally the show.html.erb
file found in app/views/posts/show.html.erb
<% content_for :page_title, @post.title %>
<section class="section">
<div class="container">
<nav class="level">
<!-- Left side -->
<div class="level-left">
<p class="level-item">
<strong>Actions</strong>
</p>
</div>
<!-- Right side -->
<div class="level-right">
<p class="level-item">
<%= link_to "Edit", edit_post_path(@post), class:"button" %>
</p>
<p class="level-item">
<%= link_to "Delete", post_path(@post), method: :delete, data: { confirm: "Are you sure?" }, class:"button is-danger" %>
</p>
</div>
</nav>
<hr/>
<div class="content">
<%= @post.content %>
</div>
</div>
</section>
<section class="section comments">
<div class="container">
<h2 class="subtitle is-5"><strong><%= @post.comments.count %></strong> Comments</h2>
<%= render @post.comments %>
<div class="comment-form">
<hr />
<h3 class="subtitle is-3">Leave a reply</h3>
<%= render 'comments/form' %>
</div>
</div>
</section>
Comments Views
Most of our comment views are nested inside the show views. We do create a comment partial as well as a comment form.
The comment partial (_comment.html.erb
) found in app/views/comments/
is what each comment contains when authored. Below is the final code of that partial.
<div class="box">
<article class="media">
<div class="media-content">
<div class="content">
<p>
<strong><%= comment.name %>:</strong>
<%= comment.comment %>
</p>
</div>
</div>
<%= link_to 'Delete', [comment.post, comment],
method: :delete, class: "button is-danger", data: { confirm: 'Are you sure?' } %>
</article>
</div>
And the comment _form.html.erb
gets embedded on the show page of our Posts
show page.
<%= simple_form_for([@post, @post.comments.build]) do |f| %>
<!--
collection.build(attributes = {}, β¦) Returns one or more new objects of the collection type that have been instantiated with attributes and linked to this object through a foreign key, but have not yet been saved. Note: This only works if an associated object already exists, not if itβs nil!
-->
<div class="field">
<div class="control">
<%= f.input :name, input_html: { class: 'input' }, wrapper: false, label_html: { class: 'label' } %>
</div>
</div>
<div class="field">
<div class="control">
<%= f.input :comment, input_html: { class: 'textarea' }, wrapper: false, label_html: { class: 'label' } %>
</div>
</div>
<%= f.button :submit, 'Leave a reply', class: "button is-primary" %>
<% end %>
Rounding out
If you made it through the video you got a good look at what it takes to build a basic blog with comments using Ruby on Rails. I invite you to download the source code to see the final result as shown in the video and to also use it as you troubleshoot your way on your own projects. Rails are good about telling you what is wrong using errors when in development. Sometimes the errors are obvious whereas others are not. Googling the error message often lead me to answers. Being that Ruby on Rails is a big convention-based framework, most people that run into errors find the answers they need without too much fuss. There are definite perks to have an opinionated stack!
I hope you enjoyed this tutorial and video. Up next I plan to build a clone of a popular application that will start to focus more on user roles, authentication, security, and more. If you enjoyed this please let me know in the comments. I'm also always happy to help troubleshoot any errors you find along the way if you follow along!
The Series So Far
- 01 - Introduction
- 02 - Installation
- 03 - Build a blog with comments - You are here
Shameless plug time
I have a new course called Hello Rails. Hello Rails is modern course designed to help you start using and understanding Ruby on Rails fast. If you're a novice when it comes to Ruby or Ruby on Rails I invite you to check out the site. The course will be much like these builds but a super more in-depth version with more realistic goals and deliverables. Sign up to get notified today!
Follow @hello_rails and myself @justalever on Twitter.
Categories
Collection
Part of the Let's Build: With Ruby on Rails collection
Products and courses
-
Hello Hotwire
A course on Hotwire + Ruby on Rails.
-
Hello Rails
A course for newcomers to Ruby on Rails.
-
Rails UI
UI templates and components for Rails.