Monday, June 8, 2026

Sugcon 2026 London - day 2

Sugcon 2026 - London - day 2


After an interesting first day at Sugcon in London we started early for a fully packed second day. People who have attented Sugcon before know that the first session on day 2 seems like a very interesting spot as you get the honor of opening the full day but it also is a very challenging spot as a lot of people are not fully awake yet. 

Friday

At 8h30 Morten Ljungberg had the pleasure of kicking it off with his Corgi branded explanation of MCP's.

It was a very nice session, especially at this hour. I recall Morten being a worried about the level but I assured him this was definitely ok. Most people might already know quite a bit of what he told but I found it still very interesting and presented in an entertaining way. As "the guy who's presentation are always stuffed with cats" I might not really be a dog person but it's fun to see him brand his presentation this way. The day started well, and still have so much to come.


I did miss a few parts of sessions on Friday. For a big part because I got into some interesting conversations with all kind of people during breaks - going from community members, mvp's to fellow Epam folks that I do not see that much in real life. As a 2026 (long-time) Sitecore MVP I was also asked to participate in creating promotional material for Sitecore's 25th birthday which they are celebrating this year. I assume you might have seen some of the #SitecoreSilver posts...

Of course, this is still a conference and even though talking to people is important and very interesting, grabbing some of the sessions surely is also. I will not list all the ones I attended, as that would be a boring list of AI, AI and more AI - sometimes working, sometimes not and usually a great idea but needs to be proven and nobody knows how.  But I'm still writing a post on Sugcon we I will talk about some sessions of course. 

I attended a session by Harald Greve, engineering manager at Macmillan Cancer Support and Sitecore Technology MVP now.  His session on "Post Quantum Cryptography" was pretty interesting and well presented. It's surely something to look into and he posted the main takeaways so you can/should read those. It was a clear explanations about encryption and why this should matter to us, not only somewhere in the future but surely also now already. 



A few years ago Sugcon started planning some "lightning talks" around lunch time on day two. It was a success and they keep on doing that. A lightning talk is a fast-paced presentation restricted to 15 minutes, typically focusing on smaller or even niche technical tips, customer showcases, or just smaller topics related to Sitecore rather than deep-dive sessions. I like them a lot and usually some of them are very good. 

This year I decided for one of them to go crazy and join the marketing track ☺.  I know Jacqueline is a good speaker and I was not disappointed. Her session on intelligent content was surely lightning, full of spirit and interesting even for non-marketers. The marketing track adventure felt good and might be repeated.


One of the AI sessions I would like to mention was from Volodymyr Nikitin and his hare and tortoise. A well presented fairy tail that gets you thinking about using AI as your developer friend. Not in a sense of I need this fully blown now right away, but it's also not something to ignore. It's about finding the right balance and the right tasks for AI and the human. And it was brought in a fun way keeping us all awake - which is an achievement right after lunch. 


Andrei Pop also had a session on AI and Sitecore's vision to Agentic Workflows.  At a certain point (in the future) the marketer using Sitecore will/should be talking to any LLM and asking that to do his work. Listening to Jacqueline we know there should be some human interference as well if you really want to stay in touch with other humans - it's all interesting and we'll see what the future brings. 


Sponsor sessions are something many conference attendees usually try to skip, but Piers Matthews from Dataweavers has proven it can be different. It does not need to be a sales pitch but can also be an informative session. I already attended his session last year in Antwerp which was ok-ish, but this year it was really interesting to hear what I was actually expecting. New security issues are popping in the (new) mach architectures, and they can be fixed but you do need to pay attention to them of course.

But all good things come to an end, and so does Sugcon. Not without a thank you to the organizers of course (sorry Anders - the picture with you on screen included was not good at all).  So once more thanks for organizing this. And good luck to Sebastian Winter who will take over Tamas' role.



And then it is time to go home. Coming from Belgium I travelled to London by train which was a nice experience to be honest. Would be nicer without those Brexit-border-controls, but still... As I do have a son who still likes Harry Potter a bit and I was in the right station, I couldn't leave without a last picture. And the relief I did not have to go through platform 9 3/4 as the line there was longer than the security line at Eurostar ☺



That was Sugcon Europe 2026. Maybe we'll meet again at the 2027 European conference. 


 


 







Sugcon 2026 London

Sugcon 2026 - London


I was able to attend the Sitecore Usergroup Conference - better known as Sugcon - in London this year and that needs a blog post afterwards. Way overdue - but never too late I assume. 

This was my 7th Sugcon so I am starting to know my way around such conferences. Which is nice as you also get to see many familiar faces again. More on that later... let's start in London. 
Due to circumstances (budget reasons) I was not able to stay in the conference hotel. It's always nicer to stay there as you automatically bump into community members all the time, but this fact also pushed me to pay a visit to a part of London that I had never been before and is definitely
worth visiting. 
I might assume everyone knows the sights of Big Ben, Westminster Abbey or the Palace of Westminster. 

I'm not sure if people would recognize the King of England, nor his palace. But his guards, yes..  everyone knows the King's Guard. And if you're in walking distance it would be silly not to pay a visit, stroll through St James Park and accidently bump into The King's Life Guard on the Mall.




Thursday morning, before the start of the conference I went for a walk. As this still is England, we couldn't spend our days here without any rain. We were still very lucky and didn't get much at all, but on Thursday a few drops brought me into the National Gallery.  A Japanese-tourist-style quick visit through a (free) museum with almost no people at that time was actually fun. And a little bit of relaxed culture before we would probably be overwhelmed with AI surely didn't hurt. 

But enough tourism, we (you) are here for Sugcon. 


Thursday

Keynotes


The keynote for the 2026 Sugcon was presented by Scott Liewehr, Global Vice President of Market Strategy & Growth at Sitecore.  It was short, to the point and well presented. Everything a keynote should be. And with a shoutout to the amazing MVP community. As a member of the MVP class of 2026 it's always nice to see and hear some recognition from Sitecore for our efforts. Thank you Scott, well done.

As a final highlight, Tamas Varga was called on the stage. Most people in the Sitecore community know already that he will be leaving us soon to realize his (sailing) dream. And although we all feel there is no-one who deserves this more than Tamas, we will of course miss him. The announcement to make him a Sitecore Honorary MVP got a well deserved standing ovation from the entire room. Tamas is joining a select group of Sitecore folks who really made a lot of impact.  I do hope to bump into him in the future somewhere - wishing you all the best Tamas!



After this first highlight, we were thrown back into the past by Richard Potter (son of Harry) from Microsoft to remind us that AI is not the end of the world but an opportunity and/or a challenge to be part of this evolution. 

It was an inspiring session and I am glad I was not the only one in the audience that had to raise his/her hand when he asked who was already working (in our digital sector) in 1993...


Breakout sessions

Time for the breakout sessions. I started with Sebastian Winslow's session on "Agentic AEO/SEO optimization". I was rather disappointed with the session as it did not deliver what I hoped for. Too vague, no concrete examples. I talked to Sebastian afterwards - because having open conversations is what we in the Sitecore community (should) do - and he knew and also explained the reasons behind it. Anyway, this happens at conferences and it did give me a good chat with Sebastian which was probably more valuable than any session content could have been.

 
I continued with a session by Simon Hauck on SitecoreAI publishing. I can't say it was not interesting because it was and it certainly is a topic that people should think about. But I also left the session with the idea that I did not learn that much - maybe because this is not (yet) really a part of my job and as such was missing a connection. I will remember thinking about it when it crosses my path though and that is all I expect from a session to be honest. 



Christian Hahn is not only a familiar face in the Sitecore community - even outside Germany - but the last few years also a recurring speaker at Sugcon. This year he brought along Romina Metnik to bring us up-to-date with the latest developments around Sitecore Studio and especially the Sitecore Marketplace. 




The final "session" on Thursday is rather traditionally the Sitecore hackathon awards ceremony. And it's always nice to see two major Sitecore dinosaurs again - Akshay and Jason, you rock!  

It was apparently rather clear that most or all participants learned how to use AI. But also amazing what has been created again by a motivated community. 


Time for the "community diner" - or just more time to mingle and have some great conversations. I managed to catch up with some (very) familiar faces and meet some new ones as well. Going from small-talk to Sitecore (obviously) to world class problems... anything van be discussed on such a community evening. I know quite a few discovered one or more London bars afterwards - to be honest I kept it rather quite, although I couldn't resist a nighttime visit to London



It was an interesting first day. Let's continue on Sugcon day two... 

 










Wednesday, March 25, 2026

Sitecore Send transactional mails

Sitecore Send transactional mails

We all get too many emails. But aren't you also frustrated when you don't get a confirmation (mail) after an online purchase? Or even after filling in a form that is important to you. Transactional mails are important for people. And not only as confirmation. They can also be used to inform your organization about events on the website like people making request or even application failures. Sometimes they are also used within the (sales) flow. Just think about codes that are being send to verify your login attempt. 

Plenty of examples and I don't think any organization needs to be convinced that transactional mails are still important. So what happens if you are running on a Sitecore XP and using EXM for those mails... because, well that is what you get with XP.  In my case this means  you do a proof of concept with Sitecore Send to show how this can be handled with a modern platform which feels a bit more reliable. 

The proof-of-concept

The idea for the poc was basically simple:
  • can we send emails from an application based on a template which is maintained by an end user
  • can we transform these emails dynamically:
    • filling specific elements with data provided by the application
    • customizing the template (show/hide areas) as needed

Asking a friend

As this seemed very basic, I asked my good friend Claude to create this for me. Now it could be my mistake and the fact that my prompt of 12 lines was still not accurate enough, but after 4 attempts and a direct link to the documentation he was able to get a function that was almost good. The payload to send to the Sitecore Send API was still wrong so I had to fix that myself. 

As it wasn't right on the spot, this felt like something to share. So here we go. 

Dynamic templates

Before we dive into the code, I would like to talk about the dynamic part. Replacing tokens and personalizing complete blocks of the template.

The Sitecore docs mention:
In the Sitecore Send API we use the Mustache templating language to generate and render personalized content based on the data received via the API. This approach allows you to customize the recipient experience by tailoring specific elements, such as text and images, to meet individual preferences and requirements.
So I checked the documentation for the Mustache templating language and found several interesting things. However (and we will come back to this later) they didn't seem to work. Furthermore, the example from Sitecore a syntax like {{#each ...}} which is actually not basic Mustache. So for the POC I ended up with 2 solutions so we can decide later which will suit us best. 

The first solution will send a request to Send for a transactional mail being send to my recipient with substitution tokens using my template. This is the basic out-of-the-box solution with the least custom coding. Sitecore Send will transform the template and handle the delivery. 

A second solution will also use that template but it will add a custom step. In this approach we first fetch the template from Send, use Mustache ourselves to transform the template and then do a request to Send which includes the resulting html. The request to Send is almost the same as in the first one but by providing the html Send will use this instead of the template. Note that we still mention the template to make sure Send can handle the tracking.

The code

To keep it simple and basic, let's skip the wrapper code for an Azure function which is the entry point I used to test the code.  Note that this is POC code - and mainly generated - so don't expect high quality code. This is the quick and somehow working version...  but good enough to get you going.
using Microsoft.Extensions.Logging;
using Stubble.Core.Builders;
using Stubble.Core.Interfaces;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

namespace FunctionAppSend
{
    public class SitecoreSendEmailService
    {
        private static readonly IStubbleRenderer stubbleRendered = new StubbleBuilder().Build();
        private readonly ILogger<SitecoreSendEmailService> _logger;
        private readonly HttpClient _httpClient;
        private readonly string _apiKey;
        private const string ApiBaseUrl = "https://api.sitecoresend.io/v3/";

        // Campaign IDs for different languages
        private readonly Dictionary<string, string> _campaignIds = new()
        {
            { "en", "123456be-6e4e-666b-8bd0-1bbbbcc02089" },
            { "de", "YOUR_GERMAN_CAMPAIGN_ID" },
            { "fr", "YOUR_FRENCH_CAMPAIGN_ID" }
        };

        public SitecoreSendEmailService(ILogger<SitecoreSendEmailService> logger, HttpClient httpClient, string apiKey)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
            _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey));
        }

        public async Task<bool> SendVerificationEmailAsync(string language, string email, string verificationCode, int? payed)
        {
            try
            {
                var campaignId = ResolveCampaignId(language);
                var requestUrl = $"{ApiBaseUrl}campaigns/transactional/send.json?apikey={_apiKey}";
                var substitutions = BuildSubstitutions(email, verificationCode, language, payed);

                var payload = new
                {
                    templateid = campaignId,
                    MailSettings = new
                    {
                        BypassUnsubscribeManagement = new
                        {
                            Enable = true
                        },
                        UnsubscribeLinkManagement = new
                        {
                            IncludeUnsubscribeLink = false
                        }
                    },
                    personalizations = new[]
                    {
                        new
                        {
                            to = new[]
                            {
                                new
                                {
                                    Email = email
                                }
                            },
                            Substitutions = substitutions
                        }
                    }
                };

                return await SendTransactionalAsync(requestUrl, payload, email, "template-based");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Exception occurred while sending verification email to {Email}", email);
                return false;
            }
        }

        public async Task<bool> SendVerificationEmailWithRenderedContentAsync(string language, string email, string verificationCode, int? payed)
        {
            try
            {
                var campaignId = ResolveCampaignId(language);
                var templateHtml = await GetCampaignTemplateHtmlAsync(campaignId);

                if (string.IsNullOrWhiteSpace(templateHtml))
                {
                    _logger.LogError("No HTML template content was found for campaign {CampaignId}", campaignId);
                    return false;
                }

                var substitutions = BuildSubstitutions(email, verificationCode, language, payed);
                var renderedHtml = stubbleRendered.Render(templateHtml, substitutions);
                var requestUrl = $"{ApiBaseUrl}campaigns/transactional/send.json?apikey={_apiKey}";

                var payload = new
                {
                    templateid = campaignId,
                    content = new[]
                    {
                        new
                        {
                            type = "text/html",
                            value = renderedHtml
                        }
                    },
                    MailSettings = new
                    {
                        BypassUnsubscribeManagement = new
                        {
                            Enable = true
                        },
                        UnsubscribeLinkManagement = new
                        {
                            IncludeUnsubscribeLink = false
                        }
                    },
                    personalizations = new[]
                    {
                        new
                        {
                            to = new[]
                            {
                                new
                                {
                                    Email = email
                                }
                            },
                            Substitutions = substitutions
                        }
                    }
                };

                return await SendTransactionalAsync(requestUrl, payload, email, "rendered-content");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Exception occurred while sending rendered verification email to {Email}", email);
                return false;
            }
        }

        private async Task<string?> GetCampaignTemplateHtmlAsync(string campaignId)
        {
            var requestUrl = $"{ApiBaseUrl}campaigns/{campaignId}/view.json?apikey={_apiKey}";

            using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            _logger.LogInformation("Fetching campaign HTML content for campaign {CampaignId}", campaignId);

            using var response = await _httpClient.SendAsync(request);
            var responseBody = await response.Content.ReadAsStringAsync();

            if (!response.IsSuccessStatusCode)
            {
                _logger.LogError("Failed to fetch campaign details for campaign {CampaignId}. Status: {StatusCode}, Error: {ErrorBody}", campaignId, response.StatusCode, responseBody);
                return null;
            }

            using var document = JsonDocument.Parse(responseBody);

            if (!TryGetPropertyIgnoreCase(document.RootElement, "Context", out var context) ||
                !TryGetPropertyIgnoreCase(context, "HTMLContent", out var htmlContentElement) ||
                htmlContentElement.ValueKind != JsonValueKind.String)
            {
                _logger.LogError("Campaign details response did not contain Context.HTMLContent for campaign {CampaignId}", campaignId);
                return null;
            }

            var htmlContent = htmlContentElement.GetString();

            if (string.IsNullOrWhiteSpace(htmlContent))
            {
                _logger.LogError("Campaign details response contained an empty Context.HTMLContent for campaign {CampaignId}", campaignId);
                return null;
            }

            return htmlContent;
        }

        private async Task<bool> SendTransactionalAsync(string requestUrl, object payload, string email, string mode)
        {
            var jsonPayload = JsonSerializer.Serialize(payload);
            using var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
            using var request = new HttpRequestMessage(HttpMethod.Post, requestUrl)
            {
                Content = content
            };

            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            _logger.LogInformation("Sending {Mode} verification email to {Email}", mode, email);

            using var response = await _httpClient.SendAsync(request);
            var responseBody = await response.Content.ReadAsStringAsync();

            if (response.IsSuccessStatusCode)
            {
                _logger.LogInformation("Verification email sent successfully to {Email}. Mode: {Mode}. Response: {Response}", email, mode, responseBody);
                return true;
            }

            _logger.LogError("Failed to send verification email to {Email}. Mode: {Mode}. Status: {StatusCode}, Error: {ErrorBody}", email, mode, response.StatusCode, responseBody);
            return false;
        }

        private string ResolveCampaignId(string language)
        {
            if (_campaignIds.TryGetValue(language.ToLowerInvariant(), out var campaignId))
            {
                return campaignId;
            }

            _logger.LogWarning("Language {Language} not supported. Falling back to English", language);
            return _campaignIds["en"];
        }

        private static Dictionary<string, object> BuildSubstitutions(string email, string verificationCode, string language, int? payed)
        {
            var substitutions = new Dictionary<string, object>
            {
                { "Email", email },
                { "VerificationCode", verificationCode },
                { "Language", language },
                { "HasPayed", payed.HasValue && payed.Value > 0 },
                {
                    "order",
                    new
                    {
                        products = new[]
                        {
                            new
                            {
                                name = "Big box",
                                quantity = 3,
                                price = new
                                {
                                    grossValue = 1200,
                                    netValue = 1000
                                },
                                valiant = new
                                {
                                    name = "red",
                                    id = "kev8484j49j9j9"
                                }
                            },
                            new
                            {
                                name = "Small box",
                                quantity = 13,
                                price = new
                                {
                                    grossValue = 120,
                                    netValue = 100
                                },
                                valiant = new
                                {
                                    name = "green",
                                    id = "kev81254"
                                }
                            }
                        }
                    }
                }
            };

            if (payed.HasValue)
            {
                substitutions["Payed"] = payed.Value;
            }

            return substitutions;
        }

        private static bool TryGetPropertyIgnoreCase(JsonElement element, string propertyName, out JsonElement value)
        {
            if (element.ValueKind == JsonValueKind.Object)
            {
                foreach (var property in element.EnumerateObject())
                {
                    if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase))
                    {
                        value = property.Value;
                        return true;
                    }
                }
            }

            value = default;
            return false;
        }
    }
}

Let's digest what is happening here. 

Solution 1: using Send's dynamic templates

As you can see we are testing various substitutions here. Some were taken from the Sitecore example, some are more towards what we are actually looking for. 

Our first solution is actually pretty simple if you know what you need to do. We need to send a request to the Send API : POST /campaigns/transactional/send.json and attach our API key in the querystring. 
The payload is the data that we send to this API and that will determine what actually happens. We are sending only what is needed for our solution so it will not include a subject or a from address as those come from the template. We are including:
  • templateId: statistics will be registered to the campaign with this ID so although it is not mandatory for the api you probably do want this - and in this scenario it is needed as we use the template to send the mail
  • mailSettings: you can handle unsubscribe settings and scheduled dispatching with these settings
  • personalizations: required and rather important part. Here you define your recipient(s) for the mail and also the substitutions. Substitutions are the values to substitute in the message content and subject for the current recipient (as key/value pairs)
We created all the values we need in a payload object, serialize it and send it with a post request to the Send API.  A few seconds later our mail arrives... 


Solution 2: custom transformation 

In a second solution we first fetch the campaign details with GET /campaigns/{CampaignID}/view.json and filter the HtmlContent from the response. We use this html content and the substitutions to render the final html using Stubble which is an implementation of the Mustache template system in C#.  

We now have the html for the mail - let's send this to Send. We use the same API as in the first solution and almost the same payload. We do add one more value called Content.  Note that this Content is an array although it should only contain one value and you do need to specify both type and value.

After posting this to Send a few seconds later we also get our mail.


The Sitecore Send campaign

I will not discuss the whole process of creating a transactional campaign in Send as that is quite straight forward (and documented). What I do want to show in order to get to our conclusion is what was included in the template. 

Simple substitutions

First of all we have the simple substitutions like "{{VerificationCode}}". Note that these can be placed both in the content of the mail as in the subject. They are replaced perfectly in both places in both solutions (to get this working in solution 2 do not forget to still send the substitutions even though you already used them for the content).

These can be very handy to place personalized data in your mail.

If (not)

We also tried to get the "if" syntax from Mustache to work. It looks like this:
    {{#Payed}} Thank you for your payment of {{Payed}}&euro; {{/Payed}}
    {{^Payed}} Don't forget to pay. {{/Payed}}

Everything inside the {{# part should only be displayed if Payed is present in the substitutions. The {{^ syntax is the negation so that will be displayed if it is not present.  Actually, if the key exists and has a non-false value, the html between the pound and slash will be rendered and displayed one or more times so it can also be used to display lists.

This is standard Mustache but it will not work if you let Send do the transformation as we did in solution 1. It will work in solution 2 as that uses all Mustache transformations.

Each

We also tried the "each" syntax as mentioned in the Sitecore Send documentation. This looks something like:
    {{#each order.products}}
    {{this.name}}
    Quantity: {{this.quantity}} - Total {{this.price.grossValue}}
    {{/each order.products}} 

This works fine in our first solution where Send does the transformation but it does not in our 2nd where we use the actual Mustache library.


Conclusion

It became clear to us that Sitecore Send is probably using their own interpretation of Mustache. There are several of those available online. To be honest I would have liked that the default (and full) mustache language was available as that would be much easier so that feedback will be going towards the Send team. 

But after all - solutions are possible. And both solutions provided here have their use cases. If you don't need more than what is provided by substitutions out-of-the-box the first solution is a very easy way to get your transactional mails out. 
If you do need more flexibility and more complex logic in your templates, the second solution also works fine and can get your mails to their recipients with tracking in Send.


Let's start Send-ing those mails!
 

Thursday, February 26, 2026

Sitecore Hackathon - tips and tricks

Sitecore Hackathon

A few more weeks and it's time for the yearly Sitecore Hackathon. And even though it's hard to match the golden years with over 90 teams participating, 2026 seems to have more than 30 teams again from all over the world. It's still a well known event in the Sitecore community - organized by people with a heart for our Sitecore community so thank you Akshay and all your "community judges" for the effort.


What is this hackathon? 
A hackathon is a fast-paced, collaborative event - in this case lasting 24 hours - where teams of developers and/or domain experts create functional prototypes or software solutions to specific problems. Originating from "hack" and "marathon," these events focus on innovation, networking, and rapid problem-solving.



So basically you get 24h to deliver a project from scratch. And your team of max 3 persons will take up all roles needed to deliver the project. 

I have participated in the Sitecore hackathon for several years with various results. Going from not delivering anything at all in the end to actually really win this thing back in 2017 (oh, time flies).  As I am not participating this year due to a number of reasons I though I might share some lessons that I learned throughout the years. As I learned from the things we did right, the things we did wrong and the many hours of blood, sweat, tears and joy - it feels like post-worthy. 


Before you begin

A good preparation is key to success and that is no different in a hackathon. A few things come to mind in the preparation phase:

Environment

You do not want to be that team that lost hours because they did not have an environment ready for their development. Make sure to check the github access - as requested actually. But your "environment" goes beyond that. Things change of course but to give a few examples of things you do not want to be doing when starting a hackathon:  searching and downloading large Sitecore setup files, windows updates...   Where things like this might sound trivial, they are important. Even cleaning your desk is. 

Also important is a collaboration tool. Even if you are working solo actually. As you will be doing a "project" you do want to keep track of the task you need or want to do. You will want to prioritize them and so on. Depending on your team this can be post-its or any (free) online tool. Pick one, and make sure it is ready to go before the hackathon starts. And you might already be able to add some tasks.

When you read the requirement for a successful entry in the hackathon competition you will notice that there is more than just coding. And subconsciously I actually gave you another task up front - yes, everyone should read the requirements. You should be ready to create documentation with screenshots and video material. Be ready for this - and especially the video. Don't let that final hackathon hour be your first time to create and upload a video as you will be tired and maybe not thinking straight anymore.

Catering

People do need to eat and drink. And as a hackathon is supposed to be fun too, why not go for some nice food and beverages. Yes, sure. Absolutely a fantastic idea as this will contribute to your well-being during a long day. But don't get your hopes up for a feast, as my experience learned me that you will probably not be that hungry at all. So yes, make sure you have everything ready for some nice snacks, drinks and meals but make them rather healthy. Just think what your brain will do after a pizza, double burger, half a bottle wine and a chocolate covered ice cream...  ok, I am exaggerating here but you do get my point.

Timing 

It's gonna be a long day - 24h is a lot and as they give the final idea one hour in advance you could call it 25h.  Some teams take part of that time for a (power) nap. Or even some more sleep if it starts for you in the middle of the night. Depending on your time zone several options are possible. There is no real best practice here as this is also rather personal but just something to think about. 

Idea

One of the challenges of the hackathon is to come up with a decent idea. We will come back to this later as this is also the first task of the day - but you might already be prepared for this. You should not be spending too much time on details up front as there is always a (big) chance everything you thought of is garbage after they announce the actual hackathon idea. But it is a good idea just to be ready with some topics in the back of your head to avoid starting the day with nothing. 


The hackathon day

As I mentioned earlier the hackathon is actually creating a project in a very short time frame. Of course, this is not a full blown project but more like a proof of concept but still. You should go to most of the steps of a project. I'll cover some here and guide you with timings. Time is crucial... 

Idea

Here is the idea again. By now you know the main idea or topic for the hackathon - this can actually also be more than one. So you can start brainstorming keeping in mind the following guidelines:
  • Do not aim too big immediately. This is a proof of concept and it's better to deliver a good mvp (no, not the Sitecore mvp but a minimum viable product). 
  • Try to find an idea that you can scope into a mvp (must) and some extra features (nice to have, if you still have time). This way you can start with the mvp. When that works you will get some peace of mind and decide how to proceed. 
  • Know you team. You should be able to work together so make sure your project can have several tasks that can be done simultaneously by the people in your team. As an example: if 3 person team has only one person who know nextjs you should probably not try a project with 80% nextjs code. 
  • Visibility. It is always nice to show something. Nobody expects a fully designed solution, but something visible is always nice. And if you would need some test content, remember who is going to judge it - it might be something stupid but with test content you can get a smile and a happy judge is better than a grumpy one.
  • Has it been done already? I still remember when I was sitting in our brainstorm room killing several ideas because they were already available in the Sitecore marketplace. And of course it is not because something already exists that it cannot be a good idea but it does make it harder to stand out of course. 
Having a good idea is very important so take your time for it. I remember some years where this took up to 3 hours of our time to get it right. It should not take more than that but do not expect it to be done in half an hour and do not panic if it's not. 

Task list 

After the idea phase (or probably already during) you will start creating a task list. This enables your team also to check the feasibility of the idea. Make sure you have a distinction between the "must haves" (the mvp and the requirements that you need for a valid entry) and the "nice to haves".  This list doesn't have to be complete of course. It's something that lives during the day but it will give a good guidance on your progress and can avoid stress towards the end. 

Coding & fun

It's a hackathon so you will write some code. Or have it generated... at least parts of it as that is how the world runs these days. Try to enjoy it as well. And it is a community thing, so share the joy with some pictures that you can share.  As this is a worldwide event with people from very different time zones, it's always nice to see other teams going for lunch while you are having breakfast ☺

The final

You probably do want to finish at least something and deliver an entry - so the final hours do matter. At least if the other parts went fine but let's assume they did. I remember the organizers sending messages when the deadline came in sight - but if you start getting those and you are not prepared for the final part the stress will start building up. 
In order to deliver a clean and presentable entry you should have a cut off point. My suggestion would be 3 up to even 4h before the deadline. Once that is reached you should be able to finish coding and not pick any new feature tickets anymore. It's time to wrap up and that means quite a few things:
  • Testing. I would assume you have done some tests already during the day but now it's time to do the final round of testing. Make sure everything works. In this era of generated code this had probably become even more important.
  • Do a (final) code review.  Check the code, make sure it is clean and consistent. Clean does not mean with comments by the way - it means that people who are a bit familiar with what you are doing are able to understand without asking an AI assistant. 
  • Documentation. If you haven't done this yet (and let's be honest, you probably will not as this is not the most fun part) it's really time to get started on it. Get the documentation, the video and the installation documentation ready.
  • Testing. Not again? Yes, again. But this time, test your installation document. Test it on a clean environment just like the judges would. This will make sure they are able to install your product because if they cannot, you're out.
  • Check the requirements one last time to make sure you really have everything.  

You might think I am exaggerating again, and you might be right. But I remember those last hours as tiresome, stressful and the brain not doing everything the way it did half a day earlier...  but maybe since AI is doing half our work anyways things have changed ☺.  But whatever you do, do not underestimate the last part as it is really painful to crash just before the finish. 


TLDR

I noticed I wrote way too many text..  so in case you skipped it, here is the summary:
  • be prepared up front
  • read the requirements
  • don't eat 3 pizza's
  • don't forget to test thoroughly - including the installation
  • don't wait for the final bell to wrap up
  • and most of all: don't forget to enjoy it

Good luck !


Wednesday, November 12, 2025

Sitecore Forms and the Content Security Policy

Sitecore Forms and the Content-Security-Policy (CSP)

The situation

A site is using Sitecore XM/XP and Sitecore Forms and has implemented a Content Security Policy (CSP) header - which is a best practice. However, Sitecore Forms apparently does not really like these policies as they do tend to block stuff. We had a (rather big) content site that had made edits to their CSP - which is manageable in Sitecore so they can adapt it per site - based on a recent penetration test. After this extension (tightening) of the CSP we noticed the forms from Sitecore Forms were not working anymore. To be more precise: the form appeared fine, but the submit action was not working anymore when it included a "Redirect to Page" action. 

The error

Luckily the error was pretty clear in a browser console:
CSP error

The CSP header was blocking the script. 
The error mentions "Executing inline script violates the following Content Security Policy directive 'script-src ... Either the 'unsafe-inline' keyword, a hash ('sha256-Dcwc6bB3ob8DnpIRKtqhRwu0Wl6bkf7uLnQFk3g6bPQ='), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked."

The solution

As the code which outputs the inline script is in the Sitecore assemblies, it does not seem an option to add a nonce value. Adding unsafe-inline everywhere is also not a good option as that would lower the quality of the CSP dramatically. So we went for another option and tried to add this unsafe-inline only when there is a form on the page.

Adding unsafe-inline conditionally 

First of all we will add an indicator in the HttpContext to tell us whether there is a form on the page. This can be done in Form.cshtml (located in \Views\FormBuilder), which is the main cshtml file of Sitecore Forms. But as we are using SXA it can also be done in the SXA Forms wrapper. This is Sitecore Form Wrapper.cshtml (located in \Views\Shared) and as we already had some customization in this file to add translations (see my previous post on this topic) we added a few lines here:
var context = HttpContext.Current;
context.Items["WeHaveAForm"] = "Y";
You can name the context item whatever you want of course.

Now we need to act on this context item. Again, there are options. We already had some code that placed a CSP in the header based on a value set in Sitecore on the Site item. But if you do not, a generic solution would be to place it in the global_asax Application_EndRequest function.
var context = HttpContext.Current?.Items["WeHaveAForm"];
if (context != null && context.Equals("Y"))
{
    var csp = Response.Headers["Content-Security-Policy"];
    if (string.IsNullOrEmpty(csp))
    {
        return;
    }

    csp = csp.Replace("script-src", "script-src 'unsafe-inline'");
    var pattern = @"'nonce-[^']+'";
    csp = Regex.Replace(csp, pattern, string.Empty);
    Response.Headers.Set("Content-Security-Policy", csp);
}
As you can see we do just a little bit more here:
  1. We check if the item is present in the context and get out if it is not
  2. We check if we have a csp value - if not we don't need to do anything so we get out
  3. We add the 'unsafe-inline' part to the script-src, if it is present in the csp
  4. We remove the complete nonce if that is present
  5. We set the new value in the Content-Security-Policy header

It is important to also remove the nonce. When a CSP header includes both a nonce and unsafe-inline, the browser ignores the unsafe-inline for scripts or styles and uses the nonce to allow specific inline elements. So if we keep the nonce, the unsafe-inline addition will not do anything.

Conclusion

We fixed the redirects on the forms without adding unsafe-inline on all pages. I would assume that is the best solution we could find here. 

Monday, October 27, 2025

Sitecore Powershell Reporting

Sitecore Powershell Reporting

The Sitecore PowerShell Extensions (SPE) module has a lot of nice features. One of them is creating reports. You get quite some reports out-of-the-box with the module, but you can also create your own. 

I recently noticed however that people are still creating custom pages to create reports for the admins of the customer. Although this gives you a lot of flexibility and perhaps options to include data that resides not in Sitecore this also has some drawbacks compared to creating reports in PowerShell. 

The SPE reports are completely integrated in the Sitecore editing environment. This makes it possible to (amongst others):
  • use context items: the output in the report can be based upon the item it was requested on
  • open items in the editor: directly open the editor from the report
  • more future-proof - especially if you would move to a saas solution
Next to that - the SPE module comes with some very handy features like the Show-ListView which gives your admins a nice toolbox without any effort. 

But enough about the benefits of SPE - let's dive into the example I wanted to show here.

Redirect module

I assume many Sitecore developers have had the request to add some sort of redirect module, enabling editors to create redirects without intervention of IT-people having to change the rewrite instructions.

There used to be several modules floating around in the Sitecore community - but to be honest none of them was ever really perfect. SXA also has it's own implementation of redirects, and that had a different approach than most of the modules. Instead of having a repository of redirect items and using pipeline code to handle them, it is also an option to place the redirects in the tree where you actually want them.

This brings me to the request that led to this post: creating redirect in a non-SXA XM project in a way that they are easily handled by the admins. 

We decided to create a page template with a layout that redirect the page based on some values that can be edited inside the page (eg url, permanent redirect, ... ). You can actually go pretty far in this if you want but as that is not our main focus here let's keep it to that. The focus is here is on answering the question from the Sitecore admins:

"Where are all my redirects?"

Let's answer that question with Sitecore PowerShell. Note that we are using redirects here as an example and this can be done for any kind of data in Sitecore. Also note that out-of-the-box there already is a report to fetch all items based on a template within a folder. But that report is very generic and we wanted a fancy one. Of course, we will copy from this one to give us a head start.

We start with some functions to assist the actual reports.

Sitecore PowerShell Functions

Functions are a way to reuse parts of the code. We will use two functions for our report. The first one is to check whether the item is published to the web database. As many pieces of software, this one is just a copy. But as we in the Sitecore community are honest, we mention our sources. Kudos to Gabriel Streza for this one.

Get-IsPublished
function Get-IsPublished {
    [CmdletBinding()]
    param( 
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [Sitecore.Data.Items.Item]$Item        
    )
   
    $WebDbItem = Get-Item web: -Id $Item.ID

    if ($null -ne $WebDbItem) {
        return $true
    }else{
        return $false
    }
}
I assume this one is fairly easy and doesn't need more explanation. 

The second function will fetch the items and show the report. This is actually the core of the whole thing. 

Show-RedirectReport
function Show-RedirectReport{ 
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, Position=0)]
        [string]$templateId,
        [Parameter(Mandatory=$true, Position=1)]
        [string]$path
    )
        
	$items = Find-Item -Index sitecore_master_index `
	   -Criteria @{Filter = "Equals"; Field = "_template"; Value = "$templateId"},
	   @{Filter = "Equals"; Field = "_language"; Value = "en"},
	   @{Filter = "StartsWith"; Field = "_fullpath"; Value = "$path" }  | Initialize-Item

        Import-Function Get-IsPublished

	if($items.Count -eq 0) {
		Show-Alert "There are no redirects here."
	} else {
	    $props = @{
		Title = "Redirect Report"
		PageSize = 25
	    }
		
	    $items |
		Show-ListView @props -Property @{Label="Name"; Expression={$_.DisplayName} },
	    	    @{Label="Updated"; Expression={$_.__Updated} },
		    @{Label="Updated by"; Expression={[Sitecore.Security.Accounts.User]::FromName($_."__Updated by", $false).Profile.FullName}},
		    @{Label="Path"; Expression={$_.ItemPath} },
		    @{Label="Target - Url"; Expression={$link = $_._.RedirectUrl
		        if ($link.IsInternal) { $link.TargetItem.Paths.Path } else { $link.Url }} },
		    @{Label="Permanent"; Expression={$_._.Permanent.Checked} },
		    @{Label="Published"; Expression={Get-IsPublished -Item $_ } }
	}
}
We will go a little deeper into this one. It has two parts.
First of all we are fetching items through the master index. The function expects a templateID and a path and we will use those to fetch all items of that template in that path (in English).

If we found some items, we are using Show-ListView to display the report. Most of the fields we are showing are quite common but a few deserve some attenion:
  • Updated by: we are using the security account here to fetch the full name of the profile instead of the Sitecore login name as that might be much more readable. Kudos to the one and only Adam Najmanowicz for this one.
  • Target: this can be an internal or external link so we use the "._" notation (short for .PSFields) to access the typed field and check what we need to show
  • Permanent: again using ._ to handle this as a checkbox
  • Published: using our Get-Published function here


The report

Once we have our two functions, the report itself is very easy. We need to collect the parameters to pass to the report function and we should be ok.
$templatePath = "master:\templates\xxx\Shared\Redirect"
$baseTemplate = Get-Item -Path $templatePath
$templateId = $baseTemplate.ID
$path = "/sitecore/content/Sites"

Import-Function Show-RedirectReport
Show-RedirectReport -templateId $templateId -path $path

Close-Window
As this report is intended to be used for one specific template we can hardcode that. We did the same for the base path for now - it's the root path for all content in this case.

Location
In our Sitecore PowerShell module we create (or go to) a folder Reports: "/sitecore/system/Modules/PowerShell/Script Library/xxx/Reports". Here we create a PowerShell Script Library that will be the folder in the reporting tools section. In that folder we place our PowerShell Script - "Redirects". That will result as shown here:




This is nice - we have our report together with the other reports. But we mentioned before that we can make it context aware. Of course, we could ask for a start item in a dialog box. But we can also use a very similar report in the context menu. 

The context menu

To add the report to the context menu and use the context item as the start path, we simply add a new script to our library.
$templatePath = "master:\templates\xxx\Shared\Redirect"
$baseTemplate = Get-Item -Path $templatePath
$templateId = $baseTemplate.ID
$contextItem = Get-Item .
$path = $contextItem.Paths.FullPath

Import-Function Show-RedirectReport
Show-RedirectReport -templateId $templateId -path $path

Close-Window

Not many differences compared to what we already had - just the path is now coming from the context item ".".

Location














We are placing the new script in the folder "Content Editor/Context Menu" in our SPE module. But note that in this case we don't only take care of the location as this location will add the script everywhere but we want to limit it a little bit. We can use the Show Rule to define where the script will be shown in the context menu. In our case, we limit it to items of 2 specific templates but you can use any rule here.

This will look like this:



The final result

I want to show a screenshot of the final result as well - especially for those people who are not so familiar with Sitecore PowerShell yet. For others it will look very familiar - but that doesn't make it less nice :) 


In conclusion I would remind you once more that if you need to bring reports to your editors or admins, think about Sitecore PowerShell. It is a really cool tool that will bring you the result you need. And on top of that you can find a lot of resources already from Sitecore community people that have shared scripts and/or snippets to give you ideas and help you write your own scripts.  And maybe this one extra post helps someone as well.  





One small final note: use this script in the desktop mode...

Thursday, September 18, 2025

EasyLingo 3.0

 EasyLingo 3.0 - update for Sitecore 10.4


Years ago I created (together with Kris - Kevin - Verheire) a module for XM/XP customers that had multiple languages in their site and wanted an easy overview of which language versions existed on every page. To be honest, I almost forgot about this module as my active customers had stopped using it but this year I was asked to consult on an upgrade project for a customer towards Sitecore 10.4 - and this customer was still using the module.

I wrote posts about the module when they got released, and the code is available on Github:

Sitecore 10.4

Apparently the latest version of the module was not compatible with Sitecore 10.4. So I had to make a few adjustments and released version 3.0 - which now again is compatible. 

Experience editor

One problem though - this release seemed to break the experience editor in a weird way. I think it is related to the way some custom javascript code was inserted , but as I am not a javascript expert I might be wrong. Anyway, I couldn't get it fixed so version 3.0.1 was released without XP editor support. 

For the customer that was no issue as they didn't use the functionality in that editor that much and time was running out so I'm afraid that for now this will be it. 

Due to circumstances this issue is still not fixed - but as the code is openly available anyone can jump  in and contribute if you want. 



I assumed (and still do) that the module was very close to the end of it's lifetime, but I was also glad to see that at least some people are actually still using it after all those years.  If you still have a customer on XM/XP with multiple languages, feel free to have a look. And fix my xp editor issue if you can 😉