#123 Subdomains (revised)
- Download:
- source codeProject Files in Zip (86.1 KB)
- mp4Full Size H.264 Video (15.9 MB)
- m4vSmaller H.264 Video (8.29 MB)
- webmFull Size VP8 Video (10.7 MB)
- ogvFull Size Theora Video (18.4 MB)
Below is a page from a blogging application. Unlike most Rails blogging apps this one supports multiple blogs and its home page shows a list of them.
Our application has three blogs, one each for Pirates, Ninjas and Robots. If we click one of the links we’ll see a list of that blog’s articles.
Each blog’s URL has a path in the form /blogs/n
, where n
is the internal id
. We’d like to clean up these URLs so that instead of using a path to reference a blog we use a subdomain. The Blog
model already has a subdomain field so we already have a name we can use for each blog’s subdomain.
There are a couple of problems with trying this while our application is in development, the most prominent being that if we add a subdomain to localhost
it won’t point to our Rails application, so we can’t use, say, http://pirates.localhost:3000/
while our application is in development. One quick solution is to use an external domain that points to 127.0.0.1
such as lvh.me
. We can access our application at http://lvh.me:3000
and subdomains will work too so we can also use, say, http://pirates.lvh.me:3000
.
As we’re running OS X another solution is to use the Pow server. This only takes a few commands to set up and once it is we can browse our application at http://blogs.dev
. Subdomains work with Pow too so we can also use http://pirates.blogs.dev
.
Routing Subdomains Point to the Right Page
If we visit http://pirates.blogs.dev
now we’ll see the same page we get if we visit the root URL but we can change this by modifying our application’s routes. We need to add a route that points to the BlogsController
’s show
action when we visit the root URL of a subdomain.
Blogs::Application.routes.draw do resources :blogs resources :articles match '', to: 'blogs#show', constraints: {subdomain: /.+/} root to: 'blogs#index' end
We do that by matching a blank URL, which will be a root URL, and pointing it to the blogs#show
action but only if a subdomain is present. We do that by adding a constraint and specifying the subdomain
option, setting it to a regular expression that checks that the subdomain is one or more characters long. It’s important that this route is placed above the root route in the file; if it isn’t the root route will take precedence and always be triggered. More specific routes should always appear higher up in the routes file.
We can try this out now and see if it works. Visiting the root URL still works but if we visit a subdomain while we’re taken to the correct action we get an exception as the show action expects an id parameter to fetch the blog by.
def show @blog = Blog.find(params[:id]) end
We should instead fetch a blog by its subdomain
.
def show @blog = Blog.find_by_subdomain!(request.subdomain) end
If we try visiting a subdomain now it will work and we’re taken to the correct blog.
If we don’t use a subdomain we’ll still be taken to the list of blogs so it looks like our code is working correctly.
Fixing The Links
The next thing we have to do is fix the links on the home page so that they point to the appropriate subdomain. If we click on of the links now we’ll be taken to, say, e.g. http://blogs.dev/blogs/1
instead of the correct subdomain. To fix this we need to change the link in the index view template. Currently we pass in the blog model directly to link_to
.
<%= link_to blog.name, blog %>
We’ll replace this with a root_url
with the subdomain
option set.
<%= link_to blog.name, root_url(subdomain: blog.subdomain) %>
This subdomain
option is new to Rails 3.1. If you’re using an earlier version of Rails then you should take a look at episode 221 to see how to do this. If we reload the home page now and click one of the links we’ll be taken to the correct subdomain.
Scoping Resources
When we’re working with multiple subdomains like this we often want to nest other resources so that they’re only accessible through the right subdomain. For example our first article about pirates has an id
of 1
and its URL is http://pirates.blogs.dev/articles/1
. If we know the id
for one of the other blogs’ articles we can still visit it through the pirates
subdomain.
To fix this we need to modify the show action so that instead of just fetching an Article
globally we fetch it through its blog.
class ArticlesController < ApplicationController def show @blog = Blog.find_by_subdomain(!request.subdomain) @article = @blog.articles.find(params[:id]) end end
If we try to access an article through the wrong blog now we’ll see an error message.
We’ll probably need to do this several times throughout this application so we’ll move this code into a before filter that we’ll call load_blog
.
class ArticlesController < ApplicationController before_filter :load def show @article = @blog.articles.find(params[:id]) end end
We’ll define the load_blog
method inside the ApplicationController
.
class ApplicationController < ActionController::Base protect_from_forgery private def load_blog @blog = Blog.find_by_subdomain!(request.subdomain) end end
Now whenever we need to nest resources through a subdomain we can add this before_filter
to a controller and we can reference models through the association by calling @blog
. This works for creating records, too. We can create records through the @blog.articles
association and this way any new articles will automatically the blog that they’re associated with.
Fixing a Few Last Problems
What we have so far works well but but there are a couple of issues that we may have to deal with before we can use subdomains in production. If we use www
before the domain we’ll run into problems because it will be treated as a subdomain instead of showing us the full list of blogs. We’ll need to modify the routes file again as its currently directing all subdomains to the blogs#show
action. Instead of messing with a regular expression we’ll use a lambda so that we can handle this with Ruby code.
Blogs::Application.routes.draw do resources :blogs resources :articles match '', to: 'blogs#show', constraints: lambda { |r| r.subdomain.present? && r.subdomain != 'www' } root to: 'blogs#index' end
The lambda has a request object passed to it and we can use this to see if a subdomain is present and, if so, that it isn’t www
.
If the logic in the lambda becomes more complex we can move it into a separate class and we demonstrated this in episode 221, but for what we have here it will work perfectly well as it is. If we visit http://www.blogs.dev/
now we’ll get the home page like we expect.
We’ll also encounter a problem if our application is going to be hosted on a domain with more than one dot in the top-level domain. For example if your site was hosted at http://www.mylovelyblogs.co.uk/
the subdomain portion of the URL won’t be detected correctly. This is easy to fix by adding the following line to the production environment file.
Blogs::Application.configure do config.action_dispatch.tld_length = 2 # Rest of file omitted end
This sets the top-level domain length to 2
which should equal the number of periods in the top-level domain. The default for this is 1
. This option is new in Rails 3.1; if you’re using Rails 3.0 then there are details in episode 221 on how to deal with this.