Generating multiple pages using XSLT 2.0

This page describes a way of using XSLT 2.0 to generate ordered HTML pages from a single XML file, with links to the previous and next pages appearing when appropriate.

For instance, you might have a single XML file that describes all of the hotels in a town, and want to produce HTML pages that list these hotels, with at most ten hotels appearing on a single page. Or you might use an XML file to catalogue the articles on your website, and want to use that file to generate a set of contents pages, such that no more than twenty-five article links are listed on each contents page.

This page assumes you already have some experience in working with XSLT.

Working with XSLT 2.0

Note that this article is about XSLT 2.0. I know of no way to generate multiple pages in XSLT 1.0, so you must be using an XSLT 2.0 processor as XSLT 2.0 offers many more advanced features than the older standard, including the result-document and for-each-group elements.

An excellent, open source XSLT 2.0 processor is Saxon-HE (home edition) which can be run on Java on any platform. It runs on the command line, so you need to read the documentation to learn how to invoke it properly to process your XML and XSLT files.

The XML file

For the purposes of example, here is a very basic XML document structure:

<?xml version="1.0" encoding="UTF-8"?>
<items>
  <item>
    <title>Title of this item</title>
    <summary>Summary of this item</summary>
  </item>
  <!-- Followed by more item elements . . . -->
</items>

The item elements here represent whatever it is you are trying to list across multiple pages, so each item element might be a hotel or an article from your site. The fact that this example structure is simple is unimportant. The important point is that using XSLT you can target the item elements. What goes in the item elements can be as complex as you need for your own purposes.

The XSLT 2.0 template

To produce pages of results from one XML file, we need to decide how many items can appear on each page, then gather our item elements into groups containing at most that quantity, then produce one output file for each of these groups. Breaking this into steps . . .

Define variables used in the XSLT template

The first thing that needs to appear in the XSLT stylesheet is a set of variables, one of which defines how many items we want to appear on each result page. After these variables I also define an output format for XHTML, but you should use whatever output format suits your needs, be it XHTML, HTML or XML.

So the XSLT stylesheet file begins like this:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns="http://www.w3.org/1999/xhtml" version="2.0">
  <!-- Define how many items should appear on each page -->
  <xsl:variable name="itemsPerPage" select="4"/>
  <!-- Store total item count in a variable -->
  <xsl:variable name="itemCount" select="count(/items/item)"/>
  <!-- Calculate how many pages will be needed in total -->
  <xsl:variable name="pagesNeeded" select="ceiling($itemCount div $itemsPerPage)"/>
  <!-- I use XHTML so I define an output format here -->
  <xsl:output name="XHTML" method="xhtml"
    version="1.0" encoding="UTF-8" indent="no"
    doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
    doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN"
    omit-xml-declaration="yes"/>

Now you can simply change the value of the itemsPerPage variable in one place if you want to vary how many items are allowed to be listed on each result page.

Gather item elements into groups using for-each-group

XSLT 2.0 introduced the for-each-group element. We can use this element (placed inside the main template) with an XPath expression to gather our item elements into groups of four (or ten, fifteen, whatever you please). The structure we use looks like this:

<!-- Start main template (matches XML document root node) -->
<xsl:template match="/">
  <xsl:for-each-group select="/items/item"
      group-by="ceiling(position() div $itemsPerPage)">
    <!-- Now process each item group here . . . -->
  </xsl:for-each-group>
</xsl:template><!-- end of main template -->

We'll get to the processing of each group in the next section, but for now let's look at the for-each-group element in detail. Its select attribute is an XPath 2.0 expression which calculates a page number for every item element in your XML file. Here is a worked example of the XPath expression using a value of 4 for $itemsPerPage.

position()
(item number)
position() div $itemsPerPagerounded up with ceiling()
(to give page number)
10.251
20.51
30.751
41.01
51.252
61.52
71.752
82.02
92.253
102.53
112.753
123.03
133.254

The above table shows how the page number for each item element is calculated. The first column shows the item number (which is the value of the XPath position function). The second column shows the result of dividing this by four (our example value for the items per page variable). And the third column shows the result of rounding this value up using the XPath ceiling function, which gives the page number to which the item belongs.

What happens next is that for-each-group gathers the item elements into groups (or pages in our case) based on these page numbers. So all the item elements with a calculated page number of 1 are found in group 1, and so on.

Generate pages using result-document

XSLT 2.0 also introduced the result-document element, which allows your template to create separate output documents with names constructed from XML data and XPath expressions. This allows us to produce our item listing pages, each page having its own name.

First we need to construct the name of the output file for this group, so the first bit of processing which appears in the for-each-group element looks like this:

<xsl:for-each-group select="/items/item"
    group-by="ceiling(position() div $itemsPerPage)">
  <xsl:variable name="pageName">
    <xsl:choose>
      <xsl:when test="position() eq 1">
        <xsl:text>index.html</xsl:text>
      </xsl:when>
      <xsl:otherwise>
        <xsl:text>index_p</xsl:text>
        <xsl:value-of select="position()"/>
        <xsl:text>.html</xsl:text>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:variable>

This simply creates a filename for the output document for the current page. I think it's wise to keep the first page named index.html so this block checks whether the current group number (which is returned by the XPath position() function) is equal to one, and sets the $pageName variable to "index.html" if so. Otherwise the value of $pageName is set to "index_p2.html" for page two, "index_p3.html" for page three, and so on.

Now we have the name for the output file for this group, we can use a result-document element (placed inside the for-each-group element) to create an output file with that name, like this:

<xsl:result-document href="{$pageName}" format="XHTML">
  <!-- Content of output document generated here . . . -->
</xsl:result-document>

Note that the format="XHTML" attribute refers to the format defined by the xsl:output element which we defined at the top of the XSLT file. I use XHTML, but you need to use an output format suitable for your needs.

Inside the result-document element goes whatever XSLT 2.0 elements you need to produce the listing for each group of item elements. To examine one item element at a time, you can use the for-each element (placed inside the result-document element) and the XPath current-group() function like so:

<xsl:for-each select="current-group()">
  <xsl:apply-templates select="." mode="summarise-item"/>
</xsl:for-each>

In this case, we're calling a specific template (which basically just dumps out the text values found in the item), but you can put whatever you like in the for-each element to generate markup for each item.

That's the basic structure needed for producing multiple output pages based on a single XML file. Next up, it's nice to add "previous" and "next" links to each page.

Adding links for the previous page and next page

To keep the for-each-group element tidy, it makes sense to create a named template to contain the XSLT which adds "previous page" and "next page" links when necessary. If the name of the template is "prev-and-next", then calling the template from within the for-each-group element looks like this:

<xsl:call-template name="prev-and-next">
  <xsl:with-param name="currentPage" select="position()"/>
  <xsl:with-param name="totalPages" select="$pagesNeeded"/>
</xsl:call-template>

We need to pass to the template the current page number (given by position(), which returns the current group number) and also the total number of pages (our $pagesNeeded variable created at the top of the XSLT file), because the template won't have access to these values otherwise.

The "prev-and-next" template looks like this:

<xsl:template name="prev-and-next">
  <xsl:param name="currentPage"/>
  <xsl:param name="totalPages"/>
  <xsl:element name="div">
    <xsl:if test="$totalPages &gt; 1">
      <xsl:element name="hr"/>
    </xsl:if>
    <xsl:element name="p">
      <xsl:if test="$currentPage &gt; 1">
        <xsl:choose>
          <xsl:when test="$currentPage eq 2">
            <xsl:element name="a">
              <xsl:attribute name="href">index.html</xsl:attribute>Previous
                  page</xsl:element>
          </xsl:when>
          <xsl:otherwise>
            <xsl:element name="a"><xsl:attribute name="href">index_p<xsl:value-of
                select="$currentPage - 1"/>.html</xsl:attribute>Previous
                page</xsl:element>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:if>
      <!-- Add a little spacer symbol between the "next"
          and "previous" links, if both are present -->
      <xsl:if test="$currentPage &gt; 1 and
          $currentPage &lt; $totalPages"> ↔ </xsl:if>
      <xsl:if test="$currentPage &lt; $totalPages">
        <xsl:element name="a"><xsl:attribute name="href">index_p<xsl:value-of
            select="$currentPage + 1"/>.html</xsl:attribute>Next page</xsl:element>
      </xsl:if>
    </xsl:element>
  </xsl:element>
</xsl:template>

This checks the current page number and the determines whether a "previous page" link is needed (if page number is greater than one), and whether a "next page" link is needed (if page number is less than the total number of pages). It also determines whether a spacer is needed (when both links appear together), and the names of the pages to which it is linking. You'll need to change this code if your page names follow a different pattern than "index.html", "index_p2.html" and so on.

Putting it all together

Often it's easiest to understand code when you can look at the entire thing at once, rather than in pieces, so here are links to compressed archives containing the XSLT 2.0 stylesheet file from which the code on this page comes, and a sample XML file to test it with:

If you're using Saxon-HE then the command to process the sample XML file with the XSLT stylesheet looks like this (should be all on one line, but split up here to fit the web page):

java -jar ~/Downloaded\ Files/Saxon-9b/saxon9.jar
    -s:/var/www/public_html/testarea/test-xml-item-file.xml
    -xsl:/var/www/public_html/testarea/test-xml-paging.xslt
    -o:/var/www/public_html/testarea/xsloutput.xml

Make sure to change the paths to match where you keep the files on your machine. Also note that it's worth having the -o flag, even if your main template doesn't produce any output, because this flag tells Saxon-HE the directory into which you want to save result-document files with a relative path. (If you specify a full local path in the filename for every result-document, then you need not worry about this.)