Developing a responsive, Retina-friendly site (Part 1)

In my last post, Designing a responsive, Retina-friendly site, I covered my design process and thoughts behind redesigning this site. I did not cover any aspects of actual development or any Jekyll specifics. This post will mainly cover coding up responsive design and the third and final post will cover retina media queries, responsive images and more.

Jekyll + Rack on Heroku

I last redesigned my blog in 2010 when I migrated from WordPress to Jekyll. I eventually forked jekyll to support a separate photos post type outside of the main site.posts. I then wrapped it in Rack::Rewrite with Rack::TryStatic so I could host it on Heroku and 301 some old permalinks. I won't cover the details of that too much, but I recall reading this post by Matt Manning when I made the switch.

Most of the configuration is in the config.ru file. I loathe URLs that end in .html so my jekyll fork is based on this gist for Apache-inspired "multiviews" support — basically it writes links without the file extension and then I get Rack to do the same.

require 'rubygems'
require 'bundler/setup'
require 'rack/request'
require 'rack/rewrite'
require 'rack/contrib/try_static'

use Rack::Deflater
# also, look into Rack::ETag

use Rack::Rewrite do
  # rewriting old WordPress permalinks I had
  # Do not show .html file extensions

  # I largely gave up /year/month/day style permalinks for the ridiculous 
  # extra page generation time in jekyll (ie if /2013 loaded its own archives page)
  r301 %r{/[0-9]{4}/[0-9]{2}/[0-9]{2}/([a-z0-9\-/]+)}, '/$1'
  r301 %r{/categories/(.*)},       '/posts'
  r301 %r{/tags/(.*)},             '/posts'
  r301 %r{/people/(.*)},           '/posts'
  r301 %r{/([0-9]{4})/([0-9]{2})}, '/posts'
  r301 %r{/([0-9]{4})},            '/posts'
  r301 '/index.html',              '/'
  r301 '/index',                   '/'
  r301 '/archives',                '/posts'
  r301 %r{/(.*).html$},             '/$1'
  
  # I set USER=Stammy in .env -- you use foreman right?? -- to ignore these in dev locally
  unless ENV["USER"] == "Stammy"
    # remove all trailing slashes.. probably a better way to do this
    r301 %r{/(.*)/}, '/$1' 

    # i have a few domains that point here like pstam.com
    # rewrite them to only use paulstamatiou.com
    r301 %r{.*}, 'paulstamatiou.com$&', :if => Proc.new {|rack_env| rack_env['SERVER_NAME'] != 'paulstamatiou.com' } 
  end
end

# serve up some static goodness
use Rack::TryStatic, :root => "_site", :urls => %w[/], :try => ['.html', 'index.html', '/index.html']

# Serve the 404 error page
error_file = '_site/404.html'
run lambda { |env| [404, { 
  'Last-Modified'  => File.mtime(error_file).httpdate, 
  'Content-Type'   => 'text/html' ,
  'Content-Length' => File.size(error_file).to_s },[ File.read(error_file)] ]
}

To ensure deflater is properly compressing markup run this and you should see Content-Encoding: gzip returned:

# change 5000 to whatever port you run locally
curl -i -H "Accept-Encoding: gzip,deflate" localhost:5000 2>&1 | grep "gzip"

grunt watches over

When I started developing the new site I wanted to automate some of my workflow. Things like Coffeescript, JavaScript and Sass compilation to production-ready assets whenever any of the source files changed.

I took a look at the grunt build tool to help me with these issues. If you use jekyll, you probably have a Rakefile1 where you have specified several tasks to aid in create new posts and so on. In layman's terms, grunt is very similar but based on node.

Installation is an npm command away: npm install -g grunt

I setup the main grunt.js file in my project directory root to do a few things:

  • Monitor all files in my style directory and compile screen.scss if any of them changed, like imported scss files.
  • Watch and compile the Coffeescript file app.coffee into JS and put it in the js directory.
  • Watch all specified js files in the _jslibs directory and minify them along with the compiled coffee file, app.js, into a single file.
  • Gzip then upload assets to Cloudfront as necessary.

I installed grunt-coffee and grunt-compass plugins to be able to work with Coffeescript and Compass for Sass. And then grunt-s3 to upload some assets to my S3 Cloudfront bucket. Finally, I installed grunt-smushit to be able to optimize images from the command line (or you can use ImageOptim if you like).

  cd ~/code/your-blog
  npm install grunt-coffee
  npm install grunt-compass
  npm install grunt-s3
  npm install grunt-smushit

In the root of the directory I created a simple package.json file. I really only use it to add a banner to the top of my js file builds but it also keeps track of dependencies so you can easily re-setup grunt on a new machine with npm install.

{
  "name": "pstam-blog",
  "description": "jekyll 'n shit.",
  "version": "1.0",
  "homepage": "paulstamatiou.com",
  "author": {
    "name": "Paul Stamatiou"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/stammy/pstam-blog"
  },
  "devDependencies": {
    "grunt": "latest",
    "grunt-coffee": ">= 0.0.6",
    "grunt-compass": ">= 0.3.7",
    "grunt-s3": ">= 0.0.9"
  }
}

Then I created the grunt.js gruntfile2.

module.exports = function(grunt){
  // https://github.com/avalade/grunt-coffee
  grunt.loadNpmTasks('grunt-coffee');
  // https://github.com/kahlil/grunt-compass
  grunt.loadNpmTasks('grunt-compass');
  // https://github.com/pifantastic/grunt-s3
  grunt.loadNpmTasks('grunt-s3');
  // https://github.com/heldr/grunt-smushit
  grunt.loadNpmTasks('grunt-smushit');

  grunt.initConfig({
    pkg: '<json:package.json>',

    // fetch AWS S3 credentials in another json file
    // name with underscore prefix or exclude in jekyll's config.yml 
    // otherwise this will end up being public!!!
    aws: '<json:_grunt-aws.json>',
    s3: {
      key: '<%= aws.key %>',
      secret: '<%= aws.secret %>',
      bucket: '<%= aws.bucket %>',
      access: 'public-read',
      gzip: true,
      upload: [
        {
          // upload search assets - get src filename from the min block below
          src: '<config:min.search.dest>',
          dest: 'assets/pstamsearch.js'
        },
        {
          // upload main js assets
          src: '<config:min.main.dest>',
          dest: 'assets/pstambuild.js'
        } 
        // etc
      ]
    },
    meta: {
      banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' + '<%= grunt.template.today("yyyy-mm-dd") %> */'
    },
    smushit: {
      // recursively run through *.png, *.jpg in img/ dir and optimize
      path: { src: 'img' }
    },
    min: {
      main: {
        // minify and bundle several js files together 
        src: [
          '<banner:meta.banner>',
          '_jslibs/head.load.min.js',
          '_jslibs/foresight.js',
          'js/app.js'
          ],
        dest: 'js/pstambuild.js',
        separator: ';'
      },
      search: {
        // separately, put search-related js files together
        // to be async loaded only when search is used
        src: [
          '_jslibs/jquery.ba-hashchange.js',
          '_jslibs/jquery.swiftype.search.js'
        ],
        dest: 'js/pstamsearch.js',
        separator: ';'
      }
    },
    coffee: {
      // compile one coffeescript file to js
      app: {
        src: ['coffee/app.coffee'],
        dest: 'js/',
        options: {
          bare: true
        }
      }
    },
    compass: {
      // compile Sass
      dev: {
        specify: 'style/screen.scss',
        dest: 'assets/',
        linecomments: false,
        forcecompile: true,
        outputstyle: 'compressed',
        require: [],
        debugsass: true,
        images: '/img',
        relativeassets: true
      }
    },
    watch: {
      // setup watch tasks. anytime a file is changed, run respective task
      coffee: {
        files: ['<config:coffee.app.src>'],
        tasks: 'js'
      },
      jslibs: {
        files: ['_jslibs/*.js'],
        tasks: 'js'
      },
      sass: {
        files: ['style/*'], 
        tasks: 'compass'
      }
    }
  });
  
  grunt.registerTask('default', 'compass js');
  grunt.registerTask('js', 'coffee min');
  grunt.registerTask('uploadjs', 'js s3');
};

Grunt has several built-in tasks, such as min. It accepts a directory or a bunch of specific JavaScript files and a single destination. One of those source files is from a compiled Coffeescript file, so it's important I only run min after the coffee task. To do that, I registered a js task that runs coffee first, then min.

I've also registered a default task (runs when grunt is called by itself) to call the compass task to compile Sass and then run the js task.

Setting up watch is the last and most important step. I configured it to run the coffee task anytime my coffee file changes, run compass anytime any file in the style directory changes, et cetera. I'm only working with a few files so it's instant.

That's it. I just run grunt watch and get back to work.

spacer
Grunt watching and compiling Coffeescript and Sass

This was a very basic overview of how I use grunt. It can do a lot more so it's worth exploring for other uses. I don't update the CSS or javascript on my site often so digestification 2 of the compiled assets wasn't important for this, but it's something I want to look into.

Currently I manually run that last task (uploadjs) to build the js and upload it to S3. I'll have to spend some time reading the grunt-s3 source but at first glance it looks like it didn't support upload subtasks, so I couldn't abstract out only css uploading, search-related js uploading, and so on. It just uploads all specified files at the same time right now.

Matt Hodan's Jekyll Asset Pipeline is an alternative to using grunt entirely.

Search

I decided to ditch Google CSE and try out Swiftype, a Y Combinator search startup that has been dubbed the Stripe for site search. I have to agree, it's pretty slick. The best thing is that Swiftype lets me control the search results. I can find popular searches and pin certain results to the top.

There are a few install methods for Swiftype but I chose their self-hosted jQuery plugin. I ended up modifying it to provide pagination controls on the top and bottom of the results, add a no-results-found state and some extra markup to help me style it.

The plugin operates by listening to hash changes that include search params. I may end up refactoring it to remove that. Ideally I don't want to have to load an additional jQuery plugin to watch for hash changes and would like to forgo jQuery in favor of the smaller zepto.

Here's what the completed search interaction looks like, thanks to Photoshop CS6's new Timeline feature that helps me create annoying gifs:

spacer

A snippet of the header markup with the search bar:

<li><a class="javascript:void(0)" id="search" class="search ir" title="Search">Search</a></li>
<div id="searchbar">
  <form>
    <input type="text" id="st-search-input" class="st-search-input" />
    <a class="javascript:void(0)" id="st-cancel" class="cancel_search ir">Cancel</a>
    <a class="javascript:void(0)" id="st-close" class="close_search">Close</a>
  </form>
</div>

I only load the Swiftype libraries when the user clicks on the search icon. No need to load all that extra JS for everyone when only a few people will end up searching. Below is the coffeescript that hooks up all of the interactions, downloads the swiftype libraries concatenated and uploaded by grunt, and runs it.

$("#search").on 'click', ->
  # cache some frequently used elements
  search_input_el = $("#st-search-input")
  # create the wrapper for the results and prepend it to the main site div
  search_results_el = $('<div/>', {id: 'st-results-container'}).prependTo '#site'
  search_bar_el = $("#searchbar").fadeIn 200
  nav_li_el = $("#headernav li").hide()

  # load swiftype search libraries
  head.js 'turbo.paulstamatiou.com/assets/pstamsearch.js', -> 
    search_input_el.swiftypeSearch
      resultContainingElement: "#st-results-container"
      engineKey: "YOUR_API_KEY"
      top_pagination: true
      perPage: 15
    search_input_el.focus()
    search_results_el.fadeIn()

  $("#st-close").on "click", ->
    # and/or bind to ESC key (event.keyCode 27)
    search_bar_el.hide()
    nav_li_el.fadeIn 150
    search_results_el.slideUp 150
    # this line clears hash fragments from the URL, not necessary but I prefer to clean up the URL
    history.pushState "", document.title, window.location.pathname + window.location.search

  search_input_el.keypress ->
    # since it's a link, jQuery wants to display: inline; with show()
    # so I manually set inline-block instead
    $("#st-cancel").css('display', 'inline-block').on 'click', ->
      $(@).hide()
      search_input_el.val("").focus()
      search_results_el.empty()
      history.pushState "", document.title, window.location.pathname + window.location.search

Take it for a spin and try searching above!


Getting Responsive

spacer
The new responsive & retina-friendly PaulStamatiou.com.

I'm sure you already know what RWD design is, but to dive a bit deeper responsive design is usually defined as multiple fluid grid layouts while adaptive design is multiple fixed breakpoints / fixed width layouts. However, most of the time you see a mixture of both: fixed width for larger layouts and fluid layouts for smaller viewports.

That's what I do here. Content is 37.62136em wide (multiply that with 16px browser default and the 103% font-size I have on content = 620px) until the smaller viewports when it expands to 100% width.

Right about here I would start talking about how I adapted my site to be responsive on mobile. Except I didn't. I began thinking mobile first and as such it was designed with only a few elements that would need to change between viewports. There was really very little to plan for; I coded it up instead of designing responsive pixel perfects first.

spacer
Responsive pstam.com at 50mm @ f/1.8. I love Chrome for iOS.

Only a few elements elements needed fiddling:

Header: Show some extra subtitle text and increase font-size for larger viewports in addition to moving the avatar to the left size. Also, showing navigation button text for larger screens ("About" next to about icon, etc)

Footer: Increase font-size considerably on smaller viewports to make links easier to tap. Apple HIG suggests at least 44pt x 44pt for tappable UI elements4.

Content: Overall, making buttons larger and full-width where necessary. Adjusting font-size.

spacer
Simplified footer (left) and full-width post-content buttons (right) for smaller viewports.

For larger sites, you'll usually hear people buzzing about content choreography — adjusting layouts and moving elements around as content reflows with smaller viewports — and responsive navigation patterns, things like collapsing larger menus into the header. You can also get a good idea for how others work with layouts by scrolling through screenshots mediaqueri.es

Responsive Setup

To get started we need to tell the browser to set the viewport width by using the device's native width in CSS pixels (different than device pixels, CSS pixels take into account ppi). We also need to disable the browser's default zoom level, called initial-scale. Setting maximum-scale to 1 ensures this zoom remains consistent when the device orientation changes. This is necessary as most smartphones set the viewport width to around 1,000 pixels wide, thus bypassing any media queries you have for smaller screens.

Apple created this viewport tag to be placed in the head:

<meta name="viewport" content="device-width, initial-scale=1, maximum-scale=1">

EDIT: While setting maximum-scale to 1 fixes the iOS orientation change bug it also limits users from manually zooming into your site. I have since removed , maximum-scale=1 from the viewport line above and updated my website to use this accelerometer-based javascript solution: iOS-Orientationchange-Fix. It's very tiny when minified and works like so:

How it works: This fix works by listening to the device's accelerometer to predict when an orientation change is about to occur. When it deems an orientation change imminent, the script disables user zooming, allowing the orientation change to occur properly, with zooming disabled. The script restores zoom again once the device is either oriented close to upright, or after its orientation has changed. This way, user zooming is never disabled while the page is in use.

I'm also taking a suggestion from normalize.css to set text-size-adjust: 100% to prevent iOS from changing text size after orientation changes and without having to set user-scalable=0 on the viewport5. Just be sure to never, ever set text-size-adjust to none — it has the nasty effect of messing up accessibility by preventing visitors from using browser zoom to increase text size.

html {
  -webkit-text-size-adjust: 100%;
  -ms-text-size-adjust: 100%;
}

Then we want to make sure less capable browsers like Internet Explorer 8 can make use of media queries. There are a myriad of ways to polyfill or gracefully degrade media queries on such browsers. I've decided to go with css3-mediaqueries.js as it supports em units.

Similarly to how you conditionally load JavaScript like html5shiv in your head, we load css3-mediaqueries.js in the same way:

<!-- Ideally you should minify + gzip and host this yourself like a boss -->
<!--[if lt IE 9]>
<script src="/img/spacer.gif"> 
<![endif]-->

WTF are Media Queries!?

Media queries are the lifeblood of any responsive website. They are used to conditionally6 load CSS for a media type (like screen, tv, print) based on at least one media feature expression. Well to be technically correct, the browser loads all of them regardless of whether they will be used. More on how to fix that later.

You're probably already familiar with them being used for screen width and resolution, but it can also be used to respond to other device characteristics like orientation, monochrome, pointer (presence and accuracy of a pointing device; i.e. use larger buttons/input fields for devices with inaccurate pointing devices to combat Fitts's law), hover and luminosity (soon).

For example, the media query below targets devices with viewport widths of at least 481px, which is pretty much the portrait width of most ta