Pretty (dated) RESTful URLs in Rails
-
20-09-2019 - |
Question
I'd like my website to have URLs looking like this:
example.com/2010/02/my-first-post
I have my Post
model with slug
field ('my-first-post') and published_on
field (from which we will deduct the year and month parts in the url).
I want my Post
model to be RESTful, so things like url_for(@post)
work like they should, ie: it should generate the aforementioned url.
Is there a way to do this? I know you need to override to_param
and have map.resources :posts
with :requirements
option set, but I cannot get it all to work.
I have it almost done, I'm 90% there. Using resource_hacks plugin I can achieve this:
map.resources :posts, :member_path => '/:year/:month/:slug',
:member_path_requirements => {:year => /[\d]{4}/, :month => /[\d]{2}/, :slug => /[a-z0-9\-]+/}
rake routes
(...)
post GET /:year/:month/:slug(.:format) {:controller=>"posts", :action=>"show"}
and in the view:
<%= link_to 'post', post_path(:slug => @post.slug, :year => '2010', :month => '02') %>
generates proper example.com/2010/02/my-first-post
link.
I would like this to work too:
<%= link_to 'post', post_path(@post) %>
But it needs overriding the to_param
method in the model. Should be fairly easy, except for the fact, that to_param
must return String
, not Hash
as I'd like it.
class Post < ActiveRecord::Base
def to_param
{:slug => 'my-first-post', :year => '2010', :month => '02'}
end
end
Results in can't convert Hash into String
error.
This seems to be ignored:
def to_param
'2010/02/my-first-post'
end
as it results in error: post_url failed to generate from {:action=>"show", :year=>#<Post id: 1, title: (...)
(it wrongly assigns @post object to the :year key). I'm kind of clueless at how to hack it.
Solution 2
It's still a hack, but the following works:
In application_controller.rb
:
def url_for(options = {})
if options[:year].class.to_s == 'Post'
post = options[:year]
options[:year] = post.year
options[:month] = post.month
options[:slug] = post.slug
end
super(options)
end
And the following will work (both in Rails 2.3.x and 3.0.0):
url_for(@post)
post_path(@post)
link_to @post.title, @post
etc.
This is the answer from some nice soul for a similar question of mine, url_for of a custom RESTful resource (composite key; not just id).
OTHER TIPS
Pretty URLs for Rails 3.x and Rails 2.x without the need for any external plugin, but with a little hack, unfortunately.
routes.rb
map.resources :posts, :except => [:show]
map.post '/:year/:month/:slug', :controller => :posts, :action => :show, :year => /\d{4}/, :month => /\d{2}/, :slug => /[a-z0-9\-]+/
application_controller.rb
def default_url_options(options = {})
# resource hack so that url_for(@post) works like it should
if options[:controller] == 'posts' && options[:action] == 'show'
options[:year] = @post.year
options[:month] = @post.month
end
options
end
post.rb
def to_param # optional
slug
end
def year
published_on.year
end
def month
published_on.strftime('%m')
end
view
<%= link_to 'post', @post %>
Note, for Rails 3.x you might want to use this route definition:
resources :posts
match '/:year/:month/:slug', :to => "posts#show", :as => :post, :year => /\d{4}/, :month => /\d{2}/, :slug => /[a-z0-9\-]+/
Is there any badge for answering your own question? ;)
Btw: the routing_test file is a good place to see what you can do with Rails routing.
Update: Using default_url_options
is a dead end. The posted solution works only when there is @post
variable defined in the controller. If there is, for example, @posts
variable with Array of posts, we are out of luck (becase default_url_options
doesn't have access to view variables, like p
in @posts.each do |p|
.
So this is still an open problem. Somebody help?
Ryan Bates talked about it in his screen cast "how to add custom routes, make some parameters optional, and add requirements for other parameters." http://railscasts.com/episodes/70-custom-routes
This might be helpful. You can define a default_url_options
method in your ApplicationController
that receives a Hash of options that were passed to the url helper and returns a Hash of additional options that you want to use for those urls.
If a post is given as a parameter to post_path
, it will be assigned to the first (unnassigned) parameter of the route. Haven't tested it, but it might work:
def default_url_options(options = {})
if options[:controller] == "posts" && options[:year].is_a?Post
post = options[:year]
{
:year => post.created_at.year,
:month => post.created_at.month,
:slug => post.slug
}
else
{}
end
end
I'm in the similar situation, where a post has a language parameter and slug parameter. Writing post_path(@post)
sends this hash to the default_url_options
method:
{:language=>#<Post id: 1, ...>, :controller=>"posts", :action=>"show"}
UPDATE: There's a problem that you can't override url parameters from that method. The parameters passed to the url helper take precedence. So you could do something like:
post_path(:slug => @post)
and:
def default_url_options(options = {})
if options[:controller] == "posts" && options[:slug].is_a?Post
{
:year => options[:slug].created_at.year,
:month => options[:slug].created_at.month
}
else
{}
end
end
This would work if Post.to_param
returned the slug. You would only need to add the year and month to the hash.
You could just save yourself the stress and use friendly_id. Its awesome, does the job and you could look at a screencast by Ryan Bates to get started.