Part 1 :: Part 2 :: Part 3 :: Part 4 :: Part 5 :: Part 6

This is part 5 of an ongoing series of posts about my experience building a custom site on top of Office SharePoint Server 2007. If you haven't yet read parts 1 through 4, I'd strongly suggest starting there.

In my last post, I discussed using detached pages and migrating content from my old site. In this post, I'll cover a few of the final touches that I added to the site.

The "All Posts" Page

In my old site, when you visited a rollup page, only the first five posts were displayed. If you wanted to see additional posts, you could search for them, or you could click one of the numbers at the bottom of the rollup page. That would take you to a different "page" that displayed items further down the list. For example, if you went to page 2, you'd see items 6-10. Page 3 contained items 11-15, and so on. This was fine, but using Content Query web parts in MOSS didn't allow me to configure this behavior right out of the box. Instead, I opted to create an "All Posts" page that would list all the posts for a given category.

In order to do this, I created a special page layout called "AllPostsLayout.aspx," and added a CQWP to a zone right in the middle of the layout. I configured that web part to get all the Pages except those that had category "Rollup Page." I then went through on every subweb and created a page using that new layout. I had to customize the individual CQWP's on every new page, to make sure they rolled up the right data, but this was pretty painless since I made sure that most of the properties were set properly when I added the web part to the layout.

The other thing I did was add a link to the "all posts" page in the Site Rollup layout. To make sure that this link worked properly regardless of what subweb the rollup page was in, I made use of some SharePoint controls I'd used previously for other things in the site. This is what the markup looked like:

<SharePointWebControls:splinkbutton runat="server" NavigateUrl="~site/Pages/AllPosts.aspx" id="onetidProjectPropertyTitle">
see all posts in <SharePointWebControls:ProjectProperty Property="Title" runat="server" /></SharePointWebControls:SPLinkButton>

This was a major change from the way things worked in my old site, but I think it's for the better. The problem with paging (showing items 1-5 on the first page, 6-15 on the second, etc.) is that people only ever use the first 2 or three pages. For example, they know that there was a very recent post that they saw and they want to get back to it. After that they use search because it's faster than sorting through every page. I could have built paging with a custom web part or control that changes the parameters of the CQWP based on some query strings, but frankly, it would have been more work than it's worth. The All Pages view was simple to put together and meets my needs. To be fair, it doesn't scale – if you have hundreds or thousands of items in a single category you'll start having problems. Not necessarily in performance, but definitely in usability. Definitely keep this in mind if you're looking at a similar solution.

The Current Date and Time

My old site had a gratuitous little feature that out put the current date and time in top right-hand corner of the page, right under the search box. This should be pretty straightforward, right? All I needed to do was call System.DateTime.Now.ToString() with a special date time format string. Unfortunately, this was slightly more complicated in SharePoint because I couldn't just get ASP code blocks to execute in my master page by default. However, I was able to enable this functionality by adding some markup in web.config:

<PageParserPaths> 
         <PageParserPath VirtualPath="/_catalogs/masterpage/*" CompilationMode="Always" AllowServerSideScript="true" IncludeSubFolders="true" /> 
</PageParserPaths> 

Once this was added to my web.config, I was able to add code blocks to items only in the master page gallery. This is important – you typically don't want to enable code blocks on every page in your site because if you do, any user with access to any library in your site can upload an ASPX page that executes code. Then they can visit that page and get code to execute on your box, leading to major pwnage. There are very good reasons why code blocks are disabled by default in SharePoint.

However, in this case, I only enabled code blocks for pages in the master page gallery (check out the VirtualPath property in the snippet above), which is much safer. Only your site designers or owners should have access to the MPG, so there's a level of trust there that is probably OK. In my case, this is further mitigated by the fact that I am currently the only person with write access to the site. However, even if I allow users to post content later, they still won't have rights to upload content to the MPG.

After the code blocks were enabled, all I had to do was paste in my code snippet into the right spot in my master page. This is the markup:


<div class="header-datestamp">
    <asp:ContentPlaceHolder ID="HeaderDate" runat="server">
        <% Response.Write(System.DateTime.Now.ToString("dddd, MMMM d yyyy @ h:mm tt")); %>
    </asp:ContentPlaceHolder>
</div>

I should point out that you can most definitely also do this sort of thing with a user control. In this case, it would be a very simple user control, but wrapping functionality in controls, marking them as safe, and putting them in your layouts is in general the best approach to custom functionality. In my case, because the functionality I needed was so basic, I opted to do it in a code block. However, if you're looking at enabling code blocks in any part of your SharePoint site, take some time to really consider the ramifications and your needs. If the functionality you need is even remotely complicated (read: more than one line of code), write a control.

RSS Feeds

The next step was configuring RSS feeds for my site. I had a couple of different options to accomplish this. The CQWP's that were already on my homepage could easily be configured to spit out feeds. However, they only contained five items, and I wanted my feed to contain ten items.

In order to accomplish this, I added a page to my site using a layout with a web part zone. This page wasn't meant to be seen by any visitors to my site, so it didn't matter which layout I used, as long as it had a zone in which I could put the web parts. I added a CQWP to this page with its RSS feeds turned on. I configured this web part with the same properties that I used for the ones on the front page, except I set it to roll up ten items instead of five.

When you turn on the RSS feed, the CQWP by default renders out a little orange feed icon that links to the feed. You can override this presentation by editing CQMain.xsl in the Style Library. Another feature of turning on the RSS feed on a CQWP is that it injects markup into the page header that lets browsers like Firefox and IE 7 discover the feed automatically.

This functionality is perfect if you want to make your feeds available only on the page that's hosting your CQWP. However, in my case the CQWP that was serving my feed was on a "hidden" page, so I needed to copy the URL of the feed and then modify my master page to include it. I also wanted some statistics around feed subscriptions, so I wrapped my feed up through FeedBurner so I could get these statistics easily.

Customizing the master page was very straightforward. I added the following markup to the header to get the auto-discovery in Firefox and IE 7:

<link rel="alternate" type="application/rss+xml" title="Recent posts on Tyler Butler.com" href="http://feeds.feedburner.com/TylerButlerAllPosts"/>

Then I added a section in right column that linked to the feed, for people who were using other browsers. In the future, I can add more feeds here, or I might opt to build out a feed landing page that lists all of my feeds, similar to what sites like CNet or GameSpot do.

At this point, I had my feed configured and linked to, but unfortunately, it wasn't outputting things exactly like I wanted. For example, I wanted the date and time of a post to be the date and time I set, not the Creation Date, which is the default. I also wanted to provide full post text with my feeds, so people who use RSS readers such as Bloglines or Google Reader don't have to visit my site if they don't want to.

Supporting this sort of feed customization was super straightforward. The thing to remember is that ultimately, the feed is supported by a Content Query Web Part. That means that all the functionality and extensibility that the CQWP provides can apply to the feed. In my case, what I needed to do was customize the fields that were returned by the web part (namely, override the CommonViewFields property on the web part). That ensured that the query that the CQWP was issuing would return the fields I wanted to include in my RSS feed. Note that this process was the same as what I did back in part 2 of series.

In order to customize exactly what content went into my feed, I had to edit RSS.xsl in the Style library. You can also create a copy of RSS.xsl, then modify the copy to suit your needs. This is a better approach, because then you can customize specific feeds by modifying the XSL that governs them specifically. There are some specific things you have to do if you go down this road, but George goes into much greater detail about this in his post on RSS customization. If this is something that interests you, his post is a great resource.

At this point, my site was pretty much migrated. It's not done, of course! I have lots of cool ideas for some customizations, and I'm still missing some core functionality, most noticeably user comments and the ability for people to sign up for an account and post. The core functionality and look and feel, though, is complete. That means this post series is about to come to an end. Not quite yet, though. :-) I will post a final post soon to discuss some of the specific challenges I encountered while working on this project and flesh out some of my ideas for future functionality. Until next time...

Part 1 :: Part 2 :: Part 3 :: Part 4 :: Part 5 :: Part 6