Nick not found

Serving an iPhone website with nginx

Jan 12 2009

I've been thinking for a while about the best way to automatically take iPhone and iPod touch users to an iPhone-enhanced version of a website when they first access it rather than requiring the users to know the iPhone-specific URL beforehand. This was the main requirement, but there were a few others. These are the problems I needed to solve:

  1. When accessing mysite.com or www.mysite.com, iPhone and iPod touch users should see an iPhone-enhanced version of the site.
  2. These users should have the option of viewing the full site at its main URL, www.mysite.com.
  3. After deciding to view the full site on an iPhone or iPod touch, it should be easy for the user to get back to the iPhone-enhanced site.
  4. The iPhone-enhanced site should share a domain with a normal mobile website, such as m.mysite.com.
  5. iPhone-specific pages should be accessible without a special prefix in the path. For instance, requesting m.mysite.com/about on an iPhone should return the iPhone-enhanced “about us” page, and accessing the same URL on a different mobile device should return the standard mobile version of the same page.
  6. For testing purposes, both the standard mobile site and the iPhone-enhanced pages of the mobile site should be accessible from a desktop browser. No special headers, request parameters, or cookies should be required.
  7. Both the standard mobile pages and the iPhone-enhanced pages should be cacheable. The /about page will need to be cached twice – once in its standard mobile form and once in its iPhone form.
  8. Users of desktop browsers should be treated normally.

Most of the requirements above are for the sake of simplicity on the part of the user. Remembering the domain name of a site is trouble enough for some users, let alone being expected to know and remember (1) that the website they want to visit has a mobile version, (2) how to get to it, and (3) if it's m.mysite.com or mobile.mysite.com or mysite.mobi… We should be happy that users lands on our site in the first place, and once that's happened, we should make it as easy as possible for them to find what they're looking for and to enjoy the experience along the way.

Now that you understand my requirements (and hopefully the rationale behind each) I'll explain how I solved this problem using nginx. I'll start out with the important pieces and will put it all together at the end. I've also broken the explanation into two sections, the first describing the config for the main website, and the second describing the config for the mobile website.

Main website

First, we need to detect when a request comes in from an iPhone or iPod touch. This is done by matching the user agent against a simple regular expression, (iPhone|iPod). If it's a match, the $iphone_request variable is set to 1 (just a value to mean true).

if ($http_user_agent ~* '(iPhone|iPod)') {
  set $iphone_request '1';
}

That was easy enough. Now that we have the $iphone_request variable, we can use that later on to check whether a request is for an iPhone or iPod touch without having to match against the user agent string again.

Next, we need to accommodate iPhone and iPod touch users who have already been sent automatically to the iPhone-enhanced mobile site but have requested to come back to view the full site. To accomplish this without doing anything special with the URL, I decided to use a cookie. I'll explain how I set and clear the cookie later. For now it's just important to know that if there is a cookie with the name iphone_mode and it has a value of full, then the user should not be redirected to the iPhone-enhanced site. This check simply clears the $iphone_request variable if the cookie is set to full. Here's how to perform this check:

if ($http_cookie ~ 'iphone_mode=full') {
  set $iphone_request '';
}

Finally, we check the value of $iphone_request, and if it is 1 (which will be the case if the request came in from an iPhone or iPod touch and the iphone_mode cookie does not exist or is not set to full), then we'll redirect the user to the mobile site using the URI that was requested.

if ($iphone_request = '1') {
  rewrite ^.+ http://m.mysite.com $uri;
}

If you want to send the user to the home page of the mobile site regardless of the specific page that was requested, simply drop $uri from the end of the mobile site URL.

Mobile/iPhone website

As with the main website, we need to detect when a request comes in from an iPhone or iPod touch. This is done just like above except that here we set two variables instead of just one. In addition to setting the $iphone_request variable, $iphone_path_prefix is set to /iphone (we'll see why next). Here's the modified config for the mobile site:

if ($http_user_agent ~* '(iPhone|iPod)') {
  set $iphone_request '1';
  set $iphone_path_prefix '/iphone';
}

So why the $iphone_path_prefix… what's that for? This is how I make it possible to have a standard mobile page and an iPhone-enhanced page that appear to the user to be at the same URL. Later, depending on certain conditions, I'll make use of this variable to rewrite the requested URL. This will have the effect that when requesting m.mysite.com/about from an iPhone or iPod touch, the page that actually gets served is /iphone/about. When requesting the same URL from any other browser or device, /about gets served.

Now, what if the user for some reason requests m.mysite.com/iphone/about directly? Using only the config shown so far, nginx would serve /iphone/iphone/about. To counter that, I needed to clear the $iphone_path_prefix variable if the path prefix was already included in the request URI. Here's the config that accomplishes this:

if ($uri ~ ^/iphone.*$) {
  set $iphone_path_prefix '';
}

Similarly, I wanted to be able to keep my images, stylesheets, and JavaScript files in their normal locations. By this I mean that I wanted to place image files in /images rather than in /iphone/images. The following config takes care of this:

if ($uri ~ '(images|stylesheets|javascripts|\.css|\.js|\.ico|\.gif|\.jpg|\.png)') {
  set $iphone_path_prefix '';
}

If you want to keep your iPhone images separate from the standard mobile site images, simply exclude this part of the config, but be mindful of where you place your apple-touch-icon.png or apple-touch-icon-precomposed.png image files based on your decision.

Finally, we'll make use of our two variables. First, we'll check to see if this is in fact an iPhone or iPod touch request, and if it is, we'll rewrite the request URI using the path prefix we set (which may at this point be empty again).

if ($iphone_request = '1') {
  rewrite (.*) $iphone_path_prefix$1;
}

Moving between the main website and mobile website

Using our main website config above, iPhone and iPod touch users requesting www.mysite.com will be redirected to m.mysite.com. This provides these users with a superior experience by avoiding zooming, horizontal scrolling, unnecessary asset downloads, and many other things. However, it's time consuming enough to manage the content for a website without the added responsibility of maintaining another version just for iPhone users. Of course you could simply render your website with the iPhone in mind using CSS media queries and other magic, but in a lot of cases, iPhone users will be coming to your site for a different experience altogether.

For instance, if you manage a restaurant website, you might have a Flash intro, news releases, a history of the restaurant's owning family, etc. All of this content might be great, but most or all users who come to your site on an iPhone won't care about any of that. Instead, they only need a few pieces of information, such as your location, hours of operation, phone number, and a menu. In such a case, it would make sense to have a separate, slimmed-down version of the website for these users. And for the few who want to know more, it's a good idea to provide a way for these users a way to view the full website.

We've already handled this ability in our main site's nginx config above, but how is it handled on the client? How is the iphone_mode cookie set and cleared? For that, we use some JavaScript code that handles cookies. I modified the code slightly to allow the domain and path to be specified when setting the value of a cookie. I needed to be able to specify the domain so that a cookie set on a page at m.mysite.com would be sent to the server when requesting a page at www.mysite.com. In other words, a cookie set on the mobile site should be accessible to the public site. Here's my variation of the JavaScript cookies code from the link above:

function createCookie(name, value, days, domain, path) {
  var expires = '';
  if (days) {
    var d = new Date();
    d.setTime(d.getTime() + (days*24*60*60*1000));
    expires = '; expires=' + d.toGMTString();
  }
  domain = domain ? '; domain=' + domain : '';
  path = '; path=' + (path ? path : '/');
  document.cookie = name + '=' + value + expires + path + domain;
}

function readCookie(name) {
  var n = name + '=';
  var cookies = document.cookie.split(';');
  for (var i = 0; i < cookies.length; i++) {
    var c = cookies[i].replace(/^\s+/, '');
    if (c.indexOf(n) == 0) {
      return c.substring(n.length);
    }
  }
  return null;
}

function eraseCookie(name, domain, path) {
  setCookie(name, '', -1, domain, path);
}

Now that we have the code to set and erase cookies, we can make use of it on both the iPhone-enhanced website and the main website. It's important to note that the cookie-handling JavaScript code above needs to be available on each page on both the main site and the mobile site that will include a “view full site” or “view iPhone site” link. On the iPhone version of the site, we want to add a “view full site” link that will take the user to the main website. Here's what that link looks like:

<a href="http://www.mysite.com" onclick="setCookie('iphone_mode', 'full', (0.25/24), 'mysite.com')">
  View full site
</a>

When a user clicks on the link, the setCookie function is called, which sets the iphone_mode cookie with a value of full at the mysite.com domain. The (0.25/24) value specifies that the cookie should expire after 15 minutes. You may want to adjust this value higher for your site if it is one you expect the user to stay on for longer than 15 minutes. After the cookie is set, the browser follows the link to the main website. Remembering our main site's nginx configuration from above, we know that when a request comes in and there is an iphone_mode cookie set to full, the request passes through unchanged, allowing iPhone and iPod touch users to view the main website as though viewing it from a desktop computer.

Once a user comes to the main site, we need to provide a way to get back to the iPhone-optimized site. This is something that I've noticed is lacking from some popular websites and which is quite irritating, especially if the “view full site” link is pressed by mistake. So how do we do it?

We use the same method we used to get to the main site in the first place, but instead of setting the iphone_mode cookie, we clear it. There's one difference with the main site, though. On the iPhone site, we can assume that it is being viewed with an iPhone or iPod touch, so we can unconditionally place a “view full site” link on any page. However, we can't make the same assumption on the main site, so we can't just put a “view iPhone site” link on it for all to see. Instead, this link should only be visible to iPhone and iPod touch users. It turns out that doing this is simple. We just include this snippet of JavaScript code wherever we want our “view iPhone site” link to appear:

<script type="text/javascript">
/*<![CDATA[*/
if (navigator.userAgent.match(/(iPhone|iPod)/)) {
  document.write('<a href="http://m.mysite.com" '
               + 'onclick="deleteCookie(\'iphone_mode\', \'mysite.com\');">'
               + 'View iPhone site'
               + '</a>;');
}
/*]]>*/
</script>

This will write the link into the document at the point that the <script> tag appears, but it will only do so for iPhone and iPod touch users. As we can see by the onclick handler, clicking this link deletes the iphone_mode cookie that was set previously.

nginx configurations

Here are the nginx configurations for the main site and the mobile site. (The mobile site is where the iPhone pages live.) I've excluded the non-pertinent parts of the config, but I've included some of the basics so you can get a feel for where in the config the examples from above belong.

Main site nginx config

upstream app_server {
  server 0.0.0.0:3000;
}

server {
  listen 80;
  server_name www.mysite.com;

  root /path/to/main_site;
  # ...

  location / {
    proxy_set_header X-Real-IP $remote_addr;
    # ...

    if ($http_user_agent ~* '(iPhone|iPod)') {
      set $iphone_request '1';
    }
    if ($http_cookie ~ 'iphone_mode=full') {
      set $iphone_request '';
    }
    if ($iphone_request = '1') {
      rewrite ^.+ http://m.mysite.com$uri;
    }

    # serve cached pages ...

    if (!-f $request_filename) {
      proxy_pass http://app_server;
      break;
    }
  }
}

Mobile site nginx config

upstream m_app_server {
  server 0.0.0.0:3001;
}

server {
  listen 80;
  server_name m.mysite.com;

  root /path/to/mobile_site;
  # ...

  location / {
    proxy_set_header X-Real-IP $remote_addr;
    # ...

    if ($http_user_agent ~* '(iPhone|iPod)') {
      set $iphone_request '1';
      set $iphone_path_prefix '/iphone';
    }
    if ($uri ~ ^/iphone.*$) {
      set $iphone_path_prefix '';
    }
    if ($uri ~ '(images|stylesheets|javascripts|\.css|\.js|\.ico|\.gif|\.jpg|\.png)') {
      set $iphone_path_prefix '';
    }
    if ($iphone_request = '1') {
      rewrite (.*) $iphone_path_prefix$1;
    }

    # serve cached pages ...

    if (!-f $request_filename) {
      proxy_pass http://m_app_server;
      break;
    }
  }
}

Summary

After it was all put together, this setup accomplished all of my requirements listed above. Namely, it's possible to view the main site, the mobile site, and the iPhone site using a standard browser (by going to www.mysite.com, m.mysite.com, and m.mysite.com/iphone, respectively), which helps enormously with testing. It's also possible for iPhone users to move easily between the main site and the iPhone site, which in my opinion is essential unless the iPhone site is fully featured.

The only viewing that isn't possible with this configuration is that an iPhone user cannot view the standard mobile website. In my situation, this is acceptable (and even preferred) because the iPhone site offers exactly the same information (plus some) that is offered on the standard mobile website. In other words, there's no reason for an iPhone user to want or need to view the standard mobile site. If your needs are different, it would be possible to use the same cookie handling on the mobile site that is used on the main site in my example above to allow this.

Regarding caching, the app can cache pages at their request paths without having to worry about the rewriting that's being done up front by nginx. If a request for /about comes in from anything but an iPhone, nginx serves up /about from the cache if it exists, and if not, passes the request to the app server, just as with any normal caching setup. If the same request comes in from an iPhone, nginx rewrites the URI to /iphone/about first, then it looks in the cache for /iphone/about, and if that doesn't exist, it sends the request for /iphone/about to the app server. Hence, in the context of caching, the app doesn't even need to check whether the user is on an iPhone or not.

I've written this in hopes that it saves someone else the time it took me to figure it all out. Please post comments if you have questions, need clarification, or have a better way of doing something I've explained here.

Nine Article Comments

links for 2009-03-04 « Bloggitation

Mar 4 '09 at 12:04 pm

[…] Serving an iPhone website with nginx (tags: sysadmin nginx web iphone mobile) […]


Cristian Nica

Mar 18 '09 at 08:17 pm

Good article. Professional and complete.Congrats ;)


Brad

Aug 16 '09 at 07:40 pm

Thanks for the well-written article. I’m sure this will come in handy when we do the mobile/iphone versions of our site.


links for 2009-11-19 « Gatunogatuno’s Weblog

Nov 18 '09 at 03:20 pm

[…] Serving an iPhone website with nginx « Nick not found (tags: nginx iphone development) […]


ScriptoX

Dec 5 '09 at 12:10 am

Excellent post..Keep them coming :) Thanks for sharing.


Beau

Oct 23 '10 at 04:56 am

Thanks for posting this! It was a great article and a good start for me. There were a couple things I noticed:

1) The javascript that goes on the iPhone/Mobile site to take you to the full site uses setCookie but it should use createCookie.

2) The last two javascript functions–readCookie and eraseCookie aren’t used and could be removed.

3) The main website doesn’t use the extra javascript functions so there’s no need to include them there.

4) You don’t have any code to redirect other mobile browsers. I assume you intentionally left this out because it was beyond the scope of this article and should be easy to piece together, but it might be useful for some people.

5) Mobile users aren’t redirected when they go to the home page. It would make sense that it’s because the “plus” in this line: rewrite ^.+ http://m.mysite.com$uri; means “one or more” and so if there’s nothing because you’re on the home page it wont get rewritten. However, I replaced the “plus” with an “asterisk” and that still didn’t do it. This is the only problem I’m still having.

Again, I really really liked this article and I consider it an unrivaled resource.


Beau

Oct 23 '10 at 09:46 pm

I found my issue with #5 above. I had put your rewrite rules after:

if (-f $request_filename) {
  break;
}
if (-d $request_filename) {
  break;
}

And since index.php exists it was stopping your rules from getting run on the home page. All I had to do was put your rules first.


Tidbits from my daily-life » Serving an iPhone website with nginx « Nick not found

Nov 25 '10 at 10:13 am

[…] Serving an iPhone website with nginx « Nick not found. 26 Jan This entry was written by morgan, posted on 26 January 2011 at 13:13, filed under Random Stuff. Bookmark the permalink. Follow any comments here with the RSS feed for this post. Post a comment or leave a trackback: Trackback URL. Browse Older: MIT OpenCourseWare | Media Arts and Sciences | MAS.962 Special Topics: New Textiles, Spring 2010 | Download Course MaterialsMIT OpenCourseWare | Media Arts and Sciences | MAS.962 Special Topics: New Textiles, Spring 2010 | Download Course Materials. […]


Glenn

Jun 20 '11 at 07:28 pm

Nice article, love the way you broke it down in different goals at first. I found this when I was looking for a way to redirect a direct request to m.byte-consult.be (doesn’t exist yet) to the mobile theme, no-matter what the user agent is, kind of the other way around. Not entirely how you do it, my idea is that anyone requesting a mobile page gets the mobile style, for testing purposes on the desktop. Right now I have browser and request detection running to sort out the mobile/web style. Thanks for posting this, it gives me ideas.


Leave a Comment * indicates a required field


(Do not fill out this field, or your comment will be ignored. This field is here to help us protect against automated comments.

Your name, URL, and comment will appear on this page after I have reviewed it. (I do this to prevent spam). Some Markdown is allowed. I may make formatting adjustments to your comment. Your email address will not be published.

RSS

« Back to all articles

More articles about:


Lightrail Online Platform

Lightrail – a solid, secure platform for insurance websites.

Find out what makes Lightrail awesome »


A little bit about me

© 2013 nicknotfound.com