Implementing a Homepage Design in Ruby on Rails
In this guide we will walk through an initial implementation of the homepage design.
Guide Tasks
  • Read Tutorial

In this guide we will walk through an initial implementation of the homepage design. I'm not a huge fan of the homepage design, it looks too much like a news site instead of a social media application, so we're going to use the same layout that we integrated into the Topic page for the homepage.

Now if you're first inclination is to copy and paste the code from the posts index page, stop! Remember we need our code to be DRY, so we can't have duplicate code. This is a perfect situation to refactor our posts view code into a partial that can be shared.

If you look at the posts/index.html.erb file you'll see that it has a call to the @posts instance variable and it would make sense for the homepage to have the same call, so the only behavior that will be different is that the homepage won't have posts nested underneath a Topic, and we can control the data in the controller, are you seeing now why it's so important to have every MVC element perform its job and nothing more? If our view code communicated with the model we wouldn't be able to share it between different parts of the application.

So let's start by creating a partial to store our posts:

touch app/views/shared/_posts.html.erb

Now let's move everything in the @posts.each block to the partial, I'm going to keep the HTML wrapper in the file since we may want to have some different style options on the homepage compared with the Topic page. So our partial should now look like this:

<!-- app/views/shared/_posts.html.erb -->

<% @posts.each do |post| %>
  <li class="clearfix">
    <div class="ratings">
      <a href="#up" class="up"></a>
      <h1>1178</h1>
      <a href="#down" class="down"></a>
    </div>

    <div class="article-image">
      <div class="mask"></div>
      <img src="https://s3.amazonaws.com/rails-camp-tutorials/pro-rails/ui/article_image.png"/>
    </div>

    <div class="article-desc">
      <h2><%= link_to post.title, topic_post_path(topic_id: @topic, id: post) %></h2>
      <p><%= post.content %></p>
      <span>By <strong><%= post.user.username %></strong> <%= post.created_at %></span>
    </div>

    <div class="article-stats">
      <ul class="pull-right">
        <li>
          <span class="icon icon-view"></span>
          <h2>347</h2>
        </li>
        <li>
          <span class="icon icon-comment"></span>
          <h2>347</h2>
        </li>
        <li>
          <span class="icon icon-connection"></span>
          <h2>347</h2>
        </li>
      </ul>
    </div>
  </li>
<% end %>

And our posts/index template will look like this:

<!-- app/views/topics/posts/index.html.erb -->

<div class="container">

  <h1><%= @topic.title %></h1>

  <div class="row">
    <div class="col-xs-9 pull-left">
      <ul class="articles">
        <%= render 'shared/posts' %>
      </ul>
    </div>
  </div>

  <%= link_to "New Post", new_post_path(topic_id: @topic.id) %>
</div>

First and foremost, doesn't that index template already look better? Even if we weren't going to be using the posts in another part of the application this would still be a good refactor to do simply to clean up the file. Now if you run rspec you'll see that all of the tests are still passing and if you navigate to the site in the browser you'll find that everything is working just like before, so all good so far.

How to Implement the Homepage Design

Now let's start by creating some Capybara integration tests to establish some expectations for what we want to see on the homepage.

We already have a placeholder test that we can keep there for regression purposes, however it looks like I have the describe block names mixed up, so let's fix that first:

# spec/features/static_spec.rb

require 'rails_helper'

describe 'homepage' do
  describe 'navigate' do
    it 'can be reached successfully' do
      visit root_path
      expect(page.status_code).to eq(200)
    end
  end
end

That's mainly for documentation and code organization purposes since it wouldn't make sense to have all of the specs nested inside a navigate block. With that squared away let's think through what what want on the homepage:

  1. We want a list of topics
  2. We want multiple posts from various topics

Eventually it would make sense to apply a custom popularity algorithm to the homepage so that the popular posts are shown, but for a new application we're simply going to show the most recent posts from all of the topics.

Let's first build a spec for ensuring that a list of the topics show on the homepage:

# spec/features/static_spec.rb

  describe 'content' do
    it 'has a list of topics' do
      FactoryGirl.create(:topic)
      FactoryGirl.create(:second_topic)

      visit root_path
      expect(page).to have_content("Sports")
      expect(page).to have_content("Coding")
    end
  end

Here we're creating both of our topics from our topics factory and we're expecting to find both of the names on the homepage. Running this spec will fail since our homepage is empty, so let's implement the most basic implementation to get this test passing, opening up the view template put in the code:

<!-- app/views/static/home.html.erb -->

<%= Topic.all.inspect %>

Woah!!! Is that a model call in the view?! Yes, remember the process of Behavior Driven Development is to first implement the most basic solution to get the test passing, and then refactor it. There is a method to the madness, if we went through the process of making changes to the controller and view, it may take us longer to debug the root of the problem since the issue could deal with the code flow, view layer, or test. Here we know that if there is an issue it's most likely with the test because code such as <%= Topic.all.inspect %> is so basic. If you run rspec spec/features/static_spec.rb you'll see that our test is passing, so now we can refactor it.

Let's start by making a call to the Topic model in the controller for the home action:

# app/controllers/static_controller.rb

class StaticController < ApplicationController
  def home
    @topics = Topic.all
  end
end

So we can call the instance variable from the view:

<!-- app/views/static/home.html.erb -->

<%= @topic.inspect %>

Running the tests again will show that the tests are still passing through the first refactor. If you startup the rails server and look at the homepage you'll see it's printing out the full hash values of the topics because we're calling the inspect method in order to print out all of the values of the database query.

large

So let's clean this up by iterating over the values in the view:

<!-- app/views/static/home.html.erb -->

<% @topics.each do |topic| %>
  <%= topic.title %>
<% end %>

If you refresh the browser you'll see that this is looking better and if you stop the server and run the tests, they're all still passing.

large

Now that we have our titles let's put them in the sidebar where they're supposed to be and we'll also implement some of the other core layout styles in order to do this:

<!-- app/views/static/home.html.erb -->

<div class="container">
  <div class="row">
    <div class="col-xs-9 pull-left">
      <ul class="articles">
      </ul>
    </div>

    <div class="col-xs-3 sidebar pull-right">
      <ul>
        <% @topics.each do |topic| %>
          <li><%= topic.title %></li>
        <% end %>
      </ul>
    </div>
  </div>

</div>

If you hit refresh you'll see that things are shaping up nicely on the homepage!

large

The last item will be to render the posts on the homepage. Let's create a test for this that confirms that the most recent posts show on the homepage:

# spec/features/static_spec.rb

  describe 'content' do
    it 'shows the most recent posts' do
        FactoryGirl.create(:post)
        FactoryGirl.create(:second_post)

        visit root_path
      expect(page).to have_content("My Great Post")
      expect(page).to have_content("Another Guide")
    end
  end

If you run the spec you'll see that it failed because it couldn't find the posts, so let's implement the partial and create an instance variable in the controller to store the posts:

# app/controllers/static_controller.rb

class StaticController < ApplicationController
  def home
    @posts = Post.all.limit(25)
    @topics = Topic.all
  end
end

And now update the homepage template:

<!-- app/views/static/home.html.erb -->

<div class="container">
  <div class="row">
    <div class="col-xs-9 pull-left">
      <ul class="articles">
        <%= render 'shared/posts' %>
      </ul>
    </div>

    <div class="col-xs-3 sidebar pull-right">
      <ul>
        <% @topics.each do |topic| %>
          <li><%= topic.title %></li>
        <% end %>
      </ul>
    </div>
  </div>

</div>

Now run the specs and you'll see an error we didn't expect ActionView::Template::Error: No route matches {:action=>"show", :controller=>"topics/posts", :id=>#<Post id: 33, title: "My Great Post", content: "Amazing content", user_id: 37, topic_id: 16, created_at: "2016-05-27 18:27:59", updated_at: "2016-05-27 18:27:59">, :topic_id=>nil} missing required keys: [:topic_id]. It looks like I made a mistake in the partial, I wrongly anticipated that there would also be a specific Topic to call for the link and I'm calling the @topic instance variable. Not to worry though, we have access to the Topic id for each post in the post itself. So let's update the partial:

<!-- app/views/shared/_posts.html.erb -->

<h2><%= link_to post.title, topic_post_path(topic_id: post.topic.slug, id: post) %></h2>

Now if we run all of the specs we'll see that everything is working. Please note that because we're using friendly_id we had to call the slug method in order to get the proper URL, if you call id it will break previous tests and will give some odd behavior in the application.

The homepage is looking great, one last item to add is to make the topics in the sidebar clickable and for them to link to their respective pages to show the posts nested underneath them. Let's create a test to make sure that each topic is linking to the correct page:

# spec/features/static_spec.rb

  describe 'links' do
    it 'has topics that link to their pages' do
      topic = FactoryGirl.create(:topic)
      visit root_path
      expect(page).to have_link(topic.title, href: topic_path(topic))
    end
  end

As expected this will fail, now let's implement the link code in the homepage template:

<!-- app/views/static/home.html.erb -->

        <% @topics.each do |topic| %>
          <li><%= link_to topic.title, topic_path(topic) %></li>
        <% end %>

Now if you run the specs you'll see all of our tests are passing and if you open the browser you'll see a very nice looking homepage with nice and shiny links to the Topic pages:

large

The only refactor I want to do before ending this guide will be in the static_spec, notice how we have a call to visit root_path in each spec? That's always a good indicator that we need to move that process into a before block, I've put the full spec below, but try to do it first before looking at the code.

# spec/features/static_spec.rb

require 'rails_helper'

describe 'homepage' do
  before do
    @topic = FactoryGirl.create(:topic)
    FactoryGirl.create(:second_topic)
    FactoryGirl.create(:post)
    FactoryGirl.create(:second_post)
    visit root_path
  end

  describe 'navigate' do
    it 'can be reached successfully' do
      expect(page.status_code).to eq(200)
    end
  end

  describe 'content' do
    it 'has a list of topics' do
      expect(page).to have_content("Sports")
      expect(page).to have_content("Coding")
    end

    it 'shows the most recent posts' do
      expect(page).to have_content("My Great Post")
      expect(page).to have_content("Another Guide")
    end
  end

  describe 'links' do
    it 'has topics that link to their pages' do
      expect(page).to have_link(@topic.title, href: topic_path(@topic))
    end
  end
end

Running the specs will now show that everything is still passing. Notice how we had to have the create calls before the visit method since those posts and topics had to be created prior to visiting the page or the tests would not have passed. This isn't a perfect test suite since we're creating a few items that we're not using in each block, but I'd prefer to err on the side of overkill at this stage and we can always come back to the spec and clean them up to improve the speed of the test suite later on.

Now that we have a great looking homepage, topic, and post views, the next step will be to implement a design for the Topic index page.

Resources