Creating Cache Profiles

It used to be that a web development team could design for the desktop and get away with it. With the ever-increasing popularity of smartphones, tablets, and other devices that introduce crazy screen resolutions, developing websites has gotten a lot harder. One size no longer fits all.

Barebones CMS offers programmers and web designers the ability to create cache profiles. A cache profile is an alternate, cached view of the same content on a page. The performance penalty for using cache profiles is insignificant in most cases. The primary use of cache profiles is to create a website that serves up good looking content to both desktop and mobile phone web browsers with minimal additional effort to get the latter working.

Cache profiles are also flexible enough to do anything else you might need them for. For instance, the Barebones CMS documentation could be converted to an offline version using a custom cache profile, a profile-specific layout, a web scraper, and some clever mojo.

Your First Cache Profile

Creating a cache profile is a two-step process. The first step is picking a strategy for a mobile website, sticking with it, and, of course, testing it thoroughly. Are you going to target just smartphones (iOS, Android, etc.) or all mobile devices (dumbphones, tablets, etc.)? Do you care if the browser supports Javascript, cookies, or other features? For this website, I chose a strategy of checking to see if the browser has Javascript (implicit) and cookies (explicit) enabled, whether or not the 'cacheprof' cookie was already set, and if the browser window is less than 920 pixels. This allows me to not have to sniff the user agent string of the browser. The user arrives to the site and, if the browser is too small and supports Javascript and cookies, the page sets the 'cacheprof' cookie to 'mobi', and the page is reloaded. The server's 'main_hook.php' file sees the cookie and loads the mobile version of the website. I chose this approach because it is generally immune to future device integration issues and allows me to easily switch between the two profiles with a regular web browser.

The second step is implementation of the cache profile itself. This process isn't magical and, since you can literally build your mobile site however you want, this part of the process is difficult to implement. Expect to take a few days to build out the complete mobile setup with lots of potential downtime. Unlike the revision system, the caching system has to be fast and remain that way. Every line of code matters.

Creating The Hook

The first step of implementing the actual cache profile is to create a file called 'main_hook.php' in the same directory as 'main.php'. This file gets loaded by 'main.php', which is the caching system, if it exists. It allows you to inject your code into the caching system and do whatever you want. Here is this website's 'main_hook.php':

<?php
	if (!defined("BB_FILE"))  exit();

	$bb_profiles["mobi"] = "Mobile";

	if (isset($_REQUEST["bb_profile"]) && isset($bb_profiles[$_REQUEST["bb_profile"]]))  $bb_profile = (string)$_REQUEST["bb_profile"];
	else if (isset($_GET["cacheprof"]) && isset($bb_profiles[$_GET["cacheprof"]]))
	{
		$bb_profile = (string)$_GET["cacheprof"];

		require_once "config.php";
		require_once ROOT_PATH . "/" . SUPPORT_PATH . "/cookie.php";

		SetCookieFixDomain("cacheprof", $bb_profile, time() + 24 * 60 * 60, ROOT_URL . "/", "");
	}
	else if (isset($_COOKIE["cacheprof"]) && isset($bb_profiles[$_COOKIE["cacheprof"]]))  $bb_profile = (string)$_COOKIE["cacheprof"];

	function bb_content_shortcode_cache_profile_pre($sname, $parent, $sid, $depth)
	{
		global $bb_profile, $bb_widget;

		if ($bb_profile == "mobi")
		{
			if ($sname == "bb_image")  $bb_widget->shortcodes[$sid]["opt-caption-width"] = 0;
			else if ($sname == "bb_syntaxhighlight")  $bb_widget->shortcodes[$sid]["opt-collapse"] = true;
		}
	}
?>

The first thing the code does is add the 'mobi' profile to the $bb_profiles array. This allows the editor to know that there are multiple cache profiles and to show the 'View Cache Profiles' entry in the menu. This new menu item opens up access to the cache profile system. The editor, the Layout widget, and the Content widget are all cache profile aware.

After setting up the new 'mobi' option, there is some logic to determine which profile to use. This is perhaps overly complex and I may clean it up in the future. The only absolutely necessary line is the last part of the if-else statements where I detect the existence of a matching cookie and select the correct profile. If you plan on using user-agent detection/browser sniffing, I recommend either Mobile ESP or WURFL. However, it is generally accepted that user-agent detection is a terrible web development practice.

The Content widget has three callback functions that can be overridden by a cache profile. I'm only using one of the available functions to temporarily alter the input arrays of specific Shortcodes. This is the cleanest solution because the Shortcodes themselves aren't aware that the information has been modified but generate the desired output. The other two functions are designed to allow the content to be modified after the HTML has already been generated. This approach can also be used to force Shortcodes to always behave a specific way or to post-alter HTML content before it is output.

I wrote the Content widget override function last. In fact, a lot of this was a work-in-progress that went in several cycles through this process to get the whole thing built. You will likely end up going through a similar set of cycles to get a finished result.

Creating The Cache Profile Widget

In order to make the website truly mobile-friendly, I need to specify the viewport for mobile handsets to correctly render the page so it is clearly readable without pinch-to-zoom and without any horizontal scrolling. The correct way to do this is to add the appropriate 'meta' tag to the 'head' tag of the HTML. The Layout widget doesn't support 'head' injection by design, so the solution, and the correct way to implement this, is to create a new widget. I called the new widget 'site_mobile_meta':

<?php
	if (!defined("BB_FILE"))  exit();

	$bb_widget->_s = "site_mobile_meta";
	$bb_widget->_n = "Mobile Meta";
	$bb_widget->_key = "";
	$bb_widget->_ver = "";

	class site_mobile_meta extends BB_WidgetBase
	{
		public function Process()
		{
			global $bb_mode, $bb_widget, $bb_css, $bb_js, $bb_profile;

			if ($bb_profile != "mobi")  return;

			if ($bb_mode == "head")
			{
?>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<?php
			}
			else if ($bb_mode == "body")
			{
			}
		}
	}
?>

This widget detects if the current profile is 'mobi' and then outputs an appropriate meta tag. It is not exactly the best name for the widget because I might eventually want to do other things with it for other cache profiles. At the moment, it works just fine for its intended purpose.

Modifying The Layout

Now that the basic plumbing is in place, it is time to start building out the new wireframe layout. This example is about setting up a mobile version of the same content, so I used the '#profile' feature of the Layout widget to add a new layout.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<link rel="stylesheet" href="Main.css" type="text/css" media="screen" />
</head>
<body>
#edit
<style type="text/css">
body {
	background-position: 0 110px;
}
</style>
#start
// Site layout, header, and footer.
@mw_$site_mobile_meta@
<script type="text/javascript" src="/js/mobile.js"></script>
<script type="text/javascript" src="/js/mobile2.js"></script>
<div class="pagewrap">
	<div class="headerwrap">
		<a class="mainlogo" href="http://barebonescms.com/"><img src="/images/main-logo.png" alt="Barebones CMS" /></a>
		<div class="quicklinks">
			<a href="/download/">Download</a> | <a href="/documentation/">Documentation</a> | <a href="/forums/">Forums</a>
		</div>
	</div>
	<div class="contentwrap">@mw_content@</div>
	<div class="footerwrap">&copy; <script type="text/javascript">document.write(new Date().getFullYear());</script> CubicleSoft<script type="text/javascript">if (Gx__mobile_supported)  document.write(' | <a href="#" onclick="return SwitchSiteProfile(\'mobi\');">Mobile website</a>');</script></div>
</div>
#end
#profile mobi
#start
// Mobile site layout, header, and footer.
@mw_$site_mobile_meta@
<script type="text/javascript" src="/js/mobile2.js"></script>
<div class="pagewrap">
	<div class="headerwrap">
		<a class="mainlogo" href="http://barebonescms.com/"><img src="/images/main-logo.png" alt="Barebones CMS" /></a>
		<div class="quicklinks">
			<a href="/download/">Download</a> | <a href="/documentation/">Documentation</a>
		</div>
	</div>
	<div class="headerdivider"></div>
	<div class="contentwrap">@mw_content@</div>
	<div class="footerwrap">&copy; <script type="text/javascript">document.write(new Date().getFullYear());</script> CubicleSoft | <a href="#" onclick="return SwitchSiteProfile('');">Full website</a></div>
</div>
#end
</body>
</html>

You may note that I also add the newly created widget to both layouts using the Layout widget feature known as the "One Widget, Master Widget" to rule them all - lame Lord Of The Rings pun intended. The Layout widget requires every master widget used in other cache profiles to be in the main cache profile or the master widget will be detached from the layout and therefore all profiles. This behavior has always existed, it just becomes more pronounced when the cache profile system is used.

Both profiles drag in at least one external Javascript file. Both of these files are used to drag in the common switching logic used. The 'mobile.js' file manages detection of a browser with a tiny display and sets the 'cacheprof' cookie and reloads the page. The 'mobile2.js' file manages the footer switching mechanism that allows the user to jump between the two profiles.

Finally, the mobile website brings in an alternate CSS file called 'Main.mobi.css'. It provides all the necessary styles to display the content correctly and is responsible for making sure Flash shortcodes don't appear at all.

Content Widget Functions

What follows are the three functions intended for use in cache profiles in conjunction with the Content widget. If a cache profile creates a function with any of these function names, the default function will not be created or called. It is up to the cache profile to act in a similar manner as the original function, modifying content appropriately.

bb_content_shortcode_cache_profile_pre($sname, $parent, $sid, $depth)

Parameters:

Returns: Nothing.

This function is called after backing up $bb_widget->shortcode[$sid] to a temporary variable but before generating any Shortcode output. Modifying $bb_widget->shortcode[$sid] is the cleanest way to override Shortcode behavior because the Shortcode is oblivious to the modifications that are made and the changes are reverted after the Shortcode generates its content.

bb_content_shortcode_cache_profile_post($sname, $parent, $sid, $depth, $data)

Parameters:

Returns: $data

When the '_pre' version of this function won't work for whatever reason, use this function to manipulate the HTML the Shortcode generates. By default, this function just returns $data.

bb_content_cache_profile($data)

Parameters:

Returns: $data

When all else fails, use this function to manipulate the HTML content output of the Content widget. By default, this function just returns $data. Not terribly efficient, but it works.

What's Next?

Once you have built your cache profile, be sure to test it thoroughly to make sure it is accurate in the final environment. If your cache profile is for a mobile device, nothing beats having the physical device in your hand, but if you can't afford an actual device, then go get the emulator for the device and try it out that way.

Cache profiles are an advanced topic. But if you need help creating one or get stuck, someone in the forums should be able to help.

© CubicleSoft