Creating a mobile version of your site in Rails 3.1

How to add mobile versions of your views, and give the user the option to switch between desktop and mobile layouts.

I recently added a mobile layout for Chess Microbase. To ensure the mobile content was just as optimized as the desktop version, and to help reduce the number of interactions (and therefore conflicts) between the desktop and mobile versions, I chose to make the mobile site run from separate view files. Here, I’ll give an explanation of how I made that work using Rails 3.1.

I started with this helpful solution by Winfield Peterson on StackOverflow:

Define a custom ‘mobile’ Rails MIME type in config/initializers/mime_types.rb that’s just HTML:

Mime::Type.register_alias "text/html", :mobile

Add a helper/filter to respond to mobile users:

def mobile_user_agent?
  @mobile_user_agent ||= ( request.env["HTTP_USER_AGENT"] && request.env["HTTP_USER_AGENT"][/(Mobile\/.+Safari)/] )
end

then..

before_filter :handle_mobile

def handle_mobile
  request.format = :mobile if mobile_user_agent?
end

Make custom mobile template:

app/views/users/show.html.erb => app/views/users/show.mobile.erb

Dispatch mobile or regular in controllers:

respond_to do |format|
  format.html { }   # Regular stuff
  format.mobile { } # other stuff
end

This was a great starting solution, although there are a few changes needed, most importantly to allow the user to manually switch between modes, following usability best practices.

To begin with, I changed the user agent RegEx to better match the mobile environments I’m familiar with (my apologies to the ones I’m not):

def mobile_user_agent?
  request.env["HTTP_USER_AGENT"].try :match, /(iphone|ipod|android)/i
end

In particular, I’m deliberately not including the iPad, as the 10” screen size tends to be better suited to desktop layouts than mobile.

At this point, you can safely start making some mobile views, so I’d like to share a few notes before I get to the switching functionality:

  • Views that end with .html.slim (or .html.erb, .html.haml, etc…) will only be used by the desktop layout, and you’ll get a missing template error if you haven’t created the .mobile.slim version for a page when you visit it. Views that end only with .slim will be used by either version - you can use this to your advantage.
  • The format to be used for a partial will be determined independently. This means you can have a common version of a partial that is shared by mobile and desktop views, or have a partial with mobile and desktop versions that is used within a shared main view.
  • You might be tempted to include you main application stylesheets and javascripts in your mobile application layout, and then add additional ones for mobile too. I’ve found it works out better to refactor your stylesheet and javascript components into separate files, and include only the ones you need in your mobile layout.

With that noted, I’ll show you how I added layout switching.

I started by adding a switch_layout action to my SessionsController (part of authentication system), but you can add it wherever makes most sense in your application. This action simply sets a session variable with the preferred layout and redirects back to the referring page:

# routes.rb
resources :sessions do
  get :switch_format, on: :collection
end

# sessions_controller.rb
def switch_format
  session[:preferred_format] = params[:preferred_format]
  redirect_to :back
end

I added links to switch layouts in the footer of both my mobile and desktop application layouts:

# application.html.slim
li Layout: <strong>Desktop</strong> | #{link_to 'Mobile', \
  switch_format_sessions_path(preferred_format: 'mobile')}

# application.mobile.slim
li Layout: #{link_to 'Desktop', switch_format_sessions_path(\
  preferred_format: 'html')} | <strong>Mobile</strong>

Finally, I added another before_filter to my ApplicationController to apply the selected layout where appropriate:

# application_controller.rb

before_filter :handle_mobile
before_filter :use_preferred_format

...

def use_preferred_format
  if request.format == 'html' && session[:preferred_format] == 'mobile'
    request.format = 'mobile'
  end
  if request.format == 'mobile' && session[:preferred_format] == 'html'
    request.format = 'html'
  end
end

In this code I made a point of detecting that I’m only switching between mobile and html formats, to ensure the user doesn’t get stuck unable to access any other formats generated by the site due to their session layout preference.

Tobias Cohen

Web + game developer

Melbourne, Australia