11

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.

8
  • This is a really good and well-written question. I think I have an idea and want to make sure I am on the right path (no pun intended). Is it the case that you want to be able to use cars_path(@car) in your link_to's and have it generate the nice hierarchical URL from your views, but also have the router recognize "partial" URLs (e.g. /cars/Audi) as a request to list Audis? Or is it an additional requirement for the link_to to be contextually aware (e.g. so it creates links to the /cars/:brand variant from some controller actions, but the full car URL from other controller actions? Commented Mar 21, 2012 at 14:34
  • Thanks, @tharrison. I think you got it right; I want to be able to use cars_path(@car) in my link_to. What action is being triggered can be controlled by constraints in the routes.rb. This means that urls for cars/Audi does not use cars_path but something like brand_path(brand: "Audi"). Commented Mar 21, 2012 at 14:56
  • OK, getting there, but I don't quite have it then :-). If you want to use named routes like brand_path, the argument would be an instance of the brand model ... although that could be like brand_path(@car.brand). Do you have a brand model and resources in the routes for that? Commented Mar 21, 2012 at 15:22
  • No, there is no model for "brand", "model_name" and "variant". It may seem counterintuitive, but it makes a lot of sense :) Besides the Car object, it's just strings. Commented Mar 21, 2012 at 15:27
  • Got it -- no, that makes sense (at least for model_name and variants, you might want to think about Brand just to ensure consistency of naming, etc.), just wanted to be sure :-) OK, now I'll see if I can figure it out :-) Commented Mar 21, 2012 at 15:31

4 Answers 4

3
+25

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

Sign up to request clarification or add additional context in comments.

Comments

1

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

3 Comments

I'm sorry @tharrison, but I think you got it wrong. The issue is not displaying brand, model_name or variant pages. That stuff already works. The problem is linking directly to one single car record. For this to work, I have to use the car ID, and additionally, I want to add brand, model_name and variant to the URL (for SEO purposes). So what I want is: link_to cars_path(@car) to point to /cars/:brand/:model_name/:variant/:id. Does that make sense?
Jonas, I think the code I provided is a millimeter away from what you want. Add or modify the match clauses as needed by adding the :id. The ':as => :foo` argument is what allows you to reference the URL as foo_path, and the to_param override is what produces the pretty path -- format it however you like and add id, or whatever other attributes of Car you like in order to get the nice URL. Right?
Maybe I wasn't clear enough. The routing works perfect the way you suggested. The problem is that Rails cannot translate the implementation of the to_param into a link. I've updated my question with a simple example.
1

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

Comments

0

You want bounded parameters to be passed to url of which some parameters are optional and some of them strictly needs to be present.

Rails guides shows you can have strict as well as optional parameters and also you can give name to particular route in-order to simplify its usage.

Guide on rails routing bound parameters

Example usage -

In below route,

  • brand is optional parameter as its surrounded by circular bracket
  • Also please note there can be optional parameters inside route but they needs to added at last /cars(/:brand)(/:make)(/:model)

    match '/cars/(:brand)', :to => 'cars#index', :as => cars

here cars_url will map to index action of cars controller.. again cars_url("Totoya") will route index action of cars controller along-with params[:brand] as Toyota

Show url route can be as below where id is mandatory and others can be optional

match '/cars/:id(/:brand(/:model_name/)(/:variant)', :to => "cars#show", :as => car

In above case, id is mandatory field. Other parameters are optional. so you can access it like car_url(car.id) or car_url(12, 'toyota') or car_url(12, 'toyota', 'fortuner') or car_url(12, 'toyota', 'fortuner', 'something else)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.