Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
Experience Sitecore! | All posts by martin

Experience Sitecore!

Martin Miles on Sitecore

Configuring role and localenv variables in Sitecore 9 - PowerShell way

In Sitecore 9, one can set up an instance into a specific 'role' that also takes predefined configurations. Further ahead, you may keep your numerous custom configurations next to each other targeting different 'roles' - that avoids clumsy config pathing and keeps settings functionally together in order to simplify maintenance. There is also 'localenv' setting that helps you to distinguish various groups of servers from the same role, but residing in the different environments. Not a nice way of changing role and adding environment by simply replacing a string occurrence:
(Get-Content Web.config).replace('    <add key="role:define" value="Standalone" />', '<add key="role:define" value="ContentManagement" /><add key="localenv:define" value="UAT" />') | Set-Content Web.config

Just a string replacement? Errrghh.... Not a nice solution! Let's make it better, by relying on XML namespace (thanks, Neil):
#$webConfigPath = "С:\path\to\your\Web.config"
#$localEnvName = "UAT"

$RptKeyFound=0;
$xml = (get-content $webConfigPath) -as [Xml];              # Create the XML Object and open the web.config file 
$root = $xml.get_DocumentElement();                         # Get the root element of the file

foreach($item in $root.appSettings.add)                     # loop through the child items in appsettings 
{ 
  if($item.key -eq "localenv:define")                       # If the desired element already exists 
    { 
      $RptKeyFound=1;                                       # Set the found flag 
    } 
}

if($RptKeyFound -eq 0)                                      # If the desired element does not exist 
{ 
    $newEl=$xml.CreateElement("add");                       # Create a new Element 
    $nameAtt1=$xml.CreateAttribute("key");                  # Create a new attribute "key" 
    $nameAtt1.psbase.value="localenv:define";               # Set the value of "key" attribute 
    $newEl.SetAttributeNode($nameAtt1);                     # Attach the "key" attribute 
    $nameAtt2=$xml.CreateAttribute("value");                # Create "value" attribute  
    $nameAtt2.psbase.value="$localEnvName";                 # Set the value of "value" attribute 
    $newEl.SetAttributeNode($nameAtt2);                     # Attach the "value" attribute 
    $xml.configuration["appSettings"].AppendChild($newEl);  # Add the newly created element to the right position
}

$xml.Save($webConfigPath)                                   # Save the web.config file
This same approach can be taken for any other XML-based transforms and replacements.

Separating content items from definition using Unicorn's NewItemsEvaluator

The vast majority of my readers are already familiar with Unicorn (if not - please take your time to familiarize with it by this link) and understand the principles of how it works. In few words, it serializes your Sitecore items into text files so that you can store them under source control along with the rest of your solution code.Then you can sync these files back into Sitecore, either manually from its admin page, or automatically by PowerShell or deployment script on CI / CD pipeline.  If the item has was changed, Unicorn automatically modifies its serialization file (so that changed file goes to source control to all other developers) and the reverse - newer items from source control will update those in Sitecore. So far, so good.

Problem: your solution infrastructure has numerous environments from lower (Dev) to higher (Prod) where the actual site is running. But not just the site itself - Sitecore also is there. Your content editors will be using Sitecore on prod, bringing plenty changes. But with next deployment all that new content will be automatically overwritten by older serialized items. What is good - now we have Helix principles, that classifies two types of Sitecore items - definition items to be passed along with the rest of solution code and actual content item. For Content items source of truth is Production, while for definition items source of truth is Development. But how do we actually define and set up content items?

Solution: the best automated approach also comes with Unicorn, however almost not documented and very little people know about it. It is called NewItemsEvaluator. Let's take a look at how it works:

By default, Unicorn predicate runs with Master Evaluator, which always overwrites Sitecore item with whatever comes from serialization. Because of Master Evaluator, content items on production will be overwritten by serialization with each deployment's sync unless actions are taken. NewItemsEvaluator also creates an item in Sitecore, but only if that item does not (yet) exist in the target environment. But if the item with such ID already exists there - it won't be affected and overwritten.

Firstly, we need to define what content items are. That typically will be all the items underneath /sitecore/YourProjectName/Home including Home item itself. Also, that will be all items within /sitecore/media library/Project/YourProjectName and items within /sitecore/content/YourProjectName/Global including Global item itself and its folder-based children that have insert options assigned.

Secondly, we need to create a new unicorn predicate for those content items identified. That predicate should be driven by New Item Evaluator:

<evaluator type="Unicorn.Evaluators.NewItemOnlyEvaluator, Unicorn" singleInstance="true" />
Thirdly, we need to exclude content items from the default website predicate driven by Master Evaluator. Luckily, Unicorn comes with handy exclusion syntax (more examples can be found at this link):
<include name="Some children" database="master" path="/sitecore/content/YourWebsite">
  <exclude children="true">
    <except name="Settings" />
    <except name="Global" />
  </exclude>
</include>

Finally, your website project layer serialization configuration file will have two predicate configurations similar to one below:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:localenv="http://www.sitecore.net/xmlconfig/localenv/">
    <sitecore role:require="Standalone or ContentManagement">
        <unicorn>
            <configurations>
                <configuration 
                    name="Project.YourProjectName.Website" 
                    description="YourProjectName content" 
dependencies="Foundation.*,Feature.*,Project.Common" extends="Helix.Project"> <predicate> <include name="Project.YourProjectName.Layouts" database="master" path="/sitecore/layout/layouts/Project/YourProjectName" />
<include name="Project.YourProjectName.PlaceholderSettings" database="master" path="/sitecore/layout/placeholder settings/Project/YourProjectName" />
<include name="Project.YourProjectName.Content" database="master" path="/sitecore/content/YourProjectName">
<exclude path="/sitecore/content/YourProjectName/Home" />
<exclude childrenOfPath="/sitecore/content/YourProjectName/Global/Button Styles" />
<exclude childrenOfPath="/sitecore/content/YourProjectName/Global/Font Styles" />
<exclude childrenOfPath="/sitecore/content/YourProjectName/Global/Image Container Css" />
<exclude childrenOfPath="/sitecore/content/YourProjectName/Global/Landing Css Classes" />
<exclude childrenOfPath="/sitecore/content/YourProjectName/Global/Links/Footer Menu" />
<exclude childrenOfPath="/sitecore/content/YourProjectName/Global/Links/Header Menu" />
<exclude childrenOfPath="/sitecore/content/YourProjectName/Global/Links/Social Media" />
</include> <include name="Project.YourProjectName.Media" database="master" path="/sitecore/media library/Project/YourProjectName">
<exclude children="true" /> </include> <include name="Project.YourProjectName.ExperienceEditor" database="core" path="/sitecore/client/Applications/ExperienceEditor/Pipelines/InitializePageEdit" />
</predicate> <!-- roles and user data stores skipped here --> </configuration> <configuration name="Project.YourProjectName.Website.AuthoredContent"
description="Sample site content (New Item Provider)" dependencies="Foundation.*,Feature.*,Project.YourProjectNamev.Website"
patch:after="configuration[@name='Project.YourProjectName.Website']"
extends="Helix.Project"> <targetDataStore physicalRootPath="$(sourceFolder)\$(layer)\$(module)\serialization\Content" type="Rainbow.Storage.SerializationFileSystemDataStore, Rainbow" useDataCache="false" singleInstance="true" /> <evaluator type="Unicorn.Evaluators.NewItemOnlyEvaluator, Unicorn" singleInstance="true" /> <dataProviderConfiguration enableTransparentSync="false" type="Unicorn.Data.DataProvider.DefaultUnicornDataProviderConfiguration, Unicorn" /> <predicate type="Unicorn.Predicates.SerializationPresetPredicate, Unicorn" singleInstance="true"> <include name="Project.YourProjectName.Home" database="master" path="/sitecore/content/YourProjectName/Home" />
<include name="Project.YourProjectName.Global.ButtonStyles" database="master" path="/sitecore/content/YourProjectName/Global/Button Styles" />
<include name="Project.YourProjectName.Global.FontStyles" database="master" path="/sitecore/content/YourProjectName/Global/Font Styles" />
<include name="Project.YourProjectName.Global.HeroCss Classes" database="master" path="/sitecore/content/YourProjectName/Global/Hero Css Classes" />
<include name="Project.YourProjectName.Global.HeroMobile Background Image" database="master" path="/sitecore/content/YourProjectName/Global/Hero Mobile Background Image" />
<include name="Project.YourProjectName.Global.ImageContainerCss" database="master" path="/sitecore/content/YourProjectName/Global/Image Container Css" />
<include name="Project.YourProjectName.Global.LandingCssClasses" database="master" path="/sitecore/content/YourProjectName/Global/Landing Css Classes" />
<include name="Project.YourProjectName.Global.Recruiters" database="master" path="/sitecore/content/YourProjectName/Global/Recruiters" />
<include name="Project.YourProjectName.Global.HigllightCssClasses" database="master" path="/sitecore/content/YourProjectName/Global/Higllight Css Classes" />
<include name="Project.YourProjectName.Global.TabsCssClasses" database="master" path="/sitecore/content/YourProjectName/Global/Tabs Css Classes" />
<include name="Project.YourProjectName.Global.Leaders" database="master" path="/sitecore/content/YourProjectName/Global/Leaders" />
<include name="Project.YourProjectName.Global.Links.FooterMenu" database="master" path="/sitecore/content/YourProjectName/Global/Links/Footer Menu" />
<include name="Project.YourProjectName.Global.Links.HeaderMenu" database="master" path="/sitecore/content/YourProjectName/Global/Links/Header Menu" />
<include name="Project.YourProjectName.Global.Links.SocialMedia" database="master" path="/sitecore/content/YourProjectName/Global/Links/Social Media" />
<include name="Project.YourProjectName.Media.Content" database="master" path="/sitecore/media library/Project/YourProjectName" />
</predicate> </configuration> </configurations> </unicorn> </sitecore> </configuration>

Hope this helps!

Creating XML Sitemap for the Helix solution

I am working on a solution that already has HTML sitemap as a part of Navigation feature. Now I got a request to add also a basic XML sitemap with common set requirements. Habitat ships with an interface template _Navigable, so let's extend this template by adding a checkbox field called

ShowInSitemap, stating whether a particular page will be shown in that sitemap:


In order to start, we need to create a handler. Having handlers in web.config is not the desired way of doing things, it will require also doing configuration transform for the deployments, so let's do things in a Sitecore way (Feature.Navigation.config file):

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <pipelines>
            <httpRequestBegin>
                <processor type="Platform.Feature.Navigation.Pipelines.SitemapHandler, Platform.Feature.Navigation"
                           patch:before="processor[@type='Sitecore.Pipelines.HttpRequest.CustomHandlers, Sitecore.Kernel']">
                </processor>
            </httpRequestBegin>
            <preprocessRequest>
                <processor type="Sitecore.Pipelines.PreprocessRequest.FilterUrlExtensions, Sitecore.Kernel">
                    <param desc="Allowed extensions">aspx, ashx, asmx, xml</param>
                </processor>
            </preprocessRequest>
        </pipelines>
    </sitecore>
</configuration>

We rely on httpRequestBegin pipeline and incline our new SitemapHandler from Navigation feature right before CustomHandlers processor.

SitemapHandler is an ordinary pipeline processor for httpRequestBegin pipeline, so is inherited from HttpRequestProcessor:

    public class SitemapHandler : HttpRequestProcessor
    {
        const string sitemapHandler = "sitemap.xml";

        private readonly INavigationRepository _navigationRepository;

        public SitemapHandler()
        {
            _navigationRepository = new NavigationRepository(RootItem);
        }

        public override void Process(HttpRequestArgs args)
        {
            if (Context.Site == null 
                || args == null
                || string.IsNullOrEmpty(Context.Site.RootPath.Trim()) 
                || Context.Page.FilePath.Length > 0 
                || !args.Url.FilePath.Contains(sitemapHandler))
            {
                return;
            }

            Response.ClearHeaders();
            Response.ClearContent();
            Response.ContentType = "text/xml";

            try
            {
                var navigationItems = _navigationRepository.GetSitemapItems(RootItem);
                string xml = new XmlSitemapService().BuildSitemapXML(flatItems);

                Response.Write(xml);
            }
            finally
            {
                Response.Flush();
                Response.End();
            }
        }

        private Item RootItem => Context.Site.GetRootItem();

        private HttpResponse Response => HttpContext.Current.Response;
    }

And XmlSitemapService code below:

    public class XmlSitemapService
    {
        public string CreateSitemapXml(IEnumerable<NavigationItem> items)
        {
            var doc = new XmlDocument();

            var declarationNode = doc.CreateXmlDeclaration("1.0", "UTF-8", null);
            doc.AppendChild(declarationNode);

            var urlsetNode = doc.CreateElement("urlset");

            var xmlnsAttr = doc.CreateAttribute("xmlns");
            xmlnsAttr.Value = "http://www.sitemaps.org/schemas/sitemap/0.9";
            urlsetNode.Attributes.Append(xmlnsAttr);
            doc.AppendChild(urlsetNode);

            foreach (NavigationItem itm in items)
            {
                doc = CreateSitemapRecord(doc, itm);
            }
            return doc.OuterXml;
        }

        private XmlDocument CreateSitemapRecord(XmlDocument doc, NavigationItem item)
        {
            string link = item.Url;

            string lastModified = HttpUtility
             .HtmlEncode(item.Item.Statistics.Updated.ToString("yyyy-MM-ddTHH:mm:sszzz"));

            XmlNode urlsetNode = doc.LastChild;

            XmlNode url = doc.CreateElement("url");
            urlsetNode.AppendChild(url);

            XmlNode loc = doc.CreateElement("loc");
            url.AppendChild(loc);
            loc.AppendChild(doc.CreateTextNode(link));

            XmlNode lastmod = doc.CreateElement("lastmod");
            url.AppendChild(lastmod);
            lastmod.AppendChild(doc.CreateTextNode(lastModified));

            return doc;
        }
    }
Also, NavigationItem is a custom POCO:
 
public class NavigationItem
{
    public Item Item { get; set; }
    public string Title { get; set; }
    public string Url { get; set; }
    public bool IsActive { get; set; }
    public int Level { get; set; }
    public NavigationItems Children { get; set; }
    public string Target { get; set; }
    public bool ShowChildren { get; set; }
}


Few things to mention.
1. Since you are using LinkManager in order to generate the links, you need to make sure you have full URL path as required by protocol, not the site-root-relative path. So you'll need to pass custom options in that case:

2. Once deployed to production, you may face an unpleasant behavior of HTTPS links generated along with 443 port number (such as . That is thanks to LinkManager not being wise enough to predict such a case. However there is a setting that make LinkManager works as expected. Not obvious
var options = LinkManager.GetDefaultUrlOptions();
options.AlwaysIncludeServerUrl = true;
options.SiteResolving = true;
LinkManager.GetItemUrl(item, options);

or better option in Heliix to rely on Sitecore.Foundation.SitecoreExtensions:

item.Url(options) from


//TODO: Update the code with the recent



That's it!

Handling 404 in Helix

When creating websites with Sitecore it is always nice to a have a nice and shiny 404-page that complies with the website styles and is content editable. Which will not work in all the cases, unfortunately. If the request falls out of site context - user won't get that nice and shiny page. So let's see what we can do in Helix, as it assumes multisite by design. The code below is written on top of Habitat, but that should not make a big difference.

Since this functionality is website agnostic and relates to Project layer, we will be using Project.Common.Website. Here is the  configuration within Common.Website.config:

<httpRequestBegin>
   <processor patch:before="processor[@type='Sitecore.Pipelines.HttpRequest.LayoutResolver, Sitecore.Kernel']"
    type="Sitecore.Common.Website.Pipelines.HandleRequestError, Sitecore.Common.Website"
    patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']" />
</httpRequestBegin>

And the processor HandleRequestError.cs:

public class HandleRequestError : HttpRequestProcessor
{
    public override void Process(HttpRequestArgs args)
    {
        if (Context.Item != null || Context.Site == null)
        {
            return;
        }

        string filePath = HttpContext.Current.Server.MapPath(args.Url.FilePath);

        if (IsValidItem() 
             || ReservedUrls.Includes(args.Url.FilePath) 
             || File.Exists(filePath)) 
        {
            return;
        }

        if (Context.Database != null)
        {
            // if we are in a context of a specific site - serve content-editable 404
            Context.Item = Context.Database.GetItem(Context.Site.StartPath + "/404");
        }

        // return 404 HTTP status code in either cases, to be picked up further ahead
        HttpContext.Current.Response.TrySkipIisCustomErrors = true;
        HttpContext.Current.Response.StatusCode = (int)HttpStatusCode.NotFound;
    }

    private bool IsValidItem()
    {
        if (Context.Item == null || Context.Item.Versions.Count == 0) return false;

        if (Context.Item.Visualization.Layout == null) return false;

        return true;
    }
}
So briefly, if we have website resolved - we serve its customized content-editable /404 page item (each of the sites has this page under the same root-relative path).

Fixing: Sitecore 9 installer unable to connect to Solr server, while it is available

Recently tried to reinstall Sitecore 9.0 update 1 and got the following message:

"Request failed: Unable to connect to remote server" (and the URL which is default https://localhost:8389/solr)


Opening Solr in browser went well, and there were no existing cores that could prevent the installation. Weird...


After experiments, I found out that was an antivirus preventing such requests. Disabling it allowed cores to be installed well and thу rest of install script succeeded.

Creating a simple workflow in Helix

This may be a not as comprehensive guidance, as it should be, however, I am using this blog post mostly for leaving notes in a cheatsheet manner for later. So, there are several steps to make things happen.

1. Let's create a security domain for our website - that should typically be in a site config on a project layer (Website1.Website.config for my example):
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
    <sitecore>
        <domainManager defaultProvider="file">
            <patch:attribute name="defaultProvider">config</patch:attribute>
            <domains>
                <domain id="website1" type="Sitecore.Security.Domains.Domain, Sitecore.Kernel">
                    <param desc="name">$(id)</param>
                    <ensureAnonymousUser>false</ensureAnonymousUser>
                </domain>
            </domains>
        </domainManager>
    </sitecore>
</configuration>


2. Create 3 roles: website1\Editor and website1\Approver on the same domain, as well as website1\everyone that is a member of sitecore\Sitecore Client Authoring and is shared between first two. Make website1\Editor and website1\Approver members of website1\everyone and website1\everyone in turn member of sitecore\Sitecore Client Authoring.Also make website1\Approver member of sitecore\Sitecore Client Publishing


3. Create user accounts. I am creating them as part of website1 domain but they may be part of sitecore domain if need them instance-wide.


4. Assign users to their appropriate roles - editors or approvers. Once assigned - the user is able to log in and load content editor, however not able to insert new item for our website or change presentation details. User also is able to open a page in experience editor but again cannot edit this item because do not have write access to it.


Image above shows how LaunchPad looks when users log into Sitecore. However due to not having permissions they will see the following message in a Content Editor:

5. Create your workflow. I won't be original calling new workflow as Website1 Workflow. The easiest for a quick start would be to clone Sample Workflow and adjust states and other refs to point within corresponding items within that newly created workflow.


6. Then assign page relevant templates into Website1 Workflow. (Standard values -> Workflow section. Set default workflow into Website1 workflow). This ensures all the new items of this template will have Workflow field set into Website1 Workflow and State field will have state preselected as per workflow's Initial state field (workflow definition item)



7. Now give permissions to the role:
Open Access Viewer, click Account from the left top corner and select website1\Editor. Then having it selected give permissions to for everything under
/sitecore/content/Website1/Home,/sitecore/content/Website1/Global (but explicitly deny editing and deleting the top node itself), and do not forget media library for that project

Once complete - users will be able to Lock and Edit items and later submit for approval. So far so good.

8. Next step is to ensure that editors will not be able to approve items. It can be done by denying permission on Awaiting Approval state for editor.


That will result in the following permissions set for Editors in Access Viewer:


While Approvers' Acess Viewer shows Awaiting Approval state available:



9. One more thing to mention - if you got your Helix solution created from Habitat - you may come into a situation when certain fields are not editable. That happens due to write permission of fields for Feature-level templates are set to deny. I have written an explanation and the solution in a separate blog post.


10. Add language permissions to a shared role website1\everyone:



11. Last, but not the least - serialization. What you will serialize? Standard values for all templates that now became part of the workflow. Workflow itself (as a part of your Website1 project serialization configuration), Roles, possibly Users (however remember that there is no way to serialize a user with password - the only option is deserializing a user by Unicorn with setting a default password, also apart having the same default password these users will have Created field updated, which in turn will trigger source control changes). Also need to serialize Languages (foundation layer) with updated permissions - .\src\Foundation\Serialization\serialization\Foundation.Serialization.Languages\Languages.yml should be serialized.


12. Testing workflows in its basic falls into three steps routine:

- Firstly, you need to log in as an Editor and create a page and provide the rest of required content. Once done - submit that for approval. An important check is opening workbox to ensure that editors can only see Draft mode but not Awaiting approval


- Secondly, re-login as Approver and open Workbox. Now you should see Awaiting Approval section and will be able to Approve using it.


- Finally, login as an admin, switch to web database and make sure all the content has been published. That includes related and child items.

How Apply-Xml-Transform works in Helix / Habitat

I was recently investigating gulp file of Habitat for interesting goodies and came across taskApply-Xml-Transform, so decided to dig deeper into the one.

What it does?

It looks within all Foundation, Feature and Project layers for config transformations (*.xdt files) in order to run each of them and transform into target Sitecore web.config from the web root folder.

What is XDT?

In very simple, XDT is just an XML file with a set of rules of what and how to transform within web.comfig. We may use them in cases when we need to somehow transform web.config outside of <sitecore> node of configuration so that we can't rely on config paths that only apply within the <sitecore> node. XDT structure corresponds to the structure of target web.config file with additional commands coming from XML-Document-Transform XML namespace. Below is an example of such XDT file, that adds Microsoft.Codedom compiler references into web.config in case they don't exist:

<?xml version="1.0" encoding="utf-8"?>
<configuration  xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <system.codedom xdt:Transform="InsertIfMissing">
    <compilers xdt:Transform="InsertIfMissing">
      <compiler xdt:Transform="InsertIfMissing" xdt:Locator="Match(language)" language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:6 /nowarn:1659;1699;1701" />
      <compiler xdt:Transform="InsertIfMissing" xdt:Locator="Match(language)" language="vb;vbs;visualbasic;vbscript" extension=".vb" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:14 /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+" />
    </compilers>
  </system.codedom>
</configuration>

I wanted to see how exactly it's being triggered, so running script in a verbose mode, brought me to the following conclusion:

How does it run?

So, configuration transform relies on msbuild to do this job. But instead of Debug, Release or Clean targets, it uses a target calledApplyTransform It accepts numerous parameters, among those we have XDT file to transform, target folder, target configuration file to be transformed and few other parameters. Entire call extracted from a batch looks like below:

C:\Program Files\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\amd64\MSBuild.exe 
/nologo /maxcpucount /nodeReuse:False 
/property:Configuration=Debug 
/property:Platform="Any CPU" 
/property:WebConfigToTransform=C:\inetpub\wwwroot\Platform.dev.local\ 
/property:TransformFile=C:\Platform\src\Feature\Accounts\code\App_Config\Web.config.xdt 
/property:FileToTransform=App_Config\Security\Domains.config 
/target:ApplyTransform 
/toolsversion:15.0 
/verbosity:diagnostic 
C:\Projects\Platform\scripts\applytransform.targets

You may modify the code above and run it on your own casual day-to-day activities outside of gulp, Habitat and other tools.

Hope this helps!

Adding Unicorn icon to Sitecore Launchpad

Seriously, I cannot understand why no one hasn't done that earlier before myself!

Recently, yet another time bookmarking a unicorn.aspx for another specific environment, I caught myself on why not to have Unicorn icon as a standard launchpad tile icon. Benefits of having it there against bookmarking:

  • it becomes available not only for myself but for the rest of admin users in particular environment
  • yes, by saying admin, I want to say that you can customize security for that shortcut
  • if serialized, this icon shortcut becomes available on all the environments it is being deploys

So please welcome Unicorn Launchpad icon. You may download the installation package at the bottom of this blog post.


So, as you might know, all the Launchpad icons are taken from core database and you may find them by the following path:/sitecore/client/Applications/Launchpad/PageSettings/Buttons. So you may add a new icon by simply creating an item called Unicorn (of the template /sitecore/client/Applications/Launchpad/PageSettings/Templates/LaunchPad-Button) as a child of any LaunchPad-Group items (for example, Tools, along with Content Editor).


The much better option would be to duplicate already existing item that is available to admins only - in such case you'll also inherit permissions set) - AppCenter for instance. Once done, adjusts all the fields correspondingly. The most important field to set is Link, so make it pointing to /unicorn.aspx (but remember you may also include parameters, such as unicorn.aspx?verb=sync).

The next challenge is to set an icon. Unicorn is not a part of Sitecore, obviously, Sitecore won't have icon packs for it. Let's create our own icon pack for Unicorn.


Each of these drops down values above (Applications, Apps, Business, Controls etc.), in fact, is a zip archive underneath folder<SITE_ROOT>\sitecore\shell\Themes\Standard.


The structure of archive is the following - unicorn.zip\Unicorn\32x32\Unicorn.png - you may have icons for the other resolutions but I am referencing this one:


That's all the job and it took a couple of minutes!

Download ready to use package (19.7kb, keep in mind that Unicorn icon will be shown to admin users only).

Introducing Sitecore Discussion Club


Introduction

Sitecore User Groups have been a commonly used way of offline knowledge sharing and socializing for long years and we all love them. However, there are some drawbacks and things that on my opinion could be improved.

As I know, there are many of participants willing to present, but typically in most cases one presentation is taken by people from sponsor, yet one more - by Sitecore employees and whatever remains - by some MVP, while there are only 3, maximum 4 speeches available. Those lucky who manage to get presented have to artificially adjust to ~40 minutes format extending their speech time frames, while in fact the core sense of most presentations can be fit into 15 minutes. In addition, user groups are the rare event, occurring once per quarter.

Another thing is that attendants are socialising only little time during breaks between the presentations, while I know few people are attending only just because of networking. At the same time some of attendees are "switched off". I suggest making user groups more interactive with participants interacting with each other apart from just socializing. Limiting presentation with shorter time frames will allow speakers to better structure their thoughts and allocate time for a bigger number of people to participate.

To address these calls, I am going to introduce and run events of a new format, called Sitecore Discussion Clubs, starting in London and that will run on monthly basis (ex. first Tuesday of each month) and in parallel to existing Sitecore User Groups, not as a replacement. This document describes what is Discussion Club and some basic rules and thoughts, however it may be subject to changes.


What is Discussion Club

Sitecore Discussion Club will be held on a monthly basis and consists of four blocks

1. Light Talks

Each of participants can present anything of his / her recent experience or some interesting aspects of work with Sitecore, that may be useful and interested to the community. We currently tend to limit these talks with 5 minutes, having up to 10 talks during the event. It serves as sort of warmup before major part - Discussion Club. Speech registration opens 48 hours before the event via Discussion Club website, with 10 slots open (first come - first spoken principle), where new participants have priority as they are obliged to present. Every new member of the club must present on first visit the club, that speech actually "opens" (or initiates) the membership.

2. Discussion Club

This is the most important part of the event. Each of participants may suggest a problem he / she currently has for a collective-mindset to challenge, but it's not limited with day-to-day issues. One would probably like to discuss new Sitecore features or whatsoever, if it is interesting to other members to discuss (ex. "can someone please tell in which cases SXA is perfect for greenfield projects and how do I 'sell' it to my client?" etc.). All suggestions are registered at the website, and upon the beginning when the organizer goes along this list - each one has 30 -60 seconds to briefly explain the problem and what is he looking from the discussion to get solved. once finished, every participant votes for topics that seems to be interested (that is done using mobile phone). Then an actual discussions start, from most to least voted suggestions on from agenda list. Each discussion limits to 10 minutes, there is also a mechanism of 5 minutes extension (if majority insist, 80% or more clicking "extend" button on their mobile phones next to current discussion).

3. What If

Is the funniest part of the event, but brings huge value. It is also a collective-mind discussion of unusual, weird or even craziest ideas we can do with our beloved platform. As a good example, I want to share few ideas that came into my head and the one I was trying to implement. The first, is a LunchPad with the live icons showing a progress or any dynamically changeable information, pulled from a Sitecore instance (or behind it), for example showing a progress of a long-running scheduled task, and clicking this live icon brings you to corresponding SPEAK app. I expect, that should be implemented with SignalR or similar technology, but very willing to discuss it in more details and potential traps. Another example is a module I have implemented in 2017, that changes item ID from a context menu - it is a great area for discussion what needs to be taken into account and things to keep in mind, such as child items, changing references to this items, reindexing, links database, etc.). These ideas unlock attendees' creativity and serve as an important source of inspiration and will be shared with Sitecore for considering and potential implementation. This is similar way how Dynamic Placeholder, Buckets and Language Fallback became part of the platform. Who knows what valuable inspiration our enthusiasts will create? Also, I am be seeking a sponsorship from Sitecore for awarding 3 most valuable of "What If" ideas, where award can be anything like free certification on choice, or free SugCon / Symposium entrance of whatsoever Sitecore decides.

4. Hiring, not Recruiting

Is the final simple block. Each member of the club can briefly say what Sitecore vacancies do they have open at the moment or in if looking for a new opportunity - make a brief self-introduction. 3-5 minutes for everything would be enough, I assume. Quick, direct, and no recruiters.


Entrance and attendance

The very first event welcomes everyone who is willing to attend - they all will be allowed and receive Discussion Club membership (but please see "First Event" section below). Then an event registration will be done via website similar to MeetUp, running with minor overbooking, just in case few people are not coming. When event bуcomes mature - new members can join the club by invites from existing member (and they still must present light talk upon first visit). Existing MVPs can attend without any limitations, cannot be denied or dismissed from the club. They also have priority for Light talks (but after newbies of course). Sponsors have another "priority lane" for Discussion Club, however not more than on 3 consecutive events, than they have "chill out" event. However sponsor can suggest a topiс on common grounds to be evaluated and voted.


Venue and sponsorship

The event will take part at sponsor's venue in central London, in 3-5 minutes of walking distance from Angel station (map: https://goo.gl/5p8v7D). Sponsor is also proving the necessary equipment, food and drinks, as normal. Dare agency is currently sponsoring the event, I am currently contracted with them building an advanced platform with Helix and Sitecore 9, they are highly interested in absorbing any of the latest Sitecore knowledge.


Benefits

Discussion Club unites the most enthusiastic members of the Sitecore scene from London and area, offering them all sharing the experience. Unlike as on the User Groups, we discuss real-life scenarios, and actual day-to-day problems, as we vote for most preferred topic from agenda. We are more engaged, communicate and discuss directly and collectively. All that enables us sharing our experience in interactive, friendly manner at unprecedented level, that becomes sort of Sitecore Community 2.0

Updating Sitecore 9 with Helix to 9.0 update 1 (rev. 171219)

I am currently developing a greenfield Helix-based solution on Sitecore 9. That is a challenging but thrilling path, resulting in nicely setup working platform with "one-click install + one click deploy" process, perfectly suitable for continuous integration. 

However, as soon as 9.0 update 1 has been released, I started anticipating upgrading my solution to that revision, but for a week or so we've been missing NuGet packages for the latest version. Since January 18-th they have been released so finally it became possible to update the solution. Below there are few things I have done to make it work.

Phase 1. Choosing an update approach to take. 

There are few options:

  • *.update file with actual version
  • upgrade zip archive with only changes + pdf guidance on how to update
  • SIF archive with CMS, xConnect and corresponding configuration *.json files

For sanity purposes, I prefer to fully uninstall the previous version and reinstall the updated afterwards, rather than overwrite things. Thanks to a flexibility of Helix, now developer should not worry about losing an existing state, as soon as everything is checked into a version control system - gulp script will pick the latest changes and do that job for us. That's why approach number 3 becomes a choice. 

As I am keeping web folder under source control (just clean install of each version in order to easily restore that state) it makes sense to move .git folder outside from web folder before we go with uninstall, as it obviously will remove entire web folder. After as we remove the previous version, and newer version settles there, one can return .git folder back and immediately benefit from seeing the difference between two versions. I will cover that below during stage 2. So, to uninstall, open PowerShell and do:

.\uninstall-xp0.ps1

Once complete, got to the next phase.

Phase 2. Preparing and installing new version

First of all, download Sitecore 9.0 Update 1 XPSingle from SDN download page. Since we're doing that for our development environment, make sure you get OnPrem edition, rather than Cloud.

Obviously, to install newer version one need to update Sitecore Install Framework that supports particular version. Luckily, Sitecore Installer knows everything it should regarding how to install the version. PowerShell (with admin rights):

Update-Module SitecoreInstallFramework

If for some reason it breaks with execution policy exception, modify current user appropriate permissions:

Set-ExecutionPolicy Unrestricted -Scope CurrentUser -Force

Copy Sitecore packages for both CMS and Xconnet into build/assets folder, along with previous version packages. In my case, these files:

Sitecore 9.0.1 rev. 171219 (OnPrem)_single.scwdp.zip and Sitecore 9.0.1 rev. 171219 (OnPrem)_xp0xconnect.scwdp.zip

and also replace *.json scripts from XP0 Configuration files 9.0.1 rev. 171219.zip

Then, quite important, update version details at settings.ps1 script:

$SitecoreVersion = "9.0.1 rev. 171219"
$InstallerVersion = "1.1.0"
Finally, run the installer:
install-xp0.ps1
and wait till it finishes. Warning: do not open Sitecore after it installs.

Phase 3. Saving clean state of web folder with new version
Previously, I have described an approach of restoring a newly installed and previously never run Sitecore from a dedicated git branch, called SitecoreFiles_CM. During this exercise given approach saved me plenty of efforts, as I had to restore initial state of web folder at least 20 -25 times, if not more. That's why, even before I first run Sitecore and it generates plenty of artefacts (logs, caches, etc.) - I do one another commit with a new version of webs folder on top of existing clean install of 9.0 (initial). As a bonus, I get a wonderful tool for identifying changes between clean install of both versions, including config changes, file structure, built-in apps and similar.

Phase 4. Update the solution
Open Visual Studio, but do not deploy as you need to update your solution with all the latest package version. A goal of that phase its to make sure that any of DLLs being published to webroot by gulp script will match existing version of their counterparty within /bin folder of webroot. Sitecore comes with 337 DLLs from the box, and one would probably need to write a decent automation tool to do matching. Again, thanks to SitecoreFiles_CM approach for simplifying my life, as git commit immediately shows all the DLLs that differ from those checked in as a part of 9.0.1 clean install commit. 
As the first step, I went and changed all Sitecore.*.NoReference libraries for each project so they all correspond to 171219 revision. Keep in mind, that some of Sitecore NuGet libraries are versioned differently and do not have revision number in their name.
Changing references in such a large solution taking much efforts, attention and time. That's why I recommend you to do quite often commits while updating your solution, as you'll likely to restore one or few times during that process. Finishing with Sitecore.* references is a good time to check the code.
Then, I went updating Glass Mapper, Unicorn, Unit tests, code generation libraries and the rest of third-party packages. Below there is a list the version I used that are compatible and function:
Autofixture 3.51.0
BuildWebCompiler 1.11.375
EnterpriseLibrary.* 6.0.1304
FluentAssertions 4.19.4
GlassMapper 4.5.0.4
HtmlAgilityPack 1.4.9.5
Kamsar.WebConsole 2.0.0
Lucene.Net 3.0.3
MicroCHAP 1.2.2.2
Moq 4.8.1
Mvp.Xml 2.3.0
netDumbster 2.0.0.1
Newtonsoft.Json 9.0.1
NSubstitute 3.1.0
Rainbow.* 2.0.0
SharpZipLib 0.86.0
Sitecore.FakeDb.* 1.7.2
Unicorn.* 4.0.3
xunit 2.3.1
Lastly, it is a good time to update System.* and Microsoft.* assemblies. 

One thing to mention is that you still need to keep several assemblies at non-final versions and do not update it to the latest, as they will break dependencies. These are listed below along with correct versions:
Microsoft.Extensions.DependencyInjection 1.0.0 
System.Reflection 4.1.0
Newtonsoft.Json 9.0.1
HtmlAgilityPack 1.4.9.5
System.Net.Http 4.0.0
Castle.Core 3.3.3
It took inadequate time to troubleshoot some of those mentioned above, thanks to git in both solution and web folder it became easier to try-test-restore the state.

Finally, it all looks great. Everything works perfect, including generation works, unit tests, Glass Mapper, Unicorn etc.  And few bonuses, of course.

Bonus 1: as soon as I merge this feature branch with updated solution into develop branch of our GitFlow repository, other developers will get the update semi-automatically - with next re-install after refreshing /build/assets folder.

Bonus 2: just nearly forgot to mention: as the reward for an update efforts, I have got an EXM module as part of the platform, yes - now since 9.0 update 1 it comes out of the box!