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