Andy from Webcrunch

Subscribe for email updates:

Portrait of Andy Leverenz
Andy Leverenz

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.

Download the source code

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, and erb 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.
  1. Download the livereload browser extension for your browser.
  2. Add the following within the development space of the Gemfile. Make sure to run bundle 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
  1. Run guard init livereload

  2. 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
    
  3. 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. ​

  4. 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.
    

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)

  1. Run rails g controller posts

  2. Update routes file in app/config/routes.rb to use resources :posts and also add root: "posts#index"

  3. Create index action on your posts_controller.rb file and index.html.erb view inside app/views/posts/

  4. 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

  1. 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.

  1. After generating the Post modal you will need to run rails 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

Shameless plug time

Hello Rails Course

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.

Link this article
Est. reading time: 13 minutes
Stats: 28,255 views

Categories

Collection

Part of the Let's Build: With Ruby on Rails collection

Products and courses