How To: Create a Dynamic Sitemap or Menu

Posted by Sean Cribbs on Thursday, September 28, 2006 | |

You’ve created a beautifully organized site in RadiantCMS and now you want to have a navigation menu that can grow as your site grows. Here’s a solution that came from discussions on the mailing list and a page on the wiki.

Getting Started

We know that we can easily loop through all of the children of the current page (in context) with r:children:each, so let's start there.

<ul>
<r:children:each>
<li><r:link /></li>
</r:children:each>
</ul>

This will get us an unordered list of all of the child pages of the current page. Simple, right? But what if we want to get the children of the child pages? We could do something like this,

<ul>
<r:children:each>
<li> <r:link /> <ul> <r:children:each> <li><r:link /></li> </r:children:each> </ul> </li> </r:children:each> </ul>

but that only gives us the children and grandchildren, not any great-grandchildren or great-great-grandchildren and so forth. Notice how I've highlighted the new section in orange. Do you see the similarity between the orange section and our first snippet? This is a textbook example of a pattern that computer scientists call "recursion".

Recursion: Do something by making yourself do it by making yourself do it...

Recursion in programmer-speak means a piece of code that does something repetitive by calling itself. What if you keep calling yourself forever, you ask? A primary element of recursion is having a "base case", or a situation that will make you quit the calling chain.

Recursion works especially well in cases where you have a lot of information that's fairly uniformly structured. In our case, Radiant's pages are organized like a tree, where every node (page) on the tree has zero or more children. So let's apply the recursion pattern to our sitemap.

That orange part is the part that's repeated, so let's figure out how to make it a snippet that calls itself.

<r:children:each>
<li>
<r:link />
<ul>
<r:snippet name="sitemap" />
</ul>
</li> </r:children:each>

Now, if we save that code as a Snippet named "sitemap", we have the basic outline of our sitemap. "Wait a minute!", you say. "Don't we need a base case? How do I know that this thing won't keep calling itself forever?" Luckily, we have the base case built into the r:children:each tag, which will only execute its contents if the current page actually has child pages. So at the deepest part of your tree, where no pages have any children, the sitemap will stop.

However, we're not completely finished with the exercise, we still have to put this in the page. Open up the page or layout where you want to put the sitemap and put in this code:

<r:find url="/">
<ul>
<r:snippet name="sitemap" />
</ul>
</r:find>

Now we have a complete site map that starts from the root page of the site. Of course, you can modify that tag to start from wherever you want.

Don't Show Them the Guts

This is great and all, but what if you've created pages that you don't want to show up in the site-map? Maybe you've created your CSS stylesheets in Radiant, but those shouldn't be navigable pages. Let's add a little enhancement to our snippet from before.

<r:children:each>
<r:unless_content part="no-map">
<li>
<r:link />
<ul>
<r:snippet name="sitemap" />
</ul>
</li>
</r:unless_content> </r:children:each>

Now, if we want to hide certain pages, all we have to do is add a "no-map" part (tab) to them in the admin interface. This will hide the page with the "no-map" part and all of its children, so be careful where you create those parts.

You Are Here

There had been some discussion on the mailing-list of how to create a contextually-sensitive site map or menu that included only the ancestor pages and their siblings and the children of the current page. Unfortunately, with the way that recursive snippets work, the tag.locals.page always equals the tag.globals.page, so you can't tell where you are in the recursion. I submitted a ticket about this, but the changes required would be so radical that it would potentially break things that already work. We'll wait and see!