Styling a Sitemap Using XSL and Coldfusion

Published: 12-Mar-2011
Author: Steven Neiland
Site Url: http://www.neiland.net/article/styling-a-sitemap-using-xsl-and-coldfusion/

As a followup to my post about creating a dynamic sitemap using Coldfusion, today I want to talk about how to style a sitemap using xsl and Coldfusion.

Note: I have tried to explain the xsl transformation rule I used here so this will be my longest post to date. However you can skip pages 3 and 4 if you are not interested in understanding the fine details.

Overcoming the limitations of Sitemaps

While xml sitemaps are a great for seo purposes, as a human usability feature they are less than optimal. They normally display in a browser as an ugly tree structure of nodes. This is not user friendly to look at, and further does not provide a direct link to the webpages listed in the node. Fortunately by using xsl we can convert our xml sitemap into a user friendly interactive webpage while webspiders such as googlebot still see a standard xml file.

In order to maintain a consistent look with the rest of our site we will use coldfusion to load our existing site template as our xsl file.

Step 1: Set the Sitemap to Use XSL

To start off lets look at a simple sitemap xml structure consisting of the xml declaration and a urlset wrapping some url nodes.

<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>http://www.neiland.net/</loc>
      <lastmod>2011-03-05</lastmod>
      <changefreq>daily</changefreq>
      <priority>1</priority>
</url>
<url>
<loc>http://www.neiland.net/blog/</loc>
      <lastmod>2011-03-05</lastmod>
      <changefreq>weekly</changefreq>
      <priority>0.8</priority>
</url>
</urlset>

In order to style of sitemap using xsl we replace the xml declaration with a call to our xsl file as follows.

<?xml-stylesheet type="text/xsl" href="http://www.neiland.net/sitemap.xsl"?>
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>http://www.neiland.net/</loc>
      <lastmod>2011-03-05</lastmod>
      <changefreq>daily</changefreq>
      <priority>1</priority>
</url>
<url>
<loc>http://www.neiland.net/blog/</loc>
      <lastmod>2011-03-05</lastmod>
      <changefreq>weekly</changefreq>
      <priority>0.8</priority>
</url>
</urlset>

Step 2: Create the XSL File

We now create the xsl file we specified in our sitemap. To begin with we will create a simple un-styled map which will convert the listed nodes into a list of hyperlinks.

<xsl:stylesheet version="2.0" xmlns:html="http://www.w3.org/TR/REC-html40" xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>

<table cellpadding="5">
<tr style="border-bottom:1px black solid;">
<th>URL</th>
      <th>Priority</th>
      <th>Change Frequency</th>
      <th>LastChange</th>
</tr>
<xsl:for-each select="sitemap:urlset/sitemap:url">
      <tr>
<xsl:if test="position() mod 2 != 1">
       <xsl:attribute name="class">even</xsl:attribute>
       </xsl:if>
<td>
<xsl:variable name="itemURL">
<xsl:value-of select="sitemap:loc"/>
</xsl:variable>
<a href="{$itemURL}"><xsl:value-of select="sitemap:loc"/></a>
</td>
<td>
            <xsl:value-of select="concat(sitemap:priority*100,'%')"/>
       </td>
       <td>
            <xsl:value-of select="concat(translate(substring(sitemap:changefreq, 1, 1),
             concat($lower, $upper),concat($upper, $lower)),
             substring(sitemap:changefreq, 2))"/>
</td>
       <td>
            <xsl:value-of select="concat(substring(sitemap:lastmod,0,11),
concat(' ',substring(sitemap:lastmod,12,5)))"/>
</td>
      </tr>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>

There is alot going on here so lets break it down. If you want to skip the breakdown you can continue onto page 5.

Note: At this stage you should test if the xsl is working by visiting your sitemap. If it is working you should see a table of hyperlinks.

XSL Breakdown

As there is so much going on in the previous code I have broken it down by the key components. I have done this by working inwards by tag pair.

XSL Declaration & Stylesheet Wrapper

The first components of our xsl sheet are the xsl version namespace declarations and the desired output format of html using utf-8 encoding.

<xsl:stylesheet version="2.0" xmlns:html="http://www.w3.org/TR/REC-html40" xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
--snip--

--snip--
</xsl:stylesheet>

Template Wrapper

Inside the stylesheet wrapper we have a template wrapper

<xsl:template match="/">
--snip--

--snip--
</xsl:template>

Variable Declarations

Inside the template wrapper are two variable declarations. These variables named upper and lower will be used further down the code.

<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>

Standard HTML

You will notice that there is standard html mixed in with the xsl code. This is no different to the way you have standard code mixed in with cfml.

For-Each Loop

This is where we start to see the power of xsl. This statement selects the urlset wrapper from our sitemap and loops through each url node in it.

<xsl:for-each select="sitemap:urlset/sitemap:url">
--snip--

--snip--
</xsl:for-each>

XSL Breakdown - Continued

Continuing our code breakdown of our xsl sheet, we come to the body of the for-each loop. Again you will note html mixed in with xsl. I am skipping the html component of the code here.

Position Test

This little snippet of code determines if we are on an even or odd number row of our loop. If it even we give the attribute "class" a value of 'even'. This is class attribute applied to the <tr> html tag.

<xsl:if test="position() mod 2 != 1">
<xsl:attribute name="class">even</xsl:attribute>
</xsl:if>

Hyperlink

The code group in the loop converts the url->loc node into a functional hyperlink.

<xsl:variable name="itemURL">
<xsl:value-of select="sitemap:loc"/>
</xsl:variable>
<a href="{$itemURL}"><xsl:value-of select="sitemap:loc"/></a>

Priority As A Percentage

This line takes the priority value converts it into a percentage by multiplying by 100 and then string concatenates it with the % symbol before outputing to the screen.

<xsl:value-of select="concat(sitemap:priority*100,'%')"/>

Capitalize Change Frequency

Of all the code this is the most complex, and probably the least useful. All it really does is take the change frequency value, gets the uppercase of the first letter and lowercase of the rest and concatenates it back into one string. This is where the upper and lower variables are used.

According to the xsl documentation it should be possible to use an upper-case function, however I have not been able to get it to work. I found this code on the net, and while its ugly it works.

<xsl:value-of select="concat(translate(substring(sitemap:changefreq, 1, 1),
concat($lower, $upper),concat($upper, $lower)),
substring(sitemap:changefreq, 2))"/>

Last Change

Finally we split the last modified value seperating the two components "date" and "time" with a space.

<xsl:value-of select="concat(substring(sitemap:lastmod,0,11),
concat(' ',substring(sitemap:lastmod,12,5)))"/>

Step 3: Insert Site Formatting

So far we have covered how to parse our xml sitemap using xsl. However we still want to make the sitemap's appearance consistent with out website.

To do this we could take our layout html and insert it inside our xsl:template tags but around our xsl for-each code like thus.

<xsl:stylesheet version="2.0" xmlns:html="http://www.w3.org/TR/REC-html40" xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<html>
<head>
<title>My Sitemap</title>
</head>
<body>
<div id="wrapper">
<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>

<table cellpadding="5">
<tr style="border-bottom:1px black solid;">
<th>URL</th>
       <th>Priority</th>
       <th>Change Frequency</th>
       <th>LastChange</th>
</tr>
<xsl:for-each select="sitemap:urlset/sitemap:url">
--snip--

--snip--
</xsl:for-each>
</div>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

While this would work its not really a great solution. This approach basically means you have to maintain two identical templates for the same site.

Fortunately we can leverage the technique I outlined for building a dynamic sitemap the same way here to dynamically build our XSL file.

Leveraging Coldfusion & Url Rewriting to Style XSL

As with building a sitemap dynamically, the first step in building a dynamic xsl file is to convert it from xsl to cfm. We simple rename it from sitemap.xsl to sitemapXsl.cfm .

At this point our sitemap.xml file is still pointing to sitemap.xsl so we either change the pointer in the sitemap file, or as I prefer we create a rewrite rule that points traffic for sitemap.xsl to sitemapXsl.cfm as follows.

#Sitemap XSL rule
RewriteRule ^sitemap.xsl/?$ sitemapXsl.cfm [L,NC]

Set the Encoding and Load the CFML

Now that we have the xsl file converted to cfm we need to instruct our cfml engine to serve it as an xsl file. To do this we place the following three directives at the top of our sitemapXsl.cfm file.

<cfsetting enablecfoutputonly="true" showdebugoutput="false">
<cfprocessingdirective pageencoding="utf-8">
<cfcontent type="text/xsl">

Override Default Encoding

It is important to note that if you have set a doctype in your site template that this must be disabled or it will conflict with the xsl declaration. To do this I use a simple param as follows.

<cfsetting enablecfoutputonly="true" showdebugoutput="false">
<cfprocessingdirective pageencoding="utf-8">
<cfcontent type="text/xsl">
<!---Override site default doc type--->
<cfset doctypeOverride = true>

Include CFML template

For the sake of simplicity here I have split my site template into two files pagetop.cfm and pagebottom.cfm. This is not the only or best way of doing it, it just best illustrates the technique.

Note: Notice that as with the dynamic sitemap example there is no space between the cfoutput tag and the first xsl tag.

<cfsetting enablecfoutputonly="true" showdebugoutput="false">
<cfprocessingdirective pageencoding="utf-8">
<cfcontent type="text/xsl">
<!---Override site default doc type--->
<cfset doctypeOverride = true>
<cfoutput><xsl:stylesheet version="2.0" xmlns:html="http://www.w3.org/TR/REC-html40" xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:template match="/">
<cfinclude template="pagetop.cfm">

<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>

<table cellpadding="5">
<tr style="border-bottom:1px black solid;">
<th>URL</th>
       <th>Priority</th>
       <th>Change Frequency</th>
       <th>LastChange</th>
</tr>
<xsl:for-each select="sitemap:urlset/sitemap:url">
--snip--

--snip--
</xsl:for-each>

<cfinclude template="pagebottom.cfm">
</xsl:template>

</xsl:stylesheet></cfoutput>
</cfcontent>

Step 4: Test and Debug Encoding

At this stage you should be able to visit your sitemap and see a list of hyperlinks inside your standard site template. However it is more likely that you will see an error.

The reason you are likely to see an error is because xsl requires certain characters to be encoded in a particular way. For example the "&" symbol should be encoded as "&amp;". My advice would be to comment out your entire template layout then slowly add back elements and tackle each encoding issue as you go.

Final Word

If you have managed to read this far then congrats. I hope I did not bore you too much. I know this has been a long one but I felt it was worth the extra effort to explain how xsl works.