In this part of the series we'll create the rest of our content nodes (pages) and start adding the XSLT macros to handle things like building the site navigation, and populating the <umbraco:Macro>
elements.
Also available in this series:
- Getting Started with Umbraco: Part 1
- Getting Started with Umbraco: Part 2
- Getting Started with Umbraco: Part 3
- Getting Started with Umbraco: Part 4
- Getting Started with Umbraco: Part 5
Creating the Rest of the Content Nodes
First we'll add the rest of our content nodes. Remember, Document Types + Templates + content nodes = pages. To view the back-end the site sill need to be open in Visual Web Developer Express (VWD). Right-click the default.aspx
page in the right hand Solution Explorer
pane and choose View in Browser
. The home page of the site should then load. To access the back-end, replace default.aspx
in the address bar with umbraco, you should then be taken to the login screen, and then to the back-end once your credentials have been entered.
All pages in a site should be created as child nodes of the home page in order for the XSLT to work correctly. Right-click on the Home
node in the top left hand panel of the back-end and choose Create
from the drop-down menu. This will give us the Create
dialog; in the Name
field enter About Us
. The Choose Document Type
select box should already be filled with the first available Document Type – Content
– this is fine, just click the Create
button at the bottom and the new node will be created as a child node of the Home
node:



Enter some text in the textarea, then switch to the Properties
tab and enter All About Us
in the Page Title
field, then save the new node using the disk icon in the toolbar. Next we'll create the News List
page; right-click the Home
node and choose Create
again. Enter News
as the name and choose the News List
Document Type in the select box. This page has no custom properties (it will be built using a macro) so the node will open up on the Properties
tab. Enter Company News
in the Page Title
field and click the save and publish icon.
Next we can create some news items; this time right-click the News
node and choose Create
. Add News Item 1
in the Name
field and click Create
(the correct Document Type will already be selected). We've a few more fields to complete this time; go ahead and pick a date, add a Headline
and the Story Text
. We might also want to add another image to the Media library and select it using the media picker (although the image isn't mandatory with News Items
). Also add a Page Title
on the Properties
tab. Once this is done create another News Item
, and complete the fields again. Make sure there is a decent amount of text in the text area for each news story as we'll be pulling some from each News Item
in to build the News List
page.
So now we've created a Content page
, the News List
page and a couple of News Items
. All that's left is to create some Hero Panels for display on the home page. Right-click the Home
node again, then add Panel 1
as the name of the new node, and choose the Hero Panel
document type. Use the textarea on the newly created node to add some text to the panel. Also, we should ensure that we switch to the Properties
tab and tick the Umbraco Hide In Navigation
checkbox. Create several panels in this way, maybe using different text, or something to differentiate between the different panels. At this point, our back-end should appear as follows:



Creating a Macro
Now that our pages have been created, we can start writing the XSLT macros that will build the more complex aspects of the site, such as the navigation, news list, etc. If we open up the BasePage.master
file in VWD, the first <umbraco:Macro>
element we find is the pageTitle
macro. We'll build this one first as it's pretty straight-forward and should provide a basic introduction to XSLT.
In the Umbraco back-end, go to the Developer
section, right-click on the XSLT folder and choose Create
. In the dialog that appears add pageTitle
as the Filename
and leave the select box set to Clean
. Note that the Create Macro
checkbox is already ticked. Click the Create
button. The new XSLT file and its matching macro will be created. Select the macro (it will be found in the Macros
folder in the node tree) and change the Alias
of the macro from PageTitle
to pageTitle
(for consistency).
Now, switch back to VWD and open up the XSLT file in the Solution Explorer
at the right. The new XSLT file we created in Umbraco should be listed. Of course, we can add the XSLT code directly through Umbraco, however, for Intellisense reasons, it is helpful to write it in VWD. Open up the XSLT file, around the middle of the file there will be a comment that says <!—start writing XSLT -->
, remove this comment and replace it with the following code:
1 |
<xsl:if test="$currentPage/pageTitle != ''"> |
2 |
<xsl:value-of select="$currentPage/pageTitle"/> |
3 |
<xsl:text> - </xsl:text> |
4 |
</xsl:if>
|
5 |
|
6 |
<xsl:value-of select="$currentPage/ancestor-or-self::* /domainName"/> |
Save the XSLT file. Essentially, XSLT is used to transform XML into either other types of XML, or other formats of XML, such as XHTML. The data that we enter into fields in the Umbraco back-end is stored in the database as XML, so we can use XSLT to process this XML and turn it into (X)HTML for display on our pages. Before we go into detail about what the above code does, we just need to look at a couple of core concepts for working with XSLT.
First of all, just after <xsl:output>
element there is an XSL parameter (<xsl:param>
) element called currentPage
. This will be set to whichever page 'calls' the XSLT file, so if a visitor views the home page of the site, the currentPage
parameter will be set to the Home
node in the back-end. If a content page, such as the About us
page, is viewed the currentPage
parameter will be set to that page instead.
On the next line we have the XSL template (<xsl:template>
) element, which is set to match /
which means that it should match all XML elements and attributes in the document (the stored XML representation of the node in the back-end).
Now let's look at the code we added to the <xsl:template>
element. We first use the <xsl:if>
element to test whether the currentPage
(in XSLT parameters must start with the $
character) contains a value for the pageTitle
element. This is the pageTitle
element in our page node, not the pageTitle
macro by the way. If it does contain a value, the next line, <xsl:value-of>
allows us to select the contents of the pageTitle
property. We then use the <xsl:text>
element to specify a hyphen to use as a separator.
Lastly we use the <xsl:value-of>
element once again to select the domainName
property. In this element we use an XPath step which consists of the axis ancestor-or-self
to select all ancestor nodes of the currentPage
as well as the currentPage
, and the ::*
nodetest to select all attributes of the selected ancestors. We narrow this down specifically to the domainName
property we set on the Home
node using the /domainName
predicate.
View the home page of the site now, and the title of the page should automatically be set to the value we entered into the domainName
property. If we view the about us page (just add /about-us.aspx
to the end of the localhost URL in the address bar), we instead find that the title is set to All About Us – The Company
. Page name – Site name is the preferred title format for SEO reasons.
In this example, pages that don't have the pageTitle
property set will simply revert to the domainName
property. Some macro authors add an extra level of protection and if the pageTitle
isn't set fall back to the nodeName
instead. This is useful if the client is likely to forget to set the pageTitle
when adding new pages to the site, but does mean that the page title could end up with camel-cased filenames in it. There are arguments for and against this method. I'll leave it up to you to decide whether or not this additional fallback is necessary.
Adding More Macros
Let's add some more macros; next we should build the navigation for the site so that it is easier to move between pages. This macro will be much more complex than the pageTitle
maco that we just looked at. We left some mark–up in the BasePage.master
file, this is the structure we will end up with once we are done. Remove this from the masterpage and add an <umbraco:Macro>
element in its place:
1 |
<umbraco:Macro Alias="topNav" runat="server"></umbraco:Macro> |
Now create a new XSLT file (and therefore a macro) called topNav
. In VWD, open the new XSLT file and add the following code directly after the currentPage
parameter:
1 |
<xsl:variable name="rootNode" select="$currentPage/ancestor-or-self::* [@isDoc and @level=1]" /></div> |
Next, add the following code to the <xsl:template> element:
1 |
<nav>
|
2 |
<ul class="clear-float"> |
3 |
<li>
|
4 |
<a href="{umbraco.library:NiceUrl($rootNode/@id)}"> |
5 |
<xsl:if test="$rootNode/@id = $currentPage/@id"> |
6 |
<xsl:attribute name="class">on</xsl:attribute> |
7 |
</xsl:if>
|
8 |
<xsl:value-of select="$rootNode/@nodeName" /> |
9 |
</a>
|
10 |
</li>
|
11 |
|
12 |
<xsl:for-each select="$currentPage/rootNode/* [@isDoc and string(umbracoNavHide) != '1']"> |
13 |
<li>
|
14 |
<xsl:if test="position() = last()"> |
15 |
<xsl:attribute name="class"> |
16 |
<xsl:text>last</xsl:text> |
17 |
</xsl:attribute>
|
18 |
</xsl:if>
|
19 |
<a href="{umbraco.library:NiceUrl(@id)}"> |
20 |
<xsl:if test="$currentPage/@id = @id"> |
21 |
<xsl:attribute name="class">on</xsl:attribute> |
22 |
</xsl:if>
|
23 |
<xsl:attribute name="title"> |
24 |
<xsl:value-of select="@nodeName"/> |
25 |
</xsl:attribute>
|
26 |
<xsl:value-of select="@nodeName"/> |
27 |
</a>
|
28 |
<xsl:if test="child::* [@isDoc]"> |
29 |
<ul>
|
30 |
<xsl:for-each select="child::* [@isDoc and string(umbracoNavHide) != '1']"> |
31 |
<xsl:choose>
|
32 |
<xsl:when test="position() < 6 and string(child::NewsItem = '1')"> |
33 |
<li>
|
34 |
<a href="{umbraco.library:NiceUrl(@id)}"> |
35 |
<xsl:attribute name="title"> |
36 |
<xsl:value-of select="@nodeName"/> |
37 |
</xsl:attribute>
|
38 |
<xsl:value-of select="@nodeName"/> |
39 |
</a>
|
40 |
</li>
|
41 |
</xsl:when>
|
42 |
<xsl:otherwise>
|
43 |
<li>
|
44 |
<a href="{umbraco.library:NiceUrl(@id)}"> |
45 |
<xsl:attribute name="title"> |
46 |
<xsl:value-of select="@nodeName"/> |
47 |
</xsl:attribute>
|
48 |
<xsl:value-of select="@nodeName"/> |
49 |
</a>
|
50 |
</li>
|
51 |
</xsl:otherwise>
|
52 |
</xsl:choose>
|
53 |
</xsl:for-each>
|
54 |
</ul>
|
55 |
</xsl:if>
|
56 |
</li>
|
57 |
</xsl:for-each>
|
58 |
</ul>
|
59 |
</nav>
|
Save the file. First of all, we set a variable that matches the root node of the site, which is the Home page. To select only the home page, we navigate from the currentPage
(using the $currentPage
parameter) through all ancestors using the ancestor-or-self
axis and the ::*
nodetest and predicate, checking that the node has the @isDoc
attribute (this is added to all page nodes by Umbraco) and that it is at level 1 (another attribute added by Umbraco). This will get us the root node of the site, which is the home page.
Then, in the within the <xsl:template>
we first create the outer containers for our navigation menu, specifically the <nav>
and <ul>
elements. We then add the first <li>
element and the first anchor element. We set the href
of the anchor using Umbraco's NiceUrl
library function, which accepts a document id
as an argument. We supply the id
of the root node, using our $rootNode
variable (like parameters, variables are prefixed with the $
character. Attributes are prefixed with @
in XSLT). The NiceUrl
function will return the stored URL for the node.
We then check whether the id
of the $rootNode
matches the id
of the $currentPage
using an <xsl:if>
statement; if it does we know that the home page is the one currently being viewed and add an on
class to the anchor. We then select the name of the node (the Name
given to the node in the Umbraco back-end, which is shown on the Properties
tab for the node) to use as the title
attribute of the anchor element, and its text, both using the <xsl:value-of>
element to select the values.
Next, we need to cycle through each page node under the home page, se we use an <xsl:for-each>
loop to select and process all nodes that are children of the $rootNode
and are also documents, but which additionally do not have the umbracoNavHide
checkbox ticked. This prevents our Hero panels from being listed in the site-wide navigation menu, which clearly, we do not want!
For each child of the $rootNode
, we create a new <li>
element. We do a quick test to see if we are processing the last node in the set, which we achieve by checking whether the position
of the node in the node set is equal to last
. If it is, we add the class name last
to the <li>
.
While not strictly necessary in this example, this is still an incredibly useful technique to utilise seeing as neither IE6, nor IE7 (which still account for a large percentage of the audience of an unfortunate number of sites) are able to use the
last-child
CSS selector.
We then create a new <a>
within the <li>
and set its href
using the NiceUrl
function once more, this time passing in the id
attribute of the current node. We set the title and text of the anchor in the same way as before. We also add an on
class if the page currently being viewed is the same as the page currently being processed. As we are already in the context of a node, we mostly don't need to use the $currentPage
parameter, using it only when we check the @id
of the current node against the @id
of the current page.
Once the link for the nav
item has been created we do another test (again using the <xsl:if>
conditional) to see whether the current node has any child nodes which are documents. If it does, we create a sub nav using a nested <ul>
, and then select each child node that is a document and doesn't have the umbracoNvHide
checkbox ticked.
We then use an <xsl:choose>
conditional, which is kind of like the XSLT equivalent to an if...else
, to limit the number of NewsItem
child menu items to five by testing whether the child has a position
of less than 6 when we are processing NewsItems
. We can check whether we are currently processing NewsItem
nodes by checking whether a test for this element (child::NewsItem
) is equal to true
.
We should limit the subnav in for the news items because, after the site has been live for a while, there could potentially be hundreds of news articles, and it would be ridiculous to list them all in the nav menu. We then create a new <li>
and <a>
for each child, up to the maximum of 5. This is in the <xsl:when>
element, which is equivalent to the if
part of if...else
. When we are not processing NewsItems
, we just create a subnav for all the items (in the <xsl:otherwise>
element, which is equivalent to the else
part of an if...else
.)
That's essentially it; if we save this now, and return to the home page of the site (the actual home page as viewed in a browser, not the Home node in the back-end) and refresh it, we should find that our top level nav
is created for us automatically:



Next we can create the nav for the footer of the site; this is almost identical to the top nav except that it doesn't have subnavs. Create the new XSLT file in the same way as before and call it footerNav
. Add the following code to the file:
1 |
<nav>
|
2 |
<ul>
|
3 |
<li>
|
4 |
<a href="{umbraco.library:NiceUrl($rootNode/@id)}"> |
5 |
<xsl:if test="$rootNode/@id = $currentPage/@id"> |
6 |
<xsl:attribute name="class">on</xsl:attribute> |
7 |
</xsl:if>
|
8 |
<xsl:attribute name="title"> |
9 |
<xsl:value-of select="$rootNode/@nodeName"/> |
10 |
</xsl:attribute>
|
11 |
<xsl:value-of select="$rootNode/@nodeName" /> |
12 |
</a>
|
13 |
</li>
|
14 |
|
15 |
<xsl:for-each select="$rootNode/* [@isDoc and string(umbracoNavHide) != '1']"> |
16 |
<li>
|
17 |
<xsl:if test="position() = last()"> |
18 |
<xsl:attribute name="class"> |
19 |
<xsl:text>last</xsl:text> |
20 |
</xsl:attribute>
|
21 |
</xsl:if>
|
22 |
<a href="{umbraco.library:NiceUrl(@id)}"> |
23 |
<xsl:if test="$currentPage/@id = @id"> |
24 |
<xsl:attribute name="class">on</xsl:attribute> |
25 |
</xsl:if>
|
26 |
<xsl:attribute name="title"> |
27 |
<xsl:value-of select="@nodeName"/> |
28 |
</xsl:attribute>
|
29 |
<xsl:value-of select="@nodeName"/> |
30 |
</a>
|
31 |
</li>
|
32 |
</xsl:for-each>
|
33 |
</ul>
|
34 |
</nav>
|
This XSLT file will also need to define the rootNode
variable near the top, just like with the topNav
, and don't forget to add the <umbraco:Macro>
element to the BasePage.master
file:
1 |
<umbraco:Macro Alias="footerNav" runat="server"></umbraco:Macro> |
These are all of the macros we need in the BasePage.master
file. The Home.master
file only has the Hero panel macro, which we'll add in another part of the series, and the Content.master
file doesn't have any macros in it at all. The next file that we need to add a macro to is the NewsList.master
page; we'll add this in the next part in the series.
Summary
In this part of the tutorial, we began writing the XSLT files that build different parts of the site based on the content nodes that have been created. This is where the site really starts coming together and feeling like a fully working web site.
XSLT and XPath are powerful languages for working with XML documents and the elements contained within those documents. If this has been your first exposure to XSLT some of it may still feel a little alien, especially if you're used to working with more intuitive languages such as HTML and CSS. But don't worry, XSLT and XPath are easy to pick up with a little practice and they are languages worth learning, jQuery for example uses XPath to select elements from (X)HTML pages.
In the next part of the series we'll add the XSLT files that will build the news list page and the news navigation menu, and then move on to look at how we can add .Net User Controls to Umbraco for even more custom functionality.