Pretty (dated) RESTful URLs in Rails
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.
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?
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).
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.
精彩评论