6 minute read time.

One of the common requests we get from customers is to be able to configure metadata on their site pages, this helps with SEO and also improves the experience when a page is shared on Facebook, Twitter or any other platform that reads extended meta tags.  With the advent of Telligent version 9 there are some improvements to the blogs and media gallery to allow the author to specify some meta data but there is not a holistic system that works across the site other than the title widget, which would mean making custom themes and does not allow you to target specific pages.

From our customers we know that they want to be able to control theses parameters in the general context and then on some specific pages they want to do very specific changes suggested by a SEO specialist.  We also know that the person responsible for SEO is not always the same person that manages the site.

As with all of our projects all of the code I discuss here can be found on GitHub specifically https://github.com/4-Roads/FourRoads.TelligentCommunity/tree/Telligent9/src/code/FourRoads.TelligentCommunity.MetaData for this one.

I started this project thinking about what meta data elements a site would want to configure and after a short while I concluded that the only standard ones were the HTML description, keywords and title tags.  All other tags were optional and based on what system you are targeting, for example Facebook uses Open Graph to pull information about a page.  The first plugin that I create will have a configuration option to allow me to specify a list of tags that I want a user to populate. I'll store this list in an class MetaDataConfiguration that can be passed into the processing logic.

There are 2 main parts to this solution, the configuration and the rendering, I'll discuss these separately.

Meta Data Rendering

The core element that is required on every page is a widget that sets the meta data elements that have been configured.  To allow me to have a completely configurable experience from the perspective of what is added to the meta data I decided that widget had another responsibility to extract all of the dynamic data that is available in the current context.  By creating the widget up front it helped me define the functions I needed to expose in NVelocity.  What you can see in the widget code is a dictionary $parameters being defined that contains all of the dynamic variables.  This is then passed into a function called $frcommon_v1_metaData.FormatMetaString that is responsible for taking the configured data and applying the tokens. Finally the foreach loop renders the meta information.

#set($currentMetaData = $frcommon_v1_metaData.GetCurrentMetaData())
#if ($currentMetaData)

#set($parameters = "%{}")
#set($added = $parameters.Add("SiteName" , $core_v2_configuration.SiteName ))
#set($added = $parameters.Add("SiteLogoUrl" , $core_v2_configuration.SiteLogoUrl ))
#set($added = $parameters.Add("ContainerName" , $core_v2_container.Current.HtmlName("web") ))
#set($added = $parameters.Add("ContainerDescription" , $core_v2_container.Current.HtmlDescription("web") ))

#set($added = $parameters.Add("PageImage" , $currentMetaData.GetBestImageUrlForCurrent()))

#if ($core_v2_application.Current)
 #set($added = $parameters.Add("ApplicationName" , $!core_v2_application.Current.HtmlName("web") ))
 #set($added = $parameters.Add("ApplicationDescription" , $!core_v2_application.Current.HtmlDescription("web") ))
 #set($added = $parameters.Add("ApplicationrImage" , $!core_v2_application.Current.AvatarUrl)) 
#else
 #set($added = $parameters.Add("ApplicationName" , "" ))
 #set($added = $parameters.Add("ApplicationDescription" ,""))
 #set($added = $parameters.Add("ApplicationrImage" ,"")) 
#end
#set($seperator = $core_v2_widget.GetStringValue('seperator' , " "))

#if ($currentMetaData.Title && $currentMetaData.Title != "")
  #set($added = $core_v2_page.SetTitle($frcommon_v1_metaData.FormatMetaString($currentMetaData.Title,$seperator , $parameters), false, false))
#end

#if ($currentMetaData.Description && $currentMetaData.Description != "")
  #set($added = $core_v2_page.AddMetaDescription($frcommon_v1_metaData.FormatMetaString($currentMetaData.Description,$seperator , $parameters)))
#end

#if ($currentMetaData.Keywords && $currentMetaData.Keywords != "")
  #set($added = $core_v2_page.AddMetaKeywords($frcommon_v1_metaData.FormatMetaString($currentMetaData.Keywords,$seperator , $parameters)))
#end

#foreach($keyValue in $currentMetaData.ExtendedMetaTags)
    #if ($keyValue.Value && $keyValue.Value != "")
    #set($value = $frcommon_v1_metaData.FormatMetaString($keyValue.Value,$seperator , $parameters))
 
    #if ($core_v2_utility.IsMatch($keyValue.Value, "og:"))
        #set($added =  $core_v2_page.AddOpenGraphMetaTag($keyValue.Value, $value))
    #else
      #set($added = $core_v2_page.AddMetaTag($keyValue.Key,$value))
    #end
    #end
#end

#end
 

So the rendering side of things is actually pretty easy and hopefully I have provided a system that is also customization if you want to provide your Meta Data administrator with different parameters.

Meta Data Configuration

The harder part of this project was the meta data configuration but it is also the most interesting because we get to use some of the new features of Telligent 9 in the form of the contextual fly out panel.  Since my list of configuration fields is infinitely long I need to use create a dynamic form.  Fortunately the platform has this facility accessed by the functions in $core_v2_dynamicForm.  So my widget for administration looks like this.

#set($hasPermission = $frcommon_v1_metaData.CanEdit)

#if($hasPermission) 
    #set($saveId = $core_v2_widget.UniqueId('save'))
    #set($metaForm = $frcommon_v1_metaData.GetDynamicFormXml())
    #set($formId =  $core_v2_widget.UniqueId('formId'))
    
    $core_v2_dynamicForm.RenderForm($formId, $metaForm)
    
    #registerEndOfPageHtml('fourroads.widgets.metadata')
        <script type="text/javascript" src="$core_v2_encoding.HtmlAttributeEncode($core_v2_widget.GetFileUrl('metadata.js'))"></script>
    #end
    
    #registerEndOfPageHtml()
        <script type="text/javascript">
            jQuery(function(){
	            jQuery.fourroads.widgets.metaDataUpdate.register({
		          getData : function() {return $core_v2_dynamicForm.GetValuesScript($formId);},
          resources : {
            save:'$core_v2_encoding.JavascriptEncode( $core_v2_language.GetResource("Save"))',
            updated:'$core_v2_encoding.JavascriptEncode( $core_v2_language.GetResource("Updated"))'
          },
			        urls : {
				        saveDataCallback:'$core_v2_encoding.JavascriptEncode( $core_v2_widget.GetExecutedFileUrl("saveData.vm"))'
			        }
		        });
            });
        </script>
    #end
#end

This is not a normal widget it will be rendered using the IScriptablePlugin plugin and I will discuss that more in a moment.  For now in the widget you can see at the top there is a call to $frcommon_v1_metaData.GetDynamicFormXml() this function returns a XML form built using the Telligent Dynamic form controls that are used throughout the system.  Notice how I've populated the default values with anything that is already configured.

public string GetDynamicFormXml()
{
    ContentDetails details = GetCurrentContentDetails();

    MetaData metaData = GetCurrentMetaData(details);

    PropertyGroup group = new PropertyGroup("Meta", "Meta Options", 0);

    PropertySubGroup subGroup = new PropertySubGroup("Options", "Main Options", 0)
    {
    };

    group.PropertySubGroups.Add(subGroup);

    int order = 0;
    subGroup.Properties.Add(new Property("Inherit", "Inherit From Parent", PropertyType.Bool, order++, metaData.InheritData.ToString()));
    subGroup.Properties.Add(new Property("Title", "Title", PropertyType.String, order++, metaData.Title));
    subGroup.Properties.Add(new Property("Description", "Description", PropertyType.String, order++, metaData.Description));
    subGroup.Properties.Add(new Property("Keywords", "Keywords", PropertyType.String, order++, metaData.Keywords));

    PropertySubGroup extendedGroup = new PropertySubGroup("Options", "Extended Tags", 1);

    group.PropertySubGroups.Add(extendedGroup);

    foreach (string extendedEntry in _metaConfig.ExtendedEntries)
    {
        extendedGroup.Properties.Add(new Property(extendedEntry, extendedEntry, PropertyType.String, order++, metaData.ExtendedMetaTags.ContainsKey(extendedEntry) ? metaData.ExtendedMetaTags[extendedEntry] : string.Empty));
    }

    StringBuilder sb = new StringBuilder();

    using(StringWriter sw = new StringWriter(sb))
    using (XmlTextWriter tw = new XmlTextWriter(sw))
        group.Serialize(tw);

    return sb.ToString();
}

There is then a call to render the form which does exactly what it says on the tin and some javascript to setup the save methods. This all looks pretty much like a standard widget the only differences to note are.  There is a variable available $context which is made accessible via the IScriptablePlugin implementation, we are not using it but it can be used to define additional context.  In the javascript the save button is set up by injecting it into the context panel at the top.

headerList = $('<ul class="field-list"></ul>')
    .append(
        $('<li class="field-item"></li>')
        .append(
            $('<span class="field-item-input"></span>')
            .append(
                $('<a href="#"></a>')
                .addClass('button save')
                .text(context.resources.save)
            )
        )
    );

$.telligent.evolution.administration.header($('<fieldset></fieldset>').append(headerList));

var button = $('a.save', headerList);

To have my new widget render and appear in the context menu I need to create a plugin that inherits from either IApplicationPanel or IContainerPanel, in my case it needs both as we want to allow meta data to be configured all the way down to a single application page. These interfaces both define several methods but the most important one is the public string GetViewHtml(Guid type, Guid id).  This is the method that is called when the panel is displayed and this is where we need to display our Scripted Widget. 

The implementation of IScriptablePlugin follows the servant pattern as with the rest of the architecture.  The method Register allows you to access the specific controller and register your scripted fragment widget.

public void Register(IScriptedContentFragmentController controller)
{
    var options = new ScriptedContentFragmentOptions(_instanceIdentifier)
    {
        CanBeThemeVersioned = false,
        CanHaveHeader = false,
        CanHaveWrapperCss = false,
        CanReadPluginConfiguration = false,
        IsEditable = true,
        
    };
    options.Extensions.Add(new PanelContext());
    
    controller.Register(options);
    
    _controller = controller;
}

Once you have registered the widget to get it to render it's simply the case of calling _controller.RenderContent() you will see in the code there is a dummy context created, I've left this in purely for demonstration it is not used.

Summary

I've missed out the middle part of this project which is how the configuration is stored and managed, the reason for this is because I don't want to repeat things I have already gone over.  I chose to store the configuration in serialized XML and used a CFS filestore.  Each record is uniquely identified by a combination of its id's.  The project hopefully demonstrates some new features of Telligent 9 and also will help improve your sites SEO when used.  I'd love to know if you use this how you get on.

  • Thanks that's interesting to know and we may well change the code to use IExplicitPanel, I think by your description the most likely candidate to use this in the first instance will be the inline content extension.

  • As an alternative to creating a globally-accessible container- and application- related panel, explicit panels can be used to enable direct loading of management UI from code-defined entry points (for example, links). The platform uses explicit panels for functions related to content that could be located anywhere in the site, for example, split/join/move in forums or poll management. An explicit panel can be loaded directly by its URL (provided through its controller). See community.telligent.com/.../51072.iexplicitpanel-plugin-type for more information.

    With an explicit panel, the SEO options could be shown to appropriate users as a persistent UI (a link below the pencil icon, for example) or as link with the header/footer. The link would work on every page to show SEO options via the explicit panel and may more directly identify that the changes affect the URL and not just the container/application.