homeASCIIcasts

221: Subdomains in Rails 3 

(view original Railscast)

Other translations: It Es

It has been nearly two years since the topic of subdomains was covered. Rails 3 introduces some added support for subdomains and we’ll look into what it offers in this episode.

The application we’ll be using is the same one used in the other episode on subdomains. It’s a simple blogging application that supports multiple blogs. Below is the homepage which lists all the blogs. Each blog has a set of articles associated with it.

Our blogging application.

If we click on the edit link for a blog we’ll see that each one has two attributes: a name and a subdomain. We want to use the subdomain attribute as a subdomain in the URL for that blog.

The edit page for a blog.

Configuring Subdomains In Development Mode

The first thing we need to set up is a way to manage the subdomains in development mode. Our application has the URL http://localhost:3000 which doesn’t allow for subdomains so we need to use an alternative method.

One way to do this would be to set up our application to use Passenger. We could then give it a domain name like http://blog.local and edit the /etc/hosts file to set up subdomains like this:

/etc/hosts

127.0.0.1 personal.blog.local company.blog.local

Ideally we’d like to use a wildcard character that would match any subdomain so that we don’t have to continually change the hosts file as we develop our application. Unfortunately we can’t do that so we’ll have to find a different solution.

One good solution is offered by Tim Pope on his blog. He has bought the domain name smackaho.st and made it a localhost wildcard. One of the people who commented on that blog posting has sone something similar with a shorter domain name, lvh.me, and we’re going to use that instead.

If we go to http://lvh.me:3000/ we’ll see the homepage of our application because lvh.me resolves to the IP address 127.0.0.1. The difference is that we can now prepend any subdomain we like to the URL and it will still point to the same application.

The blog is now available at a subdomain.

Now that we can use subdomains in our application we can configure its routes so that the personal subdomain will go to the Personal blog’s show action. Here’s what our application’s routes file currently looks like:

/config/routes.rb

Bloggit::Application.routes.draw do |map|
  resources :comments
  resources :articles
  resources :blogs
  root :to => "blogs#index"
end

Currently the root route goes to the blogs/index action. We only want that to be the case if no subdomain has been specified; where there is a subdomain the blogs/show action should be shown instead. In Rails 3 we can do this by adding a constraint.

/config/routes.rb

Bloggit::Application.routes.draw do |map|
  resources :comments
  resources :articles
  resources :blogs
  match '/' => 'blogs#show', :constraints => { :subdomain => /.+/ }
  root :to => "blogs#index"
end

In a Rails 2 application we would have needed to use a plugin to do this, but in Rails 3 this is built in. The :subdomain option takes either a string or a regular expression and here we’ve used a regular expression that matches at least one character so any subdomain will match the route. Note that it is important that the route root comes after the subdomain route. If not, the root route will catch the subdomain routes and the subdomain route will never be called. As a general rule of thumb the more specific a route is the higher up in the list it should be.

If we visit the personal subdomain now we’ll see an error, but that’s to be expected as we’re on the BlogsController’s show action and haven’t supplied an id.

The show page throws an error because the controller expects as ID.

This is easy to fix. We just need to modify the show action so that it finds a blog by its subdomain rather than by its id.

/app/controllers/blogs_controller.rb

def show
  @blog = Blog.find_by_subdomain!(request.subdomain)
end

When we reload the page now we’ll see the articles for our “Personal” blog.

The blog is now found by its subdomain.

If we remove the subdomain we’ll see the home page we had before.

With no subdomain the home page is shown.

There’s still a small problem with the routes, though. If we visit http://www.lvh.me:3000 then our application will look for a blog with the subdomain www when this should redirect to the blogs index page. We could try to fix this by doing something clever with the regular expression in the constraints in the routes file but in order to give us more flexibility and control over the subdomains we’ll do this in Ruby code instead. This is possible in Rails 3 by writing a class and moving the constraint code into there.

First we’ll modify the routes file so that it uses a new class called Subdomain. We do this by using a constraints method and passing the class as an argument.

/config/routes.rb

Bloggit::Application.routes.draw do |map|
  resources :comments
  resources :articles
  resources :blogs
  constraints(Subdomain) do
    match '/' => 'blogs#show'  
  end
  root :to => "blogs#index"
end

Next we’ll need to create that Subdomain class which will go in the /lib directory. This class will need to have a class method called matches? that takes a request object as a parameter. This request object is the same object that we have access to in our controllers and views so we can use the same methods on it that we use there. This method needs to return a boolean value whose value depends on whether the given route matches the request. In our case we want the method to return true if the request has a subdomain as long as that subdomain isn’t www so our class will look like this:

/lib/subdomain.rb

class Subdomain
  def self.matches?(request)
    request.subdomain.present? && request.subdomain != 'www'
  end
end

When we visit http://www.lvh.me:3000 now we’ll see the home page just as we want.

The subdomain www now shows the home page.

Fixing Links

Next we’ll fix the links to each blog on the home page so that they point to the correct subdomain instead of the normal show action for that blog as that will throw an error because the show action now expects a subdomain.

In the view code for the index action we have a standard link to each blog between h2 tags.

/app/views/blogs/index.html.erb

<% title "Blogs" %>
<% for blog in @blogs %>
  <div>
    <h2><%= link_to blog.name, blog %></h2>
    <div class="actions">
      <%= link_to "Edit", edit_blog_path(blog) %> | 
      <%= link_to "Destroy", blog, :confirm => 'Are you sure?', :method => :delete %>
    </div>
<% end %>

We’ll change this link so that it points to the root URL with the appropriate subdomain. Unfortunately we can’t pass a subdomain option to root_url like we can with subdomain_fu. Instead we have to create the entire host name from scratch and include the subdomain in it. The host name will be created from the blog’s subdomain and the current domain and port_string properties of the request object.

/app/views/blogs/index.html.erb

<h2><%= link_to blog.name, root_url(:host => blog.subdomain + '.' + request.domain + request.port_string) %></h2>

When we reload the home page now the links to the blogs will have the correct subdomain in them.

Cleaning Up The Code To Change The Subdomain

Everything is now working well but the code above that creates the link to the other subdomain could be a little cleaner, especially if we were going to make a lot of use of it. We’ll do this by moving it into a separate helper method that we’ll call with_subdomain and which takes the subdomain as an argument. We’ll change the code in the view first so that it calls the method we’re about to write.

/app/views/blogs/index.html.erb

<h2><%= link_to blog.name, root_url(:host => with_subdomain(blog.subdomain)) %></h2>

We’ll put the helper method inside its own module, in a file called url_helper.rb.

/app/helpers/url_helper.rb

module UrlHelper
  def with_subdomain(subdomain)
    subdomain = (subdomain || "")
    subdomain += "." unless subdomain.empty?
    [subdomain, request.domain, request.port_string].join
  end
end

The code in the module first sets the subdomain to an empty string if the value passed in is nil then appends a full stop to it if a non-empty subdomain has been supplied. Finally it joins the subdomain, domain and port_string and returns that value.

We’ll add the module to the ApplicationController too so that all of our application’s controllers can use the method.

/app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include UrlHelper
  protect_from_forgery
  layout 'application'
end

Even though we have cleaned up the code in the view quite a bit it could be even cleaner if we could just add a :subdomain option to the root_url method and this is possible if we override the url_for method. All we need to do is add the following method to our UrlHelper module.

/app/helpers/url_helper.rb

def url_for(options = nil)
  if options.kind_of?(Hash) && options.has_key?(:subdomain)
    options[:host] = with_subdomain(options.delete(:subdomain))
  end
  super
end

The overriding url_for method checks to see if the options hash has a key called :subdomain and, if so, sets the :host option to be the value of with_subdomain for that subdomain. Finally it calls super so that the method’s default code is also executed and the rest of the URL is generated properly. There is no need to use alias_method_chain here, just calling super is enough.

We can now update our view code to use the :subdomain option.

/app/views/blogs/index.html.erb

<h2><%= link_to blog.name, root_url(:subdomain => blog.subdomain) %></h2>

When we reload the index page and click one of the blog links the link still redirects to the correct URL with a subdomain.

The :subdomain option is now working.

It would be good to have a link on each blog’s page back to the index page and we can do this by calling root_url with a :subdomain value of false.

/app/views/blogs/show.html.erb

<p><%= link_to "All Blogs", root_url(:subdomain => false) %></p>

This will give us the link we want back to the index page.

Handling Different Top-Level Domains

One thing that we’ve not covered yet is how to handle domain names with more than two parts. So, while our subdomain code will work for .com domains it wouldn’t work for domain that ends with .co.uk. To make it work with a .co.uk or similar domain we’ll need to change our application everywhere it calls request.domain or request.subdomain so that we specify the number of dots that the domain name (without any subdomain) has. Rails assumes a default value of 1 but for domains like .co.uk we’ll need to set a value of 2.

We’ll have make changes to our application in two places: in the UrlHelper method and in the Subdomain class we use in our routing.

/app/helpers/url_helper.rb

def with_subdomain(subdomain)
  subdomain = (subdomain || "")
  subdomain += "." unless subdomain.empty?
  [subdomain, request.domain(2), request.port_string].join
end

/lib/subdomains.rb

class Subdomain
  def self.matches?(request)
    request.subdomain(2).present? && request.subdomain(2) != "www"
  end
end

Obviously we wouldn’t really want the value to be hard-coded. We’ll need to make it dynamic so that in development mode we can set a value of 1 (for a domain like lvh.me) while in development mode we use 2. This value could be set in an external configuration file that loads the environment correctly.

Cookies

There’s one final thing we’ll cover in this episode. If we look at our browser’s cookies and filter it by the domain name we’ve been using for this application we’ll see that a separate session is stored for each different subdomain we’ve visited. We don’t want this as it will mean that sessions aren’t shared across subdomains.

Each subdomain has a separate cookie.

A solution to this was presented in episode 123, but now in Rails 3 there is a better way to do it. In the application’s /config/initializers/session_store.rb we just need to append the :domain option to the Rails.application.config.session_store method, giving it a value of :all.

/config/initializers/sesion_store.rb

Rails.application.config.session_store :cookie_store, :key => '_bloggit_session', :domain => :all

This won’t work quite yet, however. The :domain option has been added after Rails 3.0 beta 4 which at the time of writing is the current release. We’ll need to run Edge Rails or wait for the release candidate before this will work. Once we’re using a version of Rails that supports this option then our application will be able to share sessions across subdomains. The :all option assumes that our application has a top-level domain size of 1. If not then we can specify a domain name instead and that will be used as the base domain for the session.

That’s it for this episode. Being able to handle subdomains without a plugin is a great addition to Rails 3 and will have many uses in applications.