Trebek: Building an Offline Web App

In my experimentation with HTML5, I ended up making Trebek, a web app that allows you to play along at home with Jeopardy! The idea is that by trying to answer questions before the contestants do and keeping score, you can gauge your chances of making it onto the show. According to two-time winner Karl Coryat, you have an excellent shot if you can score $28,000 or higher on a consistent basis, and Trebek helps keep track of your performance.

You can try Trebek out below. For best results, load it in Safari on your iPhone or iPad, then add it to your home screen.

https://thomaspark.co/projects/trebek/

What I want to do in this post is share some simple things I did to give Trebek a more app-like experience, despite it being a mere HTML/CSS/JavaScript bundle. If you’re interested in web development, read on.

Tailoring the interface by scaling elements

Trebek’s primary use case is leaning back on a sofa and pulling out your smartphone just as the show’s about to start. But it’s meant to be usable for – and therefore scales to – a full range of devices. You can test this out by loading it in a browser window and resizing to various shapes and sizes.

There are several ways to achieve this, but what I recommend is setting the font-size of the body to 100% and then using em as a unit of measure throughout the rest of the code.


body {
  font-size: 100%;
}

#score {
   padding: 0.2em 0 0.2em 0;
   font-size: 6em;
   text-shadow: #000000 0.05em 0.05em 0.02em;
}

The size of em depends on the font-size of the element. By default, 1 em equals 16 pixels. But by changing the base font-size, you can scale all properties that are using em simultaneously. With the following jQuery, you can update body’s font-size, and therefore all of these properties, whenever the window is resized.


$(window).resize(function(){
   $('body').css('font-size', Math.floor($(window).height() * 100 / 768) + '%');
});

You can see why using pixels instead might be a bad idea. As an absolute unit, you have to recalculate and update the pixel size of each property individually, each time the window is resized.

On the other hand, using percent to size things is appealing since it’s relative by its very nature. Using percent also means you no longer require an update function for when the window is resized. If you set an image’s width to be 25%, it’s always going to be 25% of whatever, you might assume. However, keep in mind that percentages are local, meaning they’re based on the element’s parent and not a global value. While they’re useful for site layout, they’re less effective for fine-tuned properties like padding and text-shadow. Just try using percents to set the same padding for multiple elements, when their parent elements are different sizes.

While I simply scaled the interface, you might consider media queries for more custom interfaces conditional on resolution, aspect ratio, and orientation. For example, you could provide a one-column interface for smartphones versus a two-column interface for tablets.

Cutting the cord with local storage & application cache

Local storage allows you to store data on the client in much the same way as a browser cookie. It allows you to avoid databases — nice for a lightweight app, particularly one you intend to work offline. One word of warning though: clearing the browser’s cache results in the data being lost. Trebek uses local storage to save scores and keep a history of a user’s performance.

To use local storage, bundle up your data into a JavaScript object and use the following code. First, it checks that local storage is supported. Next, it checks if a data object has been saved under the key ‘trebek’ before and if so, assigns it to a variable. In the second part of the code, a function is bound to the save button. This function adds a new entry to the data object using the current date/time as key and current score as value, and then puts the whole object in local storage under the key ‘trebek’.


if(localStorage){
   if(localStorage.getItem('trebek')){
      stats = JSON.parse(localStorage.getItem('trebek'));
   }

   $('#save').bind('mouseup', function(){
      var now = new Date();
      stats[now.toString()] = parseInt($('#score').text());
      localStorage.setItem('trebek', JSON.stringify(stats));
});

Application cache stores your essential files so your web app can work offline, useful when your smartphone is in airplane mode or you have an iPod touch sans wifi. For Trebek, the greatest benefit regardless of whether the device is offline or not is that application cache improves startup speed.

The main thing you need is a cache manifest, which is a text file that specifies the resources to be cached for future use. Name this file “cache.manifest”, with the following content, and place it in the root of your web app. Under the CACHE section, you want to list all of the assets you wish to be cached except for the manifest file itself.


CACHE MANIFEST
# Trebek version 1.0

CACHE:
index.html
css/style.css
js/jquery-1.6.3.min.js
js/script.js
img/icon-web.png

NETWORK:

FALLBACK:

Each time the web app is opened while online, the cache manifest is checked to see if it’s been changed (such as by updating the version number comment). If a change has been made, this triggers the browser to re-cache the rest of the files. If not, the cache continues to be used.

With the cache manifest in place, you want to declare it in one of your pages by adding the following attribute to the HTML tag.


<html manifest="cache.manifest">

Finally, make sure that your web server is serving files with the manifest extension as type “text/cache-manifest”. It won’t work otherwise. You can check this using your favorite browser’s developer tool. If the file type is not correct, add the following line to the .htaccess file in the root of your web server.


AddType text/cache-manifest manifest

That covers the very basics of using local storage and application cache to enable offline functionality in your web app. Obviously much has been glossed over, so if you want to learn more, check out Mark Pilgrim’s chapters on local storage and application cache.

Putting on the finishing touches for iOS

Here are a few more steps you can take to put some polish on your newly offline-capable web app. First, save your favicon for multiple resolutions: 48, 57, 72, and 114 pixels. Not only are browsers making greater use of large favicons, but they can be used as icons when your web app is saved to your home screen, giving it a bit more of that native feel.

In the header of your page, add the following elements. The first specifies a favicon for web browsers, and the next three are for the iPhone, iPad, and iPhone retina display respectively. You can skip the -precomposed tag to have iOS automatically add its customary gloss to your icon.


<link rel="icon" type="image/png" href="img/icon-web.png"/>
<link rel="apple-touch-icon-precomposed" href="img/icon-iphone.png" sizes="57x57"/>
<link rel="apple-touch-icon-precomposed" href="img/icon-ipad.png" sizes="72x72"/>
<link rel="apple-touch-icon-precomposed" href="img/icon-retina.png" sizes="114x114"/>

If you want to give your web app a custom splash screen, add the following tags as well. The images you reference must be exactly 1004×768, 768×1004, and 320×460 pixels respectively. As far as I can tell, iPhone 4’s retina resolution is not supported yet.


<link rel="apple-touch-startup-image" href="img/startup-ipad-landscape.png" media="screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation:landscape)"/>
<link rel="apple-touch-startup-image" href="img/startup-ipad-portrait.png" media="screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation:portrait)"/>
<link rel="apple-touch-startup-image" href="img/startup-iphone-portrait.png" media="screen and (min-device-width: 200px) and (max-device-width: 320) and (orientation:portrait)"/>

Add the following code to tell Safari that your site is intended as a web app, change the menu bar to black, hide the chrome to display the app full-screen, and prevent pinch-to-zoom.


<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="apple-touch-fullscreen" content="yes"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>

So there you have it. It’s great to see the progress that’s being made with the web in recent years. Although it hasn’t quite reached parity with native apps, for many of the simpler apps you find in the App Store, it’s now possible to create an equivalent experience using HTML5.

2 comments Write a comment

  1. That’s great, thank you for the detailed explanation. Did the app have success?
    How deep were you in web development at that point? Like 2 years?

    • The app ended up being mostly an educational exercise for me. But packaging it up as something like a Chrome app and doing some marketing, I think some people would have found value in it.

      At this point I had been making web pages for a while, but was at the start of learning how to build other webby stuff like apps and libraries.

Leave a Reply