Get placeholder content from page Id

Topics: General, XSLT
Apr 27, 2011 at 2:01 AM

Hello,

I am building an XSLT function, and have some selected Ids from SitemapXml. These Ids are for pages, and the pages have content placeholders in their templates. I would like to work with in my XSLT function the otherwise rendered & executed content from a particular location on various other pages, by reference.

 

How can I get--in an XSLT function--the content of a particular template placeholder (MyContent for example), given a Page Id? 

<!-- Page template bit:  -->
<rendering:placeholder id="mycontent" title="MyContent" />
<!-- Xslt Bit -->
<xsl:for-each select="/in:inputs/in:result[@name='ChildrenXml']/Page/@Id">
<!-- How to get rendered contents of MyContent from this Id? -->
...
</xsl:for-each>

The goal is to create "teasers" from a list of page ids. Each teaser will contain actual content from the "MyContent" area of the page, up until a BREAK string (if such a string exists, otherwise a set number of words). The placeholder could be wrapped in an identifiable div, if that helped.

 

Another approach might be: This teaser-bit of info could instead be set up as a metadata item on a page, that is, after content on a page is updated or created, some action occurs to create the teaser abstract and store it. Then the teaser/intro could be fetched with Get(Teasers)Xml and PageIds matched. 

If this second approach were employed, can I set up this sort of action declaratively in markup anywhere, or is C# the only way to handle Add & Edit (like I saw used in the Blog package for url titles)? Still, how do I find the newly created or updated actual content of a placeholder, even in C#? Sorry if this is a silly question, but I have most of the rest all set up, so I hope it makes sense!

thanks!

Apr 27, 2011 at 8:07 AM

xanderlih,
IMHO subscribing to create / update events sounds as good way to do:

public class StartupHandler : IApplicationStartupHandler
{
	public void OnBeforeInitialize()
	{
		return;
	}

	public void OnInitialized()
	{
		DataEventSystemFacade.SubscribeToDataAfterAdd<IPagePlaceholderContent>(ContentUpdate);
		DataEventSystemFacade.SubscribeToDataAfterUpdate<IPagePlaceholderContent>(ContentUpdate);
	}

	private static void ContentUpdate(object sender, DataEventArgs args)
	{
		var page = (IPagePlaceholderContent)args.Data;
		var content = page.Content;
		var pageId = page.PageId;
		// do your stuff here
	}
}

 than.. just create C# function and get requireed data.

Apr 27, 2011 at 8:44 AM
Edited Apr 27, 2011 at 8:44 AM

Why store the data somewhere else again when we just need to retrieve whatever is there already?

You would need to create a C# function that returns XElement. If you have the Page ID you can get a list of IPagePlaceholderContent instances using the PageManager class like this

var contents = PageManager.GetPlaceholderContent(pageId)

then you can enumerate over it and encapsulate each content in a XElement

var contents = PageManager.GetPlaceholderContent(pageId).Select(c => XElement.Parse(c.Content));

and finally you need to wrap this list of XElement's into a single one

new XElement(Namespaces.Xhtml + "html", 
                    new XElement(Namespaces.Xhtml + "head"), 
                    new XElement(Namespaces.Xhtml + "body", contents.ToArray()));

So the final function becomes something like this

public XElement GetPlaceholderContentsForPage(Guid id)
{
    var contents = PageManager.GetPlaceholderContent(id).Select(c => c.Content);
    var element = new XElement(Namespaces.Xhtml + "html", 
                    new XElement(Namespaces.Xhtml + "head"), 
                    new XElement(Namespaces.Xhtml + "body", contents.ToArray()));

    return element;
}
Apr 27, 2011 at 10:38 AM
burningice wrote:

Why store the data somewhere else again when we just need to retrieve whatever is there already

 I believe that it's just a most simple and efficient way to increace performance. Example: several K pages website.... under load... constant editing... placeholders content (it's could be several KB of text).. list of pages.... of cource you can (and actually have to) add caching to improve performance.. but still it's loads your server everytime you reset cache, so in this case suggested "denormalization" looks like an instant and pure win to me.  Strategy selection depends from concrete situation. The good news is that replacing one strategy to another with C1 is pretty simple :)

Apr 27, 2011 at 5:31 PM
Edited Apr 27, 2011 at 6:26 PM

Thanks for the tips, guys!

In this particular case I think I will first try getting something like BI's GetPlaceholderContentsForPage working as an XSLT extension.  A few other methods exposed in that manner and I will still be able to implement things similarly to as I had planned originally (I'm porting a site structure created in another CMS). Another website would have different priorities, but for this one there will be only a single editor/creator of content. As editor, I will limit to an aesthetic number the children of a page in content--however, teaser-lists will also be made from selected referenced pages and filtered instances of a global-datatype. These lists could get very long, but I'm using Composite C1's paging information for those function calls, which should keep things manageable.

Thanks again. I love it when I ask a question like, "How do I get placeholder content?" and the answer is "Use GetPlaceholderContent" (hmm, should've found that one!)!

 

EDIT: Maybe not an xslt extension, just a C1 function is best here...

Apr 28, 2011 at 1:59 AM
Edited Apr 28, 2011 at 2:46 AM

I've been successful in getting teasers for Pages. I'm very happy with this method, although it will take some more practice to learn when to call functions and when I can apply- or call-templates still. 

Anyway, I made a C# function using the C1 backend, then got it returning the requested content-placeholder by page id and  placeholder name. It looks like this (no check for empty content or no id match!)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Composite.Data;
using Composite.Data.Types;
using Composite.Core.Xml;

namespace LandingSite.Utility
{
    public static class InlineMethodFunction
    {
        public static XElement GetPagePlaceholderContent(String PlaceholderId, Guid PageId)
        {
            String PlaceholderContents = PageManager.GetPlaceholderContent(PageId).Single(ph => ph.PlaceHolderId.Equals(PlaceholderId, StringComparison.OrdinalIgnoreCase)).Content;
            return XElement.Parse(PlaceholderContents);
        }
    }
}

 

PageManager.GetPlaceholderContent...Content returns a full XHTML document as a string.

Then I call that function in my teaser template. There is a lot of flexibility here, but this works:

 

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:in="http://www.composite.net/ns/transformation/input/1.0"
  xmlns:lang="http://www.composite.net/ns/localization/1.0"
  xmlns:f="http://www.composite.net/ns/function/1.0"
  xmlns:c1="http://c1.composite.net/StandardFunctions"
  xmlns="http://www.w3.org/1999/xhtml"
  exclude-result-prefixes="c1 xsl in lang f">

  <xsl:template match="/">
    <html>
      <head></head>
      <body>
        <ul class="teasers">
          <xsl:for-each select="/in:inputs/in:result[@name='ChildrenXml']/Page">
            <li>
              <xsl:apply-templates select="current()" mode="Teaser" />
            </li>
          </xsl:for-each>
        </ul>
      </body>
    </html>
  </xsl:template>

  <xsl:template match="Page" mode="Teaser">
    <div class="teaser">
      <a class="teaserheader" title="{@Title}" href="{@URL}">
        <xsl:value-of select="@Title" />
      </a>
      <xsl:variable name="GetPagePlaceholderContent">
        <f:function name="LandingSite.Utility.GetPagePlaceholderContent">
          <f:param name="PageId" value="{@Id}"/>
        </f:function>
      </xsl:variable>
      <xsl:apply-templates mode="Abstract" select="c1:CallFunction($GetPagePlaceholderContent)"/>
      <a class="teaserfooter" title="{@Title}" href="{@URL}">
        <xsl:value-of select="/in:inputs/in:param[@name='TeaserLinkText']"/>
      </a>
    </div>
  </xsl:template>
  
  <xsl:template mode="Abstract" match="node()">
    <xsl:copy>
      <xsl:copy-of select="@*" />
      <xsl:apply-templates mode="Abstract" />
    </xsl:copy>
  </xsl:template>

  <xsl:template mode="Abstract" match="xhtml:body" xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <xsl:copy>
      <xsl:copy-of select="@*" />
      <xsl:choose>
        <xsl:when test="./xhtml:*[@id='Intro']">
          <xsl:apply-templates mode="Abstract" select="./xhtml:*[@id='Intro']"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:apply-templates mode="Abstract" select="./xhtml:p[position()=1]"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

 

It took some fiddling to get everything escaped properly and my namespaces squared away in the Abstract mode templates, but that's getting easier. Instead of a BREAK string or a word count, I instead took an element with id="Intro" if it exists (<div>s for now, but could be a <section> or <article> maybe), otherwise just the first paragraph.

XL

Coordinator
Apr 28, 2011 at 8:24 AM
Edited Apr 28, 2011 at 8:24 AM

Just a small trick, if you rewrite your basic xslt template

  <xsl:template mode="Abstract" match="node()">
    <xsl:copy>
      <xsl:copy-of select="@*" />
      <xsl:apply-templates mode="Abstract" />
    </xsl:copy>
  </xsl:template>

as

  <xsl:template mode="Abstract" match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates mode="Abstract" select="@* | node()" />
    </xsl:copy>
  </xsl:template> 

it will give you an ability to have templates that will be targeting specified attributes. And it is 1 line shorter  :)

Apr 28, 2011 at 3:57 PM

Excellent, I'm glad you spotted that!