How to write a blog in Rails 2.0.2 (not quite in 15 minutes)

Like many people, the “Creating a weblog in 15 minutes” screencast by DHH is what convinced me to try Rails. Since I have never found a version of this tutorial that works for Rails 2, I decided to roll my own; here’s my take on it. It will probably take a bit longer than 15 minutes, but I will try to include some useful advice that makes it worth the extra time. This tutorial will not use any scaffolding and I will try to explain all of the code where necessary. Also, note that I use Windows; the output might look different on your end (but not a lot).

This tutorial assumes a very basic knowledge of the Model View Controller pattern and Object Orientation.

Right, let’s get to it then! I’ll assume you have Rails 2.0.2 installed, with the proper gems (sqlite3-ruby for me, use another DB adapter at your peril; you can always change this later).

Setting up the project

Let’s start by creating a project using the default settings. This will create the project using SQLite databases,

D:\rails> rails blog
D:\rails> cd blog
D:\rails\blog> rake rails:freeze:gems

What this last step will do is unpack (“freeze”) the Rails gems into your vendor/ directory. I like to do this to make sure Rails doesn’t accidentally change while working on a project, and vendoring everything is in my opinion generally just a good idea, to make sure everybody on your project team works with the same packages.

That’s all there is to setting up the project. On to the next step, posts!

Let’s start posting

Any blog is centered around blog posts, ours is no exception. So let’s start laying the foundations for that. We’ll use a RESTful design for this, to keep things simple. To start out, we generate a resource for the Post model. It will need to have a title and a body, so we’ll add those in right away:

D:\rails\blog> ruby script/generate resource post title:string body:text

Look at all the files that created. We get a model and controller for our Post resource, a unit test stub and a migration. New is the route that was created at the bottom. Let’s look at config/routes.rb a bit. All that has been added by this generator script is the map.resources :posts call near the top. This little command generates a whole bunch of convention-magic for us: default routes for posts. More on that later.

We’ll also need to create a posts table in our database. Thankfully, due to the way we called our resource generator script, a migration has been made for us. Let’s look at it:

# db/migrate/001_create_posts.rb
class CreatePosts < ActiveRecord::Migration
  def self.up
    create_table :posts do |t|
      t.string :title
      t.text :body

      t.timestamps
    end
  end

  def self.down
    drop_table :posts
  end
end

&#91;/sourcecode&#93;

We won't have to edit anything here! By adding the fields to our generator call we have a fully working migration, so all we need to do is to run it:
<pre>rake db:migrate</pre>
Now that we have the database set up, let's start creating our application. I like to start by setting up my models with basic validation and any relations they might have to other models. As we have no other models right now, we'll skip the relations bit. Open up your Post model; we'll want to make sure every post has a title and a body, and that the title does not already exist for another Post:


# app/models/post.rb
class Post < ActiveRecord::Base
  validates_presence_of :title, :body
  validates_uniqueness_of :title
end
&#91;/sourcecode&#93;

By just adding these two simple lines, we'll make sure that for every Post we save, checks will be made to ensure it has a title and body and that the title is unique. Time to put this to use in our blog!

We'll need a way to write posts in our blog. For this we need two actions in our Post controller, by REST convention named <strong>new</strong> (for the form) and <strong>create</strong> (to actually create the Post). Let's start with the <strong>new</strong> action.


# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def new
    @post = Post.new
  end
end
&#91;/sourcecode&#93;

In order to be able to take advantage of Rails' <a title="FormHelper" href="http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html">FormHelper</a>, we'll need an empty Post object to work with. That's all this <strong>new</strong> method will be doing for now.

Now would probably be a good time to start our webserver so we can start looking at the produced work. I'm using WEBrick, but if you have Mongrel installed it will use that - don't worry, they will produce the same pages.
<pre>D:\rails\blog&gt; ruby script/server</pre>
Now point your webbrowser to <a title="Your very own blog!" href="http://localhost:3000/posts/new">http://localhost:3000/posts/new</a>. You'll be greeted by a friendly error message, telling you your view is missing. Let's fix that now, create a new file called <em>new.html.erb</em> in your <em>app/views/posts</em> directory.

<a href="https://skionrails.files.wordpress.com/2008/05/001_template_missing.png"><img class="alignnone size-medium wp-image-14" style="border:1px solid black;" src="https://skionrails.files.wordpress.com/2008/05/001_template_missing.png?w=300" alt="We\'ll need to create this template.." width="300" height="229" /></a>


<%-
# app/views/posts/new.html.erb
-%>

<h2>Write a new blog post</h2>

<%= error_messages_for 'post' %>

<% form_for @post do |post_form| %>
  <p>
    <strong>Title:</strong>
    <%= post_form.text_field :title %>
  </p>
  <p>
    <strong>Body:</strong><br />
    <%= post_form.text_area :body %>
  </p>

<%= post_form.submit 'Save post' %>
<% end %>

That’s that. By using the form_for call, we’ll have created a FormHelper object named post_form which we pass a block in which it exists. Doing this, we have access to convenient methods like post_form.text_field, which will automatically create a text field with correct settings for saving this Post later. It will also know the correct location to submit to because ActiveRecord tells the helper that @post is a new object.

The new post form

Submitting now won’t do anything, so we’ll need to implement that. By REST convention, saving new objects happens in the create action, so let’s implement that in our controller now.

# app/controllers/posts_controller.rb
# (just the create action)
  def create
    @post = Post.new(params[:post])

    if @post.save
      flash[:notice] = 'Your post was saved successfully.'
      redirect_to @post
    else
      render :action => 'new'
    end
  end

Let’s go through that step by step.

  • On line #4, we create a new Post object, instantiated by the parameters sent to us from the form. Because we used the FormHelper in the new action, this is all we need to do to create the Post.
  • On line #6, we then attempt to save the Post. The save method returns true if saving succeeded (and the object was valid) or false if it did not. This way we can use it as a condition.
  • On line #7, we set the flash message. This message will be saved in the session during the next request, after which it is discarded. We will display this later.
  • On line #8, we redirect to @post. More about that in a second.
  • If saving the post failed, we render the new action again.

This last step will re-render the form. Because @post is now set to the Post submitted by the form, the previously entered values will still be available. Also, any validation errors will be displayed at the top of the page by our call to error_messages_on.

On line #8, we’re redirecting to @post. How does Rails know where to redirect to? Now the map.resources call from config/routes.rb comes into play. Because we’re sticking to the conventions, it will know that to display a Post, it will need to call the show action. Thus when saving the Post, you will be redirected to http://localhost/posts/1 (if you have any validation errors, you’ll see something like this). Let’s implement that action and its corresponding view now.

We can\'t display an action that doesn\'t exist..

Showing the post

The controller action is simple again:


# app/controllers/posts_controller.rb
# (just the show action)

  def show
    @post = Post.find(params[:id])
  end

All we need to do is fetch the correct Post into an instance variable. The view is slightly more involved, but only contains HTML with a few substitutions:

<%-
# app/views/posts/show.html.erb
-%>

<h2><%= @post.title %></h2>
Posted at: <%= @post.created_at %><br />
<%= @post.body %>

Refresh your browser and be greeted with your very first post. But isn’t something missing? Don’t posts need an author? I think they do.

Your very first blog posting

Adding authors

We’ll need to update the database table, so let’s create a migration to do that:

D:\rails\blog>
    ruby script/generate migration add_author_to_post author:string

This will generate a migration to add the author column to the posts table:


# db/migrate/002_add_author_to_post.rb
class AddAuthorToPost < ActiveRecord::Migration
  def self.up
    add_column :posts, :author, :string, :default => 'Anonymous'
  end

  def self.down
    remove_column :posts, :author
  end
end

Most of this has been automatically generated through yet another convention. By naming our migration like this, Rails has assumed that we want to add a column called author to our Post model (read about this by running the migration generator without any arguments). We’ll add a default value for this column to make sure any already existing Posts will get an author. Let’s run this migration:

D:\rails\blog> rake db:migrate

We’ll also need to update two views, which is pretty straightforward:

<%-
# app/views/posts/new.html.erb
-%>

<h2>Write a new blog post</h2>

<%= error_messages_for 'post' %>

<% form_for @post do |post_form| %>
  <p>
    <strong>Author:</strong>
    <%= post_form.text_field :author %>
  </p>
  <p>
    <strong>Title:</strong>
    <%= post_form.text_field :title %>
  </p>
  <p>
    <strong>Body:</strong><br />
    <%= post_form.text_area :body %>
  </p>
  <%= post_form.submit 'Save post' %>
<% end %>

We can now add an author

<%-
# app/views/posts/show.html.erb
-%> 

<h2><%= @post.title %></h2>

Posted at: <%= @post.created_at %> by <strong><%= @post.author %></strong><br />
<%= @post.body %>

The authors get displayed, too

Right, that’s that; we can now add authors to our posts and they will be displayed in the show action. Now we will want to see an overview of all posts…

Writing the post index

By REST convention, an overview of all instances of a class is shown in the index action.

# app/controllers/posts_controller.rb
# (just the index action)

  def index
    @posts = Post.find(:all, :order => 'created_at DESC')
  end

All we do in this action is fetch a list of all the posts in our blog. Note how we use the :condition parameter to find in order to reverse the posts.

Next we’ll need a view for this action. What this view will do is loop through the @posts variable and display each Post in turn. But wait, don’t we already have the code in place to display a Post? We would be repeating ourselves if we wrote it again, and we don’t want to do that. So how do we go about extracting the display code into a common file? Rails has a feature called partials for that. Partials are small templates which you can reuse in several views. We’ll move some of the code from the show action into a partial called _post.html.erb. Partial names always start with an underscore.

<%-
# app/views/posts/_post.html.erb
-%>

<h2><%= link_to_unless_current post.title, post %></h2>
Posted at: <%= post.created_at %> by <strong><%= post.author %></strong><br />
<%= post.body %>
<hr />

I have made three alterations. First I have added a horizontal line at the bottom so that the posts will be separated from each other on the index page and from their (soon to be implemented) comments on the show page. Second, I changed the normal display of the title to a conditional link. This way, the title will display as a link unless it’s already on the page we’re linking to; an easy shortcut to make sure clicking the title on the index page links you to the show page of the post. Note how we only use post as the target of the link. By yet another convention in REST, Rails will know what page to link to with just this.

Thirdly, I have replaced @post with post. The latter is a local variable, only available within the partial. By default, Rails will try to send any object you have with the same name as the partial to the partial as a local. This way, our call to actually the render in the show view becomes really simple:

<%-
# app/views/posts/show.html.erb
-%>

<%= render :partial => 'post' %>

Refresh your first Post and see that it will look the same (apart from the horizontal line at the bottom). As you can see, by rendering a partial called post Rails will automatically send @post to the partial. Convention over configuration at its best :)

Excellent, now let’s get writing the index view. We’ll keep it simple, just a list of all the posts in reverse order.

<%-
# app/views/posts/index.html.erb
-%>

<%= render :partial => 'post', :collection => @posts %>

By providing a collection, this small bit of code will render the partial for each of the elements in the collection.

Now that we have an index page, let’s have the root of our site display it. We’ll need to connect the root to our posts controller, and delete the default index page:

# config/routes.rb (excerpt!)

map.root :controller => 'posts'

This will tell Rails to show the posts controller when the root of the site is requested. As the index action is the default action to call, we don’t need to specify that. Now all that’s left is to remove the default index page:

D:\rails\blog> del public/index.html

Point your browser to http://localhost:3000/ and view your beautiful weblog!

The posts index

Adding layout

Actually, it’s not that beautiful is it? Perhaps we can add some basic layout to it. We’ll add a layout that all pages share, so that we won’t have to repeat it for every page. Layouts are placed in /app/views/layouts and have the same name as their controller (although you can of course override this). I’m more of a programmer than a graphics artist, so bear with me; we’ll use a very simple layout that adds a page title and changes the fonts a bit.

<%-
# app/views/layouts/posts.html.erb
-%>

<html>
  <head>
    <title>My blog</title>
    <style type="text/css">
      body { font-family: Georgia, serif; }
      a { color: blue; }
    </style>
  </head>
  <body>
    <h1>My blog</h1>
    <% if flash&#91;:notice&#93; %>
      <div style="background-color: #c0c0c0">
        <%= flash&#91;:notice&#93; %>
      </div>
    <% end %>
    <%= yield %>
  </body>
</html>

Most of this is basic HTML, but the important part is on lines #15 through #19 and #20. In lines #15 to #19, we check whether a special variable called flash[:notice] exists, as we talked about before. If it exists, we display it (try writing a new post now, you’ll see the message). Next on line #20 we call yield, which will insert the original content of the view we’re looking at. Try it; refresh the index view and you’ll see a page with a title and some layout added. Other than that, the behavior is not changed.

Ain\'t that pretty?

Comments

Earlier on, I mentioned comments. Any blog will need to have these, else how are you going to find out what your peers think of your posts? First we’ll generate a resource (note that this should all go on one line):

D:\rails\blog> ruby script/generate resource comment
  post_id:integer author:string body:text

And migrate to create the comments table:

D:\rails\blog> rake db:migrate

I have added a post_id column because we will be using this to refer to which post a comment belongs to. This generator will have changed our /config/routes.rb file, so we can start implementing the model. We will add some validation and declare the relationship comments have to posts:

# app/models/comment.rb
class Comment < ActiveRecord::Base
  belongs_to :post
  validates_presence_of :post, :author, :body
end
&#91;/sourcecode&#93;

On line #3 we declare that every comment belongs to a post. By doing this, we get a whole bunch of convenient methods in order to manipulate comments. We'll also modify our Post model to add a similar relation:

&#91;sourcecode language='ruby'&#93;
# app/models/post.rb
class Post < ActiveRecord::Base
  validates_presence_of :title, :body
  validates_uniqueness_of :title
  has_many :comments
end
&#91;/sourcecode&#93;

Now Rails will know that every Post has zero or more comments to it. We'll use this information to display the comments later. Let's get to writing them first. A good place for a form would be below the post on the <strong>show </strong>page, wouldn't you agree? We'll need a comment to work with, so first we'll update the <strong>show </strong>action in the posts controller:


# app/controllers/posts_controller.rb
# (just the show action)
  def show
    @post = Post.find(params[:id])
    @comment = Comment.new
  end

Just like we did in the new action for creating a new post, now we have a variable called @comment to use in our form helper. Let’s add that form now then, and while we’re at it we’ll show all current comments as well:


<%-
#  app/views/posts/show.html.erb
-%>

<%= render :partial => 'post' %>

<% unless @post.comments.blank? %>
  <h2>Comments</h2>
  <% @post.comments.each do |comment| %>
    By <strong><%= comment.author %></strong> on <%= comment.created_at %>:<br />
    <%= comment.body %>
    <hr />
  <% end %>
<% end %>

<h2>Add a comment</h2>
<% form_for @comment do |comment_form| %>
  <%= hidden_field_tag :post_id, @post.id %>
  <p>
    <strong>Author:</strong>
    <%= comment_form.text_field :author %>
  </p>
  <p>
    <strong>Body:</strong><br />
    <%= comment_form.text_area :body %>
  </p>
  <%= comment_form.submit 'Save post' %>
<% end %>

The comments form

On lines #7 through #14, we display the current comments by looping through @post.comments. We get this collection “for free” by telling Rails that a Post has_many comments. Also note that if this collection is empty, we completely omit this part (including the heading).

After that comes the form for adding a new Comment. Note that there is a hidden_field_tag on line #18. We need to make sure that the action we submit to knows which post we’re writing a comment to. Let’s go and write that action now. As with creating a new post, this action is called create:

# app/controllers/comments_controller.rb
class CommentsController < ApplicationController def create @post = Post.find(params[:post_id]) @comment = Comment.new(params[:comment]) @comment.post = @post if @comment.save flash[:notice] = 'Thanks for your comment!' else flash[:notice] = 'Something went wrong while adding your comment' end redirect_to @post end end [/sourcecode] This is more or less the same as when saving a new Post, except that we're also first finding the correct Post to go with this comment and associating it with the Comment (on line #6). Here's what happens when you submit a comment: Added the first comment

Also note the flash, right under the title. This status message will be shown only once, refresh and it’s gone.

Where to go from here?

We’ve just made a really simple blog. It lacks quite a few features before it will be usable, but I hope I have explained some Rails basics here. Some things you can try as an exercise are:

  • making Posts editable (hint: the action to display an edit form is by default called edit, and for saving it the action is called update)
  • adding the number of Comments for a Post below each Post in the index view
  • adding delete buttons next to Comments, so that Comments can be moderated
  • the dates that are displayed right now don’t look too good; try to properly format them
  • adding a “back” button on the show page, to return to the index
  • use RedCloth to add Textile markup to your pages, or BlueCloth to add Markdown markup
  • .. many more things

Finally, you can download a zipped up version of this project if you don’t feel like copy/pasting all the code.

Credits

Thanks go out to mokogobo, celgin and mcarter from #rubyonrails on Freenode for proofreading this tutorial and helping me remove (hopefully all of) the errors.

20 Responses to How to write a blog in Rails 2.0.2 (not quite in 15 minutes)

  1. […] How to write a blog in Rails 2.0.2 (not quite in 15 minutes) […]

  2. Shaho says:

    Fantastic! go ahead with such these tutorials.

  3. mcarter says:

    Great tutorial!

  4. Eric Bodden says:

    This was really helpful, especially considering that the video is so shamefully outdated by now. Thanks a lot!!!

  5. slothbear says:

    Many many thanks! I have struggled through some of the not-quite-2.0 tutorials, this one was No Sweat!

    I loved how you used the sqlite default, rather than include pages on how to change to MySql, pointless for a tutorial. Freezing rails was a little heavy for a tutorial, but you kept it short.

    One 2.0 change you might consider, I learned in Ryan Bates Railscast #80, Simplify Views with Rails 2.0, http://railscasts.com/episodes/80

    You can skip the :collection in render :partial. Just pass the array and it does the right thing.
    @posts %>

  6. slothbear says:

    ACK! I should know enough to escape ERB code. The simplified render :partial looks like this:
    <%= render :partial => @products %>

  7. Olafski says:

    Good point :) I’ll keep that in mind, bit too much to change right now (I’d need to update the source zip and stuff :$)

    Thanks :) If there’s anything else you’d like a tutorial on feel free to request something :}

  8. VERY GOOD. Keep posting things like this!

  9. And Olaf didn’t even say “whoops”. Trust me, way better than DHH’s screencast. ;-)

  10. e_corona says:

    Excellent tutorial, thanks much. One question though: how did you get to list the comments from “last to first”. That is, listing the 2nd post first? In the original screencast DHH used a “@array.reverse” method but I’m not sure where this goes in this case? Thanks!

  11. e_corona says:

    Never mind! Got it. Thanks again.

  12. Gearoid says:

    Sweet tutorial, has anybody expanded on it? like added admin log in and a search feature…

  13. sandipransing says:

    Good Work !

  14. sandrar says:

    Hi! I was surfing and found your blog post… nice! I love your blog. :) Cheers! Sandra. R.

  15. Michael says:

    When I submit comments via this method, I get a flash[:notice] telling me that something didn’t go right, but I don’t get the validation error message/functionality as part of the response. What happened there?

  16. Nicholas says:

    hi i am not able to save the post id in comments.
    it returns null, what shd i do?

    Parameters: {“comment”=>{“body”=>”working wit posth”}, “commit”=>”Save post”, “authenticity_token”=>”WHFjLxlgHMVu9Lxz8B0k5sBxxuzQgBvV6Ho4JTk0nf8=”, “post_id”=>”11”}
    Post Load (1.8ms) SELECT * FROM “posts” WHERE (“posts”.”id” = 11)
    SQL (0.2ms) BEGIN
    SQL (1.5ms) INSERT INTO “comments” (“created_at”, “body”, “updated_at”, “post_id”) VALUES(‘2010-03-29 09:25:11.476271′, E’working wit posth’, ‘2010-03-29 09:25:11.476271’, NULL) RETURNING “id”
    SQL (2.0ms) COMMIT

    • Olafski says:

      Nicholas, as far as I can remember you should not include an ID in your insert query, since that ID will be automatically generated. It has been a while since I used either Rails or SQLite so it could be that SQLite does not support auto-increment columns, but that query looks alright to me.

      [edit]
      I should also note that this tutorial was written for a rather old version of Rails by now, perhaps you should try to find one that is for a more recent version =) You could of course get the specific version of Rails I used in this tutorial but that might teach you some things that are considered bad practice by now.

  17. rah00l says:

    Good job …..!
    It’s really nice blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: