Having a mobile version of your website is a pretty common thing these days. Doing it with Ruby on Rails seems pretty common as well. Yet there seems to be a lot of misguidance on the web when you search google for advice on making mobile sites in Rails. There are two prevailing suggestions for accomplishing this that I think are undesirable. I’ve come up with another variation on this that I think is more maintainable and better for the end user.
But first…some background…
TL;DR: Don’t use custom MIME formats or domain redirects. Use custom view paths. Skip to “The Final Solution” at the bottom, if you don’t care why.
Adding a Custom Format Sucks
I don’t like everyone’s suggestion of adding a
:iphone MIME type like this:
These solutions detect the User-Agent in a
before_filter and set the request format like this:
This sucks if you want to use any of your partials for both mobile and desktop pages, because they are considered to be different formats. Say you have a template
show.html.haml for the desktop version of the page, and a
show.mobile.haml template for the mobile version. You can’t have them both render the same partial. Now imagine you have a common footer you want to use in both version. You’d like to just
render :partial => 'footer' and have it work. But it doesn’t. If your partial is named
_footer.html.haml, rendering the mobile template will complain that it can’t find
_footer.mobile.haml. You’re stuck maintaining two identical copies of this partial. This really sucks if you have a lot of these kinds of partials.
Some people suggest removing the format in the filename, so you’d have
_footer.haml. I am not fond of this solution.
Adding a Custom Domain Sucks
Don’t you hate it when you see a link to an article on Twitter and click it on your desktop, only to be taken to http://m.whatever.com/ because someone shared this link from their mobile device? Now you’re reading a mobile version of this article full screen on your desktop and it looks ridiculous. Or you hit a full version URL from your mobile device and have to suffer yet another redirect. As a user, I would prefer to see one page that looks mobile-friendly on a mobile device, and looks like a full version on a desktop. As a developer, redirecting seems like a cop-out. It also feels like it violates a good RESTful design. There should be one URL for this resource and its view should be tailored to the device on which I am viewing it.
There Is A Better Way
I like to leverage the same
before_filter concept of the custom format solution to detect whether or not you are on a mobile device. You can even add a check for a query param that allows a request to set a flag in the session that overrides the mobile-or-not setting. The first step to this is to build some filters and helpers into your
1 2 3 4 5 6 7 8 9 10 11 12 13
With these in your
ApplicationControlelr, you can add
before_filter :check_for_mobile to any controller/action and have it detect whether or not a request is mobile, or is forced to be mobile (or not) with a query parameter
mobile=0). You also have a
mobile_device? method that you can call from any controller or view to see if you are currently rendering a mobile-formatted page. (This is rarely needed, but can be handy in certain situations.)
The next step is to tell Rails to render mobile versions of the templates if the request is deemed to be from a mobile device. Rather than using a custom format, use a custom view path. This is the trick used by Rails engines and plugins to extend the app with its own view templates, while still allowing them to be overridden by the app. To make this work, you need to create a separate directory structure for mobile view templates and add it to the front of the view load path. This way, you can still use the
:html format, and can still share templates between mobile and desktop templates. As an added bonus, you can still serve full versions of pages to mobile devices if you haven’t implemented the mobile version yet. This is huge if you’re trying to incrementally add mobile-friendly pages to an existing desktop-oriented site.
For purposes of this example, I’ll call this parallel mobile views directory
views_mobile. The way you prepend that to the view load path is with the
prepend_view_path method. I prefer to put this in a method in
ApplicationController that I can call from the
check_for_mobile filter if a mobile device is detected, or that I can use directly as it’s own filter:
1 2 3
Now, if you use
before_filter :prepare_for_mobile on any action, it will always be treated as mobile, rendering templates from your
app/views_mobile directory tree if they exist, falling back to those in your
app/views directory tree if they don’t. This is great if you have a mobile-first responsive design for your page that you want to always serve to mobile and desktop devices, but still have other pages (and layouts) that are fully designed for the desktop that you don’t want to mix with.
The last step is to augment the
check_for_mobile filter method to call
prepare_for_mobile if it detects a mobile device. That way you can use
before_filter :check_for_mobile on methods that have two versions. Those actions will render the mobile version from
app/views_mobile for mobile devices and from
app/views for non-mobile devices – and they can both render the same shared partials living in
The Final Solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30