Lei Wu, Web Developer

Keeping simple things simple.

Find the Name of Your Colour… With F# Html Type Provider

How many colour names are there in the world? Wikipedia lists a whopping 1,500 of them: https://en.wikipedia.org/wiki/List_of_colors

The following F# script, given any hex colour value, returns the closet match. For example, it tells you “#FFFFF1” matches “Ivory #FFFFF0”.

The script uses Html Type Provider to query the following three pages and generates a list of colour names:

It then calculates the shortest 3-D distance between your colour and the colours in the list.

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#if INTERACTIVE
#r @"..\..\packages\FSharp.Data\lib\net45\FSharp.Data.dll"
#else
module Colours
#endif

open FSharp.Data

[<Literal>]
let Url_a_f = "https://en.wikipedia.org/wiki/List_of_colors:_A%E2%80%93F"
[<Literal>]
let Url_g_m = "https://en.wikipedia.org/wiki/List_of_colors:_G%E2%80%93M"
[<Literal>]
let Url_n_z = "https://en.wikipedia.org/wiki/List_of_colors:_N%E2%80%93Z"

type ``Colours A-F`` = HtmlProvider<Url_a_f>
let colours_a_f =
    ``Colours A-F``.Load(Url_a_f).Tables.``Colors in alphabetical order A-F``.Rows
    |> Seq.map(fun r -> r.Name, r.``Hex (RGB)``)


type ``Colours G-M`` =  HtmlProvider<Url_g_m>
let colours_g_m =
    ``Colours G-M``.Load(Url_g_m).Tables.``Colors in alphabetical order G-M``.Rows
    |> Seq.map(fun r -> r.Name, r.``Hex (RGB)``)

type ``Colours N-Z`` = HtmlProvider<Url_n_z>
let colours_n_z =
    ``Colours N-Z``.Load(Url_n_z).Tables.``Colors in alphabetical order N-Z``.Rows
    |> Seq.map(fun r -> r.Name, r.``Hex (RGB)``)

// Combine the three
let colours =
    colours_n_z
    |> Seq.append colours_g_m
    |> Seq.append colours_a_f
    |> Seq.cache

let distance hex1 hex2 =

    let hexToInt s =
        System.Convert.ToInt32(s, 16)

    // convert hex string to (R:int * G:int * B:int)
    // e.g. "#FFFFFF" to (255, 255, 255)
    let getRGB (hex:string) =
        let r = hex.[1..2] |> hexToInt
        let g = hex.[3..4] |> hexToInt
        let b = hex.[5..6] |> hexToInt
        (r, g, b)

    let (r1, g1, b1) = getRGB hex1
    let (r2, g2, b2) = getRGB hex2
    (r1 - r2)*(r1 - r2) + (g1 - g2)*(g1 - g2) + (b1 - b2)*(b1 - b2)

let matchColour hex' =
    colours |> Seq.minBy (fun (_, hex) -> distance hex' hex)

colours |> Seq.length |> printfn "Total number of colours: %i"
"#FFFFF1" |> matchColour |> printfn "Best match for your colour: %A"

Using F# Twitter Type Provider

Recently I have been using F# a lot at work.

One of the things I love most about F# is Type Providers. Combined with Intellisense in Visual Studio, it’s very convenient for certain tasks, such as accessing Twitter account.

Today I need to find out the account information of a given Twitter account. But I only have the following information from a C# application:

ConsumerKey, ConsumerSecret, AccessToken, OAuthToken.

F# Twitter Type Provider to the rescue.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#if INTERACTIVE
#I @"..\packages\FSharp.Data.Toolbox.Twitter.0.19\lib\net40\"
#I @"..\packages\FSharp.Data.2.4.2\lib\net45\"
#r "FSharp.Data.Toolbox.Twitter.dll"
#r "FSharp.Data.dll"
#else
module Twitter
#endif

open FSharp.Data.Toolbox.Twitter

let key = "..."
let secret = "..."
let accessToken = "..."
let oauthToken = "..."

let twitter = Twitter.AuthenticateAppSingleUser(key, secret, accessToken, oauthToken)
twitter.RequestRawData("https://api.twitter.com/1.1/account/verify_credentials.json", []) |> printfn "%A"

Optimize Your Images With Lossless Compression

One way to make your web pages load faster is to use lossless compression.

On Linux there’s a tool called “jpegoptim” to losslessly optimize JPEG files. You can install using apt-get or yum.

Here’s how to optimize all jpg files in a directory:

find /jpg_directory/ -maxdepth 1 -type f -name "*.jpg" -exec jpegoptim --strip-all {} \;

You can remove -maxdepth 1 if you want to process the JPEGs in the subfolders recursively.

Similarly, the tool to optimize PNG files is “optipng”.

Display RSS Feed Using JQuery

While working on a static site using Middleman, I need to display an RSS feed using JavaScript.

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
<script>
    $.ajax({
        type: 'GET',
        url: '/newsfeed.rss',
        dataType: 'xml',
        success: function(data) {
            var liHTML_template = '<li><div class="media"><div class="media-ele"></div><div class="media-bd"><div class="box"><div class="box-hd box-hd_min threeLines"><h4 class="hdg"><a href="{link}" target="_blank">{title}</a></h4></div><div class="box-ft"><p class="txt_miniscule">{pubDate}</p></div></div></div></div></li>';
            var ulHTML = "";
            $(data).find("item").each(function () {
                var title = $(this).find("title").text();
                var link = $(this).find("link").text();
                var pubDate = $(this).find("pubDate").text();

                ulHTML += liHTML_template.replace('{title}', title).replace('{link}', link).replace('{pubDate}', pubDate);
            });

            $('#FeedContainer > div > div.feed-bd > ul').html(ulHTML);

        },
        error: function(jqXHR, textStatus, errorThrown ) {
            console.log(errorThrown);
        }

    });
</script>

How to Read the Toronto Star on Your Phone… As a Web Developer

The Toronto Star is one of my favourite web sites that I visit daily. Unfortunately their mobile site http://m.thestar.com is probably one of the slowest in the world. It takes at least 5 seconds for my Android phone to load the home page. Worse yet, after you finish reading an article and hit the back button to return to the home page, it takes another 5 seconds, and this time without the Please-Wait spining icon. I often find myself hitting the back button twice and leave their site altogether.

They’re current rolling out a beta of a new design. But the load time doesn’t improve much.

Looking at the source, I notice the home page has a lot of fancy stuff in Angular. The bad news is that until my Android phone has the power of a desktop computer, it will never be able to load at a decent speed.

So I decided to tackle this myself, and deployed the following Sinatra site on Heroku at http://mstar.herokuapp.com/ :-)

thestar.rb
1
2
3
4
5
6
7
8
require 'sinatra'
require 'httpclient'
require 'json'

get '/' do
  @items = JSON.parse(HTTPClient.new.get_content 'http://m.thestar.com/content/TheStarMobile/home.jsonp.html')['items']
  haml :index
end

And here’s the HAML template:

index.haml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
!!!
%html{:lang => "en"}
  %head
    %meta{:charset => "utf-8"}/
    %meta{:name => "viewport", :content => "width=device-width, initial-scale=1"}/
    %title Toronto Star
    %link{:href => "//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css", :rel => "stylesheet"}/
    %base{:href => "//m.thestar.com"}/
  %body
    .container
      - @items.each do |i|
        .bg-primary
          =i['sectiontitle']
        %hr
          - i['assets'].each do |as|
            %a{:href => "/#/article/#{as['asset']['url']}"}
              = as['asset']['headline']
            %br
            %img{:src => "https://images1-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&resize_w=250&refresh=3600&url=http%3A%2F%2Fm.thestar.com#{as['asset']['image']['url']}"}
            %div
              = as['asset']['abstract']
            %hr
      %footer
        %p &copy; The Toronto Star 2016

Note I use GoogleUserContent.com server to resize the images. More details can be found here.

Accelerated Mobile Pages(AMP): Week 3

Here are some notes from last week. Week 3 working on AMP.

Top Stories with AMP

Google has already taken AMP to the next level. “Top Stories with AMP” is a kind of enhanced AMP, so to speak. They will be shown as a carousel in Google Search results.

In order qualify for Top Stories, your AMP needs to meet some additional specs. It must have a Structured Data Markup that include the following mandatory information:

  • An image at least 696 pixels wide
  • Publisher information (including a logo no bigger than 600x60)
  • Author. Although Google’s official guidelines require a type of schema.org/Person, I noticed schema.org/Organization also passes the Structured Data validation
  • datePublished in ISO 8601 format

SVG image

Other than , inline SVG is allowed, although only a subset of attributes are supported.

Accelerated Mobile Pages(AMP): First Impression as a Developer

Two days ago I started developing my first AMP project for a mobile web site.

Obviously I’m still a beginner, but I thought it would be helpful for other people if I document some of my experience from a developer’s perspective.

AMP is simplified, but highly regulated HTML

One such example is CSS. You can only use one internal stylesheet in the header: <style amp-custom>

External CSS files are not allowed, neither are Inline style attributes in tags.

<link href="css/bootstrap.min.css" rel="stylesheet"> (Not allowed) <div style="margin-left:30px;"> (Not allowed)

Validation is easy, but the error messages aren’t

You can append #development=1 to your URL, and then go to Chrome DevTools console to see the validation results. As simple as that.

However, the error messsages can be very confusing. Some examples are:

If you have <amp-img src="/logo.png" />, you will get The implied layout 'CONTAINER' is not supported by tag 'amp-img'. where the actual cause is the missing height attribute, which is mandatory for the <amp-img> tag. (You use <amp-img> instead of <img>, by the way.)

If you mistakenly added an external stylesheet file as in <link href="css/bootstrap.min.css" rel="stylesheet">, you will be presented with a mysterious The attribute 'href' in tag 'link rel=stylesheet for fonts' is set to the invalid value 'css/bootstrap.min.css'. That’s because <link ref> can only be used for fonts.

How do I see AMP in Google search results

It’s not live in regular Google search yet. Currently you can view the demo at http://g.co/ampdemo

Search for some popular keywords, such as one of my favourite, “Electric car”.

Building a Free Conference Call System

I built a conference call system on my Windows 7 laptop, using Oracle VirtualBox, Asterisk and FreePBX. The combination of GetOnSIP and IPKall offers PSTN access. And a free account at CallCentric allows callers to dial in using iNum or SIP.

It’s a pure hobbyist project. Since I’m familiar with all the pieces, on one gloomy winter day I said to myself: “Why not put them together to make something fun?”

Install VirtualBox

Asterisk, the open source PBX system with built-in conference features, only lives on Linux. But I just have a Windows 7 laptop at my disposal for this endeavour. Hence VirtualBox.

After downloading it from https://www.virtualbox.org/wiki/Downloads , the installation is straightforward.

Install AsteriskNow

AsteriskNow is a bundled distro of Centos, Asterisk and FreePBX. Get it from here and follow the instructions: https://wiki.asterisk.org/wiki/display/AST/Installing+AsteriskNOW

Change VirtualBox network settings

FreePBX is web based admin GUI for Asterisk. Since my VirtualBox guest system is only a terminal without GUI, I can’t access any web pages from the VM. I did try a text-based browser called Lynx but unfortunately the FreePBX admin interface requires JavaScript, which Lynx doesn’t support.

Next, I tried to access the Admin interface http://10.0.2.15 from my Windows host, but it was unreachable.

Then I realized 10.0.2.15 was just a VirtualBox IP. The solution is quite simple, just change the network adapter from “NAT” to “Bridged Adapter”. This can be done from “Machine” -> “Settings” -> “Network”.

Now my guest system has an IP that’s accessible from my Windows hosts.

Create a Conference in FreePBX

Go to FreePBX admin GUI at http://IP_OF_YOUR_ASTERISK, then click “Applications” -> “Conferences” to add a new conference.

At this point, my conference is up and running. But it can’t be reached by the outside world yet.

Sign up a GetOnSIP account

GetOnSIP offers free SIP accounts. I signed up one to be used as a new trunk in FreePBX. Follow these offical instructions.

Now callers can reach my conference via SIP calls.

Sign up a IPKall account

To allow callers to reach my GetOnSIP account using PSTN (or DID, or regular phone line), I created an account at IPKall, which assigned a 206 Washington state area code phone number to be forwarded to my GetOnSIP SIP address.

I’m still not quite satisfied with my work yet. It would be nice if I can offer local dial in numbers to avoid long distance charges.

The web site http://e164.org used to offer a free service to register your phone number in their ENUM database, which mapped DID numbers to their corresponding SIP URLs. The ENUM database will be queried by other VoIP services such as SIPBroker. The latter provides local dial in numbers. Unfortunately, the registration function at http://e164.org no longer works.

As a workaround, I resorted to CallCentric.

Sign up a CallCentric free account

DID numbers are not free at CallCentric, but iNum numbers are. Callers can reach my conference room by calling the iNum number.

I know four ways to reach an iNum number:

  • If your VoIP phone provider (such as CallCentric, voip.ms, etc.) supports it, you can dial it like +883 510 xxxxxxxxx. In the United States and Canada, + means 011. The calls are free of charge, although they look like international numbers.
  • Use one of the local dial in numbers for iNum
  • Dial via SipBroker (code *8124)
  • Use SIP URI 8835100xxxxxxxx@inum.net if you know how to make a SIP call. One option is to use GetOnSIP.

Both iNum and SipBroker offer their local dial in numbers, which can complement each other.

Alternatively, because CallCentric support SIP calls, it’s also possible to call your CallCentric number 1777xxxxxxx using one of the following methods:

  • SIP peering if your VoIP provider supports it, **275 *462 1777xxxxxxx
  • Again, SipBroker (code *462)
  • SIP URI 1777xxxxxxx@in.callcentric.com

Adding CallCentric to FreePBX

Follow the official instructions from CallCentric to set it up as a new trunk in FreePBX.

In my case, I only need to configure trunk (Step 1) and inbound route (Step 4). I don’t really care about outbound route or extension.

My Asterisk version is 13, so I used the configuration for the latest Asterisk in the PEER Details section.

I also route all incoming calls to the Conference I created earlier.

With that, I added local dial in numbers to my conference room.

Sadly, the free plan from CallCentric only include two channels, meaning only two callers can dial in simultaneously. And additional channels are quite expansive.

But I had to stop it there. It’s not perfect, but at least two DID callers can avoid long distance.

Conclusion

My conference room is up and running! And completely free of charge, both for me and my callers!

Asterisk is very powerful, enough for serious business usage. I’m just dipping into a fraction of it and already amazed by its capability.

Use X-Forwarded-For in Varnish Apache/NCSA Logs

In Varnish 4.0, the command varnishncsa let you display logs in Apache style format.

By default, the log format is %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-agent}i".

But if you need to use “X-Forwarded-For” IP instead of %h (Remote host), this is how:

1
varnishncsa -F '%{X-Forwarded-For}i %l %u %t "%r" %s %b "%{Referer}i" "%{User-agent}i"'

Detect and Redirect Mobile Users With IIS URL Rewrite

If your mobile site uses “Separate URLs” instead of “Responsive web design” or “Dynamic serving” (as defined by Google’s mobile guide https://developers.google.com/webmasters/mobile-sites/mobile-seo/overview/select-config), you’ll have to detect and redirect mobile users.

Thanks to the idea of http://detectmobilebrowsers.com/, this can be done using the IIS URL Rewrite module.

A sample web.config file is available at http://detectmobilebrowsers.com/download/iis

web.config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <clear />
        <rule name="MobDedect" stopProcessing="true">
          <match url=".*" ignoreCase="false" />
          <conditions logicalGrouping="MatchAny">
            <add input="{HTTP_USER_AGENT}" pattern="(android|bb\d+|meego).+mobile|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" />
            <add input="{HTTP_USER_AGENT}" pattern="^(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\-)" />
          </conditions>
          <action type="Rewrite" url="http://detectmobilebrowser.com/mobile" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

Unfortunately, this will cause my IIS 7.5 site to crash with a message “500 URL Rewrite Module Error.”

The culprit was the RegEx in the second condition, which exceeded some kind of the maximum length allowed for URL Rewrite. I worked around it by splitting it into multiple conditions, like this:

linenos:false
1
2
3
4
5
6
7
8
9
10
11
        <rule name="MobDedect" stopProcessing="true">
            <match url=".*" ignoreCase="false" />
            <conditions logicalGrouping="MatchAny">
                <add input="{HTTP_USER_AGENT}" pattern="(android|bb\d+|meego).+mobile|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" />
                <add input="{HTTP_USER_AGENT}" pattern="^(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)" />
                <add input="{HTTP_USER_AGENT}" pattern="^(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)" />
                <add input="{HTTP_USER_AGENT}" pattern="^(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))" />
                <add input="{HTTP_USER_AGENT}" pattern="^(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\-)" />
            </conditions>
            <action type="Rewrite" url="http://m.mysite.com" />
        </rule>

Sometimes mobile users want the ability to return to the desktop version after they landed on the mobile site. We can add a parameter to the querystring to notifiy the web server, such as http://m.mysite.com?fullsite

To make it happen, a new rule is needed.

linenos:false
1
2
3
4
5
6
7
        <rule name="m -> www" stopProcessing="true">
            <match url=".*" />
            <conditions>
                <add input="{QUERY_STRING}" pattern="fullsite" />
            </conditions>
            <action type="None" />
        </rule>

Better yet, let’s add support for cookies. And now our web.config file looks like this:

web.config
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
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
        <rewrite>
            <rules>
                <rule name="has fullsite cookie" stopProcessing="true">
                    <match url=".*" />
                    <conditions>
                        <add input="{HTTP_COOKIE}" pattern="fullsite=yes" />
                    </conditions>
                    <action type="None" />
                </rule>
                <rule name="m -> www; set cookie" stopProcessing="true">
                    <match url=".*" />
                    <conditions>
                        <add input="{QUERY_STRING}" pattern="fullsite" />
                    </conditions>
                    <serverVariables>
                        <set name="RESPONSE_Set_Cookie" value="fullsite=yes" />
                    </serverVariables>
                    <action type="None" />
                </rule>
                <rule name="www -> m" stopProcessing="true">
                    <match url=".*" ignoreCase="false" />
                    <conditions logicalGrouping="MatchAny">
                        <add input="{HTTP_USER_AGENT}" pattern="(android|bb\d+|meego).+mobile|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" />
                        <add input="{HTTP_USER_AGENT}" pattern="^(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)" />
                        <add input="{HTTP_USER_AGENT}" pattern="^(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)" />
                        <add input="{HTTP_USER_AGENT}" pattern="^(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))" />
                        <add input="{HTTP_USER_AGENT}" pattern="^(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\-)" />
                    </conditions>
                    <action type="Redirect" url="http://m.mysite.com/{R:0}" />
                </rule>
            </rules>
        </rewrite>
  </system.webServer>
</configuration>