Desktop And Mobile Site With Django | custom render function, no middleware required

While creating a website, more than 95% sites don’t have separate implementation for mobile site. Ok I made up the 95% number, but you get the idea. Majority of websites rely on responsiveness to look good on mobile device which works pretty well for most cases. However if you want more control over features and styling of your mobile site, then just a responsive version will not work or you have to do lot of media query changes to make it work. Eventually you will realize the need to provide separate implementations for both desktop and mobile devices. This may sound like a lot of work, but believe me its not.

There are various ways to create a mobile website version of the existing desktop site, but some made the code too messy and some didn’t work for simultaneous requests. However the method mentioned in this article works well with simultaneous requests and can be implemented with minimal effort.

Lets jump onto the explanation and implementation of the same. I will also talk about ways which will not work properly in the later part of this article.

Lets breakdown this process into two steps

  1. Identifying whether the device is desktop or mobile
  2. Serving appropriate django templates based on device

Identifying whether the device is desktop or mobile

The very first step is the identification of the device which is typically done by analyzing the HTTP request headers transmitted by the user’s browser. The most commonly used header used for this purpose is user_agent. Assuming you are using a combo of nginx (web server) and gunicorn(app server), the idea is that nginx recognizes the device type and pass that information to gunicorn. Don’t worry if you are using Apache, this method works there as well. There are a set of user agents defined for mobile devices, the complete list can be found here http://detectmobilebrowsers.com

Following are the changes which needs to be done in nginx configuration file to achieve the same. First the variable render_mobile_website is defined and initialized with a default value of ‘no‘. After that the user_agent is compared against a regular expression which comprises of various mobile device agents. If there is a match, the variable render_mobile_website is set with value ‘yes’.

set $render_mobile_website no;

if ($http_user_agent ~* "(bb\d+|meego).+mobile|android|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp| mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino") 
 {
     set $render_mobile_website yes;
 }

 if ($http_user_agent ~* "^(1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di| \-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob| do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt| ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)| klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do| t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)| pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms| ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10| 18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61| 70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-)") 
 {
     set $render_mobile_website yes;
 }

Once you identify the device type, this information needs to be passed on to the application server. The following code is taking care of that

 location / {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $host;
      proxy_redirect off;
      proxy_set_header mobile_site $render_mobile_website;
      proxy_pass_header mobile_site;
      if (!-f $request_filename) {
          proxy_pass http://djangoapp_server;
          break;
      }
   }

So far we are able to identify the device and have transmitted this information to the application server. Lets talk about how to serve appropriate django templates based on this information.

Serving different django templates based on device

To accomplish this, I am going to define a custom render function which will be called whenever a django template needs to be rendered. But before that, lets talk about the template structure. In my case, all my templates were available in respective app folders. For instance, if I have two apps “home” and “product”, my desktop templates are arranged in the following manner

project_name
 - home
   - templates
      - home
         - index.html
         - contact.html
 - product
   - templates
      - product
         - catalogue.html

For mobile devices, I created a “mobile-templates” directory at the project level with the following structure

project_name
 - mobile-templates
    - mobile
      - home
         index.html
      - product
         catalogue.html

Note that I haven’t created contact.html in mobile-templates directory, this is for a specific reason which I will come to later.

Now that you have created separate templates for mobile, just modify the settings file to let django know the location of these mobile templates

MOBILE_TEMPLATE_DIRS = os.path.join(BASE_DIR,'mobile-templates')

TEMPLATES = [
 {
 'BACKEND': 'django.template.backends.django.DjangoTemplates',
 'DIRS': [
     # insert your TEMPLATE_DIRS here
     MOBILE_TEMPLATE_DIRS,     # This ensures django finds mobile templates
 ],
 'APP_DIRS': True,
 'OPTIONS': {
 'debug': DEBUG,
 'context_processors': [
     # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this
     'django.contrib.auth.context_processors.auth',
     'django.template.context_processors.debug',
     'django.template.context_processors.media',
     'django.template.context_processors.static',
     'django.template.context_processors.tz',
     'django.contrib.messages.context_processors.messages',
 ],
 },
 },
]

So the ‘home/index.html’ is the relative path of template you want to show on desktop site and ‘mobile/home/index.html’ is the template which should be rendered on mobile website. Keep in mind that I am using same filename for both mobile and desktop templates and only the directory is different.

Now that separate templates are created and django is told about their location, only thing left is to load the template from the proper path based on the request header (mobile_site). Lets define a custom renderer first

from django.shortcuts import render
from django.conf import settings 

def customRender(request, template_name, context=None, status=None):
   renderMobileWebsite = request.META.get('HTTP_MOBILE_SITE','no')
   if renderMobileWebsite == 'yes':
      templates = ['mobile/'+template_name, template_name]
      return render(request, templates, context, status=status)
   else:
      return render(request, template_name, context,status=status)

This function is pretty straight forward, first it identifies whether this is a mobile site or a desktop site. Post that, templates are decided and passed on to the render function. However one thing to notice is this line
“return render(request, templates, context, status=status)”

Here instead of just sending one template name, I am sending two templates. First is the mobile one and second is the desktop one. Remember I mentioned that I left “contact.html” intentionally from the mobile templates directory. The motive here is reusability because we don’t want to create a separate mobile version of each template. In this case, render function will try to find out templates in the order specified. So first it looks for “mobile/home/contact.html” which won’t be found, so the next one is searched for “home/contact.html”.

This gives us a tremendous advantage in situations where we want to modify only few pages for our mobile site. This will ensure that there is no redundant and duplicate content in our project and also makes the maintenance easy.

Now in your views function, instead of calling render function, call our newly created customRender function with the same template name.

def index(request):
   baseContext = getBaseRequestParameters(request)
   return customRender(request, 'home/index.html', baseContext)

If you are using class based views, then also the approach will remain same. You just have to define a custom function which determines what template to render and then call that function in your class. An example is shown below

from django.views.generic import TemplateView

# This function determined what templates to return
def getTemplates(request, template_name):
   renderMobileWebsite = request.META.get('HTTP_MOBILE_SITE','yes')
   if renderMobileWebsite == 'yes':
       return ['mobile/'+template_name, template_name]
   else:
       return [template_name,]

class MyTestView(TemplateView):
    def get_template_names(self):
        return getTemplates(self.request, "home/index.html")

Congratulations, you are all set now. Your website will now serve different templates for mobile and desktop devices.

You might find posts suggesting middleware creation and updating the TEMPLATE_DIRS variable. But that method ain’t gonna work as it creates a race condition when you have concurrent requests and you have to bang your heads managing those threads, so make sure to not fall for that method.

Creating a responsive website is another way but if you want very different experience for mobile and desktop users, this isn’t gonna work either. And besides this is exactly the reason I wrote this article.

I hope you find this article helpful. Let me know if you have any suggestions/ feedback in the comments section below.

Fun FactGame of thrones season 6 is back, and its episode 4 is also titled as the book of stranger 🙂

 

2 thoughts on “Desktop And Mobile Site With Django | custom render function, no middleware required

  1. Victor

    Hi,
    Great and useful article, thank you very much
    How do you use this approach in Class Based Views?
    And how do you set header when developing for desktop and mobile devices if you want to view your mobile template in desktop browser?

    1. Anurag Post author

      Thanks for the appreciation Victor. For class based views also, the approach will remain same. Let me update the article with this information.
      Assuming you are talking about setting header during development purpose, what I typically do is use two different ports while developing, one for desktop and one for mobile. Then modify the customRender function to check the appropriate port and return appropriate template.

Come on, I know you want to say it