Ektron

Add content item in a checked-in state using Ektron API

During a recent content migration exercise I had need to bulk insert a lot of content that must be added in a checked-in state, i.e. the content must not have been published previously.

I was unable to find a solution using the Framework API or even the version 7 API (e.g. Ektron.CMS.API.Content.Content) so I had to delve deeper into the past!

The code below actually comes from the Workarea itself and uses the awful Visual Basic Collections (arrrrggghhh) :

using Microsoft.VisualBasic;

public partial class CheckIn : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        // Use a VB Collection to define the content item

        Collection objCol = new Collection();
        {
            objCol.Add(1, "ContentType"); // 1 = HTML / SmartForm
            objCol.Add("<root><MyField>Value goes here</MyField></root>", "ContentHtml"); // Content (XML)
            objCol.Add("my comment", "Comment"); // Comment field
            objCol.Add(0, "ContentID"); // Content ID (zero for new content)
            objCol.Add(2057, "ContentLanguage"); // UK English
            objCol.Add("my teaser", "ContentTeaser"); // Teaser / Summary field
            objCol.Add(72, "FolderID"); // Folder where content should be added to
            objCol.Add("my search text", "SearchText");
            objCol.Add("", "GoLive");
            objCol.Add("", "EndDate");
            objCol.Add(2, "EndDateAction");
            objCol.Add("my content title", "ContentTitle"); // Content title
            objCol.Add(true, "AddToQlink");
            objCol.Add(true, "IsSearchable");
            objCol.Add("7", "MultiXmlID"); // Smartform Id
        }

        long intContentId = 0;

        ContentAPI m_refContApi = new ContentAPI();

        // The old API uses current use by default, so switch to Internal Admin
        // in order to be able to add content.
        // Remember the ID of the original user so we can switch back to that again afterwards.
        long iOrig = m_refContApi.RequestInformationRef.CallerId;
        m_refContApi.RequestInformationRef.CallerId = 
          (long)Ektron.Cms.Common.EkEnumeration.UserGroups.InternalAdmin;
        m_refContApi.ContentLanguage = 2057;

        // Adds content, then checks it in
        intContentId = m_refContApi.EkContentRef.AddNewContentv2_0(objCol);
        m_refContApi.EkContentRef.CheckContentInv2_0(intContentId);

        // Set API back to original user ID
        m_refContApi.RequestInformationRef.CallerId = iOrig;
    }
}

Hopefully this code will save you having to dig through the old workarea code.

Aliased custom 404 error page

This article describes how to create a custom 404 error page in Ektron CMS that will return a HTTP status code 404, use a friendly aliased URL and give content authors the ability to change the content within the page.

Create the template

Create a new ASPX Web Form template called Error404.aspx (or something similar).  Add a ContentBlock to the HTML source view and set the DynamicParameter property to “id”.

Add the template to the CMS in the Settings > Configuration > Template Configuration section in the Ektron workarea.

Now create a new content item in the CMS and assign it to the Error404.aspx template that you created earlier.  Assign an alias to the content item, something like “/page-not-found/”.

Test the page loads of by browsing directly to /page-not-found/.

HTTP status code 404

Now you need to tell the template to return the correct HTTP status code.  At present it is returning a status code of 200 which means “success”.  This will cause search engines to incorrectly believe that your 404 page is actually valid content – in other words, it will be indexed and returned in Google’s results!

To prevent this, go to the code-behind file (this is in C# of course):

    protected void Page_PreRender(object sender, EventArgs e)
    {
        Response.TrySkipIisCustomErrors = true;
        Response.Status = "404 Not Found";
        Response.StatusCode = 404; 
    }

What this does is set the HTTP status code to be 404.  This will indicate to search engines that they have found a missing page so they will not index it.

You can test your page by browsing to an invalid page and seeing if the custom error page is returned.

Enable custom error handling

The next step is to tell .Net that we want to use a custom error page.  This is done within the web.config file like so :

    <system.web>      
      <customErrors mode="On">
          <error statusCode="404" redirect="/page-not-found/" />
      </customErrors>
    <system.web>

This tells .Net to send any “404 page not found” errors to an alias called “/page-not-found/”.

Problems with invalid aliases

You may find that your custom 404 error page isn’t being activated if you try to browse to an invalid alias.  What I have found is that you instead get a nasty server 500 error instead:

Server Error in Application “###”

Internet Information Services 7.5

Error Summary

HTTP Error 500.0 – Internal Server Error

Internal Server Error

Detailed Error Information

Module ManagedPipelineHandler
Notification ExecuteRequestHandler
Handler ExtensionlessUrlHandler-Integrated-4.0
Error Code 0x800703e9
Requested URL http://###:80/wibble/
Physical Path C:\inetpub\wwwroot\###\wibble\
Logon Method Forms
Logon User ###

Eeek!  Where is my custom error page?!?!

I discovered that the problem was due to an incorrect setting in my web.config file.  I needed to remove a handler called “ExtensionlessUrlHandler-Integrated-4.0”, like so :

<system.webServer>
    <handlers>
        <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
    </handers> 
</system.webServer>

(Please note I am using IIS 7)

By removing this HTTP handler it allows the request to be passed through to your custom 404 error page instead.

Add a PageBuilder page using the Ektron API

Pagebuilder pages are an Ektron feature that allow content authors to create flexible web pages by dragging and dropping widgets onto the page.  A widget is a module that can be added to a web page which provides a specific purpose such as an image, video, form or a block of HTML text.

There might be instances in which, during the course of a project, it becomes necessary to add pages into the CMS en masse, for instance, during a content migration excercise.

The following block of code below will allow you to create a Pagebuilder page and add a widget into it.

EDIT: Updated as original code sample wasn’t verbose enough
The code adds an Ektron ContentBlock control to the page.

long folderId = 86; // where to add the page to
var tmpPageData = new PageData(); // temporary object for page config

string controlUrl = "ContentBlock.ascx"; // name of the widget user control in widgets folder
string dropZoneId = "Middle"; // id of the DropZone control on the wireframe

var widget = new WidgetData
{
ControlURL = controlUrl,
ColumnID = 0,
Order = 0,
Minimized = false,
DropID = dropZoneId,
Settings = String.Format("{0}", 30);
};

tmpPageData.Widgets = new List(); // initialise widgetdata list on page
tmpPageData.Widgets.Add(widget); // add ContentBlock widget to page

// Create a drop zone data object
var dropZoneMain = new DropZoneData {DropZoneID = "Middle", Columns = new List()};

// Create a column data object
var columnDataMain = new ColumnData {columnID = 0, width = 100, unit = Units.percent};

// Add the column to the drop zone
dropZoneMain.Columns.Add(columnDataMain);

// Add the drop zone to the page
tmpPageData.Zones = new List {dropZoneMain};
tmpPageData.IsMasterLayout = false;

// Call a method to add the page (see method below)
AddPage(gameToPush.Title, gameToPush.Alias, folderId, 2, tmpPageData, 2057);

So the process above is add widgets to the PageData.Widgets property then add the Drop Zones to the PageData.Zones property (which also includes Columns).

The above code depends upon a method called “AddPage()” which is given below:

private void AddPage(string pageTitle, string alias, long folderId, long wireFrameId,
Ektron.Cms.PageBuilder.PageData pageData, int langId = 2057,
ContentMetaData[] metaData = null)
{

    var pageManager = new PageModel();
    var contentManagerOld = new Ektron.Cms.API.Content.Content();
    pageManager.RequestInformation = contentManagerOld.RequestInformationRef;

    var tmpPageData = new PageData();

    // First create the page entity in the CMS
    pageManager.Create(pageTitle, folderId, alias, langId, wireFrameId, String.Empty, "summary", out tmpPageData, true);

    // Assign widgets and drop zones
    tmpPageData.Widgets = pageData.Widgets;
    tmpPageData.Zones = pageData.Zones;

    // Save to the CMS
    pageManager.CheckIn(tmpPageData);
    pageManager.Publish(tmpPageData);

    // Assign metadata
    if (tmpPageData.pageID > 0)
    {
        if (metaData != null)
        {
            if (metaData.Length > 0)
            {
                contentManagerOld.ContentLanguage = langId;

                foreach (ContentMetaData meta in metaData)
                {
                    contentManagerOld.UpdateContentMetaData(tmpPageData.pageID, meta.Id, meta.Text);
                }
            }
        }
    }
}

At first glance this seems a bit counter-intuitive.  The addition of widgets and drop zones to the page is done separately, whereas you may have thought that widgets go into columns which go into zones which go pages.  In fact the widgets and zones are accessed through different properties as you can see above.

How to configure a multi-lingual site in Ektron

Ektron CMS is a fully multi-lingual product that allows multiple different language versions of a content item to be created.  As of version 8.5 you can also create regions and add custom languages.  Even without the features of v8.5 you can still create multi-lingual sites easily.

When beginning an Ektron project that requires multi-lingual content, first decide whether you are running one domain or multiple domains.  Some organisations like to have one domain such as http://www.theglobalfund.org and within this domain the user can select the language that they wish to view the site in.  Other organisations such as http://www.selecta.co.uk/ provide region/country specific websites which may or may not then provide the option to choose a language.

Single domain multi-lingual sites

Having just one domain is certainly the easiest to implement but is not sufficient for organisations that wish to have a regional presence.  If you have just one domain you would typically provide a language switching control on the page which can use either the CMS LanguageAPI control or some custom API code.

With one domain, many websites will use a language identifier as part of the url; for example http://www.mywebsite.com/en-gb.  This can be achieved using Taxonomy automatic aliasing; the top category in your taxonomy would be “en-gb” and within that would be your information architecture.  Taxonomies themselves are multi-lingual so can be translated.  The “en-gb” taxonomy would be translated into “fr-fr” for French (France) and “fr-ch” for French (Switzerland).  If you also setup your taxonomy folder relationships then content can be auto-added into the taxonomy meaning your multi-lingual aliases are completely automatic!

A language identifier in your alias is useful as it helps to prevent different language versions of content items having the same alias.

Multi-domain multi-lingual sites

Ektron CMS provides a means to manage many websites from a single CMS installation called Multi-site.  Essentially you have more than one IIS website, each with its own web root but only one database.  The reason behind doing this is so that each website can have its own default language.  If you had http://www.mywebsite.de, chances are you want the default language to be German.  The CMS sets the default language of a site within its web.config file.  As you can only have one web.config file per .Net website this means you need separate sites in IIS.

So you would first install the “parent” site – this is the base to which all of your multi-sites are added.  The parent site would hold common content that is shared amongst the other “child” websites.  Each of the child multi-sites are then added using the dedicated Ektron Multi-site installer.  The child multi-sites also have to be configured within the Workarea by right-clicking on the root folder and choosing “Add Site”.

Remember that any content you add within the child site folder in the Workarea will have the domain of the site added to the start of the QuickLink.  This only happens within multi-site folders.  If you do not want this to happen then add the content to a non-multi-site folder, i.e. a folder belonging to the “parent”.

It is your choice whether these sites have their own ASPX templates, controls and PageBuilder wireframes.  Usually the physical files are the same so that organisations have a consistent look and feel across websites even if the domains are slightly different.

For more information on Ektron multi-site see here.

Disable Language Awareness

It is worth mentioning that your Url Aliasing settings.  In the workarea, go to Settings > Configuration > Url Aliasing > Settings and check the “Disable Language Awareness” button.  I find the wording of this setting to be confusing.  When “Disable Language Awareness” is checked you can browse to an alias from a non-default language piece of content.  If it is not checked then the CMS will attempt to load the alias using the default language – so if the alias does not exist in the default language you will get a 404 page not found error.

Time to add content

So you should now have an idea for how to approach setting up Ektron CMS for multi-lingual websites.  You may want to review Ektron’s documentation on multi-lingual content to make sure you have all bases covered; there are other items such as menus, images, taxonomies, etc to consider.

Turn off Ektron CSS aggregation for debugging

Ektron has for some time allowed developers to combine CSS files and Javascript files into single downloads; this is called aggregation. The purpose of this is to improve site performance by reducing the number of downloads in a page.

When aggregation is enabled you can spot it in the source of the HTML looking something like this :

<link id="EktronRegisteredCss" rel="stylesheet" type="text/css" href="/workarea/csslib/ektronCss.ashx?
id=EktronModalCss+css_styles+css_dropdown+css_dropdown_linear
+css_dropdown_linear_columnar+css_dropdown_themes_default+
css_dropdown_themes_default_advance+css_jquery_cluetip" />

Here you can see that a large number of CSS files have been combined into one. The names of the CSS files are evident in the “id” property that is passed to the ektronCss handler, e.g. “css_styles”, “css_jquery_cluetip”.

Although this has many advantages for a public website, it is frustrating when debugging CSS as it makes it hard to know which CSS file contains the elements you are trying to debug.

To easily disable CSS and Javascript aggregation you can modify four properties a configuration file. If you are using up to version 8.0 of Ektron CMS then it is in your web.config file.

<add key="ek_AllowJsAggregation" value="true"/>
<add key="ek_AllowJsMinification" value="true"/>
<add key="ek_AllowCssAggregation" value="true"/>
<add key="ek_AllowCssMinification" value="true"/>

If you are using version 8.5+ of Ektron CMS then you will find the settings in the ektron.cms.framework.ui.config file (located in your web root) :

<!– JavaScript –>
<add name=”AllowJavaScriptRegistration” value=”true” />
<add name=”AllowJavaScriptAggregation” value=”true” />
<add name=”AllowJavaScriptMinification” value=”true” />
<!– Css –>
<add name=”AllowCssRegistration” value=”true” />
<add name=”AllowCssAggregation” value=”true” />
<add name=”AllowCssMinification” value=”true” />
<add name=”AggregatedCssMediaAttribute” value=”all” />

Order search results by metadata field

Ektron’s Framework API gives a great deal of flexibility when it comes to searching, filtering and ordering.  An often needed requirement is to perform a search using the API and the order the results by a metadata value.  Prior to v8.5 and the Framework API this would be difficult and you would often need to do a search, then iterate through the results to get the metadata and then finally do the ordering.

Thankfully as of v8.5 it is much easier!

Firstly you will need the following “using” statements to include the right namespaces:

using Ektron.Cms;
using Ektron.Cms.Search;
using Ektron.Cms.Search.Expressions;
using Ektron.Cms.Search.Compatibility;

I now assume that you have an ASPX page with a search form that includes a submit button. Upon clicking the button a method is executed that will carry out the search.

Next we define the rows that should be returned in the search results:

KeywordSearchCriteria criteria = new KeywordSearchCriteria();

criteria.ReturnProperties = new HashSet(PropertyMappings.ContentSearchProperties);
criteria.ReturnProperties.Add(
SearchMetadataProperty.GetStringProperty(“metaStr”));;

A return column is added here : “metaStr”. This corresponds to a metadata definition called “metaStr” which in my example is of type String. You can also include types Integer, Boolean, Date and Decimal.

Then we say how the results should be ordered:

criteria.OrderBy = new List()
{
new OrderData(
Ektron.Cms.Search.SearchMetadataProperty.GetStringProperty("metaSr"),
OrderDirection.Ascending)
};

This code simply says “order by metaStr, ascending”. Now we add the actual search query:

criteria.QueryText = "ektron";

In reality your search query might be more complicated here, but I want to stick to the script. Finally we perform the search itself:

ISearchManager manager = ObjectFactory.GetSearchManager();
Ektron.Cms.Search.SearchResponseData responseData = manager.Search(criteria);

So there we saw how we can perform a simple search, include metadata in the results and then order by that metadata.

Add an Ektron ecommerce product item

Ektron CMS includes an e-commerce package that fully integrates with the rest of the CMS.  This makes it very easy to combine content with products and manage it all through a single interface.  The Ektron API also includes a dedicated e-commerce section which gives full control over the commerce process.

Below is a snippet of code for adding a new commerce product (also known as a catalogue entry) and then assigning the product to a taxonomy.

This code was written using v8.5 of Ektron CMS.

Ektron.Cms.Commerce.CatalogEntryApi entryApi = new Ektron.Cms.Commerce.CatalogEntryApi();
Ektron.Cms.Framework.Organization.TaxonomyItemManager taxItemMgr = new Ektron.Cms.Framework.Organization.TaxonomyItemManager();

Ektron.Cms.Commerce.KitData pd = new Ektron.Cms.Commerce.KitData();

// Do standard product stuff
pd.Title = title;
pd.FolderId = folderId;
pd.Sku = productId;
pd.LanguageId = 2057;
pd.ProductType = new Ektron.Cms.Commerce.ProductTypeData();
pd.ProductType.Id = 10;
pd.TaxClassId = 1;

// Pricing stuff
decimal listPrice = Convert.ToDecimal(price);
decimal salePrice = Convert.ToDecimal(price);

pd.Pricing = new Ektron.Cms.Commerce.PricingData(826, salePrice, listPrice);

// Do the smartform bit
XmlDocument contentXml = new XmlDocument();

XmlElement xelRoot = contentXml.CreateElement("root");
XmlElement xelProductId = contentXml.CreateElement("productId");
XmlElement xelDesc = contentXml.CreateElement("Description");
XmlElement xelColl = contentXml.CreateElement("Collection");

xelProductId.InnerText = productId;
xelDesc.InnerText = summary;
xelColl.InnerText = collection;

xelRoot.AppendChild(xelEAN);
xelRoot.AppendChild(xelDesc);
xelRoot.AppendChild(xelColl);
contentXml.AppendChild(xelRoot);

pd.Html = contentXml.OuterXml;

// Add the product
entryApi.Add(pd);

// Add to taxonomy categories
Ektron.Cms.TaxonomyItemData taxonomyItem1 = new Ektron.Cms.TaxonomyItemData();
taxonomyItem1.TaxonomyId = taxonomyId;
taxonomyItem1.ItemId = pd.Id;
taxonomyItem1.ItemLanguageId = pd.LanguageId;
taxItemMgr.Add(taxonomyItem1);

Targeted content widget not working

The Ektron Targeted Content widget forms part of the Marketing Optimisation Suite. The widget allows content authors to specify conditions around which different items of content will be presented on the site. You can hook into lots of things such as user properties, the referring URL, cookie values, etc. Based upon these values you can specify conditions like “if user is in the Prospects Group then show them content item A” (or something similar).

If you ever run into a problem with the Ektron Targeted Content Widget whereby it does not allow you to add a condition when in edit mode, it may be that the App_Data folder is missing from the root of the website. The App_Data folde contains a number of data files, some of which allow the geo-location lookup facility to work. The Targeted Content widget makes use of this data and so the folder and its files need to be present.

The files in question are :

  • GeoIPDomain.dat
  • GeoIPOrg.dat
  • GeoLiteCity.dat

These files are present as part of a normal Ektron install (a cms400min site).  I saw this happen recently where a site had not been deployed properly so thought it would be good to share.

Write efficient taxonomy code

Ektron’s Taxonomy feature allows categorisation of content, users and community groups.  The Ektron API allows this grouping of content to be retrieved and manipulated in code.

One common problem I have encountered on projects is poor performing API code due to a lack of understanding of just how the API works.  Your taxonomy API requests should be specific and exact: retrieve only what you need to from the CMS.

The Framework API in version 8.5 does a lot to remedy these problems so I am focusing here on the pre-Framework-API-world.

Below is some example code (edited for brevity) from a recent project that performs a taxonomy request, looking for content items:


Ektron.Cms.TaxonomyRequest taxonomyRequest = new TaxonomyRequest();
taxonomyRequest.TaxonomyLanguage = CultureInfo.CurrentCulture.LCID;
taxonomyRequest.TaxonomyId = id;
taxonomyRequest.IncludeItems = true;
Ektron.Cms.API.Content.Taxonomy taxonomyApi = new Ektron.Cms.API.Content.Taxonomy();
taxonomy = taxonomyApi.LoadTaxonomy(ref taxonomyRequest);

On face value there is nothing wrong with this code. However, if I also state that the purpose of this call was just to get the Taxonomy Path you might then spot a few issues immediately.

Firstly, the “IncludeItems” property is set to “true”. This means the API will retrieve all of the content items in addition to the taxonomy information. A taxonomy could contain tens or hundreds of items, massively increasing the amount of data that the CMS has to retrieve.

Secondly, the “Depth” property has not been set. The Taxonomy Depth property specifies how “deep” the request should go, i.e. how many levels of sub-categories should be pulled back. By not specifying this you risk getting lots and lots of categories retrieve. Combine this with the first problem and you can easily see how slow this could become!

Thirdly, as we only want to get information about the taxonomy (and not the items in the taxonomy) we should instead use taxonomyApi.ReadTaxonomy rather than taxonomyApi.LoadTaxonomy. The former performs less queries as it ignores whatever is in the categories.

There have been several projects I have worked on where there were complaints that the CMS was slow. Quite often the problem was tracked down to a poorly written taxonomy request like this one.

Create page base classes for Ektron templates

A quick time saver tip today… On my projects I like to create base classes from which items such as ASPX templates and ASCX user controls can inherit.  Base classes are Class Files that are placed into the App_Code folder.  This allows me to place commonly used code into one place that is then made available to all the templates and/or controls in the project.

For example, I have a C# class called “BasePage” which has a public property of “ContentId”.  This property holds the Ektron Content ID for the current paqe.  The Init event of the BasePage class will look for “id” or “ekfrm” or “pageid” on the querystring and use the value to populate the ContentId property.  This means that every ASPX template or ASCX control has a property called “ContentId” from which you can quickly get the content id.

Prior to this I found I was always writing code to get the content Id from the querystring (typically an Ektron template takes in a single content Id).

With these classes in place you will soon, over time, continue to add to them and ultimately this will save a little bit of time here and there.