When developing websites with Umbraco, I always use the XSLTsearch package to handle the search solution. As well as being easy and quick to install, it is customisable and very fast in searching content nodes.
As much as I love XSLTsearch, the one dilemma I always face is having to modify the HTML output that is generated. Given the way I work; with a much better front-end developer providing the mark-up; I am faced with shoehorning my HTML into the XSLTsearch template.
In many cases this is fine and acceptable. However how to handle bugfixes and overall improvements to XSLTsearch when they may be released? Having to retro-fit these into my forked/modified/hacked version is an unwanted headache. (Also having learnt from past experiences in open-source development, you do not hack the core!)
So how can we separate our desired mark-up from the logic/core of XSLTsearch? (without modifying XSLTsearch itself!)
Now we all know that the beauty of XSLT is in its ability to transform one flavour of XML into another flavour of XML; whether this be XHTML, RSS, etc. The XML is processed and returned. With this in mind, we can alter our perception of the output from XSLTsearch as a data-source, rather than HTML output.
Reviewing the XSLTsearch source, skipping over all the variable declarations, the root template runs a couple of conditions on the $search and $source parameters, then ultimately calls a template named “search” – passing through a parameter called “items” containing an XPath for the selected node-set.
Using a separate XSLT file, we can import “XSLTsearch.xslt” and override its templates with our own – this is done by using the “priority” attribute on our xsl:template. From here we can make our own call to the “search” template and pass through any XPath we desire in the “items” parameter.
To do this…
- Create a new XSLT file called “SearchResults.xslt” – you can do this either from your Umbraco back-office or on directly on the file-system (in the /xslt directory).
- Find the macro for XSLTsearch, (via the Umbraco back-office), change the “Use XSLT file” to “SearchResults.xslt” (e.g. the one you’ve just created).
- Copy-n-paste the following snippet, (or get the full XSLT from my gist):
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE xsl:stylesheet [ <!ENTITY nbsp " "> ]> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxml="urn:schemas-microsoft-com:xslt" xmlns:umbraco.library="urn:umbraco.library" xmlns:PS.XSLTsearch="urn:PS.XSLTsearch" exclude-result-prefixes="msxml umbraco.library PS.XSLTsearch"> <xsl:import href="XSLTsearch.xslt"/> <xsl:template match="/" priority="2"> <xsl:variable name="searchResults"> <xsl:call-template name="search"> <xsl:with-param name="items" select="$currentPage/ancestor-or-self::*[@level = 1]"/> </xsl:call-template> </xsl:variable> <xsl:variable name="results" select="msxml:node-set($searchResults)"/> <xmp> <xsl:copy-of select="$results"/> </xmp> </xsl:template> </xsl:stylesheet>
This is a snippet for my blog post, to get my full customised XSLT example, see here: https://gist.github.com/1879072
So, what has just happened?
We’ve included an import reference for the unmodified XSLTsearch file. Our root template (match=”/”) is given a priority of 2; this tells the XSLT processor that our template takes a higher priority that XSLTsearch’s original template.
Inside our template we declare a variable named “searchResults” (you can call this whatever you like), the value of this will be the output from XSLTsearch’s template named “search”, passing in the “items” parameter.
It is important to point out that the value of the “searchResults” variable will be a result tree fragment, meaning we can’t use it as we would a regular XML node-set. This usually causes headaches for developers – it’s generally at the point when they want to throw their laptop out of the window and pledge allegiance to the Razor Empire. So to resolve this, we need the follow-up variable called “result” – this uses a native extension method to convert the fragment back into a proper node-set.
Once we have our “$results” variable, we can manipulate this however we like. On a side note, do read the Pimp My XSLT article on “Transforming WYSIWYG output with XSLT“.
My use of the <xmp> tag is for debug purposes only … yes, yes, I know it has been deprecated since HTML 3.2, but as long as browsers support it, I’ll use it … and no, I’ve never ever used it on a production website.
From here you can do an xsl:apply-templates on the $result node-set and manipulate the search results however you desire.
<xsl:apply-templates select="$results/div" />
Another advantage of taking this approach is to be able to manipulate the XPath expression of the “items” parameter.
Let’s say that you only wanted to search against your news articles; we could modify the XPath expression to something like this…
<xsl:with-param name="items" select="$currentPage/ancestor-or-self::*[@level = 1]/descendant::NewsArticle[@isDoc]"/>
This XPath navigates all the way up the tree to the homepage node (level 1), then back down collecting all the ‘NewsArticle’ nodes.
Or how’s about something more advanced? We would like only news articles published in the last 30 days.
<xsl:with-param name="items" select="$currentPage/ancestor-or-self::*[@level = 1]/descendant::NewsArticle[@isDoc and umbraco.library:DateGreaterThanOrEqualToday(umbraco.library:DateAdd(@createDate, 'd', 30))]"/>
In summary, once you are able to change your perspective that the output from an XSLT is still XML, which you can manipulate and transform further – you have complete control over your desired mark-up.