I'm working on implementing a SEO-hiarchy, which means that I need to prepend parameters for a show action.
The use-case is a search site where the URL-structure is:/cars/(:brand)/
=> a list page/cars/(:brand)/(:model_name)?s=query_params
=> a search action/cars/:brand/:model_name/:variant/:id
=> a car show action
My problem is to make the show action URLs work without having to provide :brand
, :model_name
and :variant
as individual arguments. They are always available from as values on the resource.
What I have:
/cars/19330-Audi-A4-3.0-TDI
What I want
/cars/Audi/A4/3.0-TDI/19330
Previously, this was how the routes.rb
looked like:
# Before
resources :cars. only: [:show] do
member do
get 'favourize'
get 'unfavourize'
end
Following was my first attempt:
# First attempt
scope '/cars/:brand/:model_name/:variant' do
match ":id" => 'cars_controller#show'
match ":car_id/favourize" => 'cars_controller#favourize', as: :favourize_car
match ":car_id/unfavourize" => 'cars_controller#unfavourize', as: :unfavourize_car
end
This makes it possible to do:cars_path(car, brand: car.brand, model_name: car.model_name, variant: car.variant)
But that is obviously not really ideal.
How is it possible to setup the routes (and perhaps the .to_param
method?) in a way that doesn't make it a tedious task to change all link_to
calls?
Thanks in advance!
-- UPDATE --
With @tharrisson's suggestion, this is what I tried:
# routes.rb
match '/:brand/:model_name/:variant/:id' => 'cars#show', as: :car
# car.rb
def to_param
# Replace all non-alphanumeric chars with - , then merge adjacent dashes into one
"#{brand}/#{model_name}/#{variant.downcase.gsub(/[^[:alnum:]]/,'-').gsub(/-{2,}/,'-')}/#{id}"
end
The route works fine, e.g. /cars/Audi/A4/3.0-TDI/19930
displays the correct page. Generating the link with to_param
, however, doesn't work. Example:
link_to "car link", car_path(@car)
#=> ActionView::Template::Error (No route matches {:controller=>"cars", :action=>"show", :locale=>"da", :brand=>#<Car id: 487143, (...)>})
link_to "car link 2", car_path(@car, brand: "Audi")
#=> ActionView::Template::Error (No route matches {:controller=>"cars", :action=>"show", :locale=>"da", :brand=>"Audi", :model_name=>#<Car id: 487143, (...)>})
Rails doesn't seem to know how to translate the to_param into a valid link.
I do not see any way to do this with Rails without tweaking either the URL recognition or the URL generation.
With your first attempt, you got the URL recognition working but not the generation. The solution I can see to make the generation working would be to override the car_path
helper method.
Another solution could be, like you did in the UPDATE, to override the to_param
method of Car. Notice that your problem is not in the to_param
method but in the route definition : you need to give :brand
,:model_name
and :variant
parameters when you want to generate the route. To deal with that, you may want to use a Wildcard segment in your route.
Finally you can also use the routing-filter gem which make you able to add logic before and after the url recognition / generation.
For me, it looks like all theses solutions are a bit heavy and not as easy as it should be but I believe this came from your need as you want to add some levels in the URL without strictly following the rails behavior which will give you URL like /brands/audi/models/A3/variants/19930
OK, so here's what I've got. This works in my little test case. Obviously some fixups needed, and I am sure could be more concise and elegant, but my motto is: "make it work, make it pretty, make it fast" :-)
In routes.rb
controller :cars do
match 'cars', :to => "cars#index"
match 'cars/:brand', :to => "cars#list_brand", :as => :brand
match 'cars/:brand/:model', :to => "cars#list_model_name", :as => :model_name
match 'cars/:brand/:model/:variant', :to => "cars#list_variant", :as => :variant
end
In the Car model
def to_param
"#{brand}/#{model_name}/#{variant}"
end
And obviously fragile and non-DRY, in cars_controller.rb
def index
@cars = Car.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: @cars }
end
end
def list_brand
@cars = Car.where("brand = ?", params[:brand])
respond_to do |format|
format.html { render :index }
end
end
def list_model_name
@cars = Car.where("brand = ? and model_name = ?", params[:brand], params[:model])
respond_to do |format|
format.html { render :index }
end
end
def list_variant
@cars = Car.where("brand = ? and model_name = ? and variant = ?", params[:brand], params[:model], params[:variant])
respond_to do |format|
format.html { render :index }
end
end
You just need to create two routes, one for recognition, one for generation.
Updated: use the routes in question.
# config/routes.rb
# this one is used for path generation
resources :cars, :only => [:index, :show] do
member do
get 'favourize'
get 'unfavourize'
end
end
# this one is used for path recognition
scope '/cars/:brand/:model_name/:variant' do
match ':id(/:action)' => 'cars#show', :via => :get
end
And customize to_param
# app/models/car.rb
require 'cgi'
class Car < ActiveRecord::Base
def to_param
parts = [brand,
model_name,
variant.downcase.gsub(/[^[:alnum:]]/,'-').gsub(/-{2,}/,'-'),
id]
parts.collect {|p| p.present? ? CGI.escape(p.to_s) : '-'}.join('/')
end
end
Sample of path helpers:
link_to 'Show', car_path(@car)
link_to 'Edit', edit_car_path(@car)
link_to 'Favourize', favourize_car_path(@car)
link_to 'Unfavourize', unfavourize_car_path(@car)
link_to 'Cars', cars_path
form_for(@car) # if resources :cars is not
# restricted to :index and :show
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With