Friday, June 26, 2026

One decade as a Sitecore MVP

One decade as a Sitecore MVP


What it really means to be part of this amazing community

This year I received my 10th consecutive Sitecore MVP award. I'm still filled with gratitude for the this recognition and the incredible journey it has been. Joining the ranks of a fantastic group of professionals who have achieved this milestone feels surreal, but it also triggered me to share my thoughts on what the "Sitecore Community" and it's MVPs truly represent and why it matters so much to the Sitecore ecosystem.

The Sitecore MVP Program

The Sitecore Most Valuable Professional (MVP) program started in 2008. Two community giants, Klaus Petersen and Mark Cassidy, from that year are still MVP now.  In 2026, just over 200 professionals were honored across three categories: as usual most in Technology and a bit less in Strategy and Ambassador.

A Sitecore MVP is an individual with expertise in Sitecore who actively participate in online and offline communities to share their knowledge and expertise with other Sitecore partners and customers.

But here's what many people don't understand: being an MVP isn't about being the best developer or the most skilled employee at your company. Although most MVPs are strong developers or architects, it's not those skill that get you the recognition. MVPs are community builders and passionate advocates, who share their knowledge and made themselves valuable to the broader Sitecore community through consistent contributions during the past year. The award recognizes actively sharing expertise, helping other community members solve problems, and by doing this contributing to the collective knowledge  base and strengthening the Sitecore community ecosystem.


The true meaning behind the award

Let me be clear about something important: you don't become an MVP for money, career advancement, or personal gain. Although some employers (and maybe even customers) like to see this on your resume and might reward you for the efforts. But if you are considering it, always remember that without a genuine desire to give back to the community it is not going to work. This is the way the community stays alive and that is where the recognition comes from. MVPs are evaluated based on the quality, quantity, and impact of their contributions, not their job title or technical skills.


The vibrant Sitecore community ecosystem

There are numerous blogs and videos related to Sitecore out there. But the community is richer than just blogs - next to some more official site at https://community.sitecore.com it has some (well known) online channels on Sitecore Slack for real-time discussions and problem-solving and Sitecore Stack Exhange as a dedicated Q&A hub with a vast number op people to help you solve your issues. Both are created and maintained by volunteers from the community and have helped lots of people with their Sitecore questions.

What makes the Sitecore community special is the rich ecosystem of resources and events that bring us together. Next to the online channels people are also organizing user groups. Although I do get the feeling the number of groups and sessions is declining (e.g. my local one stopped) there are still quite a few that do meet - in person or sometimes virtual, making them available to more people globally. Time zones are sometimes painful, but it can be fun to join them and meet new people. Due to several reasons it's been a while, but I know I enjoyed speaking at the user groups in Perth, Pakistan and Hungary. 

A very special "user group" is SUGCON. You can't call this a user group anymore, it's a full blown annual conference - but still organized by community members (with a little help from Sitecore, obviously). We just had this year's events in London (Europe) and Delhi (India). If you ever get the opportunity to join the conference, do not hesitate. Join, feel the vibe, learn, talk to people, make connections and experience the Sitecore community. They will welcome you with open arms and you'll end up with new or renewed connections all over the world.  

It's impossible to mention every initiative and I am sure there are plenty that I even don't know about. I do wan to mention the #SitecoreLunch gatherings. The timing of the informal gathering is difficult for me, but it does seem to be a cheerful and fun bunch of people so I can only encourage you to try it out. One more that cannot be forgotten is the annual Sitecore Hackathon.  Every year a number of teams from all around the world try to create something amazing in 24h. And every year some of those teams really succeed in delivering true added value - free to the community. On https://sitecorehackathon.org/ you can check the entries, view photos...  or subscribe for the next edition (subscription is usually in January).


How to become a Sitecore MVP

The path to MVP recognition is straightforward but requires dedication. The program is open to anyone working with the Sitecore platform who has a passion for sharing knowledge. This question was asked and answered on Sitecore StackExchange so I will not repeat everything mentioned there.

Practically, know that applications used to open in November, with reviews happening in December and announcements in late January. They changed the "open" window though so now you can create you application anytime and add data all year long up until the deadline. If you decide to apply, please do take it seriously. Yes, the community will welcome everyone and does like new MVPs - but we also need to review all the applications. So if your application is anything like "I want to be MVP so next year I will... " or "I worked hard on all my Sitecore projects but have actually not shared anything with the community" then I would advice you to stop already and try again and better next year.

What matters is consistent and meaningful contributions to the community. Not your projects. Not your ambitions. But if you are really willing to do this, we will be very happy to have you on board. And do not give up - it took me a few years as well to get my first MVP title and look at me now...  The path to becoming an MVP starts with a single contribution.


The benefits of being an MVP

While the intrinsic reward of helping others is the primary motivation, the MVP program does offer some tangible benefits.

As mentioned before, you will not see it directly on your bank account. Although you do get discounts on certain events like Sugcon, an invitation to the Sitecore MVP Summit (usually in the US though - which might make it difficult for people nowadays) and if you do attend in-person events you will probably notice some people wearing an MVP t-shirt and/or sweater. 

There is the global recognition and the inclusion in the public MVP directory, and it does feel nice to be amongst that group of inspiring professionals. 

But in my opinion, the biggest advantage is the exclusive access to early product information and releases and especially the direct communication with Sitecore teams. Becoming an MVP opened many doors for me. I remember jumping on the SXA train and having very frequent calls with Adam Najmanowicz and his team. Many more very interesting and meaningful encounters with various people within Sitecore followed. By attending MVP webinars you connect with the right people. And if you do have questions, remarks, feedback or suggestions you do know who to talk to. And even though they might listen anyhow, that MVP tag on your name does make the Sitecorians listen even better. 


One decade  

An now I can look back at my 10-year journey as an MVP. It has brought to many people and places. I am still honored, not only to be MVP but also to be speaker on lots of Sitecore events like Sugcon and the Sitecore Symposium. I have seen Sitecore evolve from a solid Danish CMS (I started on v5 by the way) to a full blown worldly DXP to a headless CMS with a bunch of additional services. The products have changed, the organization behind it as well. The community also changed - I've seen it grow and shrink back a bit. But it still feel very good when I see many familiar faces again at a Sitecore event.  The relationships I've built, the knowledge I've gained, and the satisfaction of helping others have been immeasurable.

Joining the group of professionals who have achieved 10 (consecutive) MVP awards feels like joining an exclusive club of individuals who have demonstrated unwavering commitment to our community's success.

All those years in the community have brought me a lot - professionally of course, but I was also able to meet people from all around the world. Close to home in Europe, in both North and South America but also in Africa, Asia and even Australia. Connecting with people from all those different background and cultures makes you grow as a person - and is fun too. Attending the Sitecore events also brought me to several wonderful places. And even though it actually is working - especially when you're speaker - it also is a tremendous experience. I still remember (ok, vaguely) the party in Vegas, strolling through New Orleans, Harry Potter in Orlando but even eating kebab at 3AM at the beach in Malaga or guiding fellow community members through my (almost-home) town Ghent. And the week after the event, we are back solving each other problems again...


Changing of the guard

As we look toward the future, it's important to acknowledge the leader/mentor that has guided our community. Tamas Varga, who has served as Director of Community Programs at Sitecore and led the MVP program since joining the company in 2016, will be stepping away from the program later in 2026 to finally fulfill his sailing dream. Tamas himself was an MVP, and his leadership has been instrumental in shaping the program we know today. During Sugcon Europe 2026 he was awarded the Honorary Sitecore MVP title. Well deserved!

Sebastian Winter is expected to take over this important role, ensuring continuity in the program's excellence and community focus. Knowing Sebastian I am quite confident he will do an excellent job as he, as a former MVP as well, understands the value and true power of our community. 


The real value of community

At its core, the Sitecore MVP program represents something beautiful: a group of professionals who believe that sharing knowledge makes everyone stronger. We're not competing against each other; we're collaborating to push the entire ecosystem forward. But remember it's not all about the MVPs. It's about all of you, all community members - and all Sitecorians that engage with us.

This community is here for you, whether you're just starting your Sitecore journey or you're a seasoned professional. Engage online. Attend meetings and events. Share your experiences, thoughts, remarks or anything that you feel might be useful to others. And don't be afraid to ask questions. 

The Sitecore community has given me more than I could have ever imagined when I started this journey more than 10 years ago. So I'm not stopping here. And I hope you are not either. 

The community is waiting to welcome you.

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...