Generating short description using the Content Query Web Part

, , , ,

Probably every Web Content Management (WCM) solution out there uses some kind of content aggregation. No matter whether it’s showing the 5 most recent press releases, new job offers or upcoming events: presenting content roll-up to the visitors allows them to get to your content more easily.

If you’re building a WCM solution on top of Microsoft Office SharePoint Server (MOSS) 2007 you are very likely to use the standard Content Query Web Part (CQWP) for that purpose. CQWP is a great performing Web Part with many different configuration options which allow you to customize content roll-up to fit the branding of your site. Additionally the CQWP uses XSLT for the presentation layer which allows you to even include some logic in how the data should be presented.

If you have any experience with XSLT you probably know that one of its biggest shortcomings are the very few XPath functions supported in the default XSLT implementation. The SharePoint team has noticed that issue as well back in SharePoint Portal Server (SPS) 2003 and provided some additional functionality in the ddwrt namespace. Unfortunately that didn’t solve all of our problems. Not even the most common ones like… generating short descriptions.

The case

Let’s take the following case as an example: you have a subsite with Press Releases and would like to display the 5 most recent one on the home page. To make it more interesting to the visitors you would like your press releases roll-up to show the date, the title and a couple of lines of content of each press release. Having configured it using the CQWP you get the following output:

Default result of a content aggregation using CQWP which includes Publishing Content as the Description. Because the content contains HTML you cannot easily shorten the description

Anything odd about it? Instead of seeing the short description the content roll-up shows the whole content. Sure you could ask the content editors to enter the short description in the Description field but as a good citizen you want to keep the content management process as simple as possible.

So what do we have available that might help us solve us this issue?

Out of the box methods for generating short descriptions

XPath substring function

First thing you might want to use is the XPath substring function. The substring function takes the following parameters: the string which you want to process, the start position for extracting the substring and the optional third parameter which allows you to define how many chars you want to extract. Let’s take a look at the following example:

<xsl:variable name="text" select="'12345'"/>
<xsl:value-of select="substring($text, 1, 3)"/> <!-- Returns '123' –->

Not bad is it? While you could theoretically use it for generating short descriptions the problems start when your string contains HTML markup:

<xsl:variable name="text">
  <![CDATA[<strong>12345</strong>]]>
</xsl:variable>
<xsl:value-of select="substring($text, 1, 3)"/>
<!-- Returns '<st' -->

See the problem? The substring function is not aware of the HTML markup and therefore using it has the risk of breaking the markup of the whole page! If only we could strip the HTML markup before passing it to the substring function…

XPath string replacements

XPath in its standard implementation doesn’t contain any function for replacing one string with another. Neither does the ddwrt namespace. There is a replace function in the EXSLT namespace but how to make the standard Content Query Web Part understand the EXSLT namespace? There is a .NET implementation of the EXSLT functions but it would mean that you would have to subclass the standard CQWP and extend the ModifyXsltArgumentList method.

And even if there was a replace function available, it would have to support Regular Expressions (RegEx): how else would you replace all HTML? There is no simple string equivalent of the simple <[^>]+> RegEx pattern used to strip string of all HTML markup.

But isn’t there anything we could do using nothing else than the out-of-the-box functionality? There is one thing…

Custom template for generating short descriptions

The Content Query Web Part uses three separate XSLT files to render the output. The best part is that you can extend each one of them or even make the CQWP use one of your custom files. Given such flexibility we could create a custom XSL template to do the job for us.

Before we dive into the code let’s define the requirements.

First of all we want to be able to generate a short preview of the content starting at the beginning of the string and containing x chars.

We want the short summary to be HTML safe: all HTML markup should be removed before generating the short description. Also the length of the short description should be calculated using the string with HTML removed.

As a nice-to-have it would be great to be able to include some kind of suffix like “…” or “more” if the content turned out to be longer than the short description allows and has been shortened.

Removing HTML markup in XSLT

As we have already found out there is no standard functionality available in XSLT to remove HTML markup. What we can do though, is to define templates which in some way resemble functions: pieces of logic which you can call with different parameters.

We’ve already done this, so what we can do is to grab the existing template and use it in our solution.

The first part of our requirement is complete: we can strip a string of all HTML markup.

Imtech.GenerateSummary template v0.1

By now we should be able to write a simple template to generate short descriptions for us:

<xsl:template name="Imtech.GenerateSummary">
  <xsl:param name="Content"/>
  <xsl:param name="Length"/>
  <xsl:variable name="cleanContent">
    <xsl:call-template name="Imtech.RemoveHtml">
      <xsl:with-param name="String" select="$Content"/>
    </xsl:call-template>
  </xsl:variable>
  <xsl:value-of select="substring($cleanContent, 1, $Length)"/>
</xsl:template>

That should do the job, right? Well, there is one problem… Let’s have a look at a test case:

<xsl:call-template name="Imtech.GenerateSummary">
  <xsl:with-param name="Content">
    <!-- text wrapped for the clarity of this example
    but would normally be in one line to prevent extra chars -->
    Lorem ipsum dolor sit <strong>amet, consectetur</strong>
    adipiscing elit. Nunc a magna quis velit dapibus blandit.
    Pellentesque accumsan tortor ut est
  </xsl:with-param>
  <xsl:with-param name="Length" select="45"/>
</xsl:call-template>

Curious what the result is? “Lorem ipsum dolor sit amet, consectetur adipi”. In case you haven’t noticed the word adipiscing is being sliced in the middle. Not really nice, is it? We should know better nowadays.

That leads us to a new requirement: the short description should be of maximally the given length, but if it’s in a middle of a word, the function should go back until it finds a separator (space in our case) and break the short description there. According to this requirement the short description in the example above should be “Lorem ipsum dolor sit amet, consectetur”.

LastIndexOf in XSLT?

If you were programming in one of the .NET languages you would use the LastIndexOf function to determine the position of the last separator and substring the initial string to that position. Want to hear some bad news? There is no LastIndexOf in XSLT. Somehow you don’t seem very surprised…

Once again let’s use templates to get the job done. Instead of returning the position of the last separator, we will immediately return the string before the last separator.

<xsl:template name="Imtech.SubstringBeforeLast">
  <xsl:param name="String" />
  <xsl:param name="Char" />
  <xsl:param name="subsequent"/>
  <xsl:choose>
    <xsl:when test="contains($String, $Char)">
      <xsl:if test="$subsequent = 1">
        <xsl:value-of select="$Char"/>
      </xsl:if>
      <xsl:value-of select="substring-before($String, $Char)"/>
      <xsl:call-template name="Imtech.SubstringBeforeLast">
        <xsl:with-param name="String"
                        select="substring-after($String, $Char)" />
        <xsl:with-param name="Char" select="$Char" />
        <xsl:with-param name="subsequent" select="1"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:if test="$subsequent != 1">
        <xsl:value-of select="$String"/>
      </xsl:if>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

This template takes two parameters: the string which you want to create a substring of and the character you’re looking for. The third parameter (subsequent) is for internal purposes: it helps the template to determine whether it should output a separator or not.

How it works

First of all the template checks whether the given string contains the character passed through the Char parameter. If the character has been found the string is being passed for further processing. If not the template checks one more thing: if it’s the initial call (subsequent parameter hasn’t been set yet), the string is being output as-is (this to cover the following case SubstringBeforeLast(“Lorem”, “ ”)).

If the template finds the Char character in the string: first the template checks if it is a subsequent call or not. As separators are not being included in the String variable on subsequent calls this check is required to include all other separators than the last one. Then everything before the first occurrence of the separator is being passed to the output. Everything after the separator is being used as the String parameter for a recursive call. Additionally the subsequent parameter is being set to let the template know that it’s being called from itself.

Almost there

So far we have done a lot of work with the XSLT. Let’s put it together and have a look at the result:

Using a custom XSLT template you can easily get the short description displayed properly. One last thing missing is adding a suffix like '...' after shortening a string

Almost there, aren’t we? The last thing that we’re missing is appending a suffix after shortening the content. Let’s finish our template off with this feature:

<xsl:if test="string-length($cleanContent) &gt; $Length">
  <xsl:value-of select="$Suffix" disable-output-escaping="yes"/>
</xsl:if>

And just a last check to confirm that it’s all working properly:

Using a custom XSLT template you can generate a properly displayed short description using the full page content as input

The complete short summary template by now should look like this:

<xsl:template name="Imtech.GenerateSummary">
  <xsl:param name="Content"/>
  <xsl:param name="Length"/>
  <xsl:param name="Suffix"/>
  <xsl:variable name="cleanContent">
    <xsl:call-template name="Imtech.RemoveHtml">
      <xsl:with-param name="String" select="$Content"/>
    </xsl:call-template>
  </xsl:variable>
  <xsl:call-template name="Imtech.SubstringBeforeLast">
      <xsl:with-param name="String"
      select="substring($cleanContent, 1, $Length)"/>
      <xsl:with-param name="Char" select="' '"/>
  </xsl:call-template>
  <xsl:if test="string-length($cleanContent) &gt; $Length">
    <xsl:value-of select="$Suffix"
      disable-output-escaping="yes"/>
  </xsl:if>
</xsl:template>

<xsl:template name="Imtech.RemoveHtml">
  <xsl:param name="String"/>
  <xsl:choose>
    <xsl:when test="contains($String, '&lt;')">
      <xsl:value-of select="substring-before($String, '&lt;')"/>
      <xsl:call-template name="Imtech.RemoveHtml">
        <xsl:with-param name="String"
          select="substring-after($String, '&gt;')"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$String"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>
<xsl:template name="Imtech.SubstringBeforeLast">
  <xsl:param name="String" />
  <xsl:param name="Char" />
  <xsl:param name="subsequent"/>
  <xsl:choose>
    <xsl:when test="contains($String, $Char)">
      <xsl:if test="$subsequent = 1">
        <xsl:value-of select="$Char"/>
      </xsl:if>
      <xsl:value-of select="substring-before($String, $Char)"/>
      <xsl:call-template name="Imtech.SubstringBeforeLast">
        <xsl:with-param name="String"
          select="substring-after($String, $Char)" />
        <xsl:with-param name="Char" select="$Char" />
        <xsl:with-param name="subsequent" select="1"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:if test="$subsequent != 1">
        <xsl:value-of select="$String"/>
      </xsl:if>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

And you can use it like this:

<xsl:call-template name="Imtech.GenerateSummary">
  <xsl:with-param name="Content" select="@Description" />
  <xsl:with-param name="Length" select="255" />
  <xsl:with-param name="Suffix" select="'...'"/>
</xsl:call-template>

Summary

Content Query Web Part is a very powerful and flexible Web Part. Because it uses XSLT for data presentation it allows you to generate dynamic content aggregation matching your branding and requirements. Although the default XSLT implementation supported by the CQWP contains only the basic functionality, you can easily extend it with custom templates. While they seem more complex than a comparable solution using C# or VB the best part is, that they don’t require any custom code and work in any standard MOSS 2007 installation.

Possibly related posts

34 Responses to “Generating short description using the Content Query Web Part”

  1. Randy Drisgill Says:

    Hot, this is hot

  2. Waldek Mastykarz Says:

    Thanks, Randy :)

  3. Jeremy Thake Says:

    Mate that is bloody awesome!

  4. Waldek Mastykarz Says:

    @Jeremy: Thanks! Great to hear it hits the spot. Any other CQWP/XSLT challenges you're facing/interested in?

  5. Eddy Blanco Says:

    Really cool.

  6. Caleb Says:

    Thanks for this. There is very little documentation out there on how to do this, so you're filling a big hole. Could you provide a little more guidance as to where the and code go? Do they go in ItemStyle.xsl, ContentQueryMain.xsl, both, or neither?

  7. Waldek Mastykarz Says:

    @Caleb: It all goes into ItemStyle.xsl (or your custom equivalent).

  8. Aaron Says:

    Love this code, but maybe I am tryng to extend it too far. I have incorporated it into a custom search results page – what I thought will happen would be;

    ResultTitle1
    puretext(Body1)
    ResultTitle2
    puretest(Body2) … and so on.

    What I get is;

    ResultTitle1
    puretext(Body1)
    ResultTitle2
    puretest(Body1) … and so on.

    I can change the body buy [...name=\"Content\" select=\"=\"/dsQueryResponse/Rows/Row[something]/@Body\"…], but if I pick row 2 they are all body2, row 3 all body3 etc.

    Is there a way to loop through Row[something] to get the desired;

    ResultTitle1
    puretext(Body1)
    ResultTitle2
    puretest(Body2)

    Any thoughts / ideas appreciated!

  9. Waldek Mastykarz Says:

    @Aaron: You shouldn't pick body like that. Because you're passing the ID of the node, every time you retrieve the body, you get the same result. Instead you should change the template call to: <xsl:with-parameter name="Content" select="@Body"/>

  10. Aaron Says:

    Yeah thats what I thought – yet when I do if get nothing – as in blank.

    Maybe I have the <xsl:template name= in the wrong spot? I have it at the top right under the one. Should it go lower, somewhere after the row count has been done?

    Ive sent you my pages code – Ive gone back to an abbreviated removeHTML until I get this fixed then will upgrade to yours!!

    Thanks for your help with this – the community is alive!

  11. Waldek Mastykarz Says:

    @Aaron: I've noticed that you're using the template with the DataViewWebPart. Can you confirm that you retrieve the Body field properly (like displaying its default value without any template)?

  12. Aaron Says:

    Yes, as I said maybe Im pushing this too far. I sent to the code and the results, in cae you dont get them;

    code1 = and you get:

    Announcements
    Adelaide Evacuation Drill
    Please be advised that Wormald's will be conducting evacuation drills ..
    Aaron Boundy

    code2 = and you get:

    Announcements
    Adelaide Evacuation Drill

    Aaron Boundy

    As stated this is an abbreviate version, same thing happens with the full imtech as well.

    Thanks again – really appreciate your help with this.

  13. Aaron Says:

    Waldek I got it. A crash course in XSL and a bit of common sense. I whacked the # <xsl:call-template name=\"Imtech.GenerateSummary\"> it the <xsl:template name=\"dvt_1.rowview\">;
    <xsl:template name=\"dvt_1.rowview\">
    <xsl:variable name=\"pureText\">
    <xsl:call-template name=\"Imtech.GenerateSummary\">
    <xsl:with-param name=\"Content\" select=\"@KBContentField\" />
    <xsl:with-param name=\"Length\" select=\"400\" />
    <xsl:with-param name=\"Suffix\" select=\"\'…\'\"/>
    <xsl:with-param name=\"Char\" select=\"\' \'\"/>
    </xsl:call-template>
    </xsl:variable>

    Now each time the row is formed through dvt_1.rowview if removes the html of that rows\' @KBContentField field.

    I get the odd artifact by overall it works perfectly – again thank you for your help, when I get this up on mine Ill send you the link.

  14. Waldek Mastykarz Says:

    @Aaron: great to hear it works. Looking forward to the link :)

  15. Scott Says:

    Nice touch with the short description! Would it be possible to give a non-xsl person a gentle push in the right direction to add the ever popular [more] link along with the "…"?

    Would it go in the xsl:if test=string-length(cleanContent)section or somewhere else?

    Again, thanks on the new perspective short description!

  16. Waldek Mastykarz Says:

    @Scott: you could include it in the xsl:if indeed, but I think that it would be nicer to use the Suffix param: .
    Like this you keep the template generic and customizable.

  17. Ashfaq Says:

    Great article..!!
    i m developing a custom CQWP by extending ContentByQueryWebPart class.i m using object model and SPSiteDataQuery to query all the tasks assigned to the current User from multiple site collections in the webapplication.how i can display the results as the default CQWP do so that i may customize it using item.xslt?
    Right now i m displaying the query results on a label control.i m not using the QueryOverride property of CQWP.

  18. Waldek Mastykarz Says:

    @Ashfaq: If you're using the Content Query Web Part, why using the SPSiteDataQuery? You're basically throwing out the biggest benefit of using the CQWP – performance. Why not using the QueryOverride property instead?

  19. Ashfaq Says:

    hi.Waldek
    QueryOverride property of CQWP is limmited to site collection scope.i hav to query across the whole webapplication or multiple webapplications.further i hav to fetch data from multiple list types.
    Can i achieve all this with QueryOverride property?

  20. Waldek Mastykarz Says:

    @Ashfaq: CQWP works within a single Site Collection only. If you need a scope wider than that you would have to pick another solution like for example Search. While the multiple list types aren't a limitation, the Site Collection scope is definitely something you have to consider in your choice.

  21. Tyler Says:

    I am having troubles getting this to work on a sub site. The rest of my style shows properly but the text from the item does not seem to get pulled. Any idea what I am missing?

  22. Tyler Says:

    On a side note… this is implemented and working properly on the parent site and are utilizing the same exact style on our CQWP. And as I stated before, all of the other fields show up. Just not the text.

  23. Waldek Mastykarz Says:

    @Tyler: You could try to add <textarea><xsl :copy-of select="."/></textarea> to the ContentQueryMain.xsl root template to check the retrieved XML. If the content is there but doesn't show up in the result markup, the XSLT is the problem. If the content is not in the XML however, either the item doesn't have the content or there is a problem with the CommonViewFields property.

  24. Alex Says:

    Hello

    I really appreciate the posting about generating brief description as i was looking for some solution. However, I got an error when i tried to add the code to itemstyle.xsl and CQWP stop functioning correctly.

    Here\'s what i did.

    first, i copied default template part in itemstyle.xsl and pasted it at the end. then changed name=\"default\" to name=\"Brief Description\"

    then i placed the code in this posting below the template. (i just removed Imtech. which should not be a problem)

    then back to template named Brief Description, i changed

    <div class=\"description\">
    <xsl:value-of select=\"@Description\" />
    </div>

    to

    <div class=\"description\">
    <xsl:call-template name=\"GenerateSummary\">
    <xsl:with-param name=\"Content\" select=\"@Description\" />
    <xsl:with-param name=\"Length\" select=\"124\" />
    <xsl:with-param name=\"Suffix\" select=\"\'…\'\"/>
    </xsl:call-template>
    </div>

    that was all of my modification.

    after i saved the work and publish the xsl, i got the error.

    Could you please indicate me if i missed something or mademistakes?

    I need some guidance cause i am very new to modify xsl.

    Thank you for any help in advance.

  25. Waldek Mastykarz Says:

    @Alex: I'm not sure that it's a good idea to include a space in the template name. The first thing for me to try would be to remove the space and check whether it's working or not. Additionally, you can check the ULS log. The CQWP logs all XSLT errors so that you can exactly pinpoint what's causing the problems.

  26. Jeremy Thake Says:

    There is no end to oyur talent mate! This script is awesome!

  27. Nick Hadlee Says:

    Used this yesterday and it worked a treat. Cheers Waldek!

    One change I added in my case was to the 'Imtech.GenerateSummary' template. A quick test to see if we need to call 'Imtech.SubstringBeforeLast' or just output the content because for me it was cutting off on the last ' ' regardless of whether it needed to. Brilliant work cheers again.

    e.g.

  28. Nick Hadlee Says:

    Think I just had a comment #fail moment…the e.g. I meant to add was:

    <xsl:choose>
    <xsl:when test="string-length($cleanContent) > $Length">
    <xsl:call-template name="Imtech.SubstringBeforeLast">
    <xsl:with-param name="String" select="substring($cleanContent, 1, $Length)"/>
    <xsl:with-param name="Char" select="' '"/>
    </xsl:call-template>
    <xsl:value-of select="$Suffix" disable-output-escaping="yes"/>
    </xsl:when>
    <xsl:otherwise>
    <xsl:value-of select="$cleanContent" />
    </xsl:otherwise>
    </xsl:choose>

  29. TD1 Says:

    Hi,

    Thanks for this. It's exactly what i need. However I'm a complete SharePoint noob and just wonder if it's too much of an advanced task.

    The content is being pulled from a web-page right? Is it impossible to get this to pull from a word doc?

    Also what the initial steps in getting to edit the specific template.

    Sorry – keen to learn – but maybe am biting off more than I can chew.

  30. Waldek Mastykarz Says:

    @TD1: If you map content in your Word doc to a Field from the Document Content Type you will be able to display the content just as you would do for any other item.

  31. Michael Suyama Says:

    Have you tried it with SP 2010? When I tried applying it to the ContentQueryMain.xsl and ItemStyle.xsl of a Publishing Portal site collection it worked. But for some reason it caused the two Summary Links web parts on the page to crash with the message "Unable to display this web part…".

  32. Waldek Mastykarz Says:

    @Michael: Although I haven't tried it with SharePoint 2010, the XSLT is so simple that I can't image there would be a SharePoint 2010 specific error in it. I think that it would be useful to have a closer look at the error that you're getting. Did you know that the CQWP does some great logging into ULS, so basically every XSLT exception that you get (including the details) is being logged to ULS? It might be useful to browser through ULS to find the specific error that you're getting.

  33. varun Says:

    Hi Waldek,

    I am trying to add a CAML Query in "Query Override". I have overrided the "Apply Changes" metiod of toolpart class and setting the "Query Override" property there.

    When i click on "Apply" and then "Ok" in the Tool Part
    i am getting the below error.

    "Failed to load viewstate. The control tree into which viewstate is being loaded must match the control tree that was used to save viewstate during the previous request. For example, when adding controls dynamically, the controls added during a post-back must match the type and position of the controls added during the initial request."

    Please do reply ASAP..

  34. Waldek Mastykarz Says:

    @varun: Yes, it's a known issue which has to do how the Content Query Tool Part works. As soon as you set the value of the ContentQueryOverride property, CQTP adds an extra warning saying that not all properties are available because of the override. Because the warning wasn't visible on the previous postback, the exception is being thrown. I admit it's annoying but it's not really something we can do much about.

Leave a Reply

Security Code:

WP Theme & Icons by N.Design Studio
Entries RSS Comments RSS
Copyright © 2007 - 2010 Waldek Mastykarz

Creative Commons License