A Basic Script to Switch Between Mobile and Desktop Layout

On this page a basic PHP script will be presented to switch between a mobile and desktop version of a webpage. The main target for creating this script was to present a working model that can be used for educational purposes. This way it may become clear how such an approach can be realized and students can experience the main functions that we like to emphasize as important, which are:

  • it automatically detects what kind of browser a visitor uses;
  • it presents the appropriate version of the webpage for desktop and mobile;
  • the automated presented desktop and mobile versions of the webpage will use the same filename (One Web);
  • the script allows the visitor to overrule this automatically determined choice;
  • the visitor’s preference will be stored in a cookie so that at a next visit with the same browser he does not need to use the switch again;
  • since this last function cannot be done without redirection there is a canonical link to the main version of the webpage present (One Web).

A working example of this script can be found here

The code

Click on the filenames below to see the code of the corresponding file and the companion instructions.

.htaccess

The .htaccess file needs the following code to set the cache-control for the stylesheets. Since cache-control for PHP needs to be set with PHP you will not find the cache-control settings for the webpages in the .htacces file below.

In case you don’t have access to put a .htacces file on your server, you will find special stylesheets below that contain the cache-control with PHP.

download
0001<ifModule mod_headers.c>
0002 <FilesMatch ".(css)$">
0003 Header set Expires "Thu, 15 Apr 2040 20:00:00 GMT"
0004 Header set Content-Type "text/css; charset = utf-8"
0005 Header set Cache-Control "max-age = 1100"
0006 </FilesMatch>
0007</ifModule>

page.php

This webpage is a template for all the pages in the site. There is no need to bother about the canonical link, the script will automatically take care of it, same as for any duplicate page. The cookie will be automatically placed by the script as well, for all versions of the web page, desktop and mobile and duplicates.

The PHP code is kept out of the HTML as much as possible without losing the aim to make clear what needs to be done and how this can be done. This means there is some PHP in the webpages that can’t be left out:

  • See the comments inside the code of the webpage, don’t leave out to read them. The comments at line 9 are especially important and are about the PHP variable at line 10.
  • You can edit the value of the lifetime of the cookie in the PHP code at line 11. The companion comments explain how.
  • The switch for mobile/desktop looks like this:
    <?php echoSwitch('Desktop Page', 'Mobile Page'); ?>
    The content for either desktop or mobile can be placed between the single quotation marks. Here the content on desktop will be ‘Desktop Page’ and on mobile it will be ‘Mobile Page’. You can place this PHP code where ever you like in the content of the webpage, just make sure you paste it from the first < up to the close >. Any content outside of this code will be shown on both desktop and mobile.
  • At line 12 you will find the $maxage variable so that you can set the cache-control for each page as you like
  • At line 34 another PHP code begins that switches desktop/mobile content. If you look closely you will see that the single quotation marks as you can see them in the sample just mentioned, are missing inside the PHP code at line 34 and are replaced with HEREDOC notation. Read the comments that start at line 54 to learn more about this.

download
0001<?php
0002/*
0003* This script has been written by Alex Pot of SmartScripts (www.smartscripts.nl)
0004* The detection of $_GET['m'] and $_COOKIE['m'] is an adapted version of a script by Phil Archer (http://philarcher.org/diary/2011/mobilecontentandstyle/)
0005* The lightweight class for detection of mobile browsers can be found here: http://code.google.com/p/php-mobile-detect/
0006* You are free to adapt and use this script for your own projects, on the one condition that you keep these credits intact
0007*/
0008 
0009//if your files are in the rootfolder of your site, $subdir has no value written between the quotation marks, like this ''; else you name the subdir, like so: '/namesubdir' (note it has a slash at the start, BUT NOT AT THE END!). This sample suggests to name the folder 'switch', this is the value used below. This is correct as long as the folder is placed in the root and not in a subfolder; else the value should be '/namesubfolder/switch'.
0010$subdir = '/switch'; //change this according to the name of the folder your files are in (see also instruction above)
0011$cookie_lifetime = 60*60*24*30; //you may change the numbers: sec x min x hrs x days = how long (in seconds) must the cookie with the user preference persist?
0012$maxage = 60; //this is the value (in seconds) for the max-age header that the switcher-script sends. This time will determine how long the version op the page in your browser cache will stay unrefreshed. Note: changes in the user preference for the lay-out will only become definitive after this number of seconds.
0013 
0014//==================== DON'T CHANGE PHP VARIABLES BELOW THIS LINE ==========================
0015 
0016$root = $_SERVER['DOCUMENT_ROOT'] . $subdir;
0017$file = __FILE__;
0018 
0019//include required by the mobile-desktop switch script:
0020include($root . '/inc/switcher.php');
0021?>
0022<!DOCTYPE html PUBLIC "-//W3CDTD XHTML Basic 1.1EN"
0023  "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">
0024<html xmlns = "http://www.w3.org/1999/xhtml" xml:lang = "en">
0025<head>
0026  <title><?php echoSwitch('Desktop Page', 'Mobile Page'); ?></title>
0027  <link rel = "canonical" href = "<?php echo $full_path; ?>" />
0028  <link rel = "stylesheet" type = "text/css" href = "<?php echoSwitch('screen_styles', 'mobile_styles'); ?>.css" />
0029</head>
0030<body>
0031<p id = "mob_switch"><a href = "<?php echo $current_file['basename']; ?>?m = <?php echoSwitch('1', '0'); ?>"><?php echoSwitch('Mobile View', 'Desktop View'); ?></a> (changes will become definitive after a delay of <?php echo $maxage; ?> seconds or a reload of the page)</p>
0032<h1><?php echoSwitch('Desktop', 'Mobile'); ?> Presentation</h1>
0033<p>This static page represents the <?php echoSwitch('desktop', 'mobile'); ?> presentation of the resource available at <?php echo $full_path; ?>.</p>
0034<?php
0035echoSwitch(
0036<<<DESKTOP
0037 
0038<h2>Content with quotation marks</h2>
0039<p id = "sample">In 'reprehenderit' in voluptate lorem ipsum dolor sit amet: "consectetur adipisicing elit". Ut aliquip ex ea commodo consequat. Sed do eiusmod tempor incididunt cupidatat non proident, in reprehenderit in voluptate. Ut enim ad minim veniam, ullamco laboris nisi mollit anim id est laborum.</p>
0040<p>Eu fugiat nulla pariatur. Duis aute irure dolor lorem ipsum dolor sit amet, sunt in culpa. Velit esse cillum dolore excepteur sint occaecat qui officia deserunt. Eu fugiat nulla pariatur. Duis aute irure dolor in reprehenderit in voluptate ullamco laboris nisi.</p>
0041<p>Ut aliquip ex ea commodo consequat. Ut enim ad minim veniam, ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, in reprehenderit in voluptate excepteur sint occaecat. Ut aliquip ex ea commodo consequat. Duis aute irure dolor ut enim ad minim veniam, ullamco laboris nisi.</p>
0042 
0043DESKTOP
0044,
0045<<<MOBILE
0046 
0047<h2>Content with quotation marks</h2>
0048<p id = "sample">In 'reprehenderit' in voluptate lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut aliquip ex ea commodo consequat. Sed do eiusmod tempor incididunt cupidatat non proident, in reprehenderit in voluptate. Ut enim ad minim veniam, ullamco laboris nisi mollit anim id est laborum.</p>
0049 
0050MOBILE
0051); ?>
0052<p><a href = "page2.php">PAGE 2</a></p>
0053 
0054<!-- Read this comment about placing content with quotation marks:
0055 
0056Take a look above at for instance in line 26 where is written 'Desktop Page' and 'Mobile Page'. The text Desktop Page will be presented on desktop and the text Mobile Page on mobile. If you replace that text with a larger block of content, then that is the content that wil be presented instead. If that contant contains single quotation marks, you are in trouble: the browser will read these as PHP instead of the HTML you mean it to be.
0057 
0058If content has been placed via the PHP HEREDOC-notation (see the section above with the header 'Content with quotation marks'), then that way quotation marks in the content won't pose a problem anymore. As you can see in that section, that does contain these single quotation marks.
0059 
0060The HEREDOC-notation is done by replacing the start quotation marks with what is written instead at line 36 and at the end at line 43.
0061 
0062It is not impossible to place content in PHP code that uses single quotation marks like in the sample at line 26. All you need to do is place one backslash before any single quotation mark inside the content like this Tim\'s. This will make the browser present the content correctly. It may be clear that using HEREDOC-notation instead for larger pieces of content, is safer.
0063 
0064Note that PHP may use double quotation marks instead of single ones. In that case an HTML attribute like an ID or a class may cause similar problems. You can solve this with a backslash before the double quotaton marks like this id = \"test\".
0065 
0066By the way: you may use the same HEREDOC notations as many times as you like in one page. So DESKTOP and MOBILE as markers don't need to be unique. You may also use just D instead of DESKTOP and just M instead of MOBILE, or another variation, that's up to you, just keep the syntax similar.
0067 
0068End of comment -->
0069 
0070</body>
0071</html>

page2.php

This page is created by duplicating the page above. Just make sure that any new webpage that you create will be a duplicate of that page as well and you will be fine. Don’t forget to fill in the page title for desktop and mobile. There is no need to bother about the canonical link, the script will take care of it. Also the cookie will be placed by the script.

download
0001<?php
0002/*
0003* This script has been written by Alex Pot of SmartScripts (www.smartscripts.nl)
0004* The detection of $_GET['m'] and $_COOKIE['m'] is an adapted version of a script by Phil Archer (http://philarcher.org/diary/2011/mobilecontentandstyle/)
0005* The lightweight class for detection of mobile browsers can be found here: http://code.google.com/p/php-mobile-detect/
0006* You are free to adapt and use this script for your own projects, on the one condition that you keep these credits intact
0007*/
0008 
0009//if your files are in the rootfolder of your site, $subdir has no value written between the quotation marks, like this ''; else you name the subdir, like so: '/namesubdir' (note it has a slash at the start, BUT NOT AT THE END!). This sample suggests to name the folder 'switch', this is the value used below. This is correct as long as the folder is placed in the root and not in a subfolder; else the value should be '/namesubfolder/switch'.
0010$subdir = '/switch'; //change this according to the name of the folder your files are in (see also instruction above)
0011$cookie_lifetime = 60*60*24*30; //you may change the numbers: sec x min x hrs x days = how long (in seconds) must the cookie with the user preference persist?
0012$maxage = 60; //this is the value (in seconds) for the max-age header that the switcher-script sends. This time will determine how long the version op the page in your browser cache will stay unrefreshed. Note: changes in the user preference for the lay-out will only become definitive after this number of seconds.
0013 
0014//==================== DON'T CHANGE PHP VARIABLES BELOW THIS LINE ==========================
0015 
0016$root = $_SERVER['DOCUMENT_ROOT'] . $subdir;
0017$file = __FILE__;
0018 
0019//include required by the mobile-desktop switch script:
0020include($root . '/inc/switcher.php');
0021?>
0022<!DOCTYPE html PUBLIC "-//W3CDTD XHTML Basic 1.1EN"
0023  "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">
0024<html xmlns = "http://www.w3.org/1999/xhtml" xml:lang = "en">
0025<head>
0026  <title><?php echoSwitch('Desktop Page 2', 'Mobile Page 2'); ?></title>
0027  <link rel = "canonical" href = "<?php echo $full_path; ?>" />
0028  <link rel = "stylesheet" type = "text/css" href = "<?php echoSwitch('screen_styles', 'mobile_styles'); ?>.css" />
0029</head>
0030<body>
0031<p id = "mob_switch"><a href = "<?php echo $current_file['basename']; ?>?m = <?php echoSwitch('1', '0'); ?>"><?php echoSwitch('Mobile View', 'Desktop View'); ?></a> (changes will become definitive after a delay of <?php echo $maxage; ?> seconds or a reload of the page)</p>
0032<h1><?php echoSwitch('Desktop', 'Mobile'); ?> Presentation of page 2</h1>
0033<p>This static page represents the <?php echoSwitch('desktop', 'mobile'); ?> presentation of the resource available at <?php echo $full_path; ?>.</p>
0034<?php
0035echoSwitch(
0036<<<DESKTOP
0037 
0038<h2>Lorem Ipsum</h2>
0039<p>Eu fugiat nulla 'pariatur'. Ut aliquip ex ea commodo consequat. Ullamco laboris nisi sunt in culpa qui officia deserunt. Excepteur sint occaecat cupidatat non proident, ut enim ad minim veniam. In reprehenderit in voluptate sunt in culpa velit esse cillum dolore. Mollit anim id est laborum. Quis nostrud exercitation ut enim ad minim veniam, ut aliquip ex ea commodo consequat. Excepteur sint occaecat ut labore et dolore magna aliqua. Ut enim ad minim veniam, duis aute irure dolor velit esse cillum dolore. Ullamco laboris nisi ut labore et dolore magna aliqua. Excepteur sint occaecat sunt in culpa eu fugiat nulla pariatur. Ut aliquip ex ea commodo consequat. Ut enim ad minim veniam, ut labore et dolore magna aliqua. Sed do eiusmod tempor incididunt excepteur sint occaecat lorem ipsum dolor sit amet. Consectetur adipisicing elit, in reprehenderit in voluptate sunt in culpa. Sed do eiusmod tempor incididunt ullamco laboris nisi ut enim ad minim veniam.</p>
0040 
0041DESKTOP
0042,
0043<<<MOBILE
0044 
0045<h2>Lorem Ipsum</h2>
0046<p>Eu fugiat nulla 'pariatur'. Ut aliquip ex ea commodo consequat. Ullamco laboris nisi sunt in culpa qui officia deserunt. Excepteur sint occaecat cupidatat non proident, ut enim ad minim veniam. In reprehenderit in voluptate sunt in culpa velit esse cillum dolore. Mollit anim id est laborum. Quis nostrud exercitation ut enim ad minim veniam, ut aliquip ex ea commodo consequat.</p>
0047 
0048MOBILE
0049); ?>
0050<p><a href = "page.php">HOME</a></p>
0051</body>
0052</html>

screen_styles.css

Write your CSS for the desktop version of your webpages in this stylesheet. If you want to change the name of the file, you can change the link in the webpage in the PHP code at line 28. If your server does not allow you to put an .htaccess file, you need to use the stylesheet below, screen_styles.php, instead. Read more about this at ‘Download the files’.

download
0001body {font-family:sans-serif; color:black; background-color:#ccf}

screen_styles.php

This stylesheet is meant for those who cannot put an .htaccess file on their server. Write your CSS for the desktop version of your webpages in this stylesheet below the PHP code. Leave this PHP code in top of the stylesheet untouched, it contains the Cache-Control header instead of writing it in .htaccess. If you want to change the name of the file, you can change the link in the webpage in the PHP code at line 28. See below at ‘Download the files’ for additional instructions.

download
0001<?php
0002header("Content-Type: text/css; charset = utf-8");
0003header("Cache-Control: max-age = 36000");
0004?>
0005 
0006body {font-family:sans-serif; color:black; background-color:#ccf}

mobile_styles.css

Write your CSS for the mobile version of your webpages in this stylesheet. If you want to change the name of the file, you can change the link in the webpage in the PHP code at line 28. If your server does not allow you to put an .htaccess file, you need to use the stylesheet below, mobile_styles.php, instead. Read more about this at ‘Download the files’.

download
0001body {font-family:sans-serif; color:#ccf; background-color:black}
0002a {color:red}

mobile_styles.php

This stylesheet is meant for those who cannot put an .htaccess file on their server. Write your CSS for the mobile version of your webpages in this stylesheet below the PHP code. Leave this PHP code in top of the stylesheet untouched, it contains the Cache-Control header instead of writing it in .htaccess. If you want to change the name of the file, you can change the link in the webpage in the PHP code at line 28. See below at ‘Download the files’ for additional instructions.

download
0001<?php
0002header("Content-Type: text/css; charset = utf-8");
0003header("Cache-Control: max-age = 36000");
0004?>
0005body {font-family:sans-serif; color:#ccf; background-color:black}
0006a {color:red}

inc/switcher.php

This script is developed in teamwork with and for the Mobile Web and Apps Best Practices Training of W3C Online Training.

download
0001<?php
0002/*
0003* This script has been written by Alex Pot of SmartScripts (www.smartscripts.nl)
0004* The detection of $_GET['m'] and $_COOKIE['m'] is an adapted version of a script by Phil Archer (http://philarcher.org/diary/2011/mobilecontentandstyle/)
0005* The lightweight class for detection of mobile browsers can be found here: http://code.google.com/p/php-mobile-detect/
0006* You are free to adapt and use this script for your own projects, on the one condition that you keep these credits intact
0007*/
0008 
0009if (!isset($root)) {
0010 header("HTTP/1.0 404 Not Found"); //hide existence of this file
0011 die;
0012}
0013 
0014header("Vary: User-Agent, Accept");
0015 
0016//construct the base url (including the directory, but not the file name):
0017$base = "http://" . $_SERVER['SERVER_NAME'] . preg_replace("/[^\/]*(\?.*)?$/", "", $_SERVER['REQUEST_URI']);
0018 
0019$mobile_browser = 0;
0020$display_mode_changed = false;
0021 
0022if (isset($_GET['m']) && in_array($_GET['m'], Array("0", "1"))) { // We have a value directly from the user that we need to store
0023  setcookie('m', $_GET['m'], time()+$cookie_lifetime);	// Although we may already have a cookie, the value may
0024  $_COOKIE['m'] = $_GET['m'];						// have changed so we'll store it anyway. Also update $_COOKIE array.
0025 
0026  if ($_GET['m'] == "1")$mobile_browser++;
0027 
0028  $display_mode_changed = true;
0029}
0030 
0031elseif (isset($_COOKIE['m']) && $_COOKIE['m'] == "1") {// If we have a cookie set to 1 or if we have
0032  $mobile_browser++;								// just set it to 1, we want the mobile view
0033}
0034 
0035elseif (isset($_COOKIE['m']) && $_COOKIE['m'] == "0") { //forced Desktop View
0036  $mobile_browser = 0;
0037}
0038 
0039else {						// No indication of user preference
0040  include($root . "/inc/mobile_detection_class.php");	// include the detector script
0041  $detect = new Mobile_Detect();
0042  $mobile_browser = $detect->isMobile(); //returns 0 or 1
0043}		//OK, we're done. We know which version we want so let's return it
0044 
0045 
0046if (!$display_mode_changed)header("Cache-Control: max-age = " . $maxage);				// Set cache control before we go any further
0047else {
0048 //force a quicker refresh of (only) the current page after the visitor has changed the display mode manually:
0049 header("Expires: ".gmdate("D, d M Y H:i:s")." GMT"); // Always expired
0050 header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");// always modified
0051 header("Cache-Control: no-cache, must-revalidate, max-age = 0");// HTTP/1.1
0052 header("Pragma: nocache");// HTTP/1.0
0053 header("Cache-Control: max-age = 0"); //force refresh of this page
0054}
0055 
0056$current_file = pathinfo($file);
0057$full_path = $base . $current_file['basename'];
0058 
0059function echoDesk ($text) {
0060 global $mobile_browser;
0061 if ($mobile_browser == 0 ) {
0062  echo $text;
0063 }
0064}
0065 
0066function echoMobile ($text) {
0067 global $mobile_browser;
0068 if ($mobile_browser > 0 ) {
0069  echo $text;
0070 }
0071}
0072 
0073function echoSwitch ($desktoptext, $mobiletext) {
0074 global $mobile_browser;
0075 if ($mobile_browser > 0 ) {
0076  echo $mobiletext;
0077 }
0078 else echo $desktoptext;
0079}

inc/mobile_detection_class.php

The lightweight class for detection of mobile browsers device detection script below can also be found at http://www.opensource.org

download
0001<?php
0002 
0003/**
0004 * Mobile Detect
0005 *
0006 * @license    http://www.opensource.org/licenses/mit-license.php The MIT License
0007 * @version    SVN: $Id: Mobile_Detect.php,v 1.1 2011/07/06 14:40:34 phila Exp $
0008 */
0009 
0010class Mobile_Detect {
0011 
0012 protected $accept;
0013 protected $userAgent;
0014 
0015 protected $isMobile     = false;
0016 protected $isAndroid    = null;
0017 protected $isBlackberry = null;
0018 protected $isIphone = null;
0019 protected $isOpera      = null;
0020 protected $isPalm       = null;
0021 protected $isWindows    = null;
0022 protected $isGeneric    = null;
0023 
0024 protected $devices = array(
0025  "android"       => "android",
0026  "blackberry"    => "blackberry",
0027  "iphone"        => "(iphone|ipod)",
0028  "opera"         => "opera mini",
0029  "palm"          => "(avantgo|blazer|elaine|hiptop|palm|plucker|xiino)",
0030  "windows"       => "windows ce; (iemobile|ppc|smartphone)",
0031  "generic"       => "(kindle|mobile|mmp|midp|o2|pda|pocket|psp|symbian|smartphone|treo|up.browser|up.link|vodafone|wap)"
0032 );
0033 
0034 
0035 public function __construct () {
0036  $this->userAgent = $_SERVER['HTTP_USER_AGENT'];
0037  $this->accept    = $_SERVER['HTTP_ACCEPT'];
0038 
0039  if (isset($_SERVER['HTTP_X_WAP_PROFILE'])|| isset($_SERVER['HTTP_PROFILE'])) {
0040   $this->isMobile = true;
0041  } elseif (strpos($this->accept,'text/vnd.wap.wml') > 0 || strpos($this->accept,'application/vnd.wap.xhtml+xml') > 0) {
0042   $this->isMobile = true;
0043  } else {
0044   foreach ($this->devices as $device => $regexp) {
0045    if ($this->isDevice($device)) {
0046     $this->isMobile = true;
0047    }
0048   }
0049  }
0050 }
0051 
0052 
0053 /**
0054  * Overloads isAndroid() | isBlackberry() | isOpera() | isPalm() | isWindows() | isGeneric() through isDevice()
0055  *
0056  * @param string $name
0057  * @param array $arguments
0058  * @return bool
0059  */
0060 public function __call ($name, $arguments) {
0061  $device = substr($name, 2);
0062  if ($name == "is" . ucfirst($device)) {
0063   return $this->isDevice($device);
0064  } else {
0065   trigger_error("Method $name not defined", E_USER_ERROR);
0066  }
0067 }
0068 
0069 
0070 /**
0071  * Returns true if any type of mobile device detected, including special ones
0072  * @return bool
0073  */
0074 public function isMobile () {
0075  return ($this->isMobile) ? 1 : 0;
0076 }
0077 
0078 
0079 protected function isDevice ($device) {
0080  $var    = "is" . ucfirst($device);
0081  $return = $this->$var === null ? (bool) preg_match("/" . $this->devices[$device] . "/i", $this->userAgent) : $this->$var;
0082 
0083  if ($device != 'generic' && $return == true) {
0084   $this->isGeneric = false;
0085  }
0086 
0087  return $return;
0088 }
0089}

Download the files

All files mentioned above are available in an archive: zip-archive.

About the PHP

According the PHP there are two things worth mentioning:

  • For the use of the script your server needs to support PHP 5.
  • To keep the script out of the HTML as much as possible for this basic method, you will find the main part of the script in the folder named ‘inc’, where it is split up in two parts: one is the device detection and the other one is responsible for the switch between presentation of the mobile or desktop version of the webpage.

Version with .htaccess

For those among you that have the rights to put a .htaccess on the server, you need to do the following:

  • Write your CSS in the files named desktop_styles.css and mobile_styles.css.
  • You can delete the files named desktop_styles.php and mobile_styles.php

Version without .htaccess

For those among you that are not allowed to work with .htaccess on the server, you need to do the following:

  • Change the link in the web page(s) to the stylesheets from .css to .php
  • Write your CSS in the files named desktop_styles.php and mobile_styles.php. Be careful to leave the PHP code in the beginning of the stylesheets untouched.
  • You can delete the files named desktop_styles.css and mobile_styles.css
  • You can delete the .htaccess file

To complete the download there is a readme file included.

Force update of AppCache with PHP

AppCache can be hard to deal with. Browsers seem unpredictable in updating the AppCache. This example of a PHP-script (for PHP 5) makes sure the AppCache gets updated when pages have been changed.

Important

This script relies heavily on http-headers sent dynamically by PHP. Headers that are defined in .htaccess can never be overruled by PHP, so don’t put these headers (especially Last-Modified and Expires) in .htaccess: the script won’t work then.

The Process: two modi

The script will be executed on the server in two modi: from the manifest and from a regular webpage on your site.

To clarify this let’s name the first mode “manifest-mode” and the second mode “page-mode”. Both modi use different parts of the PHP-script in manifest-loader.php.

  • In manifest-mode the script will be activated directly by .htaccess. In this mode the script will echo the contents of manifest.appcache to the browser, but not before it has searched for pages that are more recent than the manifest, in all the folders that are listed in a special file named folders.txt. Since almost every page of your site may link to the manifest, every time one of these pages is visited, the script will start its search for modificated pages.
  • In page-mode if the script is included in the page, it will only check if the current webpage is more recent than the manifest. In page-mode the script sends cache-control-headers (Cache-Control: max-age, Last-Modified, Expires) for the current webpage to the browser.

More about the manifest-mode

  1. The script will investigate if the script itself or one of your regular webpages is more recent than the manifest.
  2. If so, the manifest on the server will be updated with an new version-number and modification-time. Then it will be sent to the browser, along with some headers that tell the browser that the manifest has expired and will have to be refreshed from the browser during the next request of a webpage.
  3. The update of the online manifest forces the visitor’s browser to update its local AppCache.
  4. In case there are no webpages more recent than the manifest, the script checks if maybe your stylesheet is more recent than the manifest. In that case the manifest also will be updated.
  5. There may be a delay before you see updates of the manifest. This delay is determined by:
    1. The number of files in the appcache that have to be checked for updates (and the time it takes to download modificated files).
    2. On the max-age send sent by the script for each file while in page-mode.

More about the page mode

  1. If the requested webpage on the server is more recent than the manifest, the script will update the manifest on the server with a new version-number and modification-time.
  2. The script also checks if the requested page is in a folder that is not yet listed in folders.txt. If so, this file will be updated with the new folder. This list of folders enables the script to check this new folder for modificated pages when it’s in manifest-mode.

The Files

You need four files, each of which you can find below:

  1. The manifest that is called manifest.appcache.
  2. The .htaccess file
  3. The script that is called manifest-loader.php
  4. A register for the folders on your site folders.txt

Also you need to include some extra PHP-code in your regular webpages, as you can see in the samples below.

All files mentioned above are available in an archive: zip-archive.

Don’t forget to make manifest.appcache and folders.txt writable! Without this permission the script will not be able to write/change/add anything in these files.

The Manifest

This is an example of the manifest thats is needed to enable AppCache. Modify according to your requirements, but don’t change the first two occurrences of lines that start with “#”.

download
0001CACHE MANIFEST
0002 
0003# Don't touch the following two lines; they will be updated dynamically by manifest-loader.php:
0004# Version: 58
0005# Updated: 2011-09-05, 16:26:57
0006 
0007CACHE:
0008 
0009/page.php
0010/css/css.css
0011 
0012#If you want to use fallback pages for pages that haven't been downloaded yet of if you want to define pages that have to be always fetched online, remove the corresponding hashtags below
0013 
0014#FALLBACK:
0015#/ /offline.php
0016 
0017#NETWORK:
0018#*

.htaccess

Only the bare minimum of rules needed for this setup are demonstrated here. This file must reside in the root directory of your site.

download
0001RewriteEngine On
0002 
0003#send the manifest:
0004rewriteRule ^manifest\.appcache$ appcache/manifest-loader.php?sendmanifest = true [L]
0005 
0006<IfModule mod_headers.c>
0007#make sure (with Etag and max-age) that modified static content will be reloaded after the manifest has been updated:
0008 <FilesMatch ".(html|htm|xml|txt|css)$">
0009  FileETag MTime Size
0010 
0011  Header set Cache-Control "max-age = 120"
0012 </FilesMatch>
0013 
0014</IfModule>

folders.txt

In this file manifest-loader.php maintains a list of folders on your site. This list enables manifest-loader.php while in manifest-mode to crawl the folders of your site in search of files that are more recent than the manifest.

download
0001a:1:{i:0;s:1:"/";}

manifest-loader.php

This is the script that does all the hard work. It has to be included in the head of your pages (see the sample below).

download
0001<?php
0002class AppCache {
0003 
0004 //first we define some properties of the class:
0005 
0006/** root folder of your site; dynamically filled in the constructor
0007* @var string */
0008 private $_root_folder = "";
0009 
0010/** The folder that contains this script and the manifest; dynamically filled in the constructor
0011* @var string */
0012 private $_appcache_folder = "";
0013 
0014/** Path to the manifest; dynamically filled in the constructor
0015* @var string */
0016 private $_manifest = "";
0017 
0018/** Contents of the manifest, fetched from manifest.appcache and sent to the browser
0019* @var string */
0020 private $_manifest_contents = "";
0021 
0022/** Will be true if and when the manifest source-file manifest.appcache has been updated. This update is executed by AppCache::_manifest_update()
0023* @var boolean */
0024 private $_manifest_updated = false;
0025 
0026/** The delay in seconds it will take before you will see updated content in your browser. 2 Minutes (120 seconds) is a good value for this: you don't have to wait overly long for updated content and your pages will still load very fast.
0027* @var numeric */
0028 private $_appcache_refresh_delay = 120;
0029 
0030/** Folders that will be checked by the dynamic manifest. Folder list will be stored in the file folders.txt in the folder appcache and will be automatically updated. Initially only the root folder of the site - "/" - is stored
0031* @var array */
0032 private $_folders = Array("/");
0033 
0034/** txt-file that stores the serialized list op folders of your site
0035* @var string */
0036 private $_folder_index = "";
0037 
0038/** Folders that contain the stylesheet(s) of your site. Has a stylesheet been updated, the manifest will also be updated, to force an update of the AppCache in the browser
0039* @var array */
0040 private $_css_folders = Array("/css/");
0041 
0042/** File modification time of the manifest, needed for computations to determine if the manifest needs an update
0043* @var numeric */
0044 private $_manifest_filemtime = 0;
0045 
0046/** True if this file has been referenced from .htaccess (when the manifest has to be echoed). If the current class has been instantiated from an include on a page, this property will stay false.
0047* @var boolean */
0048 private $_is_manifest = false;
0049 
0050/**
0051 * Constructor of the class. It will echo the manifest or updated it, if required
0052 *
0053 * @return    void
0054 */
0055 public function __construct () {
0056 
0057  //determine if this file has been loaded as stand-alone (to echo the manifest), of as an include on a page:
0058  if (isset($_GET, $_GET['sendmanifest']))$this->_is_manifest = true;
0059 
0060  //initialise the locations of the folders:
0061  $this->_root_folder = $_SERVER['DOCUMENT_ROOT'];
0062  $this->_appcache_folder = $this->_root_folder . "/appcache";
0063 
0064  $this->_folder_index = $this->_appcache_folder . "/folders.txt";
0065 
0066  //initialize some properties:
0067  $this->_folders = unserialize(file_get_contents($this->_folder_index));
0068 
0069  $this->_manifest = $this->_appcache_folder . "/manifest.appcache";
0070 
0071  $this->_manifest_filemtime = filemtime($this->_manifest);
0072 
0073  //if in manifest mode, echo the contents of manifest.appcache:
0074  if ($this->_is_manifest)$this->_echo_manifest();
0075 
0076  //otherwise: if this file has been loaded from a regular php-page:
0077  else {
0078   //determine the file modification time of the page that has called this file/class:
0079   $pagetime = filemtime(CALLER);
0080 
0081   //if the page is newer then the manifest, update the manifest:
0082   if ($pagetime > $this->_manifest_filemtime)$this->_manifest_update();
0083 
0084   //send some headers for cache-control of the page (don't use the second argument true for header(), or you'll get internal server errors):
0085   header("Cache-Control: max-age = " . $this->_appcache_refresh_delay);
0086   header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT", $pagetime);
0087 
0088   $expires = time() + 60 * 60 * 24 * 14;
0089   header("Expires: ".gmdate("D, d M Y H:i:s", $expires)." GMT"); // Far future expiration header set to 14 days
0090 
0091   //optionally update the list of folders that have to be checked for changes in the php-pages (list will be stored in appcache/folders.txt):
0092   $this->_update_folders();
0093  }
0094 
0095 } //end of __construct()
0096 
0097/**
0098 * Echoes the manifest and before that conditionally updates the manifest if pages or resources of the site have been updated
0099 *
0100 * @return    void
0101 */
0102 private function _echo_manifest () {
0103 
0104  // if this class / file has been updated, update the manifest:
0105  if (filemtime(__FILE__) > $this->_manifest_filemtime)$this->_manifest_update();
0106 
0107  //if the manifest hasn't been updated by the previous line, check the folders of the site for changes in de php-pages.
0108  //if any are found, the manifest will then be also updated, in order to force a refresh of the local AppCache of the browser
0109  if (!$this->_manifest_updated) {
0110   //will contain folders that are listed in folders.txt but don't exist anymore:
0111   $delete_folders = Array();
0112 
0113   foreach ($this->_folders as $folder) {
0114    $folder_to_check = $this->_root_folder . substr($folder, 0, -1);
0115 
0116    //if a folder doesn't exist anymore, add it to a list. The folders in this list will be purged from folders.txt later on:
0117    if (!file_exists($folder_to_check) || !is_dir($folder_to_check)) {
0118     $delete_folders[] = $folder;
0119     continue;
0120    }
0121 
0122    //determine the most recent file modification time in a folder:
0123    $most_recent_page = $this->_get_most_recent_file_in_folder($folder_to_check);
0124 
0125    //if the most recent page in a folder is more recent than the manifest, update the manifest:
0126    if ($most_recent_page > $this->_manifest_filemtime) {
0127     $this->_manifest_update();
0128 
0129     break; //because the manifest has been updated, the folders don't have to be checked anymore for files with more recent modification times
0130    }
0131   }
0132 
0133   //if there are any folders listed in folders.txt that don't exist anymore, delete them from this file:
0134   if (count($delete_folders)) {
0135    $this->_folders = array_diff($this->_folders, $delete_folders);
0136    file_put_contents($this->_folder_index, serialize($this->_folders));
0137   }
0138  }
0139 
0140 
0141  // only if an update of the manifest has not been neccessary until now, check the shylesheets for modification. If a stylesheet exists that is more recent than the manifest, update the manifest:
0142  if (!$this->_manifest_updated) {
0143   foreach ($this->_css_folders as $folder) {
0144    $folder_to_check = $this->_root_folder . substr($folder, 0, -1);
0145 
0146    $most_recent_page = $this->_get_most_recent_file_in_folder($folder_to_check, Array("extension" => "css"));
0147 
0148    if ($most_recent_page > $this->_manifest_filemtime) {
0149     $this->_manifest_update();
0150 
0151     break; //because the manifest has been updated, the other folders don't have to be checked anymore for files with more recent modification times
0152    }
0153   }
0154  }
0155 
0156  //send some headers for the manifest. Specifically needed for FireFox, which browser has a tendency to cache the manifest and therefor not to update the AppCache. With these headers the manifest will be marked as always expired, even for Firefox:
0157 
0158  header("Content-Type: text/cache-manifest", true);
0159  header("Cache-Control: must-revalidate, proxy-revalidate, max-age = 0", true);
0160  header("Expires: ".gmdate("D, d M Y H:i:s")." GMT", true); // Always expired
0161  header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT", true);// always modified
0162 
0163 
0164  //echo the contents of the sourcefile manifest.appcache:
0165  readfile($this->_manifest);
0166 
0167 } //end of _echo_manifest()
0168 
0169 
0170/**
0171 * Update the manifest, to force an update of the AppCache in the browser
0172 *
0173 * @return    void
0174 */
0175 private function _manifest_update () {
0176 
0177  $this->_manifest_contents = file_get_contents($this->_manifest);
0178 
0179  $this->_manifest_contents = preg_replace_callback("/Version:\s*(\d+)/", create_function(
0180    '$var',
0181    '
0182    $var[1] += 1;
0183    return "Version: "  . $var[1];
0184    '
0185  ), $this->_manifest_contents);
0186 
0187  $this->_manifest_contents = preg_replace("/# Updated: .+/", "# Updated: " . date("Y-m-d, H:i:s"), $this->_manifest_contents);
0188 
0189  file_put_contents($this->_manifest, $this->_manifest_contents);
0190 
0191  //log the modified-status of the manifest; this way further checks for the need to update the manifest will be skipped
0192  $this->_manifest_updated = true;
0193 
0194 } //end of _manifest_update()
0195 
0196/**
0197 * If an update thereof is required, update the list of folders with pages in folders.txt
0198 *
0199 * @return   void
0200 */
0201 private function _update_folders () {
0202 
0203  //determine the path to files relative to the document root of the site:
0204  $folder = preg_replace("/[^\/]+$/", "", $_SERVER['REQUEST_URI']);
0205 
0206  //if "http" has been used in the request_uri, a hacker probably is trying things. So we won't continue with the method in that case.
0207  if (stristr($folder, "http"))return;
0208 
0209  //you have to make sure the folder index file folders.txt exists AND it has to be writable.
0210  // if a new folder has been detected that isn't in this list, it has to be added to the index file:
0211  if (!in_array($folder, $this->_folders)) {
0212   $this->_folders[] = $folder;
0213   file_put_contents($this->_folder_index, serialize($this->_folders));
0214  }
0215 }
0216 
0217/**
0218 * Returns the most recent file in a folder
0219 *
0220 * @param string $folder The folder that is being searched
0221 * @param array $options Options for the method
0222 *
0223 * @return    numeric file modification time
0224 */
0225 private function _get_most_recent_file_in_folder ($folder, $options = Array()) {
0226 
0227  if(!file_exists($folder) || !is_dir($folder))return "";
0228 
0229  extract($options);
0230 
0231  if (!isset($extension))$extension = "php";
0232 
0233  $list = scandir ($folder); //scandir() returns a list of files (only the filenames) in the folder
0234  if (count($list) == 2)return ""; // then only .. and . found, which are no regular files
0235 
0236  $most_recent_filemtime = 0;
0237 
0238  foreach ($list as $file) {
0239 
0240   $full_path = $folder . "/" . $file;
0241 
0242   if (in_array($file, Array(".", "..")) || is_dir($full_path) || ($extension != "" && !preg_match("/\." . $extension . "$/", $file)))continue;
0243 
0244   $time = filemtime($full_path);
0245   if ($time > $most_recent_filemtime) {
0246    $most_recent_filemtime = $time;
0247   }
0248  } //end of the loop through all files
0249 
0250  return $most_recent_filemtime;
0251 
0252 } //end of get_most_recent_file_in_folder()
0253 
0254} //end of class AppCache
0255 
0256//instantiate the class, so that the manifest will be echoed or updated:
0257new AppCache();

Sample files

Below you find examples of three regular pages (two in the root folder of your site and one in a subfolder “subdir”) and of the stylesheet for these pages.

Notice how the manifest loader is being loaded in the start section of your pages. You only have to add some extra PHP-code above the doctype and in the HTML element of your pages. Of course your webpages must have the extension .php for this to work.

In the root folder: page.php (in the manifest)

download
0001<?php
0002 define("CALLER", __FILE__);
0003 require_once($_SERVER['DOCUMENT_ROOT'] . "/appcache/manifest-loader.php");
0004?><!DOCTYPE HTML>
0005 
0006<!--the manifest seemingly resides in the root folder of your site, when actually it is located in the subfolder /appcache -->
0007<html manifest = "/manifest.appcache">
0008<head>
0009   <meta http-equiv = "Content-Type" content = "text/html; charset = UTF-8" />
0010   <title>AppCache demo page 1</title>
0011 
0012   <link href = "/css/css.css" rel = "stylesheet" />
0013</head>
0014 
0015<body>
0016<h1>Page 1</h1>
0017<p>This is the only page that is explicitly listed in the manifest. The other pages will be added to the AppCache when the visitor loads them in his browser.</p>
0018<p>To <a href = "page2.php">page 2</a></p>
0019<p>To <a href = "subdir/page3.php">page 3</a></p>
0020</body>
0021</html>

In the root folder: page2.php (not in the manifest)

download
0001<?php
0002 define("CALLER", __FILE__);
0003 require_once($_SERVER['DOCUMENT_ROOT'] . "/appcache/manifest-loader.php");
0004?><!DOCTYPE HTML>
0005 
0006<!--the manifest seemingly resides in the root folder of your site, when actually it is located in the subfolder /appcache -->
0007<html manifest = "/manifest.appcache">
0008<head>
0009   <meta http-equiv = "Content-Type" content = "text/html; charset = UTF-8" />
0010   <title>AppCache demo page 2</title>
0011 
0012   <link href = "/css/css.css" rel = "stylesheet" />
0013</head>
0014 
0015<body>
0016<h1>Page 2</h1>
0017<p>This page is not listed in the manifest; so it will only be added to the AppCache if and when the visitor has visited this page in his browser.</p>
0018<p>To <a href = "page.php">page 1</a></p>
0019<p>To <a href = "subdir/page3.php">page 3</a></p>
0020</body>
0021</html>

In a subfolder: page3.php (not in the manifest)

download
0001<?php
0002 define("CALLER", __FILE__);
0003 require_once($_SERVER['DOCUMENT_ROOT'] . "/appcache/manifest-loader.php");
0004?><!DOCTYPE HTML>
0005 
0006<!--the manifest seemingly resides in the root folder of your site, when actually it is located in the subfolder /appcache -->
0007<html manifest = "/manifest.appcache">
0008<head>
0009   <meta http-equiv = "Content-Type" content = "text/html; charset = UTF-8" />
0010   <title>AppCache demo page 3</title>
0011 
0012   <link href = "/css/css.css" rel = "stylesheet" />
0013</head>
0014 
0015<body>
0016<h1>Page 3 (in subfolder)</h1>
0017<p>This page is not listed in the manifest; so it will only be added to the AppCache if and when the visitor has visited this page in his browser.</p>
0018<p>To <a href = "../page.php">page 1</a></p>
0019<p>To <a href = "../page2.php">page 2</a></p>
0020</body>
0021</html>

The stylesheet: css/css.css (in the manifest)

The contents of this file are not very relevant. I’ve only added it here because this sheets also will be checked for modifications by manifest-loader.php.

download
0001body {
0002 background: red;
0003 color: white;
0004}
0005 
0006a {
0007 color: white;
0008 text-decoration: underline;
0009}
0010 
0011a:hover {
0012 text-decoration: none;
0013}

Know your AppCache

AppCache can significantly speed up the loading of the pages of your site and lets visitors surf your site even when they are offline and haven’t downloaded all pages yet.

In this post I’ll present a solution to get AppCache functioning correctly (which can be quite problematic). I’ll conclude with some interesting facts about AppCache and how different browsers cope with it.

How does AppCache work?

AppCache is a new feature of HTML5. In the html-tag of web pages must be a link present to the “manifest” of the AppCache (preferably with the extension .appcache):

<html manifest="/manifest.appcache">

Now this manifest is used by the browser as central point of reference: in this manifest the webmaster of a site can sum up all the pages, images and documents that have to be downloaded and cached in the background. Instead of checking online for each and every page if a new version thereof exists, the browser consults the manifest to check if that has been changed. If it has not and the requested page/file is present in the AppCache, the local copy will be loaded instead of the online one. Since the manifest is only one small file, checking this for updates instead of the numerous online pages and files is much, much faster.

For more detailed information and instructions how to write a manifest see the links at the end of this page.

The main subject of this post are the problems you can encounter when trying to use AppCache and how to solve them. As easy as the use of AppCache may seem in theory, in practice it soon will surprise you with its seemingly unreliable behavior. It can prove quite hard to force your browser to update it’s AppCache with modified online content.

The problem: AppCache conflicts with normal cache-control headers

The AppCache of any browser will only be renewed when the contents of the manifest have been changed. A simple way to make this change of content is to add a comment in the manifest in which you simply write down a version number. Whenever this version number changes, meaning the content of the manifest has changed, the browser will update the AppCache.However, any cache-control with Cache-Control and Expires headers set in a .htaccess-file in the root of a site, can and most probably will conflict with the cache-management by the AppCache. I noticed that whenever I used this standard method of cache-control, AppCache wouldn’t function correctly anymore: it would never update its cache afterwards with newer versions of the same pages. This meant that the visitor would only and forever get to see the out-of-date version of any page once it has been stored in the AppCache. That is, until the visitor deletes the AppCache of his browser.

Problems with AppCache and HTML forms

AppCache can also be problematic on pages that contain a form. When a visitor posts the form, you have make adaptations to ensure that the post will indeed get through. I’ll write about this subject in another post.

The solution to the problem

I have found a solution to this problem. The central point of it is that the Cache-Control headers are never to be set in .htaccess, but always to be sent dynamically by a scripting language. An example how to accomplish this with PHP can be found elsewhere in this blog. I have tested this solution thoroughly after including it in Site Optimizer, a system I wrote to optimize sites.

In the example script I’ve set the refresh time for the AppCache to 2 minutes. I found that if changed pages were present, the AppCache would indeed be updated after this interval (or a little longer). So the visitor will see changed content after a relatively short delay. When visiting the same page some hours later, the visitor will surely see the changed content of the AppCache, since the max-age of two minutes has by then expired long since. And as long as, in between visits, the manifest hasn’t changed, the pages will be shown from the browser’s AppCache.

The result is very interesting:

  1. Despite the low max-age you still have avoided unnecessary downloading of pages that were already downloaded before and that haven’t been changed in the meantime.
  2. What’s more: your visitor can see the contents of pages listed in the manifest even when he is offline and hadn’t visited this pages while online.
  3. Only the files that actually have changed will be re-downloaded to the AppCache.

You may get penalty points in PageSpeed for the missing expires-headers. If so then PageSpeed obviously is not (yet) smart enough to detect that they are compensated with the advantages of AppCache. The most recent test in YSlow though does indeed compensate for this.

AppCache in various browsers

Note: I’ve only tested AppCache on a Windows computer with the browsers available to me on that platform
Chrome
  • Chrome has good support for AppCache.
  • Very handy feature: when the user deletes the conventional cache of his browser, the AppCache will be cleared also.
  • If you want, you can get a list of all the AppCaches managed by Chrome by visiting chrome://appcache-internals/ in this browser. In this list you can delete AppCaches on a per site basis.
  • On the visitor’s harddisk the AppCache is being stored in the directoryC:\Users\[User Name]\AppData\Local\Google\Chrome\User Data\Default\Application Cache, but in a non readable format.
  • Luckily you can download the extension Service Pages for Google Chrome. One of the utilities in this suite is “View HTTP Cache”. In this utility you can also see which pages Chrome has stored in it’s AppCache. If you click on a page, you see the contents thereof and the headers that were issued by the server when the page was downloaded. This led me to believe that the AppCache not only delivers the contents of pages to the browser, but also reconstructs and sends the original headers.
Firefox
  • Firefox has the laudable but also annoying tendency to pop up a dialog that alerts the visitor that “this site is trying to store content for offline use”. In my mind this leaves the visitor with the impression that a site is engaged in shady activities.
  • Edit: in my current version of Firefox 6.0.2 I can disable this dialog by unticking a checkbox: Options > Options > “Tell me when a website asks to store data for offline use”. If I remember correctly this checkbox wasn’t available in previous versions of Firefox.
  • Firefox’s AppCache will not be deleted when you empty this browser’s conventional cache manually. Instead you have to follow another pathway in the menu: Options > Options > Advanced > Network. In the lower part of this dialog you’ll notice the AppCaches that Firefox manages; these can be erased manually in this dialog. Rather a pity that Firefox didn’t follow the same approach as Chrome i.e.: deleting the AppCaches simultaneously with the conventional cache.
  • Edit: in more recent versions of FireFox (6.0.2 and newer) the AppCache will also be deleted when the visitor empties the local cache of Firefox. Huzza!
  • Firefox stores it’s meta information about the AppCache in a SQLite database: C:\Users\[Username]\AppData\Local\Mozilla\Firefox\Profiles\[ProfileOfCurrentUser]\OfflineCache\index.sqlite. So this information can be accessed in a SQLite-viewer, for example in the Firefox-extension SQLite Manager. See here an example of the information FireFox stores:
    Example of metadata Firefox stores about the AppCache of a site
  • When the browser is in the process of downloading the resources that are being listed in the manifest and the users goes off-line during this process, the browser crashes.
Internet Explorer, all versions
  • All versions of Internet Explorer up to version 9 don’t seem to support AppCache: none of the AppCache pages were shown to me when I went off-line. In Internet Explorer 9 only the pages I explicitly downloaded by actually visiting them where available to me when offline.
Opera
  • Although the latest version of Opera claims that it supports AppCache, in fact I noticed that it did not work. None of the pages in my manifest were shown to me when I went off-line and tried to surf the site.
  • Rather strange, because I did notice that Opera stores AppCache information. You can see what Opera has stored in it’s AppCache when you visit View > Developer Tools > Temporal Storage (maybe the exact wording differs, because it’s my translation of the menu-items in Dutch).
  • In C:\Users\[UserName]\AppData\Local\Opera\Opera\application_cache resides a file cache_groups.xml. In this file you can see which AppCaches the browser has stored.
  • The contents of the stored pages can be found in dcache4.url in the same folder as cache_groups.xml.
Safari for Windows
In first instance AppCache didn’t seem to work in this browser, because the pages in the manifest weren’t show to me when I went off-line. However, after adding the header “Pragma: cache” Safari handled the AppCache exactly like it should.

Using AppCache for mobile browsers

Since HTML5 is needed to leverage the advantages of AppCache, you have to determine if the browser supports HTML5 at all. For a mobile browser this can be done by checking in the database of TeraWurfl if it has support for the HTML5 canvas element. To have your website work properly in both desktop and mobile browsers with AppCache, you will need to automate this and more:

  1. Have the correct http-headers send for using AppCache as mentioned above: the Cache-Control and Last-Modified headers
  2. Adjustment of the version number inside the manifest
  3. Detection of the desktop or mobile browser
  4. Check if this browser support HTML5
  5. Converting the pages to HTML5 (or back to XHTML, depending on your approach)

You can test for yourself the advantages of AppCache in mobile browsers by visiting www.smartscripts.nl in your mobile browser. The system I mentioned above, Site Optimizer, has automated the functions mentioned above, and more, like dynamically changing the content and the layout of pages for the mobile version of site.

403/forbidden-oddities

Sometimes when a visitor has been denied access to a page (403 error), the offline page defined in the manifest gets displayed instead of the “access denied” message. So I’m inclined to conclude that if a offline page has been defined in the manifest and a visitor tries to load a non existing page, this offline page wil be displayed. This is good to know, so you can adapt the text of the offline page, alerting the visitor to the fact that he either is offline or that he tried to load a private page.

Links