Mobile enable your Ruby on Rails site for small screens

Published 01 December 2009

As mobile devices and small screens accessing web sites become more common place, having your website tailored for these devices is becoming increasingly important.\r\n\r\nFortunately Ruby on Rails makes this reasonably easy. I recently went through the process of mobile-enabling our \\ Ruby on Rails site.\r\n\r\nAs 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).\r\n\r\nHere’s the process I followed which is reasonably straight-forward and took about 1 day.\r\n\r\nThe 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.\r\n\r\nThe 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.\r\nThe 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).\r\n\r\nThe steps are:\r\n\r\n# Add mobile detection to your controllers.\r\n# Have your controller return the appropriate layout.\r\n# Duplicate and customise the application.html.erb layout.\r\n# Duplicate and customise the css file – this is the most time consuming part.\r\n# Resize large images for mobile screens.\r\n# Add a link to switch between mobile and \“normal\” screen sizes.\r\n\r\nI 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.\r\n\r\nFirst 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.\r\n\r\nThe header parameter we care about for mobile detection is the HTTP_USER_AGENT. When I connect from my browser the value I get is:\r\n@@@\r\nMozilla/5.0 (X11; U; Linux i686; en-GB; rv: \r\nGecko/20091109 Ubuntu/9.10 (karmic) Firefox/3.5.5\r\n@@@\r\np. You should recognise a few meaningful pieces of information here, notably I’m running Firefox 3.5 on Ubuntu Linux 9.10.\r\nLikewise when I connect from my Google Android (an HTC Magic branded phone) the header is:\r\n@@@\r\nMozilla/5.0 (Linux; U; Android 1.6; en-gb; HTC Magic Build/DRC92)\r\nAppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1\r\n@@@\r\np. Apple weenies connecting from an iPhone supply a user agent string which looks like this:\r\n@@@\r\nMozilla/5.0 (iPhone; U; CPU iPhone OS 3_1_2 like Mac OS X; en-us)\r\nAppleWebKit/528.18 (KHTML, like Gecko) Mobile/7D11\r\n@@@\r\np. 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\ our controllers extend from one ApplicationController, which in turn extends from ActionController::Base.\r\nBeing the parent class to all other controllers means we can define the browser detection and return the appropriate layout in one place.\r\n\r\nUsing the HTTP_USER_AGENT in the request headers we do a pattern match array of mobile user agents.\r\n@@@ ruby\r\nclass ApplicationController < ActionController::Base\r\n helper :all\r\n protect_from_forgery\r\n layout :detect_browser\r\n\r\n private\r\n 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\”,\“\”,\“mmp\”,\“symbian\”,\“smartphone\”, \“midp\”,\“wap\”,\“vodafone\”,\“o2\”,\“pocket\”,\“kindle\”, \“mobile\”,\“pda\”,\“psp\”,\“treo\”]\r\n\r\n def detect_browser\r\n agent = request.headers[\“HTTP_USER_AGENT\”].downcase\r\n MOBILE_BROWSERS.each do |m|\r\n return \“mobile_application\” if agent.match(m)\r\n end\r\n return \“application\”\r\n end\r\n@@@\r\np. 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.\r\n\r\nIf any user agent string matches we’ll render the mobile_application layout, otherwise just the usual application layout.\r\n\r\nSo in my app/views/layouts I copy application.html.erb to mobile_application.html.erb and make appropriate modifications.\r\n\r\nThe first thing I did was replace stylesheet_link_tag \"main\" with stylesheet_link_tag \"mobile\" and copy public/stylesheets/main.css to public/stylesheets/mobile.css\r\n\r\nSince 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.\r\n\r\nOur 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.\r\nThen in my mobile.css and mobile_application.html.erb files I linked these new, smaller images.\r\n\r\nNext is the tedious process of going through your mobile.css stylesheet and tayloring it for small screens.\r\nWhat I did was remove the boxes which float right (tweets and client testimonials). These I put inline below the main page content.\r\nLikewise the copyright notice and client login button which sit at the top right I moved down to the bottom of the page.\r\nI 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.\r\n\r\nThis 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.\r\n\r\nThe 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.\r\n\r\nSites 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.\r\n\r\nSo in the footer bar of my site application.html.erb file I added the following:\r\n@@@ rhtml\r\n<%= link_to \“MOBILE SITE\”, :controller => \“home\”, :action => \“set_layout\”, :mobile => \“1\” >\r\n@@@\r\np. and likewise in the mobile_application.html.erb file I added:\r\n@@@ rhtml\r\n<= link_to \“REGULAR SITE\”, :controller => \“home\”, :action => \“set_layout\”, :mobile => \“0\” %>\r\n@@@\r\np. I then modified the home_controller.rb file to stick the chosen layout in the session:\r\n@@@ ruby\r\ndef set_layout\r\n session[\“layout\”] = (params[:mobile] == \“1\” ? \“mobile\” : \“normal\”)\r\n redirect_to :action => \“index\”\r\nend\r\n@@@\r\np. And finally I amend detect_browser in the application_controller.rb, having the session variable takes precendence over my auto-detection:\r\n@@@ ruby\r\nclass ApplicationController < ActionController::Base\r\n helper :all\r\n protect_from_forgery\r\n layout :detect_browser\r\n\r\n private\r\n 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\”,\“\”,\“mmp\”,\“symbian\”,\“smartphone\”, \“midp\”,\“wap\”,\“vodafone\”,\“o2\”,\“pocket\”,\“kindle\”, \“mobile\”,\“pda\”,\“psp\”,\“treo\”]\r\n\r\n def detect_browser\r\n layout = selected_layout\r\n return layout if layout\r\n agent = request.headers[\“HTTP_USER_AGENT\”].downcase\r\n MOBILE_BROWSERS.each do |m|\r\n return \“mobile_application\” if agent.match(m)\r\n end\r\n return \“application\”\r\n end\r\n \r\n def selected_layout\r\n session.inspect # force session load\r\n if session.has_key? \“layout\”\r\n return (session[\“layout\”] == \“mobile\”) ? \r\n \“mobile_application\” : \“application\”\r\n end\r\n return nil\r\n end\r\nend\r\n@@@\r\np. As an interesting aside I have to force the session to be loaded with session.inspect otherwise session.has_key? always returns false, since it’s lazy loaded.\r\nIf there’s a better way to do this please add a comment and let me know.\r\n\r\nI tested this using the Google Android emulator which you can download to trial various screen sizes without having to actually use the hardware.