<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Søren Kottal</title>
    <description>The latest articles on DEV Community by Søren Kottal (@skttl).</description>
    <link>https://dev.to/skttl</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F27865%2F0834f766-c388-4f18-a40c-1fc69b6d96da.jpg</url>
      <title>DEV Community: Søren Kottal</title>
      <link>https://dev.to/skttl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/skttl"/>
    <language>en</language>
    <item>
      <title>Customizing Umbraco ModelsBuilder Output</title>
      <dc:creator>Søren Kottal</dc:creator>
      <pubDate>Mon, 02 Feb 2026 11:33:05 +0000</pubDate>
      <link>https://dev.to/skttl/customizing-umbraco-modelsbuilder-output-1243</link>
      <guid>https://dev.to/skttl/customizing-umbraco-modelsbuilder-output-1243</guid>
      <description>&lt;p&gt;Umbraco's ModelsBuilder is a fantastic tool that generates strongly-typed models from your document types. But what if you want to customize the generated output? Maybe you want to add property alias constants or other custom code to your models.&lt;/p&gt;

&lt;p&gt;In this article, I'll show you how to create a custom &lt;code&gt;ModelsGenerator&lt;/code&gt; that post-processes the generated files to add your own customizations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;When working with Umbraco, you often need to reference property aliases as strings - for example, when using &lt;code&gt;SetValue()&lt;/code&gt; while creating or editing content through the ContentService, or maybe you need the dynamic property access from &lt;code&gt;IPublishedContent&lt;/code&gt;. This means scattering magic strings throughout your codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// setting a property value on IContent&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pageTitle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"New Title"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// checking for a property on IPublishedContent&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"featuredImage"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a property alias changes, you'll need to hunt down every occurrence. It would be much better to have strongly-typed constants for each property alias, so you get compile-time safety and IntelliSense support.&lt;/p&gt;

&lt;p&gt;ModelsBuilder does provide a way to get the property aliases from the document type, but it is rather verbose, and it requires a dependency on &lt;code&gt;IPublishedContentTypeCache&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;@using&lt;/span&gt; &lt;span class="n"&gt;Umbraco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PublishedCache&lt;/span&gt;
&lt;span class="n"&gt;@inject&lt;/span&gt; &lt;span class="n"&gt;IPublishedContentTypeCache&lt;/span&gt; &lt;span class="n"&gt;_contentTypeCache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;blocksPropertyAlias&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ContentPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetModelPropertyType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_contentTypeCache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Blocks&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="n"&gt;Alias&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;We can create a custom &lt;code&gt;ModelsGenerator&lt;/code&gt; that runs after the standard generation and post-processes the output files. This approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doesn't require modifying Umbraco's source code&lt;/li&gt;
&lt;li&gt;Works with the existing ModelsBuilder pipeline&lt;/li&gt;
&lt;li&gt;Allows for any kind of text manipulation on generated files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the full implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Hosting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text.RegularExpressions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Umbraco.Cms.Core.Configuration.Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Umbraco.Cms.Infrastructure.ModelsBuilder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Umbraco.Cms.Infrastructure.ModelsBuilder.Building&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Umbraco.Extensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;MyProject.ModelsGenerators&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyOwnModelsGenerator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;UmbracoServices&lt;/span&gt; &lt;span class="n"&gt;umbracoService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IOptionsMonitor&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ModelsBuilderSettings&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;OutOfDateModelsStatus&lt;/span&gt; &lt;span class="n"&gt;outOfDateModels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IHostEnvironment&lt;/span&gt; &lt;span class="n"&gt;hostingEnvironment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;ModelsGenerator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;umbracoService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outOfDateModels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hostingEnvironment&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;IModelsGenerator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;GenerateModels&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GenerateModels&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;modelsDirectory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ModelsDirectoryAbsolute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hostingEnvironment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelsDirectory&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelsDirectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"*.generated.cs"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fileContents&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;newFileContents&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;AddPropertyAliasConstants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fileContents&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newFileContents&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;fileContents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newFileContents&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;AddPropertyAliasConstants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;fileContents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"public new const string ModelTypeAlias ="&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fileContents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fileContents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;@"\[.*ImplementPropertyType\(""([^""]+)""\)\][\s\S]*?public\s+(?:virtual\s+)?[\w\.:&amp;lt;&amp;gt;]+\s+([A-Za-z0-9_]+)\s*(?:{|=&amp;gt;)"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fileContents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StringBuilder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"public new static class ModelPropertyAliases\n\t\t{\n"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Match&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="k"&gt;alias&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;propName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"\t\t\tpublic const string &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;propName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; = \"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\";\n"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\t\t}\n\n\t\t"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fileContents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The custom generator does a few key things:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Inheriting from ModelsGenerator
&lt;/h3&gt;

&lt;p&gt;By inheriting from &lt;code&gt;ModelsGenerator&lt;/code&gt; and implementing &lt;code&gt;IModelsGenerator&lt;/code&gt;, we can override the generation behavior while still using all the built-in functionality.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyOwnModelsGenerator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ModelsGenerator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IModelsGenerator&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Post-Processing Generated Files
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;GenerateModels()&lt;/code&gt; method first calls &lt;code&gt;base.GenerateModels()&lt;/code&gt; to run the standard generation, then iterates through all &lt;code&gt;*.generated.cs&lt;/code&gt; files to apply our customizations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;GenerateModels&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GenerateModels&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;modelsDirectory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ModelsDirectoryAbsolute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_hostingEnvironment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelsDirectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"*.generated.cs"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Post-process each file...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Adding Property Alias Constants
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;AddPropertyAliasConstants&lt;/code&gt; method uses regex to find all properties marked with &lt;code&gt;[ImplementPropertyType]&lt;/code&gt; and extracts both the alias and property name. It then generates a nested static class containing constants for each property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ModelPropertyAliases&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;PageTitle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"pageTitle"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;FeaturedImage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"featuredImage"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"blocks"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Registering the Custom Generator
&lt;/h2&gt;

&lt;p&gt;To use your custom generator, you need to register it with Umbraco's dependency injection. Add this to your startup configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IModelsGenerator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MyOwnModelsGenerator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if you're using a composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ModelsGeneratorComposer&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IComposer&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Compose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IUmbracoBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IModelsGenerator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MyOwnModelsGenerator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using the Generated Constants
&lt;/h2&gt;

&lt;p&gt;Once the models are regenerated, you can use the constants throughout your code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: magic strings&lt;/span&gt;
&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pageTitle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"New Title"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// After: strongly-typed constants&lt;/span&gt;
&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelPropertyAliases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PageTitle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"New Title"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Works great in LINQ queries too&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;featured&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Children&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelPropertyAliases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FeaturedImage&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now if a property alias changes, you'll get a compile-time error instead of a runtime surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Customizing ModelsBuilder output gives you more control over your generated code without fighting against the built-in tooling. By post-processing the generated files, you can add compile-time safety features like property alias constants while keeping all the benefits of automatic model generation.&lt;/p&gt;

&lt;p&gt;The approach shown here - inheriting from &lt;code&gt;ModelsGenerator&lt;/code&gt; and processing files after generation - is clean, maintainable, and survives Umbraco upgrades since it doesn't modify any core code.&lt;/p&gt;

</description>
      <category>umbraco</category>
    </item>
    <item>
      <title>Enhanced Event Logs and Deployment Monitoring for Kudu in Umbraco Cloud</title>
      <dc:creator>Søren Kottal</dc:creator>
      <pubDate>Mon, 26 Jan 2026 09:11:18 +0000</pubDate>
      <link>https://dev.to/skttl/enhanced-event-logs-and-deployment-monitoring-for-kudu-in-umbraco-cloud-i66</link>
      <guid>https://dev.to/skttl/enhanced-event-logs-and-deployment-monitoring-for-kudu-in-umbraco-cloud-i66</guid>
      <description>&lt;p&gt;When working with Umbraco Cloud, I occasionally need to access Kudu for debugging and deployment tasks. You can access Kudu by clicking the "Kudu" button in your site's dashboard, or directly via the URL: &lt;code&gt;https://&amp;lt;site-name&amp;gt;.scm.&amp;lt;cloud-region&amp;gt;.umbraco.io&lt;/code&gt; (where the cloud region might be &lt;code&gt;euwest01&lt;/code&gt;, for example).&lt;/p&gt;

&lt;p&gt;When a site fails to start or a deployment fails without clear errors, the event log in Kudu can provide crucial insights. However, the raw experience leaves much to be desired.&lt;/p&gt;

&lt;p&gt;To solve this problem, I created two userscripts that enhance the Kudu interface.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is a userscript?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A userscript is a program, usually written in JavaScript, for modifying web pages to augment browsing. Uses include adding shortcut buttons and keyboard shortcuts, controlling playback speeds, adding features to sites, and enhancing the browsing history.&lt;/p&gt;

&lt;p&gt;To use a userscript, you need a userscript manager. There are several available, but the most popular is &lt;a href="https://www.tampermonkey.net/" rel="noopener noreferrer"&gt;Tampermonkey&lt;/a&gt;. Tampermonkey is available for Chrome, Firefox, and Edge.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Visualizing Event Logs
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzga0elryhsaqshmfcuul.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzga0elryhsaqshmfcuul.png" alt="Screenshot of the Event Log Viewer provided by the Kudu Event Log Viewer userscript" width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First thing to look at is the event log of the site. The event log contains information from IIS, and tells you any problems when starting the site. It can also contain information about failed deployments, and other issues.&lt;/p&gt;

&lt;p&gt;But, it's not the most user-friendly experience. It's a raw XML file, and you have to manually parse it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The event log is located at &lt;code&gt;/LogFiles/eventlog.xml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;It can be a massive XML file with thousands of entries&lt;/li&gt;
&lt;li&gt;The most recent (and relevant) information is at the bottom&lt;/li&gt;
&lt;li&gt;There's no syntax highlighting or formatting&lt;/li&gt;
&lt;li&gt;You have to manually parse XML in your head&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This userscript adds a new "Event Log" tab to the Kudu interface, providing a much better experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parsed and formatted&lt;/strong&gt;: Events are displayed in a clean table format&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most recent first&lt;/strong&gt;: The latest events appear at the top (no more scrolling)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Color-coded&lt;/strong&gt;: Each event type has its own color for quick identification&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured columns&lt;/strong&gt;: Event time, type, message, and source are clearly separated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The event log helps you diagnose issues like missing DLLs, IIS startup failures, configuration problems, and data-related errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to use it
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install the Kudu Event Log Viewer userscript from my &lt;a href="https://github.com/skttl/umbraco-userscripts" rel="noopener noreferrer"&gt;umbraco-userscripts&lt;/a&gt; repository&lt;/li&gt;
&lt;li&gt;Navigate to your Umbraco Cloud Kudu interface (e.g., &lt;code&gt;*.scm.euwest01.umbraco.io&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Click the "Event Log" link in the navbar&lt;/li&gt;
&lt;li&gt;Click "Load Event Log" to fetch and display events&lt;/li&gt;
&lt;li&gt;Events are displayed in Bootstrap panels with full details&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Tracking Deployments
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmsxb6l114rg30pcceh2q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmsxb6l114rg30pcceh2q.png" alt="Screenshot of the Deployment Viewer provided by the Umbraco Cloud Deployment Viewer userscript" width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When using CI/CD with Umbraco Cloud, deployment visibility is limited compared to traditional git push deployments. You lose the immediate feedback from git logs about build status and deployment progress.&lt;/p&gt;

&lt;p&gt;Normally, when a deployment fails, you need to manually navigate through files in &lt;code&gt;/site/deployments&lt;/code&gt; to find:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which deployment is currently active&lt;/li&gt;
&lt;li&gt;Individual deployment folders with logs and status files&lt;/li&gt;
&lt;li&gt;Scattered information across multiple locations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Deployment Viewer userscript consolidates all this information into a single dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deployment status&lt;/strong&gt;: See the current state of all deployments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live logs&lt;/strong&gt;: View logs for each deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-refresh&lt;/strong&gt;: Enable live updates to monitor deployments in real-time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual triggers&lt;/strong&gt;: Start a new deployment without making a git change&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How to use it
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install the Umbraco Cloud Deployment Viewer userscript from my &lt;a href="https://github.com/skttl/umbraco-userscripts" rel="noopener noreferrer"&gt;umbraco-userscripts&lt;/a&gt; repository&lt;/li&gt;
&lt;li&gt;Navigate to your Umbraco Cloud Kudu interface (e.g., &lt;code&gt;*.scm.euwest01.umbraco.io&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Click the "Deployments" link in the navbar&lt;/li&gt;
&lt;li&gt;View the latest deployment status and log&lt;/li&gt;
&lt;li&gt;Click "Auto-refresh" to enable live log and status updates&lt;/li&gt;
&lt;li&gt;Click "Trigger New Deployment" to start a new deployment&lt;/li&gt;
&lt;li&gt;Click on any deployment in the history table to view full details in a modal&lt;/li&gt;
&lt;li&gt;Click file counts to view the deployment manifest&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;While Umbraco Cloud's dashboard provides excellent visibility for most scenarios, these userscripts fill important gaps when you need to dig deeper. They transform Kudu from a basic file browser into a powerful debugging and deployment monitoring tool.&lt;/p&gt;

&lt;p&gt;Both userscripts are available on &lt;a href="https://github.com/skttl/umbraco-userscripts" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Give them a try the next time you're troubleshooting a deployment issue or investigating a site failure.&lt;/p&gt;

</description>
      <category>umbraco</category>
      <category>kudu</category>
      <category>userscripts</category>
    </item>
    <item>
      <title>Make Umbraco’s Welcome Dashboard Your Own</title>
      <dc:creator>Søren Kottal</dc:creator>
      <pubDate>Mon, 19 Jan 2026 07:26:41 +0000</pubDate>
      <link>https://dev.to/skttl/make-umbracos-welcome-dashboard-your-own-i1a</link>
      <guid>https://dev.to/skttl/make-umbracos-welcome-dashboard-your-own-i1a</guid>
      <description>&lt;p&gt;Umbraco comes with a default Welcome dashboard in the backoffice. It displays news from Umbraco HQ along with handy links to documentation, training, and other community resources.&lt;/p&gt;

&lt;p&gt;That’s great for developers, but when you’re building Umbraco sites for clients, those links are often not the most relevant. In fact, the traditional recommendation has often been to simply remove the Welcome dashboard entirely.&lt;/p&gt;

&lt;p&gt;But with the current backoffice (which I should probably stop calling &lt;em&gt;new&lt;/em&gt; by now), there are plenty of extension points - and the Welcome dashboard is one of them.&lt;/p&gt;

&lt;p&gt;So instead of removing it, why not use it for your own content?&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending the Welcome dashboard
&lt;/h2&gt;

&lt;p&gt;The Welcome dashboard is powered by a service in the backend, and you can replace or extend its behaviour by implementing the &lt;code&gt;INewsDashboardService&lt;/code&gt; interface in your own C# code.&lt;/p&gt;

&lt;p&gt;Once you do that, Umbraco will use your implementation to populate the dashboard.&lt;/p&gt;

&lt;p&gt;All you need to return is a &lt;code&gt;NewsDashboardResponseModel&lt;/code&gt;, which contains a collection of items to display. Each item supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A header&lt;/li&gt;
&lt;li&gt;Body text&lt;/li&gt;
&lt;li&gt;An image (URL + alt text)&lt;/li&gt;
&lt;li&gt;A link URL&lt;/li&gt;
&lt;li&gt;Button text&lt;/li&gt;
&lt;li&gt;A priority (High, Medium, or Low)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dashboard will automatically sort the items by priority before rendering them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A (slightly useless) example implementation
&lt;/h2&gt;

&lt;p&gt;As a simple demonstration, here’s an intentionally useless implementation that displays the weather from 12 random locations on Earth every time the dashboard loads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text.Json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Umbraco.Cms.Api.Management.Services.NewsDashboard&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Umbraco.Cms.Api.Management.ViewModels.NewsDashboard&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UselessNewsDashboardService&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;INewsDashboardService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;_httpClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AppCaches&lt;/span&gt; &lt;span class="n"&gt;_appCaches&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;GiNewsDashboardService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AppCaches&lt;/span&gt; &lt;span class="n"&gt;appCaches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_httpClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_appCaches&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;appCaches&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NewsDashboardResponseModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetItemsAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// fetch some data - this depends on your usecase!&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;locations&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_appCaches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RuntimeCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCacheItemAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"randomWeather"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;GetWeatherFromRandomLocationsAsync&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

        &lt;span class="c1"&gt;// build the response model&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NewsDashboardResponseModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;locations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NewsDashboardItemResponseModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;Header&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"High"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// or Medium or Low&lt;/span&gt;
                &lt;span class="n"&gt;Url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;ImageUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ImageUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShortDescription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;ButtonText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Go to website"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// below are methods for communicating with the weather API &lt;/span&gt;
    &lt;span class="c1"&gt;// - you don't need these&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WeatherResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetWeatherAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// this is just to prevent 529 Too many requests&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="s"&gt;$"https://api.open-meteo.com/v1/forecast"&lt;/span&gt;
            &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;$"?latitude=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;longitude=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;$"&amp;amp;current_weather=true"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetStringAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RootElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"current_weather"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;GetDouble&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;weatherCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"weathercode"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;GetInt32&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weatherCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;imageUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetImageUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weatherCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WeatherResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;°C"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ShortDescription&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$"Weather at &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ImageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$"https://www.open-meteo.com/en/forecast?latitude=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;longitude=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;WeatherResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ShortDescription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ImageUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Clear sky"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Partly cloudy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="m"&gt;45&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;48&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Fog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="m"&gt;51&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;53&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;55&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Drizzle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="m"&gt;61&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;63&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;65&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Rain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="m"&gt;71&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;73&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;75&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Snow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="m"&gt;80&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;81&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;82&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Rain showers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="m"&gt;95&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Thunderstorm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Unknown weather"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetImageUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"https://cdn-icons-png.flaticon.com/512/869/869869.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"https://cdn-icons-png.flaticon.com/512/1163/1163661.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="m"&gt;61&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;63&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;65&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"https://cdn-icons-png.flaticon.com/512/3351/3351979.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="m"&gt;71&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;73&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;75&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"https://cdn-icons-png.flaticon.com/512/642/642102.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="m"&gt;95&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"https://cdn-icons-png.flaticon.com/512/1146/1146869.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"https://cdn-icons-png.flaticon.com/512/1779/1779940.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You also have to register the service with a Composer, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Umbraco.Cms.Core.Composing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UselessNewsDashboardComposer&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IComposer&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Compose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IUmbracoBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;INewsDashboardService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UselessNewsDashboardComposer&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there you have it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frl5tzrs373it81y3c5pb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frl5tzrs373it81y3c5pb.png" alt="A useless news dashboard in Umbraco showing weather information from random places on Earth" width="800" height="841"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This example obviously isn’t very useful - but it shows how little code is required to take control of the dashboard content.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical use case
&lt;/h2&gt;

&lt;p&gt;Where this does become useful is for agencies and teams building Umbraco sites for clients.&lt;/p&gt;

&lt;p&gt;Instead of showing Umbraco HQ news, you can use the Welcome dashboard to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Share important project updates&lt;/li&gt;
&lt;li&gt;Announce new features or releases&lt;/li&gt;
&lt;li&gt;Provide onboarding instructions&lt;/li&gt;
&lt;li&gt;Link to internal documentation or support channels&lt;/li&gt;
&lt;li&gt;Communicate maintenance windows or known issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, it becomes a direct communication channel inside your client’s CMS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Rather than removing the Welcome dashboard altogether, extending it allows you to turn an otherwise generic screen into something genuinely valuable for your clients.&lt;/p&gt;

&lt;p&gt;With just a small custom service, you can make the Umbraco backoffice feel more tailored, more informative, and more aligned with the people actually using it.&lt;/p&gt;

</description>
      <category>umbraco</category>
    </item>
    <item>
      <title>Readable, simple Chinese URL segments in Umbraco with NPinyin</title>
      <dc:creator>Søren Kottal</dc:creator>
      <pubDate>Fri, 09 Jan 2026 08:29:17 +0000</pubDate>
      <link>https://dev.to/skttl/readable-simple-chinese-url-segments-in-umbraco-with-npinyin-ajp</link>
      <guid>https://dev.to/skttl/readable-simple-chinese-url-segments-in-umbraco-with-npinyin-ajp</guid>
      <description>&lt;p&gt;On a recent project, the client had a Chinese version of their site. The texts and titles were, of course, written using Chinese characters, which meant that the URLs were automatically generated using those characters as well.&lt;/p&gt;

&lt;p&gt;For SEO and UX reasons, we wanted the URLs to use Latin characters instead. While search engines are perfectly capable of indexing Chinese URLs, we felt that Latin URLs would perform better in practice.&lt;/p&gt;

&lt;p&gt;Our reasoning was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Latin URLs are more readable (especially since the audience was not exclusively Chinese), which we assumed would lead to higher click-through rates&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hyphenated Latin words provide clearer URL keyword signals&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sharing URLs and linking to them is much easier with Latin characters than with encoded Chinese characters&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, a title like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;击掌！你太棒了！
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;would normally generate a URL segment using Chinese characters. With pinyin conversion applied, it instead becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/ji-zhang-ni-tai-bang-le
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The latter is much easier to read, share, and reason about, especially in mixed-language environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Converting Chinese characters to Latin using pinyin
&lt;/h2&gt;

&lt;p&gt;Umbraco already tries to convert special characters when generating URLs, but the default character replacement set is quite limited. You can see the default configuration in the request handler settings in the documentation article about &lt;a href="https://docs.umbraco.com/umbraco-cms/reference/configuration/requesthandlersettings" rel="noopener noreferrer"&gt;Request Handler Settings&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Because of the nature of Chinese characters, having more than 50,000 of them, mapping each character to a Latin equivalent is not feasible. Instead, we can use pinyin, which is a Latin-based phonetic transcription system for Chinese characters.&lt;/p&gt;

&lt;p&gt;Using pinyin allows us to convert Chinese text into readable Latin equivalents without maintaining huge character maps. There is an open-source NuGet package called &lt;a href="https://www.nuget.org/packages/NPinyin.Core" rel="noopener noreferrer"&gt;NPinyin.Core&lt;/a&gt; that makes this conversion straightforward. It’s lightweight and integrates cleanly into existing logic.&lt;/p&gt;

&lt;p&gt;This approach also works well for mixed-language titles, such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;击掌！You Rock!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which results in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/ji-zhang-you-rock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using a custom URL segment provider in Umbraco
&lt;/h2&gt;

&lt;p&gt;With the conversion logic in place, the next step was to make Umbraco use it when generating URL segments.&lt;/p&gt;

&lt;p&gt;When Umbraco generates URL segments, it runs through a set of URL segment providers, and it’s possible to create a custom one. The process is simple and &lt;a href="https://docs.umbraco.com/umbraco-cms/reference/routing/request-pipeline/outbound-pipeline#url-segment-provider" rel="noopener noreferrer"&gt;well documented&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By adding a small amount of additional logic to separate Chinese characters with dashes, we ended up with a short and simple custom segment provider that generates readable, SEO-friendly Latin URLs for Chinese pages.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;** Note: **&lt;br&gt;
This approach converts each Chinese character to its pinyin representation and inserts dashes between characters. While this is not full linguistic word segmentation, it produces consistent, readable URL segments without adding unnecessary complexity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Example implementation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NPinyin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Umbraco.Cms.Core.Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Umbraco.Cms.Core.Strings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Project.Core.Urls&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Custom URL segment provider that wraps Umbraco's default provider&lt;/span&gt;
&lt;span class="c1"&gt;// and applies Chinese to Pinyin conversion on top.&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChineseUrlSegmentProvider&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IUrlSegmentProvider&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// We delegate to the default provider first, so we keep&lt;/span&gt;
    &lt;span class="c1"&gt;// Umbraco's built-in behavior for non-Chinese characters.&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;DefaultUrlSegmentProvider&lt;/span&gt; &lt;span class="n"&gt;_defaultUrlSegmentProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ChineseUrlSegmentProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IShortStringHelper&lt;/span&gt; &lt;span class="n"&gt;shortStringHelper&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_defaultUrlSegmentProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DefaultUrlSegmentProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shortStringHelper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;GetUrlSegment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IContentBase&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;culture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Let Umbraco generate the initial URL segment,&lt;/span&gt;
        &lt;span class="c1"&gt;// then post-process it with our custom logic.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ChineseToPinyinWithDashes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;_defaultUrlSegmentProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetUrlSegment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;culture&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Converts Chinese characters to pinyin and inserts dashes&lt;/span&gt;
    &lt;span class="c1"&gt;// between consecutive Chinese characters to improve readability.&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;ChineseToPinyinWithDashes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StringBuilder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lastWasChinese&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;IsChinese&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Insert a dash between consecutive Chinese characters&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;lastWasChinese&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="c1"&gt;// Convert the individual character to pinyin&lt;/span&gt;
                &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Pinyin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetPinyin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
                &lt;span class="n"&gt;lastWasChinese&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Non-Chinese characters are passed through unchanged&lt;/span&gt;
                &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;lastWasChinese&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Simple Unicode range check for CJK Unified Ideographs&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsChinese&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0x4E00&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0x9FFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;With this approach in place, Chinese pages now automatically generate clean, readable, and shareable URL segments without any manual intervention. It works equally well for fully Chinese titles and mixed-language content, and it integrates cleanly into Umbraco’s existing routing pipeline.&lt;/p&gt;

&lt;p&gt;The solution is lightweight, easy to maintain, and avoids the complexity of large character-mapping tables or full linguistic word segmentation — making it a practical choice for real-world Umbraco projects.&lt;/p&gt;

</description>
      <category>umbraco</category>
    </item>
    <item>
      <title>Say Hello to uMux: Sync videos from Umbraco to Mux effortlessly</title>
      <dc:creator>Søren Kottal</dc:creator>
      <pubDate>Tue, 23 Dec 2025 19:06:54 +0000</pubDate>
      <link>https://dev.to/skttl/say-hello-to-umux-sync-videos-from-umbraco-to-mux-effortlessly-1kng</link>
      <guid>https://dev.to/skttl/say-hello-to-umux-sync-videos-from-umbraco-to-mux-effortlessly-1kng</guid>
      <description>&lt;p&gt;uMux is a brand new, free and open source, package for Umbraco 17+, that takes the hassle out of hosting videos on your website. Just drop your videos into Umbraco, and uMux will automatically upload and sync them with Mux. That means you get all the power of Mux’s streaming, thumbnails, and analytics, while still working in the CMS you love.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why uMux?
&lt;/h2&gt;

&lt;p&gt;For years, I found myself recommending platforms like Vimeo or similar to clients because they’re user-friendly and handle video streaming well. But there was always a catch: editors had to jump between systems, upload videos to Vimeo, then copy and paste IDs or embed codes back into Umbraco. &lt;/p&gt;

&lt;p&gt;It’s clunky, error-prone, and just not the smooth experience editors deserve.&lt;/p&gt;

&lt;p&gt;With uMux, my goal was to keep everything in one place.&lt;/p&gt;

&lt;p&gt;Editors can upload and manage videos right inside Umbraco, just like they’re used to, while still getting all the benefits of a modern video platform like Mux. No more juggling logins, no more copy-pasting codes, just a seamless workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does it do?
&lt;/h2&gt;

&lt;p&gt;uMux handles all the behind-the-scenes magic—your videos get sent to Mux, and Mux handles encoding, so the videos are ready to stream anywhere. Editors don’t have to worry about file types, encoding settings, or technical video stuff. It just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use a third party video host?
&lt;/h2&gt;

&lt;p&gt;Serving big video files from your own server can be a pain, especially if you’re on Umbraco Cloud or have bandwidth limits. Here’s why letting Mux handle your videos is a game-changer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Save your bandwidth&lt;/strong&gt;&lt;br&gt;
Mux takes care of the heavy lifting, so your Umbraco site stays speedy and you don’t get hit with surprise bandwidth bills.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Smooth streaming for everyone&lt;/strong&gt;&lt;br&gt;
Mux automatically streams the best quality for each viewer’s device and connection, so videos load fast and play without hiccups.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Global delivery&lt;/strong&gt;&lt;br&gt;
Your videos are delivered from servers close to your users, wherever they are in the world.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In addition to those obvious benefits, Mux also gives you some handy features around the videos you host with them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instant thumbnails &amp;amp; GIFs&lt;/strong&gt;&lt;br&gt;
Need a thumbnail or animated preview? Mux generates them for you—no extra work needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Awesome analytics&lt;/strong&gt;&lt;br&gt;
See how your videos are performing and where viewers might be dropping off.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security &amp;amp; scalability&lt;/strong&gt;&lt;br&gt;
No worries about traffic spikes or unauthorized access—Mux has you covered.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Works everywhere&lt;/strong&gt;&lt;br&gt;
Embed videos in any player that supports HLS, or use Mux’s own slick player.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatic encoding&lt;/strong&gt;&lt;br&gt;
Mux encodes your videos in all the right formats, so you don’t have to think about codecs or compatibility.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Install uMux:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  dotnet add package Umbraco.Community.uMux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Add your Mux credentials to your appsettings.json or as environment variables.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add the Mux Sync property editor to your media types in Umbraco.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Upload your videos as usual - uMux takes care of the rest!&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For all the nitty-gritty details, check out the &lt;a href="https://github.com/skttl/umbraco-mux" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>umbraco</category>
    </item>
    <item>
      <title>Umbraco Marketplace Package Sync Bookmarklet</title>
      <dc:creator>Søren Kottal</dc:creator>
      <pubDate>Fri, 28 Mar 2025 12:59:54 +0000</pubDate>
      <link>https://dev.to/skttl/umbraco-marketplace-package-sync-bookmarklet-3oe3</link>
      <guid>https://dev.to/skttl/umbraco-marketplace-package-sync-bookmarklet-3oe3</guid>
      <description>&lt;p&gt;The &lt;a href="https://marketplace.umbraco.com/" rel="noopener noreferrer"&gt;Umbraco Marketplace&lt;/a&gt; automatically lists NuGet packages with a specific tag. New packages are indexed daily at 4:00 AM UTC, while updates to existing packages are checked every two hours.&lt;/p&gt;

&lt;p&gt;However, waiting for the automatic indexing can be a test of patience when you're eager to see your new package listed and verify everything is correct.&lt;/p&gt;

&lt;p&gt;Fortunately, the Umbraco Marketplace API allows you to trigger indexing for a single package using an HTTP POST request, as &lt;a href="https://docs.umbraco.com/umbraco-dxp/marketplace/listing-your-package#synchronization-with-nuget-and-package-owner-data" rel="noopener noreferrer"&gt;detailed in the documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To streamline this process, I've created a bookmarklet that you can use directly from the NuGet package details page. This bookmarklet automatically extracts the package ID and sends it to the Marketplace endpoint for immediate indexing.&lt;/p&gt;

&lt;p&gt;I'm happy to share it with the community, so feel free to grab it and use it yourself!&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/skttl/embed/bNGLBvm?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Use
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Navigate to a NuGet package details page on nuget.org (e.g., &lt;a href="https://www.nuget.org/packages/Umbraco.Community.Contentment" rel="noopener noreferrer"&gt;https://www.nuget.org/packages/Umbraco.Community.Contentment&lt;/a&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click the "Sync with Marketplace" bookmarklet in your bookmarks bar.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An alert will confirm if the sync was initiated successfully or display any error messages. Eg. error 429, if you are being too eager, with too many requests.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>umbraco</category>
      <category>nuget</category>
    </item>
    <item>
      <title>Say Goodbye to Tedious Icon Imports in Umbraco with Icoover!</title>
      <dc:creator>Søren Kottal</dc:creator>
      <pubDate>Mon, 17 Mar 2025 15:06:39 +0000</pubDate>
      <link>https://dev.to/skttl/say-goodbye-to-tedious-icon-imports-in-umbraco-with-icoover-1c36</link>
      <guid>https://dev.to/skttl/say-goodbye-to-tedious-icon-imports-in-umbraco-with-icoover-1c36</guid>
      <description>&lt;p&gt;We all know how important icons are for a clean and intuitive backoffice&lt;br&gt;
experience in Umbraco. They help content editors quickly identify document&lt;br&gt;
types, elements, and actions, making their lives easier. But manually&lt;br&gt;
importing and managing those icons? That can be a real time-sink.&lt;/p&gt;

&lt;p&gt;That's why I'm thrilled to introduce &lt;strong&gt;Icoover&lt;/strong&gt;, a new Umbraco package&lt;br&gt;
designed to automate the process of importing SVG icons into your Umbraco&lt;br&gt;
backoffice. Say goodbye to the days of manually adding icons one by one!&lt;/p&gt;
&lt;h2&gt;
  
  
  What is Icoover?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://marketplace.umbraco.com/package/umbraco.community.icoover" rel="noopener noreferrer"&gt;Icoover&lt;/a&gt; is a simple yet powerful package that automatically imports SVG icons&lt;br&gt;
from a dedicated folder within your Umbraco project. Just drop your SVG files&lt;br&gt;
into the designated directory, restart Umbraco, and boom! Your icons are&lt;br&gt;
ready to be used across your document types and the entire backoffice.&lt;/p&gt;
&lt;h2&gt;
  
  
  Key Features:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Automatic Import:&lt;/strong&gt; Icoover automatically detects and imports SVG icons,
eliminating manual uploads.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Simple Setup:&lt;/strong&gt; Just install the package and add your icons to the
correct folder. Icoover automatically "hoovers" up SVG files in the &lt;code&gt;App_Plugins/Icoover/icons&lt;/code&gt; folder and imports them into Umbraco.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Best Practices for SVG Icons in Umbraco
&lt;/h2&gt;

&lt;p&gt;To ensure your icons render correctly and are fully customizable within the Umbraco backoffice, keep these tips in mind:&lt;/p&gt;
&lt;h3&gt;
  
  
  Use &lt;code&gt;viewBox&lt;/code&gt;:
&lt;/h3&gt;

&lt;p&gt;Instead of setting &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; attributes on your root SVG element, use &lt;code&gt;viewBox&lt;/code&gt;. This allows Umbraco to handle the sizing of the icon dynamically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Don't:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"24"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"24"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Use &lt;code&gt;currentColor&lt;/code&gt;:
&lt;/h3&gt;

&lt;p&gt;For any strokes or fills that should inherit the color from Umbraco's theme, use &lt;code&gt;currentColor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M12 12L12 12"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Don't:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M12 12L12 12"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"#283a97"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Icoover is designed to simplify your Umbraco development workflow by automating the icon import process. Give it a try and let me know what you think! I hope this package saves you valuable time and makes your Umbraco projects even better.&lt;/p&gt;

</description>
      <category>umbraco</category>
    </item>
    <item>
      <title>Vibe coding a nifty Umbraco tool</title>
      <dc:creator>Søren Kottal</dc:creator>
      <pubDate>Sun, 16 Mar 2025 12:50:34 +0000</pubDate>
      <link>https://dev.to/skttl/vibe-coding-a-nifty-umbraco-tool-3jic</link>
      <guid>https://dev.to/skttl/vibe-coding-a-nifty-umbraco-tool-3jic</guid>
      <description>&lt;p&gt;What started as a slow Friday turned into a productive coding session using &lt;a href="https://codeium.com/" rel="noopener noreferrer"&gt;Windsurf&lt;/a&gt;, Codeiums AI-powered IDE, to create a block thumbnail generator for Umbraco block editors.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TLDR; I made a &lt;a href="https://skttl.github.io/umbraco-block-thumbnail-generator/" rel="noopener noreferrer"&gt;block thumbnail generator for Umbraco&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thumbnails for blocks in Umbraco is typically examples of what the block renders and they greatly improve the content editing experience. I wanted to try out "&lt;a href="https://en.wikipedia.org/wiki/Vibe_coding" rel="noopener noreferrer"&gt;vibe coding&lt;/a&gt;" on something useful, and this project aims to automate that process, saving time and ensuring consistency across Umbraco projects.&lt;/p&gt;

&lt;p&gt;I opened up Windsurf, and gave it the following prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create a webcomponent (possible with webcomponents inside) that contains an upload field (you should also be able to drop a file into it, or have a button where you can get image data from the clipboard). When uploading an image it should render it into a canvas of 500×300, but crop the image to fit the width. The top part of the image should be visible. If there is remaining vertical space, it should fill with the most prominent color of the outer 1 pixel of the uploaded image.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The initial results were surprisingly accurate. Over the next few hours, I refined the component, adding several key features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Custom Background Color&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I added the option to manually set the background color. While the automatic color detection generally worked well, providing a manual override ensures consistently good-looking thumbnails, especially when the automatically detected color isn't ideal.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tinting/Colorization&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To avoid overly colorful block catalogs, I implemented a tinting feature. This grayscales the uploaded image and then overlays a selected color using the screen blending mode. The screen blending mode was chosen because it creates a subtle color overlay that preserves the image's details while applying the desired tint. This is especially useful for applying a brand color to all thumbnails.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Padding&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To easily incorporate screenshots taken directly from design tools like Figma (where layers can be copied), I added padding options. The padding is filled with either the automatically detected background color or the custom-selected background color, ensuring a clean and consistent border around the image.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fit Options&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I expanded the image fitting behavior. Instead of always fitting the uploaded image to the width of the canvas, users can now select whether to fit to the width or the height, providing greater control over the final composition.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Download Options&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, I added options for downloading the resulting image in PNG, JPEG, and WebP formats. The filesize of each format is displayed, allowing users to make informed decisions about image quality and file size optimization.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Saving preferences to local storage&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also saves your selected preferences for background color, tint, padding and fit to the local storage in the browser. This way you don't have to set these things for every thumbnail you generate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding polish
&lt;/h2&gt;

&lt;p&gt;The first result was somewhat pretty, but still very prototype like. I wanted to give it an Umbraco feel, so I picked some colors from the Umbraco 12 login background, and fed it into the AI, asking it to recolor the site using them, without any further restrictions.&lt;/p&gt;

&lt;p&gt;It then colored everything nicely, including versions for both light and dark mode. I did however had to make it reconsider some of its choices for accessibility and contrast reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refactoring with Lit and TypeScript
&lt;/h2&gt;

&lt;p&gt;To improve the component's maintainability and prepare it for potential use within an Umbraco package, I had Windsurf convert the web component to use Lit and TypeScript. &lt;/p&gt;

&lt;p&gt;This also provided a more structured and type-safe codebase. Although - I haven't really read the code, and this is also one of my painpoints with this kind of technology. You never really feel in control of the output, unless you meticulously read through every change it does.&lt;/p&gt;

&lt;p&gt;And just for the sake of it, I had Windsurf turn it into a Progressive Web App, so it can be "installed" for local use. So now I have a thumbnail generator sitting on the taskbar, ready to be used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was it worth it
&lt;/h2&gt;

&lt;p&gt;One of the biggest challenges was that Windsurf occasionally reintroduced older bugs or reverted to previous requirements, such as the original image size of 500×300 (which should have been 400×250). These regressions required careful monitoring and reiteration of the correct specifications. These issues occurred approximately 2-3 times during the development process.&lt;/p&gt;

&lt;p&gt;All in all, using Windsurf as an "assistant developer" was a surprisingly productive experience. While I could have likely implemented some of the individual features manually in a similar timeframe, Windsurf significantly accelerated the overall development process.&lt;/p&gt;

&lt;p&gt;For this type of fun idea, it was definitely worth it. Getting a project from idea to working project was fast. I'm not sure if I would use it on anything non-trivial, that I need to support going forward though. I really don't feel like I know what the code is doing, and while I can read through the code for this project - it was never my intention to do that. But for simple changes and features, I can definitely see the benefits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try out the thumbnail generator yourself!
&lt;/h2&gt;

&lt;p&gt;You can try out (and install if you like) the thumbnail generator tool at &lt;a href="https://skttl.github.io/umbraco-block-thumbnail-generator/" rel="noopener noreferrer"&gt;https://skttl.github.io/umbraco-block-thumbnail-generator/&lt;/a&gt; – it's completely free to use, so go nuts! &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F06h0dgdwn98siygle2wb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F06h0dgdwn98siygle2wb.png" alt="Screenshot of Umbraco Block Thumbnail Generator" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>umbraco</category>
      <category>codeium</category>
    </item>
    <item>
      <title>Easier Responsive Emails for Umbraco Forms with MJML</title>
      <dc:creator>Søren Kottal</dc:creator>
      <pubDate>Thu, 27 Feb 2025 08:45:03 +0000</pubDate>
      <link>https://dev.to/skttl/easier-responsive-emails-for-umbraco-forms-with-mjml-3hk0</link>
      <guid>https://dev.to/skttl/easier-responsive-emails-for-umbraco-forms-with-mjml-3hk0</guid>
      <description>&lt;p&gt;HTML emails are notoriously difficult to style due to inconsistent rendering across email clients. Fortunately, MJML provides a modern solution that simplifies email creation while ensuring compatibility.&lt;/p&gt;

&lt;p&gt;Basically, HTML emails are a Frankenstein’s monster of old web practices, because they have to work in the wild west of email clients. If they were too advanced, half of your audience wouldn't be able to read them properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exhibit A: The default Umbraco Forms Email Template
&lt;/h2&gt;

&lt;p&gt;Here’s an excerpt of the default email template in Umbraco Forms. While functional, it requires extensive inline styling, uses outdated table-based layouts, and lacks flexibility.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;    &lt;span class="nt"&gt;&amp;lt;style &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/css"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;/* CLIENT-SPECIFIC STYLES */&lt;/span&gt;
    &lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;-webkit-text-size-adjust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;-ms-text-size-adjust&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;td&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;mso-table-lspace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0pt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;mso-table-rspace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0pt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;-ms-interpolation-mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bicubic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;/* RESET STYLES */&lt;/span&gt;
    &lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;table&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;border-collapse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;collapse&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;/* iOS BLUE LINKS */&lt;/span&gt;
    &lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;x-apple-data-detectors&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

 &lt;span class="c"&gt;/* MOBILE STYLES */&lt;/span&gt;
 &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;600px&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
  &lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32px&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32px&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;/* ANDROID CENTER FIX */&lt;/span&gt;
    &lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;style&lt;/span&gt;&lt;span class="o"&gt;*=&lt;/span&gt;&lt;span class="s1"&gt;"margin: 16px 0;"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"background-color: #f4f4f4; margin: 0 !important; padding: 0 !important;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="na"&gt;border=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;cellpadding=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;cellspacing=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"margin-bottom: 40px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- LOGO --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;bgcolor=&lt;/span&gt;&lt;span class="s"&gt;"#413659"&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="c"&gt;&amp;lt;!--[if (gte mso 9)|(IE)]&amp;gt;
                    &amp;lt;table align="center" border="0" cellspacing="0" cellpadding="0" width="600"&amp;gt;
                    &amp;lt;tr&amp;gt;
                    &amp;lt;td align="center" valign="top" width="600"&amp;gt;
                &amp;lt;![endif]--&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;table&lt;/span&gt; &lt;span class="na"&gt;border=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;cellpadding=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;cellspacing=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"max-width: 600px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;td&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"center"&lt;/span&gt; &lt;span class="na"&gt;valign=&lt;/span&gt;&lt;span class="s"&gt;"top"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"padding: 40px 10px 40px 10px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"http://umbraco.com"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"_blank"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                                &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Logo"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"@assetUrl/umbraco-logo.png"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"40"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"40"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display: block; width: 40px; max-width: 40px; min-width: 40px; font-family: 'Lato', Helvetica, Arial, sans-serif; color: #ffffff; font-size: 18px;"&lt;/span&gt; &lt;span class="na"&gt;border=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
                &lt;span class="c"&gt;&amp;lt;!--[if (gte mso 9)|(IE)]&amp;gt;
                    &amp;lt;/td&amp;gt;
                    &amp;lt;/tr&amp;gt;
                    &amp;lt;/table&amp;gt;
                &amp;lt;![endif]--&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Introducing MJML: The simpler approach
&lt;/h2&gt;

&lt;p&gt;Writing HTML email templates like this can be tedious and error-prone. This is where MJML comes in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mjml.io/" rel="noopener noreferrer"&gt;MJML (Mailjet Markup Language)&lt;/a&gt; is a powerful open-source framework designed to simplify the creation of responsive email templates. Instead of manually coding complex HTML tables and inline styles, MJML offers an intuitive, component-based syntax that automatically generates well-structured and mobile-friendly emails.&lt;/p&gt;

&lt;p&gt;Instead of handwriting nested tables, and conditional Internet Explorer comments, you use easy to remember elements like &lt;code&gt;&amp;lt;mj-section&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;mj-column&amp;gt;&lt;/code&gt; , &lt;code&gt;&amp;lt;mj-text&amp;gt;&lt;/code&gt; and so forth. MJML then converts your markup to real robot-barf-like HTML tables, with all the needed weird quirks to make it work in different email clients.&lt;/p&gt;

&lt;p&gt;I won’t go into detail about how to write MJML, you can see that in their documentation. In this article, I want to show you how you can utilize MJML for creating email templates to be used with Umbraco Forms.&lt;/p&gt;

&lt;p&gt;First up, I’ve created an MJML implementation of the default example email template provided with Umbraco Forms. Converting it from real robot-barf-like HTML tables to clean MJML, brings it down from 221 lines of code to just 103. And with the added benefit of getting much cleaner markup, that you would be less afraid of touching. &lt;a href="https://www.diffchecker.com/x4dBlmHt/" rel="noopener noreferrer"&gt;You can see the difference here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But how would you get the MJML converted to HTML before sending the email?&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering MJML to HTML in .NET
&lt;/h2&gt;

&lt;p&gt;Since MJML doesn’t have official .NET support, you need a library to convert MJML into standard HTML before sending the email. Fortunately, a &lt;a href="https://github.com/SebastianStehle/mjml-net" rel="noopener noreferrer"&gt;community-driven fork of MJML&lt;/a&gt; is available on NuGet, implementing all MJML features and actively maintained.&lt;/p&gt;

&lt;p&gt;Without having to create your own workflow for Umbraco Forms, to convert the MJML into HTML renderable by email clients, you can use Mjml.Net directly in the razor views used by the default Razor workflow provided with Umbraco Forms.&lt;/p&gt;

&lt;p&gt;This would typically require you to save all your markup to a string variable, and run that through the MjmlRenderer, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mjmlRenderer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MjmlRenderer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;@"
        &amp;lt;mjml&amp;gt;
            &amp;lt;mj-head&amp;gt;
                &amp;lt;mj-title&amp;gt;Hello World Example&amp;lt;/mj-title&amp;gt;
            &amp;lt;/mj-head&amp;gt;
            &amp;lt;mj-body&amp;gt;
                &amp;lt;mj-section&amp;gt;
                    &amp;lt;mj-column&amp;gt;
                        &amp;lt;mj-text&amp;gt;
                            Hello World!
                        &amp;lt;/mj-text&amp;gt;
                    &amp;lt;/mj-column&amp;gt;
                &amp;lt;/mj-section&amp;gt;
            &amp;lt;/mj-body&amp;gt;
        &amp;lt;/mjml&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mjmlRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This converts the MJML into fully compatible HTML, which you can then send as an email.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using MJML in Razor Views
&lt;/h2&gt;

&lt;p&gt;I don’t like putting everything into a variable like this. Especially when I need to loop over form fields, and concatenate values in.&lt;/p&gt;

&lt;p&gt;Instead of storing MJML as a string in your code, a cleaner approach is to use Razor views and the layout feature to process MJML dynamically. You can create an &lt;code&gt;MjmlLayout.cshtml&lt;/code&gt; file like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;@using&lt;/span&gt; &lt;span class="n"&gt;Mjml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Net&lt;/span&gt;
&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// get the body content as string&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;RenderBody&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// render using mjml.net&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mjmlRenderer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MjmlRenderer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mjmlRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// print the html&lt;/span&gt;
    &lt;span class="n"&gt;@Html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in your email template, simply reference the layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Layout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"MjmlLayout.cshtml"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, the email template content is processed through the MJML renderer dynamically, allowing you to use Razor’s built-in functionality while leveraging MJML’s cleaner syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By integrating MJML with Umbraco Forms, you achieve cleaner, more maintainable email templates while ensuring broad compatibility across email clients. This approach allows developers to focus on design and content rather than battling email client inconsistencies, ultimately simplifying the email creation process.&lt;/p&gt;

</description>
      <category>umbraco</category>
      <category>mjml</category>
      <category>email</category>
    </item>
    <item>
      <title>Make your layouts dynamic with Quantity Queries in Tailwind CSS</title>
      <dc:creator>Søren Kottal</dc:creator>
      <pubDate>Tue, 18 Feb 2025 07:33:06 +0000</pubDate>
      <link>https://dev.to/skttl/make-your-layouts-dynamic-with-quantity-queries-1h39</link>
      <guid>https://dev.to/skttl/make-your-layouts-dynamic-with-quantity-queries-1h39</guid>
      <description>&lt;p&gt;CSS has come a long way, giving us all sorts of cool ways to handle layouts. One neat trick that’s often overlooked is quantity queries. These let you style elements based on how many of them exist inside a container. That means you can create more flexible designs without relying on JavaScript or other external programming languages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Quantity Queries?
&lt;/h2&gt;

&lt;p&gt;Quantity queries are basically CSS selectors that let you style things based on how many child elements a parent has. While CSS doesn’t have a built-in feature for this, you can get creative using the :has selector  with :nth-child() and :nth-last-child() to make it work.&lt;/p&gt;

&lt;p&gt;For example, you can style a container differently depending on how many items it holds. This is super handy for building adaptable grid layouts, menus, lists, and more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 1: Changing Styles Based on the Number of Items
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.container&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;3&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;lightblue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, if a &lt;code&gt;.container&lt;/code&gt; has at least three children, it gets a light blue background.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/skttl/embed/NPWWjVO?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 2: Adjusting a Grid Layout Automatically
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.container&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;5&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.container&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;3&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="nd"&gt;:not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This setup makes a container use a 5-column grid if it has exactly 5 children. If it only has 3 (but not 5), it switches to a 3-column layout instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Awesome
&lt;/h2&gt;

&lt;p&gt;Quantity queries let you tweak styles without needing media queries or JavaScript. Elements adjust their look based on how many siblings they have, making things more flexible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quantity Queries with TailwindCSS
&lt;/h2&gt;

&lt;p&gt;If you're a Tailwind CSS user, you can use the &lt;a href="https://www.npmjs.com/package/tailwindcss-quantity-queries" rel="noopener noreferrer"&gt;tailwindcss-quantity-queries&lt;/a&gt; plugin to integrate quantity queries into your workflow. This plugin allows you to apply Tailwind utility classes based on the number of child elements, making it easy to create dynamic, responsive designs without writing custom CSS. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"children-[&amp;gt;3]:bg-blue-500 children-[5]:grid-cols-5"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;Item 1&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;Item 2&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;Item 3&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this setup, the parent div gets a blue background if it has three children and switches to a 5-column grid if it has five children. This plugin is a great way to make your Tailwind projects even more flexible and powerful.&lt;/p&gt;

&lt;p&gt;You can also use the plugin in combination with the &lt;code&gt;group&lt;/code&gt; utility to style the children, based on a groups number of children. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"group"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"group-children-[&amp;lt;3]:bg-red-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Child 1&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"group-children-[&amp;gt;5]:bg-blue-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Child 2&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"group-children-[2-5]:bg-green-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Child 3&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin is compatible with both the new v4 of Tailwind, and the old v3. You simply drop in the plugin in your config, eg. in the new css style config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss-quantity-queries"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or in the old javascript format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tailwindcss-quantity-queries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Quantity queries in CSS give you new ways to create flexible and responsive designs.&lt;/p&gt;

&lt;p&gt;Give it a shot and see how it changes your workflow!&lt;/p&gt;

</description>
      <category>css</category>
      <category>tailwindcss</category>
    </item>
    <item>
      <title>Enhancing 404 Pages with Search in Umbraco</title>
      <dc:creator>Søren Kottal</dc:creator>
      <pubDate>Mon, 10 Feb 2025 19:39:48 +0000</pubDate>
      <link>https://dev.to/skttl/enhancing-404-pages-with-search-in-umbraco-1jad</link>
      <guid>https://dev.to/skttl/enhancing-404-pages-with-search-in-umbraco-1jad</guid>
      <description>&lt;p&gt;A well-designed 404 page enhances user experience when a requested page doesn’t exist. There are tons of examples of &lt;a href="https://www.awwwards.com/awwwards/collections/404-error-page/" rel="noopener noreferrer"&gt;fun and creative error pages&lt;/a&gt; if you want to lighten up your user's mood. But you can also try to be helpful for users who just want to find the content they’re looking for.&lt;/p&gt;

&lt;p&gt;404 errors can have many causes.&lt;/p&gt;

&lt;p&gt;One common cause is a page being moved to a new URL. Luckily, &lt;a href="https://docs.umbraco.com/umbraco-cms/reference/routing/url-tracking" rel="noopener noreferrer"&gt;Umbraco handles this automatically for us&lt;/a&gt;, creating the necessary redirects from the old url to the new one.&lt;/p&gt;

&lt;p&gt;But they can also come from mistyped urls, or simply the user guessing the location of a page. This is where this idea comes in handy.&lt;/p&gt;

&lt;p&gt;If you combine your error page, with search results based on the current context - in this example, the requested URL - the user might get lucky, and get pointed towards the content they were actually looking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo Time
&lt;/h2&gt;

&lt;p&gt;To demonstrate for you, I've created a new site based on the infamous Umbraco Starter Kit, containing some demo content ready to get going.&lt;/p&gt;

&lt;p&gt;Umbraco comes with &lt;a href="https://docs.umbraco.com/umbraco-cms/reference/searching/examine" rel="noopener noreferrer"&gt;Examine&lt;/a&gt; out of the box for searching the site content. And while that is often enough for most users, I can't resist using this opportunity to create some awareness for my package &lt;a href="https://marketplace.umbraco.com/package/our.umbraco.fulltextsearch" rel="noopener noreferrer"&gt;Full Text Search&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This package renders every page, and indexes all text content into Examine, including content referred from other pages or simply hardcoded into your templates. The result is a Google like index, where you can search for each and every word, the user sees on each page. It also makes more complex pages, like pages built with blocks in either Block List or Block Grid easier to search.&lt;/p&gt;

&lt;p&gt;And it offers an &lt;a href="https://github.com/skttl/umbraco-fulltextsearch8/blob/v5/dev/docs/developers-guide-v5.md#the-easy-way" rel="noopener noreferrer"&gt;easy API&lt;/a&gt; for creating the search, handling things like multi relevance, boosting, summarizing etc. for you without breaking a sweat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the 404 page
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5eulttmp6wa8zcexx0yf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5eulttmp6wa8zcexx0yf.png" alt="Screenshot of Umbraco setup containing a content type for the Not Found Page, and the Not Found page itsefl" width="800" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I created a document type to contain the page itself. To integrate with the starter kit, I added the existing Navigation Base composition. This is a composition content type found in the starter kit, that I use just to get the necessary properties needed to hide the page from the navigation. You can do what you need in your own project.&lt;/p&gt;

&lt;p&gt;I also create a page in the Content Tree, for the 404 page, and to make it work as the actual 404 page, I add the necessary setup in my appsettings.json file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Umbraco"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CMS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Error404Collection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Culture"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en-US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"ContentKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4b7b8f17-9a64-4165-b753-9584ca36bc52"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note, I don't think this way of adding 404 pages is very editor friendly. What if the editor deletes it and creates it over, then you have to change config of your site. Or what if you have multiple sites sharing the same language, then you can't configure 404 pages like this. I have a better way, which I might show in another blogpost. &lt;/p&gt;

&lt;h2&gt;
  
  
  Adding search results to 404 page
&lt;/h2&gt;

&lt;p&gt;Instead of just showing an error, let’s make our 404 page actually useful — by adding smart search results!&lt;/p&gt;

&lt;p&gt;In the template for the 404 page, I reused some HTML from existing templates to maintain consistency and reduce effort.. But instead of the content, I create a search result using Full Text Search.&lt;/p&gt;

&lt;p&gt;To do this, I first need to inject in the Full Text Search helper using&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;@inject&lt;/span&gt; &lt;span class="n"&gt;Our&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Umbraco&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FullTextSearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FullTextSearchHelper&lt;/span&gt; &lt;span class="n"&gt;FullTextSearchHelper&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and I can then go and create the search result using the helper.&lt;/p&gt;

&lt;p&gt;To generate the search query, I extract the requested path and replace non-alphanumeric characters with spaces. For example, &lt;code&gt;/this/path/does/not/exist&lt;/code&gt; becomes &lt;code&gt;this path does not exist&lt;/code&gt;. I could also have looked at the referrer, or other data.&lt;/p&gt;

&lt;p&gt;The search process is straightforward - simply passing the query to Full Text Search handles the rest. But there is several extra options, if you wish to &lt;a href="https://github.com/skttl/umbraco-fulltextsearch8/blob/v5/dev/docs/developers-guide-v5.md#configuring-the-search" rel="noopener noreferrer"&gt;configure it yourself&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If the search then returns any results, they get shown, leading the user to other related pages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"section"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;The page you were looking for, could not be found!&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
            @{
                // generate search query from request path
                var query = Context.Request.Path.ToString().ReplaceNonAlphanumericChars(" ");

                // search for the generated query
                var search = FullTextSearchHelper.Search(query, Model.GetCultureFromDomains(), 1);

                @if (search.TotalResults &amp;gt; 0)
                {
                    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Maybe these pages can help you?&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

                    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"blogposts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        @foreach (var result in search.Results)
                        {
                            &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"@result.Content.Url()"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"blogpost"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                                &lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"blogpost-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;@result.Title&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
                                &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"blogpost-excerpt"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;@result.Summary&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                            &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
                        }
                    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                }
            }
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Now, if a user visits non-existing URLs like &lt;code&gt;/produits&lt;/code&gt;, &lt;code&gt;/products/unikorn&lt;/code&gt;, &lt;code&gt;/blog/another&lt;/code&gt;, or &lt;code&gt;/jan-skovgaard&lt;/code&gt;, they are guided toward relevant pages instead of encountering a dead end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3z3uofa1cg2xsm8e86yv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3z3uofa1cg2xsm8e86yv.png" alt="Screenshots of the Not Found Page in action on four different non-existing pages" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can try this yourself! The source code, including uSync files, is available on &lt;a href="https://github.com/skttl/umbraco-example-search-enhanced-404-page" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; for easy download and experimentation.&lt;/p&gt;

</description>
      <category>umbraco</category>
    </item>
    <item>
      <title>Learnings from upgrading an existing Umbraco project to TailwindCSS 4</title>
      <dc:creator>Søren Kottal</dc:creator>
      <pubDate>Fri, 24 Jan 2025 09:53:09 +0000</pubDate>
      <link>https://dev.to/skttl/learnings-from-upgrading-an-existing-umbraco-project-to-tailwindcss-4-20pc</link>
      <guid>https://dev.to/skttl/learnings-from-upgrading-an-existing-umbraco-project-to-tailwindcss-4-20pc</guid>
      <description>&lt;p&gt;As a developer relying on TailwindCSS for crafting responsive designs effortlessly, the release of TailwindCSS v4 was an exciting occurrence. I jumped immediately into the project I’m currently working on, and started upgrading to the latest Tailwind version.&lt;/p&gt;

&lt;p&gt;We use a somewhat simple approach to TailwindCSS in our Umbraco (ASP.net MVC) projects. In the website project, we initalize an npm package with &lt;code&gt;npm init&lt;/code&gt; and then install TailwindCSS and necessary scripts for building. The source files is placed in &lt;code&gt;./UI/CSS/main.css&lt;/code&gt; and the config in &lt;code&gt;./UI/CSS/tailwind.config.js&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving config to CSS, spacing changed
&lt;/h2&gt;

&lt;p&gt;One of the big headlines of the new Tailwind version, is the addition of &lt;a href="https://tailwindcss.com/blog/tailwindcss-v4#css-first-configuration" rel="noopener noreferrer"&gt;config in CSS&lt;/a&gt;. With that, all configurable values are now defined as custom properties, which adds some exciting new possibilities.&lt;/p&gt;

&lt;p&gt;I like to stay as close to the default config as possible, so moving this over to CSS was not a big hurdle.&lt;/p&gt;

&lt;p&gt;Though, I used to generate spacing values using an npm package I built, &lt;a href="https://www.npmjs.com/package/generate-tailwind-scale" rel="noopener noreferrer"&gt;generate-tailwind-scale&lt;/a&gt;, I opted to leave this one behind, as the new &lt;a href="https://tailwindcss.com/blog/tailwindcss-v4#dynamic-utility-values-and-variants" rel="noopener noreferrer"&gt;spacing scale is dynamic&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I did use the same package for defining font-sizes though, but for this project, most of the font-sizes used already matched the default font-sizes. And the rest I simply changed into using arbitrary values, eg. &lt;code&gt;text-[10px]&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic content scanning
&lt;/h2&gt;

&lt;p&gt;In TailwindCSS v3, developers had to manually specify content paths to ensure Tailwind scanned the correct files for class usage. TailwindCSS v4 automates this process, scanning the entire directory it's executed from (except files specified in &lt;code&gt;.gitignore&lt;/code&gt;). This clever tweak reduces configuration overhead.&lt;/p&gt;

&lt;p&gt;Initially, I was worried that my &lt;code&gt;output.css&lt;/code&gt; file would be considered for class generation since we don’t git-ignore it in our projects. If it were considered, removing a class would be impossible. Fortunately, it's not included, and when I added a one-time class like &lt;code&gt;m-[1337ch]&lt;/code&gt;, it appeared in the output but was promptly removed when unused elsewhere.&lt;/p&gt;

&lt;p&gt;For content files ignored by .gitignore or located outside the project path, developers can specify paths via &lt;code&gt;@source "../path/to/something"&lt;/code&gt;, offering flexibility for more complex directory structures.&lt;/p&gt;

&lt;h2&gt;
  
  
  No more content transforms (for now?)
&lt;/h2&gt;

&lt;p&gt;One missing feature in this content definition method is the ability to transform content during scanning. Previously, I configured content to allow writing &lt;code&gt;@@container&lt;/code&gt; to add the &lt;code&gt;@container&lt;/code&gt; class used by the container query plugin (now built into Tailwind!). Since my frontend is written in Razor/cshtml, &lt;code&gt;@container&lt;/code&gt; would cause errors, while &lt;code&gt;@@&lt;/code&gt; worked as an escape character. For now, I have to wrap those like &lt;code&gt;@("@container")&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  UTF-8 BOM encoding issue when working from Visual Studio
&lt;/h2&gt;

&lt;p&gt;A notable challenge during the upgrade was related to file encoding. If your main Tailwind input file (&lt;code&gt;main.css&lt;/code&gt;) was created using Visual Studio, it might be saved in UTF-8 BOM format. TailwindCSS v4 cannot correctly process UTF-8 BOM, causing incorrect or incomplete outputs.&lt;/p&gt;

&lt;p&gt;This issue initially puzzled me, as the &lt;code&gt;output.css&lt;/code&gt; file still contained Tailwind-specific directives like &lt;code&gt;@utility&lt;/code&gt; and &lt;code&gt;@import "tailwindcss"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The simple solution is to save your CSS file as UTF-8 without BOM. Once converted, TailwindCSS functions as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safelisting classes is deprecated
&lt;/h2&gt;

&lt;p&gt;In a significant change, TailwindCSS v4 no longer supports class safelisting. For simple classes, I recommend documenting them in comments within your CSS files. I previously used regex for safelisted classes but had to refactor. Instead of using direct dynamic class definitions like &lt;code&gt;class="bg-@Model.Color"&lt;/code&gt;, I transitioned to custom properties: &lt;code&gt;class="bg-(--my-bg)" style="--my-bg: var(--color-@Model.Color)"&lt;/code&gt;. This approach also results in a cleaner, more maintainable solution, and fewer &lt;code&gt;bg-&lt;/code&gt; and &lt;code&gt;color-&lt;/code&gt; classes.&lt;/p&gt;

&lt;h2&gt;
  
  
  New method for custom utilities and variants
&lt;/h2&gt;

&lt;p&gt;Another significant change is the method for defining custom utilities. The reliance on &lt;code&gt;@layer utilities&lt;/code&gt; blocks is gone. Instead, in TailwindCSS v4, you can define them using a new directive: &lt;code&gt;@utility&lt;/code&gt;. For example, to create a utility to hide scrollbars:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@utility&lt;/span&gt; &lt;span class="n"&gt;scrollbar-hidden&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="c"&gt;/* css here */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For variants it has become very simple, heres an example of a custom variant I had in this project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@custom-variant&lt;/span&gt; &lt;span class="n"&gt;mouse-only&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;media&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fine&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Simpler plugin setup
&lt;/h2&gt;

&lt;p&gt;TailwindCSS v4 introduces a more streamlined way to incorporate plugins. Plugins are now embedded directly using directives such as &lt;code&gt;@plugin "@tailwindcss/typography"&lt;/code&gt;. This approach simplifies the integration process, reducing boilerplate code and setup time.&lt;/p&gt;

&lt;p&gt;And it seems like v3 plugins are supported out of the box. At least my &lt;a href="https://www.npmjs.com/package/tailwindcss-quantity-queries" rel="noopener noreferrer"&gt;quantity query plugin&lt;/a&gt; just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  JavaScript config still possible
&lt;/h2&gt;

&lt;p&gt;While TailwindCSS v4 still supports JavaScript configuration files, the authors advises against using them. I think they will end up completely deprecated at some time, but at least not before an eventual v5 or v6 of TailwindCSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Updating to TailwindCSS v4 was quite straight forward. If not for the BOM encoding issue, I would say it was as easy as pie.&lt;/p&gt;

&lt;p&gt;Next, I think I need to link into using the &lt;a href="https://tailwindcss.com/blog/standalone-cli" rel="noopener noreferrer"&gt;standalone CLI&lt;/a&gt; in the build pipeline, or maybe even when starting the website, to make our projects even simpler to work with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus tip
&lt;/h2&gt;

&lt;p&gt;Working with TailwindCSS is a lot better, with the right tools. For VSCode, Tailwind Labs has their own &lt;a href="https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss" rel="noopener noreferrer"&gt;intellisense extension&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For full Visual Studio, an &lt;a href="https://marketplace.visualstudio.com/items?itemName=TheronWang.TailwindCSSIntellisense" rel="noopener noreferrer"&gt;unofficial extension&lt;/a&gt; offers similar functionality. Although it's not supporting v4 yet, the author is trying to make it happen in the next few weeks.&lt;/p&gt;

</description>
      <category>umbraco</category>
      <category>tailwindcss</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
