Responsive Ecommerce and Personalized Online Product Design

I’ve just put the finishing touches on a several month project and an experiment for me in responsive ecommerce design.  The site is http://letteringhq.com a vinyl lettering site where you can design your own lettering online.  My goal was to design a site that worked equally as well on phones and tablets as it did on desktops.  The real trick was making an online product designer that is usable on smaller devices.  The cornerstone of the site is being able to design your own lettering online, so that was the first thought.  When I was done I wanted to be able to walk from lettering design all the way to payment from my phone and not feel the need to “switch to desktop site” or download an app.

The State of Mobile Web

How to build a mobile site is still changing.  Perhaps it will never stop changing?  Being this is mid 2013 right at this moment the concept of how a mobile enabled site should work has changed alot since the days of Blackberry and Windows CE.  As of today most people agree you should not be pimping your app when someone lands on your site.  In fact I find it annoying and the internet agrees, so does Google.  You can now lose ranking if you redirect improperly or force your app on mobile users.  A mobile site ideally should present the same information in a better format based on the size of the screen.  This is still often poorly executed for various reasons.  Some of them relate to what Google is currently penalizing for, others relate to simply the lack of functionality in my opinion.  Often I have found myself immediately switching to the desktop site because the mobile version doesn’t have the same functionality!

One method of creating a mobile site it to utilize a responsive design which is one that responds to the size of the screen.  Twitter bootstrap makes this process pretty easy to accomplish without much work.  In fact Microsoft has just added bootstrap to the new Asp.net templates for Visual Studio 2013.  I opted to go this route as bootstrap already plays nice with most of the other JS libraries I was using.  What this means as far as development I will only have one set of views to manage and not a mobile code base to maintain.

In summary the web stack I settled on:

  • Twitter bootstrap with Flat UI Pro for responsive design
  • My Custom ASP.net MVC 4 Multi Tenant Ecommerce platform
  • Entity Framework 5  + MySql Db
  • Knockout.js
  • IonApi to generate the lettering preview images

 Improving Mobile Bounce Rates and Conversions

My litmus for all of this comes back to a few metrics.  Ultimately I want users to buy if they are on their phones or tablets, so increasing the conversion rate is the ultimate goal.  I most definitely don’t want the dreaded bounce, which is landing on a page then never returning.  If conversions improve on mobile I should see better bounce rates also.  I have some significant findings that show the bounce rate to be 20-30% higher when it comes to mobile phones on similar sites.  Interestingly it seems tablets generally don’t exhibit this behavior.  My thoughts are that most desktop sites are usable on tablets in general.  It is possible to inadvertently use a jquery plugin or some UI feature that doesn’t work well on a tablet, but in the last few years I have made it a point to check for touch usability.

On the opposite spectrum it never ceases to amaze me the amount of people that will struggle through a website that was designed years ago before mobile was a big consideration and somehow still manage to checkout using their phone.  Tablets I can understand, but some sites (even sites I’ve written) just don’t work well on phones.

Responsive Mobile Product Design

responsive-letteringhq

 

Easily my biggest challenge and biggest unknown is adapting a vinyl lettering design tool I’ve developed to adapt to the screen size.  In order to do this it also meant the design tool must react immediately if the screen is rotated and it cannot use any plugins that don’t work well on touch.  I had to completely re evaluate the complexity of my design tool and in some cases strip out some features that didn’t work well on mobile browsers (such as a gradient color picker).  I’m pretty happy with how it turned out though, you can give it a try on your phone or tablet and let me know what you think.  In the end no matter what device you use the design tool on you aren’t missing any features or functionality.

Responsive Mobile Checkout Process

Part two of my project was creating a mobile checkout process that didn’t suck.   The first hurdle was creating a cart view that does not appear too compacted or crowded on a phone.  I settled on presenting a totally separate shopping cart view for mobile phones because it was just too much information to cram into a horizontally formatted table.  As you can see below Bootstrap and flat UI pro combination make a nice contrasting cart experience on the phone.

shopping-cart-page

 

 

Why no registration?

I’ve created a handful of ecommerce platforms over the years and registration is yet another really easy thing to get wrong.  I’ll save the rant, but say this.  If you are going to do registration make it an optional step, put the registration form on the thank you page of your site, have a rock solid password/username reset feature, and use something like Stripe or PayPal Vault to actually make the fact they registered useful (their Credit cards are stored for later use).  Other than that you’ll find that not having registration removes a real barrier to checkout for a lot of people.

Google Wallet is doing it right

While I didn’t really like how Google Checkout worked, I do however think that the new Google Wallet really fits in well with the direction mobile e commerce is going.  Similar to Paypal Express Checkout Google Wallet will take an existing account and pass all of the information directly to the website so the customer doesn’t have to enter anything at all.  After watching the video presentation on their site it looks really slick.  I’ll be integrating this feature at some point as it truly would be useful for some point (I would use it).  Why doesn’t Apple have something like this?

Areas of Improvement

Rarely anything I do is considered “done”.  There is always room for improvement.  As far as usability I think adding Google Wallet once the site really gets on its feet will be a great convenience for Android users.  My only other direct compliant with the finished product is the speed and page load times.  There are many areas I’m going to start tackling this though!  More often than not your customers will tell you what you need to fix/add next.

 

Tagged: , , ,

Multi Tenant Architecture with Asp.net MVC 4

I’ve been faced with a daunting challenge the last few months which is how to effectively create a multi-tenant architecture utilizing asp.net MVC 4. Creating an architecture like this can work several different ways depending on what you want to do. My particular project has a few goals.

First, the application should support tenants as “sites”. The entire project is hosted on one IIS configuration with one application pool. I’m not really going to get into the benefits of Multi-tenant architectures but one inherent benefit is that any number of sites can run on one code base. That is the approach I want to take.

Second, I’m basically converting an existing project to be multi tenant. What my approach does is introduce a Tenant table in the database which exposes a few basic properties about each tenant including a TenantID. This TenantID needs to be introduced into any table you want to be able to segregate based on tenant. You’ll find at least three schools of thought how to segregate tenant data in a database. You can create tables for each tenant making it easy to back up and restore only one tenant. You can also even create separate databases. My method is keeping all the data together and segregating by TenantID.

Third, I need a custom view engine to facilitate the view organization I desire. The structure I would like to end up with is View -> Tenant Name -> Views. I also want a View -> Global -> Views folder which is available if the view is not found in the Tenant folder. This allows me to share similar views such as in my project eCommerce shopping cart, check out, and payment code. This image below gives a nice image of what I’m talking about. More on the view engine in a bit.

Handling Static Files

You can also see I’ve restructured the /Content folder creating Global Folders for images and styles, then breaking out another Tenants folder for tenant specific resources. One issue you will run into quite quickly is how to deal with files such as robots.txt or favicon.ico. These two files are common on most any site (including many more) and must have copies for each tenant. My solution is to utilize the IISrewrite feature storing the rewrite rules directly in my web.config. An example below routes the favicon for site 1 to the proper folder. This isn’t ideal in my opinion since the web.config can get large quickly, but it does work quite well.

 

 

Storing the Tenant List in Memory

As you will soon see we need to know the list of tenants every request to determine which tenant is requesting the page. In order to do this I load the current list of tenants at the application_start(). This is a simple FetchAll() into a list using Entity Framework 5. I’m not an expert on thread safety but once this data is loaded it will only be read from this point on.

    public class MvcApplication : System.Web.HttpApplication
    {
        public static List Tenants;
 
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
 
            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
 
            System.Web.Mvc.ViewEngines.Engines.Clear();
            System.Web.Mvc.ViewEngines.Engines.Add(new MulitTenantRazorViewEngine());
 
            Tenants = tenant.FetchAll();
        }
    }

Determining the Tenant at the Controller

It is useful for several reasons to always know which tenant you are dealing with at the controller level. For this reason I overloaded Controller and created my own MultiTenantController that inherits Controller. All my controllers now inherit from MultiTenantController. I can do a few useful things now including intercepting the MasterName property of the ViewResult and setting that manually if needed. More importantly this is where I determine the tenant based on the domain name.

public class MultiTenantController : Controller
    {
        public tenant CurrentTenant;
 
        protected override void OnResultExecuting(ResultExecutingContext filterContext)
        {
            var viewResult = filterContext.Result as ViewResult;
            if (viewResult != null)
            {
                viewResult.MasterName = "_Layout";
            }
 
            Debug.Assert(filterContext.HttpContext.Request.Url != null, "filterContext.HttpContext.Request.Url != null");
            CurrentTenant = GetCurrentTenant(filterContext.HttpContext.Request.Url.Host.ToLower());
        }
 
        internal static tenant GetCurrentTenant(string host)
        {
            if (host == null)
            {
                host = "";
            }
            var Tenant = MvcApplication.Tenants.Where(p => //This Tenants Loaded in memory on Application_start()
            {
                var match = p.FolderName + "."; //p.FolderName holds "site1" or "site2" etc...
                return host.StartsWith(match); //is it http://site1.com?
            }).FirstOrDefault();
            if (Tenant == null)
            {
                Tenant = MvcApplication.Tenants.Where(p =>
                {
                    var match = p.FolderName + ".";
                    return host.Contains("." + match); //is it http://www.site1.com?
                }).FirstOrDefault();
            }
 
            return Tenant ?? MvcApplication.Tenants[0];
        }

At this point each controller has access to CurrentTenant which hold the current tenant that is requesting the View. This is really useful because now we can create views based on which tenant. You could also swap key information on the page based on which tenant is looking at the page. Finally current tenant can be passed further down the application creating whatever behavior you need.

Custom View Engine

This is something that might not fit your needs exactly, but this is a pretty generic approach to handling views. As I discussed above I want a global folder which allows me to create views that are shared across all tenants. I also want to be able to specify views for specific tenants since each site can have different features.

Here is sort of what I based my approach off of.
http://weblogs.asp.net/imranbaloch/archive/2011/06/27/view-engine-with-dynamic-view-location.aspx

This view engine is pretty straight forward. I’m using razor in my project so this extends RazorViewEngine. The key part is that I’m pulling out of the controllerContext the controllerContext.Controller and casting as my MultiTenantController. Once I do this step I can now access my CurrentTenant variable we just talked about. At this point the %1 is simply replaced with the CurrentTenant folder name.

 public class MulitTenantRazorViewEngine : RazorViewEngine
    {
        public MulitTenantRazorViewEngine()
        {
            AreaViewLocationFormats = new[] {
            "~/Areas/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{2}/Views/{1}/{0}.vbhtml",
            "~/Areas/{2}/Views/Shared/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/{0}.vbhtml"
            };
 
            AreaMasterLocationFormats = new[] {
            "~/Areas/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{2}/Views/{1}/{0}.vbhtml",
            "~/Areas/{2}/Views/Shared/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/{0}.vbhtml"
            };
 
            AreaPartialViewLocationFormats = new[] {
            "~/Areas/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{2}/Views/{1}/{0}.vbhtml",
            "~/Areas/{2}/Views/Shared/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/{0}.vbhtml"
            };
 
            ViewLocationFormats = new[] {
            "~/Views/%1/{1}/{0}.cshtml",
            "~/Views/%1/{1}/{0}.vbhtml",
            "~/Views/%1/Shared/{0}.cshtml",
            "~/Views/%1/Shared/{0}.vbhtml",
            "~/Views/Global/{1}/{0}.cshtml",
            "~/Views/Global/{1}/{0}.vbhtml",
            "~/Views/Global/Shared/{0}.cshtml",
            "~/Views/Global/Shared/{0}.vbhtml"
            };
 
            MasterLocationFormats = new[] {
            "~/Views/%1/{1}/{0}.cshtml",
            "~/Views/%1/{1}/{0}.vbhtml",
            "~/Views/%1/Shared/{0}.cshtml",
            "~/Views/%1/Shared/{0}.vbhtml",
            "~/Views/Global/{1}/{0}.cshtml",
            "~/Views/Global/{1}/{0}.vbhtml",
            "~/Views/Global/Shared/{0}.cshtml",
            "~/Views/Global/Shared/{0}.vbhtml"
            };
 
            PartialViewLocationFormats = new[] {
            "~/Views/%1/{1}/{0}.cshtml",
            "~/Views/%1/{1}/{0}.vbhtml",
            "~/Views/%1/Shared/{0}.cshtml",
            "~/Views/%1/Shared/{0}.vbhtml",
            "~/Views/Global/{1}/{0}.cshtml",
            "~/Views/Global/{1}/{0}.vbhtml",
            "~/Views/Global/Shared/{0}.cshtml",
            "~/Views/Global/Shared/{0}.vbhtml"
            };
        }
 
        protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
        {
            var PassedController = controllerContext.Controller as MultiTenantController;
            Debug.Assert(PassedController != null, "PassedController != null");
            return base.CreatePartialView(controllerContext, partialPath.Replace("%1", PassedController.CurrentTenant.FolderName));
        }
 
        protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
        {
            var PassedController = controllerContext.Controller as MultiTenantController;
            Debug.Assert(PassedController != null, "PassedController != null");
            return base.CreateView(controllerContext, viewPath.Replace("%1", PassedController.CurrentTenant.FolderName), masterPath.Replace("%1", PassedController.CurrentTenant.FolderName));
        }
 
        protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
        {
            var PassedController = controllerContext.Controller as MultiTenantController;
            Debug.Assert(PassedController != null, "PassedController != null");
            return base.FileExists(controllerContext, virtualPath.Replace("%1", PassedController.CurrentTenant.FolderName));
        }
    }

The Final Result

At this point you can have any number of tenants hosted under one code base. On my particular project this allows me to maintain much less code while any enhancements I make to say checkout pages or features shared across tenants are all echoed immediately. I also have the ability to create totally different views and experiences for each tenant while still sharing key parts of my application. From an eCommerce standpoint my orders now funnel into one administration area making it much simpler to manage.

Further Considerations

Here are some other things I will be considering during this project

SSL

For an eCommerce site it is important to have a means to SSL secure. Since the site is hosted under one application in IIS it is not possible to use separate SSL certificates for each domain name. My strategy involves a wildcard SSL certificate for your “main” site. This can be difficult if your tenants maintain no relation at all, but mine do. Because of the way I route static files, and test domain name my system already works for sub domain based SSL. If you need SSL on site2 you would simply do https://site2.site1.com assuming site one is the “main” site you want to create the certificate for. The routes and domain test are set up to only read the information before the first, so site2 is flagged as the CurrentTenant.

Modular Design

This is more a design consideration. When creating features for multi tenant sites it is wise to create features that are modular. Perhaps visualize something you can turn on and off arbitrarily for each tenant. By doing so you can create features that can be easily shared among current or future tenants.

Update – A Sample Project

Ok, I get it everybody wants a sample project to try this. One of the reasons I haven’t yet is because this configuration isn’t something you can just open and hit run. You’re going to have to configure some custom settings to make the demo work, but here it is!

Keep in mind there are various ways to route to static files such as robots.txt/favicon.ico. The demo shows a pretty bare bones way of doing it, it could easily be tweaked to be slightly more maintainable such filtering each file run into one rule for each file.

Set Up the Hosts File

You need to add two entries to your local DNS server or HOST file in windows. If you don’t know how, read this

127.0.0.1 site1.com
127.0.0.1 site2.com

Setting Up the Sample Project

Download the sample project here

  1. You’ll need to run visual studio as administrator.  Right click Visual Studio > Run as Administrator
  2. Extract and open the project.  You might get an error about IIS.
  3. We need to set up an IIS site to run the project, we will not be using the built in web server.  Create a new site in IIS, map it to the folder of your project.   Add two bindings as show below.
    IIS Setup
  4. Head back into Visual Studio and goto Project > MvcMultiTenant Properties then set up as belowCapture2
  5. The previous step requires administrative access so that is why you need to start visual studio with Admin privileges.
  6. At this point you should be able to build the project and start/Debug

Running the Project

Browse to Site1.com and Site2.com you’ll notice the page changes style sheets and views. You can examine the views folder to see how this works. This behavior can be extended to your entire site. You’ll also notice you can browse http://site1.com/Home/Contact and it uses the view from the global folder and shares on both sites. If you browse to http://site1.com/robots.txt and http://site2.com/robots.txt you’ll notice they are serving different files as expected.

Capture4

Tagged: , , ,