As mobile devices and small screens accessing web sites become more common place, having your website tailored for these devices is becoming increasingly important.
Fortunately Ruby on Rails makes this reasonably easy. I recently went through the process of mobile-enabling our www.arctickiwi.com Ruby on Rails site.
As you can see it’s a simple site, a few static pages with a bit of database-driven content (the blog and recent tweets on the sidebar).
Here’s the process I followed which is reasonably straight-forward and took about 1 day.
The key is detecting the client (browser) connecting to the server and rendering the appropriate layout. For simplicity we have just 2 layouts, one for mobile and one for regular browsers.
The beauty of Ruby on Rails is the layout applies across our entire site, so I just make my changes in one place and they flow across all pages.
The content remains the same on each site and we can control the presentation in the CSS. This is where the true benefit of the MVC layers comes into play, and the advantage of defining all styles in the CSS (rather than inline).
The steps are:
- Add mobile detection to your controllers.
- Have your controller return the appropriate layout.
- Duplicate and customise the application.html.erb layout.
- Duplicate and customise the css file – this is the most time consuming part.
- Resize large images for mobile screens.
- Add a link to switch between mobile and “normal” screen sizes.
I have a Google phone with screen dimension of 320px wide by 480px high, so being selfish that’s the screen size I’m targetting. One of my goals was to avoid the annoying horizontal scroll bar.
First is mobile detection. Whenver a browser or mobile device connects to a web server it sends a bunch of headers, such as the content type it accepts, language and the type of browser making the connection.
The header parameter we care about for mobile detection is the HTTP_USER_AGENT. When I connect from my browser the value I get is:
Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:126.96.36.199) Gecko/20091109 Ubuntu/9.10 (karmic) Firefox/3.5.5
You should recognise a few meaningful pieces of information here, notably I’m running Firefox 3.5 on Ubuntu Linux 9.10.
Likewise when I connect from my Google Android (an HTC Magic branded phone) the header is:
Mozilla/5.0 (Linux; U; Android 1.6; en-gb; HTC Magic Build/DRC92) AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1
Apple weenies connecting from an iPhone supply a user agent string which looks like this:
Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_1_2 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Mobile/7D11
Since there’s no standard way of identifying a mobile device (and the definition is vague and constantly changing as new technology is released), I just use a known array of mobile user agents from here.
All our controllers extend from one ApplicationController, which in turn extends from ActionController::Base.
Being the parent class to all other controllers means we can define the browser detection and return the appropriate layout in one place.
Using the HTTP_USER_AGENT in the request headers we do a pattern match array of mobile user agents.
class ApplicationController < ActionController::Base helper :all protect_from_forgery layout :detect_browser private MOBILE_BROWSERS = ["android", "ipod", "opera mini", "blackberry", "palm","hiptop","avantgo","plucker", "xiino","blazer","elaine", "windows ce; ppc;", "windows ce; smartphone;","windows ce; iemobile", "up.browser","up.link","mmp","symbian","smartphone", "midp","wap","vodafone","o2","pocket","kindle", "mobile","pda","psp","treo"] def detect_browser agent = request.headers["HTTP_USER_AGENT"].downcase MOBILE_BROWSERS.each do |m| return "mobile_application" if agent.match(m) end return "application" end
Notice the layout is simply a method pointer to
detect_browser which does a pattern match on each item in my array of mobile user agents.
If any user agent string matches we’ll render the
mobile_application layout, otherwise just the usual
So in my
app/views/layouts I copy
mobile_application.html.erb and make appropriate modifications.
The first thing I did was replace
stylesheet_link_tag "main" with
stylesheet_link_tag "mobile" and copy
Since your application has a lovely clean separation of layout to content, you only need to make changes to your style sheet, right? If you’ve embedded styling elements in your HTML then you may have some work to do moving them into a stylesheet first.
Our site has a 900 pixel wide banner image which clearly would not fit well in a mobile browser. So I created a
public/images/mobile directory and resized all the big images and stuck them in there.
Then in my
mobile_application.html.erb files I linked these new, smaller images.
Next is the tedious process of going through your
mobile.css stylesheet and tayloring it for small screens.
What I did was remove the boxes which float right (tweets and client testimonials). These I put inline below the main page content.
Likewise the copyright notice and client login button which sit at the top right I moved down to the bottom of the page.
I also squished up the menu, removing excess padding and any fixed width content. I prefer to let my browser render the content to fill the available space, the way HTML was intended.
This fiddling with CSS and moving content around is the most time consuming part. And not being a designer it takes me a while. Fortunately our site is small and the layout is quite clean, with most changes being made just in the CSS and layout file.
The final, optional step is allowing the user to override your browser detection. We may have incorrectly detected their user agent so giving them the option to switch layout is a nice feature.
Sites like Google and Wikipedia use a different URL for their mobile site but I decided to store a variable in the session if the user chooses to override the automatic browser detection. If you have a strong opinion on which is a better method I’d be interested to hear why.
So in the footer bar of my site
application.html.erb file I added the following:
<%= link_to "MOBILE SITE", :controller => "home", :action => "set_layout", :mobile => "1" %>
and likewise in the
mobile_application.html.erb file I added:
<%= link_to "REGULAR SITE", :controller => "home", :action => "set_layout", :mobile => "0" %>
I then modified the
home_controller.rb file to stick the chosen layout in the session:
def set_layout session["layout"] = (params[:mobile] == "1" ? "mobile" : "normal") redirect_to :action => "index" end
And finally I amend
detect_browser in the
application_controller.rb, having the session variable takes precendence over my auto-detection:
class ApplicationController < ActionController::Base helper :all protect_from_forgery layout :detect_browser private MOBILE_BROWSERS = ["android", "ipod", "opera mini", "blackberry", "palm","hiptop","avantgo","plucker", "xiino","blazer","elaine", "windows ce; ppc;", "windows ce; smartphone;","windows ce; iemobile", "up.browser","up.link","mmp","symbian","smartphone", "midp","wap","vodafone","o2","pocket","kindle", "mobile","pda","psp","treo"] def detect_browser layout = selected_layout return layout if layout agent = request.headers["HTTP_USER_AGENT"].downcase MOBILE_BROWSERS.each do |m| return "mobile_application" if agent.match(m) end return "application" end def selected_layout session.inspect # force session load if session.has_key? "layout" return (session["layout"] == "mobile") ? "mobile_application" : "application" end return nil end end
As an interesting aside I have to force the session to be loaded with
session.has_key? always returns false, since it’s lazy loaded.
If there’s a better way to do this please add a comment and let me know.
I tested this using the Google Android emulator which you can download to trial various screen sizes without having to actually use the hardware.