Generate sluggish URLs
The idea is to generate pretty URLS i.e. /username/post-title
instead of /1/post/2
1.) Add slug
field to your table (as string) through a migration, for each table that has content in its columns you want to use as slugs (posts, categories, comments etc.)
class CreateSlugs < ActiveRecord::Migration
def change
add_column :posts, :slug, :string
add_column :categories, :slug, :string
add_column :users, :slug, :string
end
end
2.) In your model add a method
# /models/post.rb
#...
after_validation :generate_slug # keep business logic in mind (this changes the url when a title of a post is changed. to keep the sluggish url the same use "before_create" which is a different Active Record Callback, see them listed here: http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html)
#Which Active Record callback to use is a business, not a development decision.
#...
def generate_slug
self.slug = self.title.gsub(' ', '-').downcase # use regular expressions to take care of edge cases, like question marks etc. that can't be in urls (replace all non-alpha numerical characters with a dash)
#self.save - to save it to the database
# alternatively to "after_validation :generate_slug" on top of model use "self.save" here within the method
end
def to_param
self.slug # See HTML comment above for why we write this method
end
3.) In the controller
# post_controller.rb
def set_post
@post = Post.find_by slug: params[:id]
end
4.) Add reload
to show.html.erb (it worked without this in my case) for validation errors.
<h5>Comments:</h5>
<% @post.reload.comments.each do |comment| %> <!-- it was @post.comments.each do before -->
<%= render 'comments/comment', comment: comment %>
<% end %>
5.) Wherever I reference the id of a post/comment previously I now have to reference the slug, so
<span id="post_<%= post.slug %>_votes"> <%= post.total_votes %> votes </span>
<!-- this said "post.id" instead of "post.slug" before -->
6.) To generate slugs for entries done prior to slug set up, go into $ rails console
# $ rails console
Post.all.each { |post| post.save } # make sure you don't have validation errors for this to work.
Note
In your view you can add .to_param
to your link (not necesary, since .to_param is called indirectly)
<%= link_to ("#{post.comments.size} comments", post_path(post.to_param)) %>
<!-- We call .to_param explicitly here and create the to_param method in the model (see below), because normally calling .to_param would return the id of the object. In our case we want it to return the slug-->