Problem: With a large number of content editors, publishing content across a large site. Site admins wanted to be alerted when certain areas of the site get updated. The client did not want to use workflows, as they felt this slowed the content creation process.

Solution: Custom Publishing Alerts, with hooks into a couple of Sitecore events and some tinkering and we are there.

Built and tested against Sitecore v9/v9.3, but should work with other versions, after some tweaks.


let’s get started, firstly we need to allow content editors to setup an alert for a specific area of the site, so we created a new template “Publishing Alert Options”

Fields:

  • Email To (Single Line Text) – email address(s) that alert will be sent to

  • Root (droptree) – root item to monitor for publishes to trigger alert

  • IncludedTemplates (TreeList) with data source set to your page template root e.g. /sitecore/templates/User Defined/Ark/Project/Website/Page Types- templates that we care about, this could be set to be just news articles or any other page type, if left empty alert will trigger for all templates.
Create a root item in Sitecore to hold the publishing alert option item, we use “/sitecore/content/Global/Publishing Alerts”, and add the new template to its insert options to make it easier for editors.

Create a new publishing alert item to allow for testing

Events: we need to hook into 2 sitecore events

  • publish:itemProcessed – triggered when an item has been published
  • publish:complete – triggered when publish job has finished
so, first event is publish:itemProcessed, we need to keep a track of items that have been published, as Sitecore doesn’t do this by default.
 
Required custom code: – this is not 100% productionised code, so please use with caution
 
        public static ConcurrentDictionary<string, List> publishedItems = new ConcurrentDictionary<string, List>();

        public void ItemProcessed(object sender, System.EventArgs args)
        {
            //get event args
            ItemProcessedEventArgs evntArgs = (ItemProcessedEventArgs)args;

            List tmpPublishedItems;
            //check to see if we already have a dictionary entry for this publish job, using the RecoveryId as an identifier, if we dont have one, create one, and setup new List
            if (!publishedItems.TryGetValue(evntArgs.Context.PublishOptions.RecoveryId.ToString(), out tmpPublishedItems))
            {
                publishedItems.AddOrUpdate(evntArgs.Context.PublishOptions.RecoveryId.ToString(), new List(), (key, oldValue) => new List());
                tmpPublishedItems = new List();
            }
            //add published item ID to our collection
            tmpPublishedItems.Add(evntArgs.Context.ItemId);
            publishedItems.AddOrUpdate(evntArgs.Context.PublishOptions.RecoveryId.ToString(), tmpPublishedItems, (key, oldValue) => tmpPublishedItems);
        }

In the above code we are grabbing the ID of the item being published and adding to a thread-safe collection to be used later

now we have a collection of the item ids that have been published, we now need to check if they relate to any setup Publishing Alerts, here steps in the publish:complete event

        public void PublishComplete(object sender, System.EventArgs args)
        {
            var sitecoreArgs = args as Sitecore.Events.SitecoreEventArgs;
            if (sitecoreArgs == null)
                return;

            Database master = Sitecore.Configuration.Factory.GetDatabase("master");
            //get publisher, so we can then get its recovery ID to check our collection of published items
            IEnumerable publisherList = sitecoreArgs.Parameters[0] as IEnumerable;
            DistributedPublishOptions publisher = publisherList.ToList()[0];
            List pubItemIds = new List();
            //see if we have any items in our collection for this publish job recovery ID
            if (publishedItems.TryGetValue(publisher.RecoveryId.ToString(), out pubItemIds))
            {
                //check we are using a content DB, not core etc.
                Database CntxtDb;

                if (Sitecore.Context.Database != null && Sitecore.Context.Database.Name != "core")
                {
                    CntxtDb = Sitecore.Context.Database;
                }
                else
                {
                    CntxtDb = Sitecore.Configuration.Factory.GetDatabase("web");
                }
                if (pubItemIds != null && pubItemIds.Count > 0)
                {
                    pubItemIds = pubItemIds.Distinct().ToList();
                    List lstPublished = new List();
                    List lstRemoved = new List();
                    foreach (ID itemId in pubItemIds)
                    {
                        //get item from context DB
                        Item i = CntxtDb.GetItem(itemId);
                        //if context db is web, this is an update/create
                        if (CntxtDb.Name.ToLower() == "web")
                        {
                            if (i != null)
                            {
                                lstPublished.Add(i);
                            }
                            else
                            {
                                //if item is null, it is a delete
                                lstRemoved.Add(itemId);
                            }
                        }
                    }
                    //pass to function, and sub-task to let other events fire without any slowdown
                    System.Threading.Tasks.Task.Run(() => ProcessPublishingAlerts(lstPublished, lstRemoved));
                }
            
        }

the above event handler gathers the processed item IDs together and works out if it is an update or delete.

then passes the results to the underlying ProcessPublishingAlert function,  which does the actual email send

        public string Username = Sitecore.Configuration.Settings.GetSetting("MailServerUserName", "UserName");
        public string Password = Sitecore.Configuration.Settings.GetSetting("MailServerPassword", "Password");
        public string DefaultFromAddress = Sitecore.Configuration.Settings.GetSetting("MailDefaultFromAddress", "from@address.com");
        public string SmtpHost = Sitecore.Configuration.Settings.GetSetting("MailServer", "smtp.yourserver.com");
        public string DefaultPort = Sitecore.Configuration.Settings.GetSetting("MailServerPort", "25");
        public string alertRootItemId = Sitecore.Configuration.Settings.GetSetting("PublishingAlerts.RootFolderId", "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}");
        public string emailSubject = Sitecore.Configuration.Settings.GetSetting("PublishingAlerts.EmailSubject", "Publishing Alert");

        public void ProcessPublishingAlerts(List insertedUpdatedItems, List removedItems, bool isAutoPublish = false)
        {
            try
            {
                //get alert options from sitecore
                Database master = Sitecore.Configuration.Factory.GetDatabase("master");
                if (master != null)
                {
                    Item alertRoot = master.GetItem(alertRootItemId);
                    if (alertRoot != null)
                    {
                        List lstAlertOptions = new List();
                        foreach (Item alertItem in alertRoot.GetChildren())
                        {
                            if (alertItem != null)
                            {
                                lstAlertOptions.Add(alertItem);
                            }
                        }

                        foreach (Item alertOpts in lstAlertOptions.Where(t => !string.IsNullOrEmpty(t["Email_To"])))
                        {
                            //get templates we are bothered with
                            Sitecore.Data.Fields.MultilistField templatesToInclude = alertOpts.Fields["IncludedTemplates"];
                            //get the IDs
                            List tmpsToAlertOn = templatesToInclude.GetItems().Select(t => t.ID).ToList();
                            //filter new/updated items by the root of this alert
                            List lstTempPublished = insertedUpdatedItems.Where(y => y.Paths.LongID.ToLower().Contains(alertOpts["Root"].ToString())).ToList();
                            //as these have been removed from web, we need to get them form master to check templates etc.
                            List lstTempRemoved = new List();
                            foreach (ID id in removedItems)
                            {
                                Item tmp = master.GetItem(id);
                                if (tmp != null)
                                {
                                    lstTempRemoved.Add(tmp);
                                }
                            }
                            //filter removed items by the root of this alert
                            List lstRemove = lstTempRemoved.Where(y => y.Paths.LongID.ToLower().Contains(alertOpts["Root"].ToString())).ToList();
                            //if we have included templates filter the list by them, if not just process all
                            if (tmpsToAlertOn.Count > 0)
                            {
                                lstTempPublished = lstTempPublished.Where(y => tmpsToAlertOn.Contains(y.TemplateID)).ToList();
                                lstRemove = lstRemove.Where(y => tmpsToAlertOn.Contains(y.TemplateID)).ToList();
                            }
                            //start building email body
                            StringBuilder emailBody = new StringBuilder();
                            if (lstTempPublished.Count > 0)
                            {
                                emailBody.Append("Created/Updated:");
                                foreach (Item pub in lstTempPublished)
                                {
                                    emailBody.Append(Environment.NewLine + pub.Paths.FullPath);
                                }
                            }

                            if (lstRemove.Count > 0)
                            {

                                emailBody.Append(Environment.NewLine + "Removed (no longer in web database):");
                                foreach (Item rem in lstRemove)
                                {
                                    emailBody.Append(Environment.NewLine + rem.Paths.FullPath);
                                }

                            }

                            if (!string.IsNullOrEmpty(emailBody.ToString()) && !string.IsNullOrEmpty(alertOpts["Email_To"]))
                            {
                                //send email
                                MailMessage email = new MailMessage();
                                email.Body = emailBody.ToString();
                                email.Subject = emailSubject;
                                if (isAutoPublish)
                                {
                                    email.Subject = emailSubject + " - Auto Publish";
                                }
                                email.From = new MailAddress(DefaultFromAddress);
                                email.To.Add(new MailAddress(DefaultFromAddress));
                                email.Bcc.Add(alertOpts["Email_To"]);
                                using (var smtpClient = new SmtpClient(SmtpHost, System.Int32.Parse(DefaultPort)))
                                {
                                    smtpClient.EnableSsl = true;
                                    smtpClient.UseDefaultCredentials = false;
                                    smtpClient.Credentials = new NetworkCredential(Username, Password);
                                    smtpClient.Send(email);
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Sitecore.Diagnostics.Log.Error("Publish Alert Error:" + ex.Message, ex, this);
            }
        }

the above code get the Publishing Alert Items from Sitecore, iterates over them and generates/sends the email as required.

Config required, use a patch file

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:x="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:search="http://www.sitecore.net/xmlconfig/search/" xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform" xmlns:localenv="http://www.sitecore.net/xmlconfig/localenv/"  xmlns:messagingTransport="http://www.sitecore.net/xmlconfig/messagingTransport/">
  <sitecore>
    <events timingLevel="custom">
      <event name="publish:itemProcessed" help="Receives an argument of type ItemProcessedEventArgs (namespace: Sitecore.Publishing.Pipelines.PublishItem)">
        <handler type="Ark.SC.PublishingAlerts, Ark.SC.PublishingAlerts" method="ItemProcessed" />
      </event>
      <event name="publish:complete">
        <handler type="Ark.SC.PublishingAlerts, Ark.SC.PublishingAlerts" method="PublishComplete" />
      </event>
    </events>
    <settings>
      <setting name="PublishingAlerts.RootFolderId" value="{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" />
      <setting name="PublishingAlerts.EmailSubject" value="Website Publishing Alert" />
      <setting name="MailServerUserName" value="UserName" />
      <setting name="MailServerPassword" value="Password" />
      <setting name="MailDefaultFromAddress" value="from@address.com" />
      <setting name="MailServer" value="smtp.yourserver.com" />
      <setting name="MailServerPort" value="25" />
    </settings>
  </sitecore>
</configuration>

Boom, we are done, when an item is published which matches the root / templates set in our Publishing Alert item, an email is sent with the required information.

further development could extend to include what changed between published versions (if versioning is enabled), details of the user who triggered the publish, counts of numbers processed etc. all depends on what you want to be alerted with.

if you need help with any Sitecore issues, please get in touch