Handling rich text content fields in SDL Tridion CMS

In this article, I will explain a little bit about Rich Text Fields in SDL Tridion CMS and how to customize and restrict the usage of styling to some extent with a basic example.

Introduction to RTF fields in Tridion

Tridion supports Rich Text Fields for allowing content managers to create better-looking content. All kinds of HTML elements are supported like the typical Italic, Bold and Underline. By default when enabling RTF features for a text field in your schema design all common tags are allowed:

  • Editing (or copy/paste) as HTML
  • Possible to select some text and apply a style via the buttons in the ribbon bar:

Customization options in Tridion

All this freedom for a content manager may drive front-end developers insane since they need to support all of it with their CSS. Luckily, there are 3 customization options within Schema design to limit the RTF features:

  1. Disable buttons in the ribbon bar
  2. Configure custom styles (matching the class names in CSS)
  3. Filter unwanted HTML (with help of XSLT)

These can be configured per schema content field:

Example case - Transforming HTML

XSLT is a complex but powerful language for transforming XML from one format to another. HTML is just a form of XML and thus XSLT can be used to free RTF content from unwanted tags.

Here, I want to demonstrate how to implement the following requirements:

  1. Make sure all bold texts are using <strong> tag element (and not <b> tag, which is default). This mechanism can later also be applied for converting italic <i> tag to <em> tag, etc.
  2. Only paragraphs, breaks and bold should be allowed in the HTML. This results in the following HTML tags: <p>, <br /> and <strong>

Converting from one HTML tag to another

The 1st requirement can be implemented by inserting the following fragment into the default XSLT:

  <xsl:template match="b">
        <xsl:element name="strong">
          <xsl:apply-templates select="node()"></xsl:apply-templates>
        </xsl:element>
  </xsl:template>

What it does is: when a tag is <b> found, replace it with a <strong> tag and continue XSLT rendering for the inner value of the node.

Filtering unwanted tags

For filtering you basically have two approaches:

  • Black-listing: unwanted tag elements. Use when you want to allow all tags, and exclude a few unwanted tags.
  • White-listing: specify the tag elements you want to keep. Use when the set of allowed tags is known.

In general (as in security), white-listing is the preferred method. Explicitly allowing specific tags gives you the most control over the output and rule out any 'forgotten' tags to appear in the HTML. 

The 2nd requirement can be implemented by using the following fragment in XSLT:

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

This fragment is basically processing all X(HT)ML elements and it copies only the allowed tags to the output to be processed further.

Complete XSLT solution

Below the complete file, including a conversion for <i> to <em>.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output omit-xml-declaration="yes" method="xml" cdata-section-elements="script"></xsl:output>
  <xsl:template match="*[      (self::br or self::p or self::div)     and      normalize-space(translate(., ' ', '')) = ''     and      not(@*)     and      not(processing-instruction())     and      not(comment())     and      not(*[not(self::br) or @* or * or node()])     and      not(following::node()[not(         (self::text() or self::br or self::p or self::div)        and         normalize-space(translate(., ' ', '')) = ''        and         not(@*)        and         not(processing-instruction())        and         not(comment())        and         not(*[not(self::br) or @* or * or node()])       )])     ]">
    <!-- ignore all paragraphs and line-breaks at the end that have nothing but (non-breaking) spaces and line breaks -->
  </xsl:template>
  <xsl:template match="br[parent::div and not(preceding-sibling::node()) and not(following-sibling::node())]">
    <!-- Chrome generates <div><br /></div>. Renders differently in different browsers. Replace it with a non-breaking space -->
    <xsl:text> </xsl:text>
  </xsl:template>
  <!-- added bold to strong conversion -->
  <xsl:template match="b">
        <xsl:element name="strong">
          <xsl:apply-templates select="node()"></xsl:apply-templates>
        </xsl:element>
  </xsl:template>
  <!-- added italic to em converter -->
  <xsl:template match="i">
    <xsl:element name="em">
          <xsl:apply-templates select="node()"></xsl:apply-templates>
        </xsl:element>
  </xsl:template>
  <xsl:template match="/ | node() | @*">
    <xsl:copy>
      <xsl:apply-templates select="/ | p | b | i | br"></xsl:apply-templates>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>


 

 

 

 

Tridion publishing in debug mode

Publishing is the most critical process for Tridion users and when publishing fails (or takes too long) they become unhappy and the sysadmin has to explain why and fix it asap. Here is my Tridion tip for today: how to debug the publishing process.

Pre-requisites:

  • Admin access to machine where Publisher service runs
  • Nobody is publishing from that CMS

Steps for setting up the publisher in DEBUG mode:

  • Stop the Publisher windows service
  • Open command prompt (or Powershell)
  • Navigate to <TRIDION_HOME>\bin\ folder
  • Run 'TcmPublisher.exe -debug'

Now the publisher service is listening in DEBUG mode to the publish queue. Now you can publish the page that is causing issues and see what the publisher is doing under the hood and what is going wrong. 

Example response of publishing page

Below is an example result (modified a bit for readability):

10:33:29.5042 <12152> Retrieved queue message 73 from PublishQueue
10:33:29.5042 <12680> Working on queue message 73
10:33:29.7073 <12680> Extension initialized: CME System Privileges Extensions (Tridion.Web.UI.CME.TcmExtensions.SystemPr
ivileges)
10:33:29.7230 <12680> Extension initialized: CME Event System Extensions (Tridion.Web.UI.CME.TcmExtensions.EventHandlers
)
10:33:29.7542 <12680> Extension initialized: XPM - TCM Extensions (Tridion.SiteEdit.TcmExtensions.EventHandlers)
10:33:30.4376 <12680> Handling Publish Transaction [tcm:0-2-66560]
10:33:31.6916 <12680> Publishing item [tcm:1-41-64] from publication [tcm:0-1-1] to target [tcm:0-1-65537]
10:33:31.8836 <12680> Storage location: c:\Temp\tcm_0-2-66560.Content\
10:33:32.0197 <12680> Using resolver [Tridion.ContentManager.Publishing.Resolving.PageResolver]
10:33:32.1291 <12680> Resolving the pages [tcm:1-41-64] took 00:00:00.1135300
10:33:32.1447 <12680> Page tcm:1-41-64 resolved to 1 items. Resolving took: 00:00:00.1412808
10:33:32.1760 <12680> Rendering item [tcm:1-41-64] 'Test Page' with template [tcm:1-34-128] 'Default Page Template' in p
ublication target [tcm:0-1-65537] 'Staging'
10:33:32.9103 <12680> Referenced assemblies from the GAC:

10:33:32.9103 <12680> Referenced assemblies from the custom path:

10:33:32.9103 <12680> Source code: /* C# sourcecode removed for readability */
            
10:33:34.3745 <12680> Rendering of item [tcm:1-41-64] 'Test Page' with template [tcm:1-34-128] 'Default Page Template' i
n publication target [tcm:0-1-65537] 'Staging' took: 00:00:02.1851037
10:33:34.3865 <12680> Adding rendered item to the transport package: item [tcm:1-41-64] 'Test Page' with template [tcm:1
-34-128] 'Default Page Template' in publication target [tcm:0-1-65537] 'Staging'
10:33:34.3905 <12680> Using transport package handler: Tridion.ContentManager.Publishing.Transporting.DefaultPageHandler

10:33:34.3945 <12680> Adding [Page tcm:1-41-64] to the transport package.
10:33:34.3995 <12680> Writing file: c:\Temp\tcm_0-2-66560.Content\Pages\test.html
10:33:34.5353 <12680> Updating the publish transaction with the list of processed items
10:33:34.5509 <12680> Saving the transport package.
10:33:34.5978 <12680> Sending tranport package for transaction [tcm:0-2-66560] with deploy control [Commit] to transport
 service.
10:33:34.8514 <12680> Sending information to transport service took: 00:00:00.2454763.
10:33:34.8544 <12680> Transport service reported state [ReadyForTransport] for Publish Transaction [tcm:0-2-66560] with
response: <TransportSummary referenceId="tcm:0-2-66560" state="Ready for transport"><Processing><Context topic="Content
Delivery"><IsRollbackOnFailure>false</IsRollbackOnFailure></Context><Step href="simple" type="Prepare transport" state="
Ready for transport"><RemoteEndpoint connector="DiscoveryService" id="DgHAheAA2zLe19e5+Pe/tQ==" name="Discovery Service
Upload" state="Ready for transport" windowSize="0" /></Step><Step href="simple" type="Transporting" state="None"><Remote
Endpoint connector="DiscoveryService" id="DgHAheAA2zLe19e5+Pe/tQ==" name="Discovery Service Upload" state="None" windowS
ize="0" /></Step><Step href="simple" type="Deployment preparation" state="None"><RemoteEndpoint connector="DiscoveryServ
ice" id="DgHAheAA2zLe19e5+Pe/tQ==" name="Discovery Service Upload" state="None" windowSize="0" /></Step><Step href="simp
le" type="Deploying" state="None"><RemoteEndpoint connector="DiscoveryService" id="DgHAheAA2zLe19e5+Pe/tQ==" name="Disco
very Service Upload" state="None" windowSize="0" /></Step><Step href="simple" type="Deployment committing" state="None">
<RemoteEndpoint connector="DiscoveryService" id="DgHAheAA2zLe19e5+Pe/tQ==" name="Discovery Service Upload" state="None"
windowSize="0" /></Step></Processing></TransportSummary>
10:33:34.9102 <12680> Handling Publish Transaction [tcm:0-2-66560] took: 00:00:04.4792882
10:33:34.9102 <12680> Cache statistics: SimpleCache: 5 Regions, 5 Objects, 11 Hits, 2 Misses
10:33:34.9102 <12680>   Region 'Component': 0 Objects, 0 Hits, 0 Misses.
10:33:34.9102 <12680>   Region 'PublishTransaction': 3 Objects, 6 Hits, 1 Misses.
10:33:34.9102 <12680>   Region 'TargetType': 1 Objects, 4 Hits, 0 Misses.
10:33:34.9102 <12680>   Region 'PublicationTarget': 1 Objects, 1 Hits, 0 Misses.
10:33:34.9258 <12680>   Region 'Tdse': 0 Objects, 0 Hits, 1 Misses.
10:33:35.0352 <12152> Deleting queue message: 73 from PublishQueue.
10:33:35.4883 <6132> Transport service response: <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
/* XML response is removed for readability */
10:33:35.5234 <6132> Receiving deployment feedback for publish transaction [tcm:0-2-66560] with status: WaitingForDeploy
ment
10:33:39.7672 <6132> Transport service response: <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
/* XML response is removed for readability */

10:33:39.7922 <6132> Receiving deployment feedback for publish transaction [tcm:0-2-66560] with status: Success

What good to know is that this also works for other TCM services as well. After you finished your investigation, don't forget to start the service again.

Tridion Content Types for XPM explained

In this article, I want to explain how to create, configure and use Tridion Content Types with Experience Manager (XPM).

Background

Lots of clients want to have Experience Manager (XPM) enabled for their sites to have inline editing capabilities in the staging environment. For moderating existing content this is working fine. For new content, the Content Manager often falls back to the 'classic' Content Management Explorer (CME). Their reason given is that XPM does not add value. This can be improved by defining a lesser known feature: Content Types.

What are Content Types?

Content Types are pre-set definitions for adding a new component presentation to your page. Within the definition you can define content creation settings:

  • Component prototype (which is copied to bootstrap your new component)
  • Auto-generated component title (optional)
  • Default storage location (restricted or optional)
  • Default component template
  • Insert location (top or bottom of the page)

Content Types can be used to create and insert new content within XPM. The content types are presented on the left-hand side panel.

How to create a Content Type?

Content Types are managed on publication level. When opening the properties of a publication there is a tab:

The title and description fields are shown in XPM for the content editor to get a notion of the purpose.

Create Content Type mapping to Page Template(s)

After creating a new Content Type you do not see it in XPM directly. First, you need to enable for 1 or more Page Templates. This can be done via Tridion Settings page - Inline Editing - Content Type mappings - [your publication]. When selecting a page template you should see something like this:

You can see that I unchecked 'Inherit settings from Parent' since in my case I only want to enable my new 'Campaign Story' content type on campaign site publication level. If you want to create a content type to used in every publication, you have to create it on the highest possible publication. And then create the PT mapping there too to inherit it to lower publications. 

Happy XPM-ing :-)

Upgrading to SDL Web 8.5 can break your DD4T JMS decaching

Below a story about non-working decaching after upgrading to SDL Web 8.5 CMS.

Project setup

The project I work on has this environment configuration:

  • SDL Web 8 CMS (with update SP1 and Cache Channel Service)
  • DD4T 2.1 based .NET web application
  • ApacheMQ as JMS solution
  • We use DD4T.Caching.ApacheMQ NuGet package
  • Hosted in Azure cloud

Given this setup, everything is configured to flush the Application cache when a Content Manager publishes a Page, Dynamic Component Presentation or Taxonomy from the CMS. This works really well and we achieve the two most important goals: High-performance websites + No outdated content.

Upgrade to Web 8.5 problem

But then we decided to upgrade to SDL Web 8.5 (with a new server). The upgrade process itself is not as painful as it was in the past, but we had an issue: de-caching mechanism did not work anymore. We did the most obvious checks:

  • Check config settings of microservices
  • Firewall settings (ports open?)
  • MQ dashboard (publishers, subscribers, and messages)
  • Check log files for exceptions
  • Review the Topology Management config (java -jar .\discovery-registration.jar read)

Mysteriously, everything was fine. After some WebApp debugging sessions I found the issue. The JMS messages were changed and the TCM URI is now prefixed with '1:'. Maybe SDL decided to include the CacheEventTypeId? Examples:

  • SDL Web 8 sends 17:3565 for pages.
  • SDL Web 8.5 sends 1:17:3565 for pages.
  • SDL Web 8 sends 17:52916:154 for DCPs.
  • SDL Web 8.5 sends 1:17:52916:154 for DCPs

RCA and solving the issue

This causes a lookup issue when looking for depending cache items to flush within the TridionBackedCacheAgent. The agent stores pages in cache with 17:3565 and CCS tries to flush the cache with 1:17:3565. Since we have a custom implementation of the CacheAgent (for either reasons) we could fix it easily by using this code fragment in the CacheAgent:

private Regex RemoveFirstElemWhenWeb85 = new Regex("^(1:)(.*)");

private string GetDependencyCacheKey(string tcmUri)
{
            return "Dependencies:" + this.RemoveFirstElemWhenWeb85.Replace(tcmUri, "$2");
}

This code backward compatible with older version of Web/Tridion as long as you do not have a publication with the ID of 1 :-)