XQuery/Sitemap for Content Management System
Motivation
editYou want to use eXist to manage your web site and use each collection as a web-content folder. You want a way to automatically create a sitemap for the site so that as you add a new collection in your web root folder the site navigation menus will automatically be updated.
Method
editWe will use the eXist get-child-collections() function to get all of the child collections for a root collection. We create a recursive function that traverses the collection tree.
From the eXist function library, here is the description of get-child-collections function.
xmldb:get-child-collections($a as xs:string) xs:string* Returns a sequence of strings containing all the child collections of the collection specified in $a. The collection parameter can either be a simple collection path or an XMLDB URI.
If we have a collection called /db/webroot we could pass this string as a parameter to this function and all the child collections would be returned as sequence of strings.
We can then create a recursive function that works on each of these child collections.
Sample use of get-child-collections()
editHere is a very simple use of the function get-child-collections(). You just pass it a single argument which is a path to a collection. It will return a sequence of all the child collections in that collection.
xquery version "1.0";
let $children := xmldb:get-child-collections('/db/webroot')
return
<results>
<children>{$children}</children>
</results>
Sitemap Function: Version 1
editdeclare function local:sitemap($collection as xs:string) as node()* {
if (empty(xmldb:get-child-collections($collection)))
then ()
else
<ol>{
for $child in xmldb:get-child-collections($collection)
return
<li>
<a href="{concat('/exist/rest', $collection, '/', $child)}">{$child}</a>
{local:sitemap(concat($collection, '/', $child))}
</li>
}</ol>
};
This recursive function takes a single input argument of a string and returns a complex node. The result is an HTML ordered list structure. It first does a test to see if there are any children elements in the collection. If there are not any, it just returns. If there are new children elements, then it creates a new ordered list and iterates through all the child elements in that collection creating a new list item for each child and then calling itself. Note that this could have been written so that the conditional operator only calls itself if there are child elements in a collection.
This is an example of tail recursion. This pattern occurs frequently in XQuery functions.
Sample Sitemap Program
editWe can now call this program within an XHTML page template to create a web page:
Source Code
editxquery version "1.0";
declare option exist:serialize "method=xhtml media-type=text/html indent=yes";
declare function local:sitemap($collection as xs:string) as node()* {
if (empty(xmldb:get-child-collections($collection)))
then ()
else
<ol>{
for $child in xmldb:get-child-collections($collection)
return
<li>
<a href="{concat('/exist/rest', $collection, '/', $child)}">{$child}</a>
{local:sitemap(concat($collection, '/', $child))}
</li>
}</ol>
};
<html>
<head>
<title>Sitemap</title>
</head>
<body>
<h1>Sitemap for collection /db/webroot</h1>
{local:sitemap('/db/webroot')}
</body>
</html>
Adding Titles
editSometimes the title for the navigation bar will be different from the name of the collection. By convention collection names usually are just short lowercase letters without spaces or uppercase letters. Navigation bars typically have labels that contain spaces and uppercase letters.
Here is an example that uses a lookup table to look up the title from a an XML file.
xquery version "1.0";
declare function local:sitemap($collection as xs:string) as node()* {
if (empty(xmldb:get-child-collections($collection)))
then ()
else
<ol>{
for $child in xmldb:get-child-collections($collection)
let $db-path := concat($collection, '/', $child)
let $path := concat('/exist/rest', $collection, '/', $child)
let $lookup :=
doc('/db/apps/sitemap/06-collection-titles.xml')/code-table/item[$db-path=path]/title/text()
order by $child
return
<li>
<a href="{if (empty($lookup))
then ($path)
else (concat($path, "/index.xhtml"))}">
{if (empty($lookup)) then ($child) else ($lookup)}
</a>
{local:sitemap(concat($collection, '/', $child))}
</li>
}</ol>
};
Screen Image
editNote that the child collections are all sorted alphabetically. In some cases this may not be the order you would like to display your site navigation menus. You can add an element a sort-order parameter to the XML file that displays the titles and use that field to sort the child collections.
Collection Titles file
editPut this file in the following location: /db/apps/sitemap/06-collection-titles.xml
<code-table>
<item>
<path>/db/webroot/about</path>
<title>About</title>
</item>
<item>
<path>/db/webroot/training</path>
<title>Training</title>
</item>
<item>
<path>/db/webroot/faqs</path>
<title>Frequently Asked Questions</title>
</item>
<item>
<path>/db/webroot/training/xforms</path>
<title>XForms</title>
</item>
<item>
<path>/db/webroot/training/rest</path>
<title>ReST</title>
</item>
<item>
<path>/db/webroot/training/xquery</path>
<title>XQuery</title>
</item>
<item>
<path>/db/webroot/training/tei</path>
<title>Text Encoding Initiative</title>
</item>
<item>
<path>/db/webroot/training/exist</path>
<title>eXist</title>
</item>
<item>
<path>/db/webroot/products</path>
<title>Products</title>
</item>
<item>
<path>/db/webroot/support</path>
<title>Support</title>
</item>
</code-table>
Customizing Your Sitemap Function
editNot all collections should be displayed in a sitemap. Some collections may contain private administrative data that you do not want to display in a public sitemap. There are two ways to handle this. You can keep a general "published collections" list in a separate XML file. Alternatively you can store a small XML file in each collection that displays the properties of that collection . By default this collection may be either public or private, depending on how you write your function.
The second option is more portable if you and your associates are each building web applications you would like to share. By standardizing on your collection-properties.xml files you can store properties in a collection and then exchange them with other eXist sites by just exchanging the collections as folders that can be zipped.
Counting Files In Collection and SubCollections
editHere is the pseudo code:
declare function local:count-files-in-collection($collection as xs:string) as xs:integer {
let $child-collections := xmldb:get-child-collections($collection)
if (empty($child-collections))
then
(: return the count of number of files in this collection :)
else
(: for each subcollection call local:count-files-in-collection($child) :)
};