Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom parameters in URL for show action

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.

like image 431
Jonas Bylov Avatar asked Mar 21 '12 14:03

Jonas Bylov


3 Answers

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

like image 132
Adrien Coquio Avatar answered Sep 20 '22 20:09

Adrien Coquio


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
like image 38
Tom Harrison Avatar answered Sep 18 '22 20:09

Tom Harrison


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
like image 43
Ian Yang Avatar answered Sep 16 '22 20:09

Ian Yang