I've almost finished my first Umbraco project, which is probably the most awesome CMS I've ever seen.

In case you've been under a rock (which, after having rooted around this thing, I am starting to feel like), Umbraco is a brilliant, ingenious, pure-XML-powered .NET CMS - sorry to say, but much better than DotNetNuke, and fully validates with XHTML 1.1 due to it's XML nature. It supports nested master pages, Intellisense with Visual Studio, custom user controls, XSLT rendering, Web Services and Metaweblog, and a whole bunch of other shiny stuff (multiple host headers too, but I've yet to play around with this bit).

New link will be posted up shortly, but in the meantime I'll post up some code for a custom XSLT module for rendering either single images or iterating through a folder of media items. It takes a few parameters just for sensibility, but it's pretty easy to follow through and it works a treat - there's five places on the new site where I use this one block of code (80 lines of XSLT) to achieve four different effects - one link to files, one list of promotional image banners, one individual promotional banner, and four jQuery galleries (yes, off this one block of code) - and I suspect that it'll be a lifesaver for many future projects to come.

Without further ado, here's the code:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet [ &lt;!ENTITY nbsp "&#x00A0;"> ]>
     xmlns:umbraco.library="urn:umbraco.library" xmlns:Exslt.ExsltCommon="urn:Exslt.ExsltCommon" xmlns:Exslt.ExsltDatesAndTimes="urn:Exslt.ExsltDatesAndTimes" 
xmlns:Exslt.ExsltMath="urn:Exslt.ExsltMath" xmlns:Exslt.ExsltRegularExpressions="urn:Exslt.ExsltRegularExpressions" xmlns:Exslt.ExsltStrings="urn:Exslt.ExsltStrings" 
     exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets ">
     <!-- Copyright Code Gecko Developments Ltd. 2009. All rights reserved.                  -->
     <!-- Released under a BSD License (http://www.benjaminhowarth.com/home/ctl/terms.aspx)  -->
     <!-- "Think free speech, not free beer" - Richard Stallman                              -->
     <xsl:output method="xml" omit-xml-declaration="yes"/>
     <xsl:param name="currentPage"/>
     <xsl:param name="mediaID" select="macro/mediaID" />
     <xsl:param name="separator" select="macro/separator" />
     <xsl:param name="useBulleted" select="macro/useBulleted" />
     <xsl:param name="useLinks" select="macro/useLinks" />
     <xsl:param name="formatString" select="macro/formatString" />

     <xsl:template match="/">
       <xsl:value-of select="formatString" disable-output-escaping="yes" />
           <xsl:when test="count(umbraco.library:GetMedia($mediaID/node/@id, 'true')/node) &gt; 1">
               <xsl:when test="string($useBulleted)='true'">
                   <xsl:for-each select="umbraco.library:GetMedia($mediaID/node/@id, 'true')/node">
                       <xsl:apply-templates select="." />
                 <xsl:for-each select="umbraco.library:GetMedia(macro/mediaID/node/@id, 'true')/node">
                   <xsl:apply-templates select="." />
                   <xsl:if test="position()!=last()">
                     <xsl:value-of select="$separator" disable-output-escaping="yes"/>
           <xsl:when test="count(umbraco.library:GetMedia($mediaID/node/@id, 'true')) = 1">
             <xsl:apply-templates select="$mediaID/node" />

    <xsl:template match="node">
        <xsl:when test="string($useLinks)='true'">
            <xsl:attribute name="href">
              <xsl:value-of select="data [@alias='umbracoFile']"/>
            <xsl:call-template name="nodeContents" />
          <xsl:call-template name="nodeContents" />

    <xsl:template name="nodeContents">
      <xsl:variable name="fileName"><xsl:value-of select="data [@alias = 'fileName']"/></xsl:variable>
        <xsl:when test="string($formatString)!=''">
          <xsl:value-of select="umbraco.library:Replace($formatString, '{1}', @nodeName)" disable-output-escaping="yes" />
          <xsl:element name="img">
            <xsl:attribute name="src"><xsl:value-of select="data [@alias = 'umbracoFile']"/></xsl:attribute>
            <xsl:attribute name="alt"><xsl:value-of select="@nodeName"/></xsl:attribute>

OK, so what've we got here... (for newbies, the bumpf of code above is called XSLT - eXtensible Stylesheet Markup Language).

Umbraco uses an entire XML-based system for all of it's content - even images.
This is great because then you can set up a hierarchical structure which your users can manage using the CMS administration system without having to worry as a developer about your beautiful folder structure - Umbraco handles all of that for you.

When executing an XSLT macro on an Umbraco page, you get a <macro>...</macro> XML document passed to the XSLT to be transformed. Umbraco lets you specify your own parameters to the macro, which get passed in as nodes with values.
In this instance, our parameters are mediaID, separator, useLinks, useBulleted and formatString (which all have their own purposes). This then turns into the following XML document to be thrown to the XSLT:

    <formatString>&lt;span&gt;This is {1} file&lt;/span&gt;</formatString>

So, our XSLT parameters pick up the macro/nodename parameters passed to the macro and then use them for determining the output of the XSLT. Sounds simple enough.
The one nuance with this macro is this - the mediaID field must be passed in as a Media Picker page field, you cannot specify a media item ID. However, this has it's benefits - by specifying a Media Picker item, it means you can specify a custom field on your page, which your users can then manage and edit themselves through the Umbraco admin interface, without having to do some horrible XSLT to obtain the node ID "behind the user's back". This way, your coding overhead is kept to a minimum (which means you can leave at 4pm instead of 5.30pm :-D).

So to call our macro from a master page is pretty simple:

<umbraco:Macro mediaID="[#mediaPicker]" useLinks="false" useBulleted="false" separator="&lt;br/&gt;" formatString="Download {1}" Alias="MediaRepeater" runat="server" />

Don't forget to set your parameters up in the admin system otherwise your macro will fail cue Twitter fail-whale.

Then, you specify your Media Picker page field in mediaID (note use of square brackets and # - do not attempt to use an <umbraco:Item> object here otherwise your macro will, again, fail), and hey presto - your users can manage the media objects loaded into your repeater.

I've used this to great success on a website where both a jQuery gallery rotator and a plain image repeater (promotional banner images) were required to be displayed. The same block of code powers both, but on the page where the gallery is required I've added my jQuery to the master page code, which allows me to re-use the above code for the promo images - and the client can look after their own promo images without knowing a single line of code.

A quick code run-through:
Lines 13-18 pick up the <nodename> parameters.
Lines 20-50 specify the base template for the macro (match="/" - the "/" denotes the root node, check out W3Schools if you need to learn XPath) - some fancy formatting to a) see if you want a bulleted list in the output - the useBulleted parameter - and b) check if the specified media is a single file, or a folder of files to be iterated through.
Lines 52-66 test if you want each item to be rendered as a link (the useLinks parameter), then renders the individual item using the nodeContents template.
Lines 68-81 renders the item depending on whether formatString is specified or not - this is where you can customise the output of each rendered item. I only had need for two purposes - render an image, or some text linking to a file, hence the simplicity of this code. However, it's extensible and I'm releasing it under the BSD license cause I can see it's gonna be flipping useful. Might even start some Umbraco tutorials if I find the time.
Please note change of site T&Cs to incorporate BSD license into any code published via my blog.

Have fun!