Magento 2 - Varnish Device Detection

Magento 2 Varnish Device Detection

Today we are going to talk about how to serve different content on the same URL based on the different devices when full page cache is on using Varnish. We had the similar requirement on one of our clients website where they wanted to serve different content on their home, category and product pages based on the device. The problem they were facing was that varnish doesn’t differentiate between devices so it will be serve the same content which was cached by the first visitor.

For example if the website is first viewed on mobile then it will serve the same content to other users regardless of their device. If you have different content and different size images for mobile, tablet and desktop then your site will give bad user experience especially when mobile cached page is served to desktop visitor and vice versa.

Magento 2 out of the box, serve different content on the same URL based on the following parameters using context variable  -:

  • Customer group
  • Selected language
  • Selected store
  • Selected currency
  • Whether a customer is logged in or not

More information can be found on context variable and how they can be used using the following link here.

Context Variable to detect mobile device

Initially we thought context variable should be our answer because we should be able to detect device using the following function in PHP and it will serve different content based on the device

 /** * @return bool */ private function isMobile(){ //return true; $regex_match = "/(nokia|iphone|ipad|motorola|^mot-|softbank|foma|docomo|kddi|up.browser|up.link|" . "htc|dopod|blazer|netfront|helio|hosin|huawei|novarra|CoolPad|webos|techfaith|palmsource|" . "blackberry|alcatel|amoi|ktouch|nexian|samsung|^sam-|s[cg]h|^lge|ericsson|philips|sagem|wellcom|bunjalloo|maui|" . "symbian|smartphone|mmp|midp|wap|phone|windows ce|iemobile|^spice|^bird|^zte-|longcos|pantech|gionee|^sie-|portalmmm|" . "jigs browser|hiptop|^ucweb|^benq|haier|^lct|operas*mobi|opera*mini|320x320|240x320|176x220" . ")/i"; //DISPLAY DESKTOP THEME ON HAUWEI TAB if(preg_match("/(huaweimediapad)/i", strtolower($_SERVER['HTTP_USER_AGENT']))){ return false; } if (preg_match($regex_match, strtolower($_SERVER['HTTP_USER_AGENT']))) { return true; } if ((strpos(strtolower($_SERVER['HTTP_ACCEPT']),'application/vnd.wap.xhtml+xml') > 0) or ((isset($_SERVER['HTTP_X_WAP_PROFILE']) or isset($_SERVER['HTTP_PROFILE'])))) { return true; } if(stripos($_SERVER['HTTP_USER_AGENT'],"Android") && stripos($_SERVER['HTTP_USER_AGENT'],"mobile")){ return true; } if(stripos($_SERVER['HTTP_USER_AGENT'],"Android")){ return false; } $mobile_ua = strtolower(substr($_SERVER['HTTP_USER_AGENT'], 0, 4)); $mobile_agents = array( 'w3c ','acs-','alav','alca','amoi','audi','avan','benq','bird','blac', 'blaz','brew','cell','cldc','cmd-','dang','doco','eric','hipt','inno', 'ipaq','java','jigs','kddi','keji','leno','lg-c','lg-d','lg-g','lge-', 'maui','maxo','midp','mits','mmef','mobi','mot-','moto','mwbp','nec-', 'newt','noki','oper','palm','pana','pant','phil','play','port','prox', 'qwap','sage','sams','sany','sch-','sec-','send','seri','sgh-','shar', 'sie-','siem','smal','smar','sony','sph-','symb','t-mo','teli','tim-', 'tosh','tsm-','upg1','upsi','vk-v','voda','wap-','wapa','wapi','wapp', 'wapr','webc','winw','winw','xda ','xda-'); if (in_array($mobile_ua,$mobile_agents)) { return true; } if (isset($_SERVER['ALL_HTTP']) && strpos(strtolower($_SERVER['ALL_HTTP']),'OperaMini') > 0) { return true; } return false; }

We created our plugin using the following code as specified in Magento 2 documentation but soon we realised that it is not going to work. Because X-Magento-Vary cookie which gets used to generate different content for the same URL would not be there because that cookie gets created only on PUT / POST requests and NOT on GET requests.
 namespace ScommerceContextVariablePlugin; use MagentoFrameworkAppHttpContext as HttpContext; /**
* Plugin on MagentoFrameworkAppHttpContext
*/
class ContextVariablePlugin
{ const USER_AGENT_CONTEXT_VARIABLE = 'USER_AGENT_CONTEXT_VARIABLE'; const DEFAULT_VALUE = 'desktop'; /** * @param HttpContext $subject * @return array */ public function beforeGetVaryString(HttpContext $subject) { //Identifying if user is on mobile browser or not if($this->isMobile()) { $browserStatus = 'mobile'; } else{ $browserStatus = self::DEFAULT_VALUE; } $subject->setValue( self::USER_AGENT_CONTEXT_VARIABLE, $browserStatus, self::DEFAULT_VALUE ); return []; }
}

Ultimate and Optimised Solution for Varnish and Mobile Device Detection


The solution which ultimately worked for us involved the following steps -:
  1. Create devicedetect.vcl to determine device used for the given page
  2. Detect the client using devicedetect.vcl and call it
  3. Device Detect VCL file will help you set req.http.X-UA-Device to find out if it is a mobile or tablet or desktop device
  4. Check req.http.X-UA-Device and change VARY headers to generate different content

Device Detect VCL file


The following VCL file will provide you the correct header information which can be further set in varnish.vcl file and can be used in your code as well to determine device type -:
sub devicedetect { unset req.http.X-UA-Device; set req.http.X-UA-Device = "pc"; # Handle that a cookie may override the detection alltogether. if (req.http.Cookie ~ "(?i)X-UA-Device-force") { /* ;?? means zero or one ;, non-greedy to match the first. */ set req.http.X-UA-Device = regsub(req.http.Cookie, "(?i).*X-UA-Device-force=([^;]+);??.*", "1"); /* Clean up our mess in the cookie header */ set req.http.Cookie = regsuball(req.http.Cookie, "(^|; ) *X-UA-Device-force=[^;]+;? *", "1"); /* If the cookie header is now empty, or just whitespace, unset it. */ if (req.http.Cookie ~ "^ *$") { unset req.http.Cookie; } } else { if (req.http.User-Agent ~ "(compatible; Googlebot-Mobile/2.1; +http://www.google.com/bot.html)" || (req.http.User-Agent ~ "(Android|iPhone)" && req.http.User-Agent ~ "(compatible.?; Googlebot/2.1.?; +http://www.google.com/bot.html") || (req.http.User-Agent ~ "(iPhone|Windows Phone)" && req.http.User-Agent ~ "(compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm")) { set req.http.X-UA-Device = "mobile-bot"; } elsif (req.http.User-Agent ~ "(?i)(ads|google|bing|msn|yandex|baidu|ro|career|seznam|)bot" || req.http.User-Agent ~ "(?i)(baidu|jike|symantec)spider" || req.http.User-Agent ~ "(?i)pingdom" || req.http.User-Agent ~ "(?i)facebookexternalhit" || req.http.User-Agent ~ "(?i)scanner" || req.http.User-Agent ~ "(?i)slurp" || req.http.User-Agent ~ "(?i)(web)crawler") { set req.http.X-UA-Device = "bot"; } elsif (req.http.User-Agent ~ "(?i)ipad") { set req.http.X-UA-Device = "tablet-ipad"; } elsif (req.http.User-Agent ~ "(?i)ip(hone|od)") { set req.http.X-UA-Device = "mobile-iphone"; } /* how do we differ between an android phone and an android tablet? http://stackoverflow.com/questions/5341637/how-do-detect-android-tablets-in-general-useragent */ elsif (req.http.User-Agent ~ "(?i)android.*(mobile|mini)") { set req.http.X-UA-Device = "mobile-android"; } // android 3/honeycomb was just about tablet-only, and any phones will probably handle a bigger page layout. elsif (req.http.User-Agent ~ "(?i)android 3") { set req.http.X-UA-Device = "tablet-android"; } /* Opera Mobile */ elsif (req.http.User-Agent ~ "Opera Mobi") { set req.http.X-UA-Device = "mobile-smartphone"; } // May very well give false positives towards android tablets. Suggestions welcome. elsif (req.http.User-Agent ~ "(?i)android") { set req.http.X-UA-Device = "tablet-android"; } elsif (req.http.User-Agent ~ "PlayBook; U; RIM Tablet") { set req.http.X-UA-Device = "tablet-rim"; } elsif (req.http.User-Agent ~ "hp-tablet.*TouchPad") { set req.http.X-UA-Device = "tablet-hp"; } elsif (req.http.User-Agent ~ "Kindle/3") { set req.http.X-UA-Device = "tablet-kindle"; } elsif (req.http.User-Agent ~ "Touch.+Tablet PC" || req.http.User-Agent ~ "Windows NT [0-9.]+; ARM;" ) { set req.http.X-UA-Device = "tablet-microsoft"; } elsif (req.http.User-Agent ~ "Mobile.+Firefox") { set req.http.X-UA-Device = "mobile-firefoxos"; } elsif (req.http.User-Agent ~ "^HTC" || req.http.User-Agent ~ "Fennec" || req.http.User-Agent ~ "IEMobile" || req.http.User-Agent ~ "BlackBerry" || req.http.User-Agent ~ "BB10.*Mobile" || req.http.User-Agent ~ "GT-.*Build/GINGERBREAD" || req.http.User-Agent ~ "SymbianOS.*AppleWebKit") { set req.http.X-UA-Device = "mobile-smartphone"; } elsif (req.http.User-Agent ~ "(?i)symbian" || req.http.User-Agent ~ "(?i)^sonyericsson" || req.http.User-Agent ~ "(?i)^nokia" || req.http.User-Agent ~ "(?i)^samsung" || req.http.User-Agent ~ "(?i)^lg" || req.http.User-Agent ~ "(?i)bada" || req.http.User-Agent ~ "(?i)blazer" || req.http.User-Agent ~ "(?i)cellphone" || req.http.User-Agent ~ "(?i)iemobile" || req.http.User-Agent ~ "(?i)midp-2.0" || req.http.User-Agent ~ "(?i)u990" || req.http.User-Agent ~ "(?i)netfront" || req.http.User-Agent ~ "(?i)opera mini" || req.http.User-Agent ~ "(?i)palm" || req.http.User-Agent ~ "(?i)nintendo wii" || req.http.User-Agent ~ "(?i)playstation portable" || req.http.User-Agent ~ "(?i)portalmmm" || req.http.User-Agent ~ "(?i)proxinet" || req.http.User-Agent ~ "(?i)windows ?ce" || req.http.User-Agent ~ "(?i)winwap" || req.http.User-Agent ~ "(?i)eudoraweb" || req.http.User-Agent ~ "(?i)htc" || req.http.User-Agent ~ "(?i)240x320" || req.http.User-Agent ~ "(?i)avantgo") { set req.http.X-UA-Device = "mobile-generic"; } }
}

Varnish VCL file


The following changes you need to do it in your Varnish VCL file to get different content based on your device detected or value set in X-UA-Device variable -:
 include "devicedetect.vcl";
sub vcl_recv { call devicedetect;
}
# req.http.X-UA-Device is copied by Varnish into bereq.http.X-UA-Device # so, this is a bit conterintuitive. The backend creates content based on the normalized User-Agent,
# but we use Vary on X-UA-Device so Varnish will use the same cached object for all U-As that map to
# the same X-UA-Device.
# If the backend does not mention in Vary that it has crafted special
# content based on the User-Agent (==X-UA-Device), add it.
# If your backend does set Vary: User-Agent, you may have to remove that here.
sub vcl_backend_response { if (bereq.http.X-UA-Device) { if (!beresp.http.Vary) { # no Vary at all set beresp.http.Vary = "X-UA-Device"; } elsif (beresp.http.Vary !~ "X-UA-Device") { # add to existing Vary set beresp.http.Vary = beresp.http.Vary + ", X-UA-Device"; } } # comment this out if you don't want the client to know your classification set beresp.http.X-UA-Device = bereq.http.X-UA-Device;
} # to keep any caches in the wild from serving wrong content to client #2 behind them, we need to
# transform the Vary on the way out.
sub vcl_deliver { if ((req.http.X-UA-Device) && (resp.http.Vary)) { set resp.http.Vary = regsub(resp.http.Vary, "X-UA-Device", "User-Agent"); }
}

For perfomance improvement on Magento 2, please checkout out section 9 of our tips and tricks SEO article
Please email us at core@scommerce-mage.com and we will be more than happy to help you resolve any of the above issues or any other issue you are facing in regards to your website in Magento 2.
 
 

Similar Posts