Sitemap and Robots Generator for Commerce Episerver CMS

Sitemap and Robots Generator for Commerce Episerver CMS

This blog post is a continuation to the blog post about the Sitemap and Robots Generator for Episerver CMS, where you can find how to install and configure the plugin. In here, we are going to explain how we can configure the plugin to add commerce products and the images belonging to those products to the sitemap. In addition, we will use the latest version available of the plugin which at the time of writing is 1.0.0.6. You can find the plugin developed by Verndale in this link. If you cannot find the latest version in the episerver feed please use the nuget feed instead.

Install-Package Verndale.Sitemap.Robots.Generator -Version 1.0.0.6

As soon as you have your plugin installed and configured properly. We will start creating the classes needed to make it work with Episerver Commerce.

First, we will create or use the image media data class used for your images in commerce. An example of such a class is shown above. Please pay special attention that the class inherits from Commerce Image class instead of Media Data directly.

using EPiServer.Commerce.SpecializedProperties;
using EPiServer.DataAnnotations;
using EPiServer.Framework.DataAnnotations;

namespace Example.Shop.Features.Media.Models
{
    [ContentType(GUID = "0A89E464-56D4-449F-AEA8-2BF774AB8730")]
    [MediaDescriptor(ExtensionString = "jpg,jpeg,jpe,ico,gif,bmp,png")]
    public class ImageFile : CommerceImage
    {
        public virtual string Caption { get; set; }
        public virtual string Title { get; set; }
        public virtual string Credits { get; set; }
    }
}

Then, we will need a set of extension methods which will be capable of get all asset images from a product. We will write all those methods inside a static class which we can use later in other places. Again, pay special attention to the two episerver group names that are set as constants in the code. If you use different groups or you have more groups please add them accordingly.

using System.Collections.Generic;
using System.Linq;
using EPiServer.Commerce.Catalog.ContentTypes;
using System;
using EPiServer.Commerce.SpecializedProperties;
using EPiServer.Core;

namespace Example.Shop.Features.Shared.Extensions
{
    public static class AssetContainerExtensions
    {
        private const string EpiserversDefaultGroupName = "default";
        private const string EpiserversPrimaryGroupName = "primary";

        public static IEnumerable<ContentReference> GetAssetLinks(this IAssetContainer assetContainer)
        {
            var images = new List<ContentReference>();
            var primaryImage = assetContainer.GetAssetLinks(EpiserversPrimaryGroupName).FirstOrDefault();

            if (primaryImage != null)
            {
                images.Add(primaryImage);

                // User can include more than one primary image (which is a content issue)
                images.AddRange(assetContainer.GetAssetLinks(EpiserversPrimaryGroupName)
                    .Where(p => p.ID != primaryImage.ID));
            }

            images.AddRange(assetContainer.GetAssetLinks(EpiserversDefaultGroupName));

            return images;
        }

        public static IEnumerable<ContentReference> GetAssetLinks(this IAssetContainer assetContainer, string groupName)
        {
            if (groupName == null) throw new ArgumentNullException(nameof(groupName));
            return assetContainer.GetAssetLinks(new[] { groupName });
        }

        public static IEnumerable<ContentReference> GetAssetLinks(this IAssetContainer assetContainer, string[] groupNames)
        {
            if (groupNames == null) throw new ArgumentNullException(nameof(groupNames));
            if (assetContainer.CommerceMediaCollection == null) return new List<ContentReference>();

            IEnumerable<CommerceMedia> media = assetContainer.CommerceMediaCollection;

            if (groupNames.Length > 0)
            {
                media = media.Where(
                    x => groupNames.Any(
                        groupName => x.GroupName.Equals(groupName, StringComparison.InvariantCultureIgnoreCase)));
            }

            return media
                .Select(m => m.AssetLink)
                .Where(x => x != null)
                .ToList();
        }
    }
}

Finally, we will create a sitemap extend class which will override the behavior of some of the configuration when the plugin generates the sitemap.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml;
using EPiServer;
using EPiServer.Commerce.Catalog.ContentTypes;
using EPiServer.Core;
using EPiServer.Web;
using Example.Shop.Features.Media.Models;
using Example.Shop.Features.Shared.Extensions;
using Mediachase.Commerce.Catalog;
using Verndale.Sitemap.Robots.Generator.Sitemap;

namespace Example.Shop.Infrastructure
{
    public class CommerceSitemapExtend : ISitemapExtend
    {
        private readonly ReferenceConverter _referenceConverter;
        private readonly IContentRepository _contentRepository;
        private readonly IContentLoader _contentLoader;

        public CommerceSitemapExtend(ReferenceConverter referenceConverter,
            IContentRepository contentRepository,
            IContentLoader contentLoader)
        {
            _referenceConverter = referenceConverter;
            _contentRepository = contentRepository;
            _contentLoader = contentLoader;
        }

        // Adding commerce pages here
        public List<IContent> AddOtherNotCmsPages()
        {
            var rootContentReference = _referenceConverter.GetRootLink();
            IList<ContentReference> catalog = _contentRepository.GetDescendents(rootContentReference).ToList();

            var pages = new List<IContent>();
            pages.AddRange(catalog
                .Select(x => _contentRepository.Get<IContent>(x))
                .Where(x => !(x is VariationContent) && !(x is CatalogContent)));

            return pages;
        }

        // Adding images that belong to commerce pages
        public void AddImagesForOtherNotCmsPages(IContent item, XmlDocument doc,
            XmlElement urlNode, SiteDefinition site)
        {
            if (item is EntryContentBase entryContentBase)
            {
                var assets = entryContentBase.GetAssetLinks()?.ToList();

                if (assets != null)
                {
                    foreach (var asset in assets)
                    {
                        XmlNode imgNode = doc.CreateElement("image", "image", SitemapManagerConfiguration.XmlNsImage);
                        urlNode.AppendChild(imgNode);

                        var imageFile = _contentLoader.Get<ImageFile>(asset);

                        XmlNode imgLocNode = doc.CreateElement("image", "loc", SitemapManagerConfiguration.XmlNsImage);
                        imgNode.AppendChild(imgLocNode);
                        imgLocNode.AppendChild(doc.CreateTextNode(SitemapManager.GetItemUrl(asset, site)));

                        XmlNode imgCaptionNode = doc.CreateElement("image", "caption", SitemapManagerConfiguration.XmlNsImage);
                        imgNode.AppendChild(imgCaptionNode);
                        imgCaptionNode.AppendChild(doc.CreateTextNode(imageFile.Caption));

                        XmlNode imgTitleNode = doc.CreateElement("image", "title", SitemapManagerConfiguration.XmlNsImage);
                        imgNode.AppendChild(imgTitleNode);
                        imgTitleNode.AppendChild(doc.CreateTextNode(string.IsNullOrEmpty(imageFile.Title)
                            ? imageFile.Credits
                            : imageFile.Title));
                    }
                }
            }
        }

        // Alternative when priority is not set
        public void CreatePriorityPropertyDifferently(XmlDocument doc, XmlElement urlNode, string url)
        {
            var priorityNode = doc.CreateElement("priority");
            urlNode.AppendChild(priorityNode);
            priorityNode.AppendChild(doc.CreateTextNode(GetPriority(url)));
        }

        // Alternative when change frequency is not set
        public void CreateChangeFrequencyPropertyDifferently(XmlDocument doc, XmlElement urlNode)
        {
            var chgNode = doc.CreateElement("changefreq");
            urlNode.AppendChild(chgNode);
            chgNode.AppendChild(doc.CreateTextNode("weekly"));
        }

        private static string GetPriority(string url)
        {
            var depth = new Uri(url).Segments.Length - 1;

            return Math.Max(1.0 - (depth / 10.0), 0.5).ToString(CultureInfo.InvariantCulture);
        }
    }
}

There are several important things to explain about the code above, so we will explain bit by bit. First, the class must inherit the interface ISitemap Extend.

public class CommerceSitemapExtend : ISitemapExtend

This will require to implement the following methods

 List<IContent> AddOtherNotCmsPages();

 void AddImagesForOtherNotCmsPages(
      IContent item,
      XmlDocument doc,
      XmlElement urlNode,
      SiteDefinition site);

void CreatePriorityPropertyDifferently(XmlDocument doc, XmlElement urlNode, string url);

 void CreateChangeFrequencyPropertyDifferently(XmlDocument doc, XmlElement urlNode);

The table below explains briefly what each method is for.

MethodDescription
AddOtherNotCmsPagesAllows to add commerce pages or any other not cms page to the sitemap
AddImagesForOther
NotCmsPages
Allows the not cms pages added in the method above to include images
CreatePriorityProperty
Differently
Overrides the default logic of the priority attribute for each page in the sitemap. (Default behavior is to take the value from the property of the page if there is a value)
CreateChangeFrequency
PropertyDifferently
Overrides the default logic of the frequency attribute for each page in the sitemap. (Default behavior is to take the value from the property of the page if there is a value)

The method AddOtherNotCmsPages will search for pages you want to include to the sitemap, in this case product pages, and return a list of those items. If you do not want to override this method, you can return an empty list.

        // Adding commerce pages here
        public List<IContent> AddOtherNotCmsPages()
        {
            var rootContentReference = _referenceConverter.GetRootLink();
            IList<ContentReference> catalog = _contentRepository.GetDescendents(rootContentReference).ToList();

            var pages = new List<IContent>();
            pages.AddRange(catalog
                .Select(x => _contentRepository.Get<IContent>(x))
                .Where(x => !(x is VariationContent) && !(x is CatalogContent)));

            return pages;
        }

The next method AddImagesForOtherNotCmsPages will check if the page is a commerce product and if that is the case it will get all the image assets of the product using the extension method we created before and for each asset we will add a new xml node image with the attributes: loc, caption and title. If you do not want to modify this method, you can leave this method empty.

        // Adding images that belong to commerce pages
        public void AddImagesForOtherNotCmsPages(IContent item, XmlDocument doc,
            XmlElement urlNode, SiteDefinition site)
        {
            if (item is EntryContentBase entryContentBase)
            {
                var assets = entryContentBase.GetAssetLinks()?.ToList();

                if (assets != null)
                {
                    foreach (var asset in assets)
                    {
                        XmlNode imgNode = doc.CreateElement("image", "image", SitemapManagerConfiguration.XmlNsImage);
                        urlNode.AppendChild(imgNode);

                        var imageFile = _contentLoader.Get<ImageFile>(asset);

                        XmlNode imgLocNode = doc.CreateElement("image", "loc", SitemapManagerConfiguration.XmlNsImage);
                        imgNode.AppendChild(imgLocNode);
                        imgLocNode.AppendChild(doc.CreateTextNode(SitemapManager.GetItemUrl(asset, site)));

                        XmlNode imgCaptionNode = doc.CreateElement("image", "caption", SitemapManagerConfiguration.XmlNsImage);
                        imgNode.AppendChild(imgCaptionNode);
                        imgCaptionNode.AppendChild(doc.CreateTextNode(imageFile.Caption));

                        XmlNode imgTitleNode = doc.CreateElement("image", "title", SitemapManagerConfiguration.XmlNsImage);
                        imgNode.AppendChild(imgTitleNode);
                        imgTitleNode.AppendChild(doc.CreateTextNode(string.IsNullOrEmpty(imageFile.Title)
                            ? imageFile.Credits
                            : imageFile.Title));
                    }
                }
            }
        }

The next method CreatePriorityPropertyDifferently will get the priority for each page using the URL segments instead of using the property priority from the page. You can keep this method empty if you do not want to override the default behaviour.

        // Alternative when priority is not set
        public void CreatePriorityPropertyDifferently(XmlDocument doc, XmlElement urlNode, string url)
        {
            var priorityNode = doc.CreateElement("priority");
            urlNode.AppendChild(priorityNode);
            priorityNode.AppendChild(doc.CreateTextNode(GetPriority(url)));
        }

        private static string GetPriority(string url)
        {
            var depth = new Uri(url).Segments.Length - 1;

            return Math.Max(1.0 - (depth / 10.0), 0.5).ToString(CultureInfo.InvariantCulture);
        }

The method CreateChangeFrequencyPropertyDifferently will change the changefreq attribute of each element to weekly instead of using the property from the page. You can keep this method empty if you do not want to override the default behaviour.

    // Alternative when change frequency is not set
    public void CreateChangeFrequencyPropertyDifferently(XmlDocument doc, XmlElement urlNode)
    {
        var chgNode = doc.CreateElement("changefreq");
        urlNode.AppendChild(chgNode);
        chgNode.AppendChild(doc.CreateTextNode("weekly"));
    }

Finally, you must add the just implemented extender to replace the implementation provided by the plugin. To do this you must add the following code wherever you have your structure map initializations.

if (context.Services.Contains(typeof(ISitemapExtend)))
{
    context.Services.RemoveAll(typeof(ISitemapExtend));
    context.Services.AddTransient<ISitemapExtend, CommerceSitemapExtend>();
}

The final result of a product in the sitemap with its corresponding images will look similar to this

That is it. If you have any questions or suggestions please let me know in the comments. I hope this blog post can help someone and as always keep learning !!!

Written by:

Jorge Cardenas

Developer with several years of experience who is passionate about technology and how to solve problems through it.

View All Posts

1 COMMENT

Leave a Reply