24th October 2009, 2 comments, tagged with
user experience, usability, search, analyticsHow do you improve product search? Look at who your users are and what they are doing.
Figure 1. Lightbulbs Direct search circa 2005
When it came to rebuilding the Lightbulbs Direct Ltd website in 2008, the primary aims were to get customers to products faster, and through the checkout as easily as possible. In this retrospective I will discuss the improvements we made after analysing user behaviour and grouping user journeys.
To begin, this is the original search results page from 2005 (right).
The problems with this page extend beyond its visual design:
- taxonomic classification is good for search engines but user journeys comprise several consecutive pages just to narrow product choices
- many users quickly reverted to complex keyword searches, either specific part numbers or ambiguous “red bulb” or “fridge door”
- “Advanced Search” (personal search via a contact form) was used regularly, implying search results unsatisfactory
- search results page looked visually complex, could not be easily scan-read and required a lot of scrolling for pagination
- a “Buy” link directly from results page is optimistic and pushy
- search results cannot be re-filtered or easily amended
Almost importantly of all there was little feedback to the user that their search keywords had been correctly interpreted. How could they be sure that the website knew their search keywords “gls 60w” actually meant “Find me 60 Watt GLS bulbs”?
During the design phase, the user journeys through product search were the main focus of my attention, with a view to solving all of the above problems. We decided on three key entry points into product search, which matched three distinct user groups of varying levels of domain knowledge. Each group would have a search journey tailored to their specific needs, aligned closely with their observed behaviour:
1. “What [XYZ] do they stock?”
These visitors know about product types (bulb, fluorescent tube, spotlight) and they are looking for a specific item of that type. This lends itself very well to the original (albeit poorly named) “Bulb Finder” concept which is essentially a taxonomic drill-down through product categories. By displaying familiar top-level category names on the home page and in an obvious place on every page, users who are familiar with the terms can use them as their entry point.
After several levels of ever-narrowing categories choices, a product listing is displayed. The information shown in the table of products was carefully chosen so that a user can decide whether the product is the right item without having to click through to a product page. In the technical world of electronics, specifications speak louder than captions and descriptions. Nevertheless a small photograph is essential for the user’s decision making process.
Figure 2. Wireframes and renders of category drill-down
2. “My bulb has just blown, I need a replacement now!”
Do you know your 40W from your 60W; your pearl from your white; your Edison from your Bayonet cap? Probably not, and why should you? But imagine that your desk lamp bulb has just blown and you are holding it in your hand. There are no distinguishing marks except for a faded “60W” stamped on the clear glass. This is where the “Bulb Finder” works its magic, allowing a user to provide details up to four pieces of common information easily realised from a busted bulb: Wattage, Voltage, Cap/fitting (push-fit, screw-in) and Colour.
And instead of making this a trial-and-error process, the Bulb Finder gives live feedback as you enter the bulb details. You don’t have to click “Search” and wait for a results page to tell you how many matches there are. This serves the purpose of giving the user control as to when they view the matching products. 600 items found? Maybe you want to narrow it down a bit before jumping in at the deep end. Changes a drop down. No items found? Maybe one of your choices is incorrect, or we simply don’t have what you’re after. Changes a drop down 12 items found? It’s likely we’ve got what you want, take a look.
Figure 3. Changing states of the Bulb Finder
3. “I know exactly what I want”, or “Panic! I don’t know what I want at all!”
The keyword search satisfies a group of other broader, yet still distinct, users. Perhaps they’ve headed to the search box immediately because they don’t know what “40W Phillips” scrawled on the side of their tube means; or perhaps they’ve been browsing a little and can’t find that elusive niche item; or perhaps they know a specific manufacturer part number for a quick price comparison. Or perhaps they’re simply looking for information about “energy saving” and simply don’t want to be told to purchase something.
Everyone has their own mental model (consciously or unconsciously) of how a search works, and they supply their keywords accordingly. This means that building a good keyword search is incredibly difficult. When developing the keyword search we went to great length to ensure that it remained as flexible and useful as possible:
Converting keywords into technical parameters
Figure 4. Keywords can be complex technical specifications
Products are stored in an abstract data model similar to the entry-attribute-value pattern, allowing a product to have a specification defined by any number of attributes. However, all items have a set of default attributes comprising the four used in the Bulb Finder (Wattage, Voltage, Cap/Fitting, Colour).
The keyword search uses some complex string-parsing algorithms to translate a search phrase such as “60 WATT bc 240volt” into a product search on Wattage: 60, Cap: Bayonet, Voltage: 240. In the UI, known technical attributes are pre-selected in their respective drop downs and can easily be tweaked or removed.
Figure 5. Technical attributes are pre-selected using keyword text
Modifying search filters
If the user can not find what they want, the search needs to be easily repeatable by tweaking parameters. The attribute drop-downs provide technical clarity, but free-text keywords also need be addressed. Standard keywords are displayed in a familiar strip of tags from which the user can remove individual words if they are deemed to be reducing accuracy.
Figure 6. Individual words can be removed
Scanning and pagination
Products are shown in a simple table, up to ten per page. The small thumbnail allows for instant recognition of more recognisable shapes/colours, and the natural-blue hyperlinked product name is an obvious call to action. Subtle zebra-striping aids scanning horizontally, while wide column spacing and consistent alignment makes vertical scanning a cinch. The total number of results is clearly labelled, and pagination is repeated at the head and foot of the table. This layout displays 2–4 more products above the 1024x768 fold compared to the previous design.
Figure 7. The full search results page and product listing
This article has shown that with careful observation of your target audience you can design page elements that are more closely aligned with their behaviour. By adopting this user-centred approach, the site is more useful to its users and becomes instantly more easy to use.
20th October 2009, one comment, tagged with
Symphony, XSLTXSLT 1.0 has no native way of achieving a for loop, without iterating through XML. Until now.
XSLT is planted firmly in the declarative programming style — something that regularly frustrates newcomers from procedural languages such as PHP or JavaScript. You need to change your programming mindset to approach familiar concepts in new ways. One such example is the traditional for loop. In XSLT there exists a for-each element which iterates over an XML nodeset:
<xsl:for-each select="countries/country">
<xsl:value-of select="text()"/>
</xsl:for-each>
As dandy as this is, it relies on having XML to iterate over. How can you achieve a for loop to provide, say, ten iterations, from 1 to 10. In PHP this would be a simple as:
for($i=1; $i<=10; $i++) {
echo($i);
}
To achieve this using XSLT you need to take a different approach. Below I’ll outline two ideas for generating a for loop using XSLT 1.0 — one a simple hack, the other more advanced and elegant.
1. Tokens at the ready
Using the tokenize() function of the EXSLT strings library one can break a string of a given length into individual characters and loop through each:
<xsl:for-each select="str:tokenize('..........')/token">
<xsl:value-of select="position()"/>
</xsl:for-each>
There are ten full-stops in the string, meaning that ten token nodes will be created. Not particularly elegant or maintainable. This can be improved on slightly by generating the string dynamically using the EXSLT padding() function, making it easier to set its length.
<xsl:for-each select="str:tokenize(str:padding(10,'.'))/token">
<xsl:value-of select="position()"/>
</xsl:for-each>
2. Recursive to the max
The more ‘XSL-like’ approach is to use a recursive template for the iteration. The template is supplied with start and end conditions and repeatedly calls itself until the end condition is met. The finest explanation I’ve seen is Loop with recursion in XSLT on IBM developerWorks. Recommended reading.
Writing a recursive template to achieve the above requirement is relatively trivial:
<xsl:template name="for-loop">
<xsl:param name="count" select="1"/>
<xsl:if test="$count > 0">
<xsl:value-of select="$count"/>
<xsl:call-template name="for-loop">
<xsl:with-param name="count" select="$count - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
The problem here is that the template output is muddled into the same template as the iteration/recursion logic. It would make more sense to have a single “iterator” template to perform the recursion, and have the output rendered in a separate template. Consider the following scenario:
You’re building a form and need to fill a select drop down with ten options (1–10), but you also need to render five checkboxes. Using the above example you would need to replicate the template twice, naming them select-loop and checkbox-loop. Inside each respective template you would change the output to return either an <option> element or a checkbox.
My colleague Paul Garrett and devised a cunning way for a call-template to execute a callback template which is where the custom rendering is achieved.
Consider this markup to render a select box with ten options, followed by five checkboxes:
<xsl:template match="data">
<select>
<xsl:call-template name="iterator">
<xsl:with-param name="iterations" select="'10'"/>
<xsl:with-param name="callback" select="'render-option'"/>
</xsl:call-template>
</select>
<xsl:call-template name="iterator">
<xsl:with-param name="iterations" select="'5'"/>
<xsl:with-param name="callback" select="'render-checkbox'"/>
</xsl:call-template>
</xsl:template>
<xsl:template match="render-option" mode="iterator-callback">
<xsl:param name="position"/>
<option>
<xsl:value-of select="$position"/>
</option>
</xsl:template>
<xsl:template match="render-checkbox" mode="iterator-callback">
<xsl:param name="position"/>
<input type="checkbox" name="fields[checkbox-{$position}]"/>
</xsl:template>
We can call a generic iterator template passing the number of iterations required (as you would a traditional for loop) while providing the name of a callback template. The iterator template creates an XML node of the same name and selects it using an apply-templates. The callback template is passed the position in the loop providing context.
The full iterator template:
<xsl:template name="iterator">
<xsl:param name="start" select="'1'"/>
<xsl:param name="iterations" select="$iterations"/>
<xsl:param name="direction" select="'+'"/>
<xsl:param name="callback"/>
<xsl:param name="callback-params"/>
<xsl:param name="count" select="$iterations"/>
<xsl:if test="$callback and $count > 0">
<xsl:variable name="position">
<xsl:choose>
<xsl:when test="$direction='-'"><xsl:value-of select="$start - ($iterations - $count)"/></xsl:when>
<xsl:otherwise><xsl:value-of select="$start + ($iterations - $count)"/></xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="callback-node">
<xsl:element name="{$callback}">
<xsl:if test="$callback-params">
<xsl:copy-of select="$callback-params"/>
</xsl:if>
</xsl:element>
</xsl:variable>
<xsl:apply-templates select="exsl:node-set($callback-node)" mode="iterator-callback">
<xsl:with-param name="position" select="$position"/>
</xsl:apply-templates>
<xsl:call-template name="iterator">
<xsl:with-param name="start" select="$start"/>
<xsl:with-param name="iterations" select="$iterations"/>
<xsl:with-param name="direction" select="$direction"/>
<xsl:with-param name="callback" select="$callback"/>
<xsl:with-param name="callback-params" select="$callback-params"/>
<xsl:with-param name="count" select="$count - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
Additional parameters can be passed for more advanced iterations and callbacks:
start allows you to start the iteration from a specific number, defaults to 1:
<xsl:with-param name="start" select="'3'"/>
iterations is the number of iterations to perform:
<xsl:with-param name="iterations" select="'100'"/>
direction allows you to count down (“-“) rather than the default up (“+”)
<xsl:with-param name="direction" select="'-'"/>
callback is the name of the XML node created to match on with your callback template
<xsl:with-param name="callback" select="'my-template'"/>
callback-params accepts XML which are then passed to the callback template
<xsl:with-param name="callback-params">
<name>Nick Dunn</name>
</xsl:with-param>
<xsl:template match="render-user" mode="iterator-callback">
<xsl:param name="position"/>
<p>Name: <xsl:value-of select="name"/></p>
</xsl:template>
(Note: you will need to include the EXSLT common namespace in your stylesheet to use the node-set function in the template above!)
Conclusion
We have seen the different ways of achieving a numeric for loop in XSLT, and in doing so created a re-usable recursive template with a dynamic callback. I’ve not seen this callback method documented anywhere else, so this may be a new technique that has novel applications beyond this example. Recurse to the max, don’t tokenize.
28th July 2009, 2 comments, tagged with
SymphonyPixel pushing, CSS, and XSLT content management. All in a day’s work. Can I sleep now?
19th July 2009, 17 comments, tagged with
browsers, Internet Explorer, CSSLook ma, no images!
There’s an awful lot of noise at the moment regarding dropping IE6 and forging ahead with CSS3 properties for the finer touches on web layouts. Do websites need to look exactly the same in every browser?
One such example is adding drop shadows to content blocks. There are countless ways of achieving this, most requiring additional HTML markup and one or more PNG images. Not to mention hacks for IE6 to get render PNGs properly. All of this begins to degrade page performance, when really the drop shadow is merely a secondary design flourish.
CSS3 provides the box-shadow property to achieve just that. Thankfully Safari/Webkit has supported this for some time, and Firefox more recently with Firefox 3.5. Internet Explorer provides a DropShadow filter but this provides a hard-edged shadow unlike its CSS3 counterpart. By combining the Glow and Shadow filters however, we can achieve something that fairly closely resembles the rendered CSS3 shadow. Hurrah!
Here’s an example page and the result across major browsers:
Figure 1. CSS drop shadows rendered in major browsers
Disadvantages
- no support in Opera or Firefox pre-3.5
- filters may clash if you also need to set container opacity (e.g. fading using jQuery)
- the shadows aren’t exactly the same
Advantages
- no additional markup, images or IE6 hacking
- faster performance than rendering images (especially IE6 PNG fix)
- shadow changes can be tweaked using CSS
- no restriction on size — shadow grows as the content grows
Update (19 July 2009, 17:47)
Figure 2. A cleaner IE6 version
As ZigPress points out in the comments the IE representation is still rather ugly.
By removing the Glow filter and using Shadow filters in its place, and reducing the weight of the shadow, we approach something more forgiving in Internet Explorer using the following CSS:
div {
filter:
progid:DXImageTransform.Microsoft.Shadow(color=#eeeeee,direction=0,strength=7)
progid:DXImageTransform.Microsoft.Shadow(color=#dddddd,direction=90,strength=10)
progid:DXImageTransform.Microsoft.Shadow(color=#dddddd,direction=180,strength=10)
progid:DXImageTransform.Microsoft.Shadow(color=#eeeeee,direction=270,strength=7);
}
30th June 2009, 0 comments, tagged with
Sifter XMLSifter offers a super-smooth user experience for tracking bugs and issues, but no exporting?
The Sifter API has been a long time coming and there’s nothing on the horizon yet. I recently embarked on my own set of scripts to automate the process of creating a report from Sifter. The idea is to parse the issues HTML list into XML and then transform to whatever the user desires using XSLT (a printable HTML page, an XML file, a CSV). But I digress.
So I found a natty little update to Sifter that provides native export of your issue list as a CSV file!
Spot /issues?s=... in the URL and change it to /issues.csv?s=. Tada!
I’ll hold out for the API.