Monday 28 October 2013

Using an Object's Slug as the Subdomain in Rails 3

Recently I had the problem of allowing each user in my system to have their own subdomain. There are various blog posts about subdomains in Rails, but none seem to really handle this requirement.

The way I did it was to leave my routes file intact, and use Rack middleware to rewrite urls to match the routes (this also has the advantage of not breaking existing urls).

I used the rack-rewrite gem (https://github.com/jtrupiano/rack-rewrite) to rewrite the urls as follows

application.rb
 config.middleware.insert_before(Rack::Lock, Rack::Rewrite) do  
    rewrite /.*/,  
     Proc.new { |path, rack_env|  
      slug = rack_env['SERVER_NAME'].split(".")[0]  
      "/users/#{slug}#{path}"  
     },  
     :if => Proc.new {|rack_env|   
      rack_env["HTTP_ACCEPT"] =~ /(text\/html|application\/json)/ && !(rack_env['SERVER_NAME'] =~ /www\./i)  
     }  
   end  

Then I had to make sure all of the urls point to the correct domain and path. For this, I overwrote the url_for method as follows

 def with_subdomain(subdomain)  
   subdomain = (subdomain || "")  
   subdomain += "." unless subdomain.empty?  
   [subdomain, request.domain, request.port_string].join  
  end  
  def url_for(options = nil)  
   if options.is_a?(User)   
    return "http://#{with_subdomain(options.slug)}"  
   end  
   if options.kind_of?(Hash)  
    options[:only_path] = false  
    options[:port] = nil  
    if options[:_positional_args] && user = options[:_positional_args].find {|pa| pa.is_a?(User)}  
     options[:host] = with_subdomain(user.slug)  
     return super(options).gsub(/\/users\/\d+/, '')  
    end  
    options[:host] = with_subdomain("www")  
   end  
   super  
  end