Monday, April 11, 2011

An Event Apart - Seattle (Day 1, part 2)

Moved here

Previously.

Web Typography

Jason Santa Maria, self-described font geek, gave a talk on fonts in general and the huge opportunities sites like TypeKit give for improving the look and feel of your own sites. As recently as three years ago, we were still stuck using the 18 Web Fonts or hacks like sIFR to get real fonts on websites. Publications like the New Yorker, who are instantly recognizable from the typefaces they use, have had a hard time carrying that brand identity to their websites. Many have resorted to rendering each headline in the correct font into images, creating a maintenance nightmare. With the ability to use things like font-face, existing publications can carry their brand identity with them to the web, and new publications can enhance their own identity with their font selections. Even simple things like the ability to use condensed fonts on the web has totally changed the way well-designed sites look.

How do I choose the right typeface?

There is a difference between display typefaces, which are most useful for headlines and titles, and text typefaces, which are best for body text. A display typeface like Bodoni can be great for an article's title, but can be difficult to read in a smaller size. Most people will have the best bet finding a few good fonts with many faces, like FF Meta or Proxima that can be used successfully in many different scenarios. Good typography is about building contrast, texture, and feeling on the page that help with comprehension and enjoyment of the thing being read. "Good typography is invisible."

When using typography, there are a few guidelines that can be useful, but should be broken when appropriate:

  • All things being equal, err on the side of the text size being too big
  • Contrast is the most important tenet of good design - make parts of the text that differ in some way look different. Play with size, weight, coler, etc. to build the right amount of contrast.
  • Line spacing should be directly proportional to the line length and color of the font

There are lots of things to consider when picking the right fonts, but there are ways to help choose among them. Noting the qualities you want your site and your content to convey will help you narrow long lists of fonts down. Finding fonts that come in both serif and sans serif faces can make pairing simpler and will help create contrast where it's needed. Fonts that have the same feel as a commonly used font can help you pick similar but more interesting fonts. And finally, trying different fonts out by setting some text in a range of typefaces and playing with them will almost always be really helpful. Jason left us with a few sites that use web typography well: Lost World's Fairs and Voltage: Fashion Amplified.

Why Designers Fail

Scott Berkun was next, with a talk about design failures and how to learn from them. Mindful practice and studying other people are the two primary ways people learn new things, and studying failure in particular is an incredible untapped resource of knowledge. In the design world, it's difficult to find good analysis of design failures. Especially in fields like architecture, where highly in-demand designers tend to move on to new projects frequently, it's sometimes difficult for these designers to see and hear the feedback that only comes from using the product. How do you avoid falling into common design traps? Understand that all designers fail most of the time (whether it's on the drawing board or in the real world), own the mistakes that you do make, analyze the cause (whether it's setting the wrong goals or failing to meet them), and studying common causes of failure and possible mitigations.

Scott showed an amazing video of designer Matt Wiley laying out a magazine article, and how many intermediate failure states he went through in the process of coming up with a great design. These are the things that many people don't see when they look at a finished product. There's a constant process of experimenting, failing, and revising that is absolutely critical to the creative process, but isn't visible at all in the final product.

So why do designers fail? Some of it comes from falling into common traps: problem-solving for its own sake (Puzzle traps), obsessively categorizing (Category traps), confusing measurements with reality (Number traps), and loving the sketch more that what it actually represents (Drawing traps). Some of it is related to skill: there's often a mismatch between the skills that designers have and the skills that an organization needs, especially around persuading decision-makers that good ideas are good ideas. Designers should be "ambassadors for good ideas", and that involves knowing how to sell these ideas to decision makers. Luckily, persuasion, like most skills, can be learned, and it is something that designers should take the time to learn.

Scott did some research to discover which of these failures were the most common and the most important. By far, the biggest overall issue was organizational: "Non-designers making design decisions." Others were psychological: Not seeking enough data before designing, and not being receptive to feedback. Less common and important were skill-related issues, with the top being lack of awareness of business fundamentals, and lack of idea-pitching skills. Scott publicly posted his data, which is really worth a look, and his slides are also available. Hopefully, being able to identify these causes of failure will help us, as a profession, avoid them in our future work.

Monday, April 4, 2011

An Event Apart - Seattle (Day 1, part 1)

Moved here

I went to An Event Apart Seattle last week and it was the best conference I've ever been to. I've started to gather my notes together and will, over the next few posts, talk about what I was able to take away from each of the presentations. Before I go in detail on the talks, though, I wanted to mention a few themes that seemed to flow through multiple presentations during the conference.

HTML5 and CSS3 are ready now.

This theme was the most related to the work I do on a daily basis. Every talk that touched web development explored features that weren't available in every browser (one talk was built around them), and it seems that the web design community has finally abandoned the idea that sites should act the same way in every browser. Most of the speakers accepted that sites should still work in some way in every browser, but it seemed accepted as given that we should start using HTML5 and CSS3 features tastefully to enhance the experience of people using modern browsers (and especially modern mobile browsers).

Context is everything

Whether it was talking about simplicity as delivering the correct experience to the correct user or pairing the correct font or design or architecture or business goal to the correct content or tailoring a mobile web application to fit the behavior of a user who just wants to get the information they need and get on with their life, context was brought up constantly. Building the right context for the job you're trying to do involves asking pointed, well-thought-out questions, and several of the presentations had slide after slide of thought-provoking questions intended to bring the assumed context out into the open.

The things we have been talking about as important before, are now critical

From the data some of the presentations revealed, we are beginning to hit a critical moment in many areas the web touches. Mobile is the most obvious example: Luke Wroblewski was fond of mentioning that smartphone shipments overtook PC shipments a full two years earlier than expected. There were others, too: the rise of HTML5 and CSS3, the ability of sites like TypeKit to drastically help increase your site's branding, and the growth of content strategy as a discipline to deliver the right information to the right people at the right time. These things that were important before are now becoming necessary, because people are beginning to expect great experiences from the sites they visit, and if they can't get it from your site, they will get it somewhere else.

A whirlwind tour of the history of the web

Jeffrey Zeldman started the conference with a fast and funny history of the web. Beginning with Movable Type (Gutenberg, not Six Apart), passing through AOL, the browser wars, an unfortunately de-pixelated Zeldman on the cover of "Designing with Web Standards", and the fall of XHTML, and finishing up with mobile browsing, HTML5/CSS3, web fonts, and the IE9 beta, it did a fantastic job of setting the context for the rest of the conference. Today, thanks to the widespread adoption of web standards, the web has grown up. We can now make use of skills that were not previously useful online to create a better web experience for everyone. And since the web is now everywhere, those experiences can affect people beyond what was previously possible.

Better user experiences through psychology

Sarah Parmenter was next, giving a talk called "Crafting User Experiences." Sarah discussed how well-known psychological principles could be used to build great user experiences. I noticed a lot of parallels to one of my favorite recent books, Predictably Irrational (which should be next on your list if you haven't yet had the opportunity to read it), and I found it really interesting that a lot of this knowledge that used to seem to be confined to sales and marketing is now making its way into mainstream web design.

Snap judgements and sensation transference were the first topics hit. We usually deny that snap judgements are as important as they are, since we tend to thing that "the quality of a decision is directly related to the time and effort that went into making it." In reality, judgements made in under 5 minutes are drastically more important to our final decisions than we realize. This is related to to sensation transference: our judgement of the packaging of a product can have real effects on our perception of the actual product. For example, adding yellow coloring to 7up made people percieve more lime taste while drinking it. This led to the first appearance of the Dark Patterns wiki during the conference. Like any skill, though, these skills can be used for good as well as evil.

Better user experiences through psychology can be delivered by concentrating on speed, simplicity, surprise, social proof, and stirring emotions. Speed involves getting people the product they want quickly without any fuss. From working closely on your site's structure and building great navigation to creating quick video overviews of a service, the faster you can get information to someone, the happier they'll be. This goes hand-in-hand with simplicity. Simplicity is delivering the right content to the right person at the right time, and involves really studying your audience and what context they're in when they visit your site. Simplicity is heavily colored by perception, and this can be used to your advantage—telling your users it will take 30 seconds to sign up on your site when it actually takes 10 will make your site seem easier to use and will make that user feel smarter and better about themselves.

Surprise is challenging the expectations of your users in some small way. You can evoke this by using complimentary highlight colors on calls to action, or provoking the user to ask questions they need to click through to answer (what is that weird old tip for a tiny belly?), or even picking a product or company name that provokes surprise and questions from a user (like Sarah herself did with "You Know Who"). Social proof is as simple as pre-filling a tip jar (so it seems normal to leave money) or public follow/like counts on your site (30 million people and counting apparently love FarmVille). Finally, Sarah talked about how people are much more likely to make judgments on emotion and only later justify them rationally. Things like using lots of whitespace for luxury items or natural textures for organic items create an emotional attraction that can work beautifully when trying to attract a user to your products. Etsy does a great job of this, as did Apple when creating a "How it's made" for their iPhoto holiday cards. Anthropomorphized icons can also work great for creating an emotional attachment, and can be as easy as adding eyes and a smile to, say, an illustration of a house. There was a ton of great stuff in her talk, and I'm looking forward to trying some of the things she mentioned myself.

Wednesday, March 9, 2011

Shortcut Keys for the HipChat Fluid App

Moved here

As I mentioned last week, we've started to use HipChat for our internal chat here at Avvo. I've decided to forego their default AIR app and try running their web interface through Fluid to get a more native mac-like experience. One of the few things that bothered me about the Fluid app was the lack of keyboard controls for accessing and closing the tabs for each chat room. So I added them.

One of the great things about Fluid is its support for userscripts to add functionality to the pages it wraps. I created a userscript for the HipChat web interface that adds the following keyboard shortcuts:

  • Command-W: Closes the current tab (if the tab can be closed)
  • Command-1 through Command-9: Selects the tab at the specified position
  • Command-{ and Command-}: Select the previous tab and next tab, respectively.

To install it, when running your HipChat fluid instance, open 'Preferences', go to the 'Advanced' section and add *.user.js as a pattern. Then, go to 'File', 'Open Location' and paste in the following url:

https://github.com/justinweiss/hipchat_shortcut_keys/raw/master/hipchat_shortcut_keys.user.js

Install the script when it asks, and you should be good to go.

Wednesday, March 2, 2011

HipChat without AIR

Moved here

We've been investigating HipChat for internal chat lately, and I was a little disappointed that it used Adobe AIR on all platforms, instead of providing a native app. To be fair, it's the most well-done AIR app this side of Basalmiq, but there were still the little inconsistencies that I couldn't help but get annoyed by. I'm not going to describe the exact things that bothered me here, since Alex Payne does such a great job describing the problems here, but it felt weird enough that I couldn't see myself using the app as often as I'd use, say, Propane.

Heading through their support pages to get an idea of how I could improve my experience, I noticed that there seemed to be a fair amount of support for running the web version of HipChat through Fluid, which, while not as pleasant as a native app, still felt much faster and more native on the Mac than the AIR app did. HipChat did an amazing job of building support for things like Growl notifications and Dock badge notifications into the web interface, and seem to want to make the site work as well in Fluid as it does through AIR. Given the constraints they've set for themselves, HipChat has done a tremendous job of building support for people that would like an alternative to AIR.

Accessing HipChat through Fluid

There were only a few minor gotchas I ran into when creating the HipChat Fluid app. First, after downloading and running Fluid, the URL should be set to https://www.hipchat.com/chat. The icon can be left as the website favicon, which awesomely still takes a full-size icon from HipChat's site, rather than the actual tiny favicon the site uses. Once inside the HipChat Fluid app, you'll have to adjust the preferences. Under the Advanced category, you'll have to add the pattern *hipchat.com/sign_in*, otherwise you won't be able to sign in. Finally, if you want Growl and Dock notifications, you have to enable notifications through the AIR app—HipChat doesn't expose that setting in the web app.

There are some cons to running the web app—although notifications, file drag-and-drop, and most of the other functionality works, the web app is missing the file/link dock on the right hand side, the tabs are a little flakier, you can't video chat, and there's a big "Download App" button that would be much more valuable as room for another chat tab. However, the use of native Mac OS X controls, the smoother scrolling, Growl support (rather than the built-in notifications that show up in the wrong place), better integration with the system, and the ability to script the site with CSS and JavaScript make the Fluid app the way to go.

Finally, while I was digging through the JavaScript, I noticed that HipChat has what looks like decent support for running it as a Chrome app, which might be worth trying if you're not on OS X or just prefer Chrome.

Wednesday, February 23, 2011

Deploy only tested code with Jenkins and Ruby

Moved here

Deploying software builds that don't pass the unit tests is a waste of time. It prevents QA from doing their job, it causes delays while a fixed build is prepared, and it could even lead to more ops time to fix if the broken build breaks something on the machines it's being deployed to. If you use Jenkins to run your tests, there's a better way. Using the Jenkins API, we can get the last revision that passed for a project, and adjust our behavior based on that revision number.

Jenkins.rb is a Ruby wrapper for the Jenkins API. gem install jenkins.rb should give you everything you need. Then, you can whip up a quick ruby script:

require 'jenkins'
require 'fileutils'

Jenkins::Api.setup_base_url(:host => 'jenkins-host.example.com')

# Pull the project name from the command line
project = ARGV[0]

# Retrieve the last successful build number (123, for example)
successful_build = Jenkins::Api.job(project)["lastSuccessfulBuild"]["number"]

# Retrieve information about the actual build, and grab the last built revision out of it
build_info = Jenkins::Api.get("/job/#{project}/#{successful_build}/api/json")
sha = build_info['actions'].detect {|h| h["lastBuiltRevision"] }["lastBuiltRevision"]["SHA1"]
puts sha

When this script is run with ruby script.rb project-name, it'll return the most recent revision of project-name that passed all the tests, which the build script can use by calling git reset --hard number or something else like that.

Wednesday, February 16, 2011

Paginating custom objects with will_paginate

Moved here

will_paginate makes pagination easy, and we use it whenever we need to show lots of ActiveRecord objects. Once the will_paginate gem is installed, it provides a built-in #paginate method for classes that inherit from ActiveRecord::Base, but we still had our own crazy hacked-up pagination for the responses we would get back from solr. In the interest of making things more consistent and deleting as much code as possible, we thought it'd be a good idea to look into getting our solr objects into the will_paginate model.

This turned out to be much easier than expected. The will_paginate view helper expects an instance of WillPaginate::Collection, which is simple enough to initialize. It was as easy as adding the following method to our search models:

def paginated_docs(options = {})
  WillPaginate::Collection.create(options[:page] || 1, options[:per_page] || PER_PAGE) do |pager|
    
    @response = $solr_client.query(self.search_options.merge({
      :offset => pager.offset,
      :limit => pager.per_page}))
    
    pager.replace(@response.docs)

    # pager.replace sets total_entries if we're on the
    # last page. Otherwise, we'll just populate it.
    unless pager.total_entries
      pager.total_entries = @response.total
    end
  end

As long as a WillPaginate::Collection is returned, will_paginate will do the right thing. Something similar should work for using other services that support limit/offset, but don't have built-in pagination.

Wednesday, February 9, 2011

Bulletproof Asset Hosting and CDNs with Rails

Moved here

After setting up a CDN like CloudFront to quickly serve your Rails site's assets worldwide, you'll probably want Rails to actually link to the assets through the CDN, instead of locally. Rails makes this easy with ActionController::Base.asset_host. The easiest way to use asset_host is by putting the following code in a Rails initializer or production.rb:

ActionController::Base.asset_host = "assets.example.com"

After that, calls like image_tag("logo.png") will produce links like:

<img alt="Avvo" src="http://assets.example.com/images/logo.png?1297189653" />

Going past the basics

While this will work for very simple cases, more complicated scenarios need more code. If you put the above snippet in an initializer, you'll have to wrap it in

if Rails.env.production?
  ActionController::Base.asset_host = "assets.example.com"
end

Why would you want to do that when you could stick the code in production.rb? For our site, it's because we rely on configuration set up in other initializers, which won't be ready by the time production.rb runs.

YSlow will tell you about the next thing that needs to be done. It turns out that some browsers will only open two requests to any given hostname during a web request. In order to improve the speed at which a browser can fetch assets, you'll need to do something a little more complex with asset_host.

Giving a Proc to asset_host

Along with taking a string, ActionController::Base.asset_host can also take a proc. This gives us more flexibility when generating these hosts. We can solve the above problem by using a proc instead of a string:

if Rails.env.production?
  # Source is the asset path as a string, request is a request object
  ActionController::Base.asset_host = Proc.new do |source, request|
    # hosts can also be pulled from a config file for more flexibility
    # or skipping the CDN in staging environments
    hosts = ['http://assets1.example.com', 'http://assets2.example.com']

    # Get a semirandom numeric hash that is consistent for the same source,
    # to make sure that the same asset file always maps to the same asset host
    # (This helps the browser cache these assets)
    hash = source.hash

    # Return a semirandomly selected host from the hosts array
    hosts[hash % hosts.length]
  end
end

Dealing with secure pages

The next thing you'll notice, especially if you don't have SSL certificates for your asset hosts, is that you'll start getting warnings when accessing resources on secure pages. This can be easily fixed by adding another condition to the above proc:

if Rails.env.production?
  # Source is the asset path as a string, request is a request object
  ActionController::Base.asset_host = Proc.new do |source, request|

    if request.ssl?
      "#{request.protocol}#{request.host_with_port}"
    else
      # hosts can also be pulled from a config file for more flexibility
      # or skipping the CDN in staging environments
      hosts = ['http://assets1.example.com', 'http://assets2.example.com']
      
      # Get a semirandom numeric hash that is consistent for the same source,
      # to make sure that the same asset file always maps to the same asset host
      # (This helps the browser cache these assets)
      hash = source.hash
      
      # Return a semirandomly selected host from the hosts array
      hosts[hash % hosts.length]
    end
  end
end

It also turns out that when referring to assets in ActionMailer, you sometimes won't get a request. We need to handle that case, too:

if Rails.env.production?
  # Source is the asset path as a string, request is a request object
  ActionController::Base.asset_host = Proc.new do |source, request|

    if !request #request == false for emails, apparently
      "http://#{AppSettings.host}"
    elsif request.ssl?
      "#{request.protocol}#{request.host_with_port}"
    else
      # hosts can also be pulled from a config file for more flexibility
      # or skipping the CDN in staging environments
      hosts = ['http://assets1.example.com', 'http://assets2.example.com']
      
      # Get a semirandom numeric hash that is consistent for the same source,
      # to make sure that the same asset file always maps to the same asset host
      # (This helps the browser cache these assets)
      hash = source.hash
      
      # Return a semirandomly selected host from the hosts array
      hosts[hash % hosts.length]
    end
  end
end

Dynamic assets

There also might be 'dynamic assets' that the app might use, like captchas, images that are generated on the fly by the server, or anything else that's not stored on the filesystem. For these you might just want to use your web server instead of the asset server:

if Rails.env.production?
  # Source is the asset path as a string, request is a request object
  ActionController::Base.asset_host = Proc.new do |source, request|

    # Redirect dynamic assets to the web server
    is_asset = source.match(/images|javascripts|assets|stylesheets/)
    
    if !request #request == false for emails, apparently
      "http://#{AppSettings.host}"
    elsif request.ssl? || !is_asset
      "#{request.protocol}#{request.host_with_port}"
    else
      # hosts can also be pulled from a config file for more flexibility
      # or skipping the CDN in staging environments
      hosts = ['http://assets1.example.com', 'http://assets2.example.com']
      
      # Get a semirandom numeric hash that is consistent for the same source,
      # to make sure that the same asset file always maps to the same asset host
      # (This helps the browser cache these assets)
      hash = source.hash
      
      # Return a semirandomly selected host from the hosts array
      hosts[hash % hosts.length]
    end
  end
end

Hardcoding exceptions

Finally, you might run into some javascript libraries that don't like being loaded from a different domain than the page being loaded. You can hardcode these to be loaded from the web server instead, for the final bit of code:

if Rails.env.production?
  # Source is the asset path as a string, request is a request object
  ActionController::Base.asset_host = Proc.new do |source, request|

    # Some assets aren't happy being loaded from the assets server :-(
    borked_assets = ['tiny_mce']
    
    # Redirect dynamic assets to the web server
    is_asset = source.match(/images|javascripts|assets|stylesheets/)
    
    if !request #request == false for emails, apparently
      "http://#{AppSettings.host}"
    elsif request.ssl? || !is_asset || borked_assets.any? {|asset| source.match(asset)}
      "#{request.protocol}#{request.host_with_port}"
    else
      # hosts can also be pulled from a config file for more flexibility
      # or skipping the CDN in staging environments
      hosts = ['http://assets1.example.com', 'http://assets2.example.com']
      
      # Get a semirandom numeric hash that is consistent for the same source,
      # to make sure that the same asset file always maps to the same asset host
      # (This helps the browser cache these assets)
      hash = source.hash
      
      # Return a semirandomly selected host from the hosts array
      hosts[hash % hosts.length]
    end
  end
end

This snippet of code should handle pretty much any asset you throw at it, and is easily expandable using whatever type of environment-specific configuration you use. Give it a try, and let me know what you think.