<?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>Forem: Luuk Peters</title>
    <description>The latest articles on Forem by Luuk Peters (@luukpeters).</description>
    <link>https://forem.com/luukpeters</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%2F3016095%2Fbdab388c-2960-4847-80f7-fb64dbe40655.jpg</url>
      <title>Forem: Luuk Peters</title>
      <link>https://forem.com/luukpeters</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/luukpeters"/>
    <language>en</language>
    <item>
      <title>Upgrade Umbraco 13 to 17: Property Editors + Property Value Converters</title>
      <dc:creator>Luuk Peters</dc:creator>
      <pubDate>Thu, 12 Mar 2026 13:30:20 +0000</pubDate>
      <link>https://forem.com/luukpeters/upgrade-umbraco-13-to-17-property-editors-property-value-converters-22kb</link>
      <guid>https://forem.com/luukpeters/upgrade-umbraco-13-to-17-property-editors-property-value-converters-22kb</guid>
      <description>&lt;p&gt;This is part five in a series about common tasks you'll encounter when upgrading Umbraco 13 to 17. In this part, we’ll look at updating Property Editors and Property Value Converters.&lt;/p&gt;

&lt;p&gt;When upgrading several packages, one area that caused confusion was Property Editors. In Umbraco 17 there is a much clearer separation between the UI in the backoffice and the data handling and validation on the backend (C#).&lt;/p&gt;

&lt;p&gt;Because of this change, Umbraco introduced migrations that convert existing Data Types (which are essentially instances of a Property Editor) to the new format. In Umbraco 13 a Data Type only referenced a single editor alias, but in Umbraco 17 it contains two: an &lt;strong&gt;editor alias&lt;/strong&gt; (backend) and an &lt;strong&gt;editor UI alias&lt;/strong&gt; (frontend). This makes it possible, for example, to use multiple interchangeable UIs that work with the same underlying data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why won't my property editor work anymore?
&lt;/h2&gt;

&lt;p&gt;After upgrading my database to Umbraco 17 I ran into a couple of issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How does Umbraco know that it should use my newly created Property Editor UI for existing properties?&lt;/li&gt;
&lt;li&gt;Why does Models Builder suddenly return a &lt;code&gt;JsonDocument&lt;/code&gt; instead of my custom &lt;code&gt;VideoPlayerValueConverterModel&lt;/code&gt;?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answer lies in understanding what the migration from Umbraco 13 to 14+ actually does and what you need to do with the result. This post walks through that process.&lt;/p&gt;

&lt;p&gt;This blog explains how you can update your Property Editors to work in Umbraco 17 without having to create a custom migration. It also helps to read the &lt;a href="https://docs.umbraco.com/umbraco-cms/customizing/property-editors/composition" rel="noopener noreferrer"&gt;Umbraco documentation on Property Editor composition&lt;/a&gt;. That documentation explains the different pieces that make up a Property Editor in Umbraco 17 and provides useful context for what follows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starting point before the migration
&lt;/h2&gt;

&lt;p&gt;There are two different starting points for Property Editors in Umbraco 13, and each leads to a slightly different outcome during the migration to Umbraco 14+. The difference depends on whether the Property Editor is defined in a &lt;strong&gt;package.manifest&lt;/strong&gt; file or in &lt;strong&gt;C# code&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Starting point 1: manifest-only Property Editor
&lt;/h3&gt;

&lt;p&gt;In Umbraco 13 a Property Editor can be defined purely in a &lt;code&gt;package.manifest&lt;/code&gt;, for example:&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="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"propertyEditors"&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;"alias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"proudnerds.videoplayer.editor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Video player"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"icon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icon-play"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Proud Nerds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"editor"&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;"view"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/App_Plugins/ProudNerds.Umbraco.VideoPlayer/umbraco.videoplayer.html"&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="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is enough to create a simple data editor. The value is stored as a string in the database and there is no server-side validation. Anything can be stored as long as it fits into a string.&lt;/p&gt;

&lt;p&gt;If needed, you can still transform that value later using a Property Value Converter before it ends up in the cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  Starting point 2: Property Editor defined in code
&lt;/h3&gt;

&lt;p&gt;You can also &lt;a href="https://docs.umbraco.com/umbraco-cms/13.latest/tutorials/creating-a-property-editor#setting-up-a-property-editor-with-csharp" rel="noopener noreferrer"&gt;define a Property Editor in C#&lt;/a&gt;, for example:&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.PropertyEditors&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;YourProjectName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DataEditor&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;"Suggestions editor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Suggestions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"/App_Plugins/Suggestions/suggestion.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Common"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Icon&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"icon-list"&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;Suggestions&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DataEditor&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;Suggestions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IDataValueEditorFactory&lt;/span&gt; &lt;span class="n"&gt;dataValueEditorFactory&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="n"&gt;dataValueEditorFactory&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Defining a Property Editor in code gives you more control over validation and how the data is stored.&lt;/p&gt;

&lt;h3&gt;
  
  
  The resulting database records
&lt;/h3&gt;

&lt;p&gt;When you create a Data Type in Umbraco 13 that uses a Property Editor, the resulting database record is almost identical for both approaches. Each Data Type simply stores a property editor alias and configuration in the &lt;code&gt;umbracoDataType&lt;/code&gt; table.&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%2Fuicwlvzwi8diylzqvd6h.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%2Fuicwlvzwi8diylzqvd6h.png" alt=" " width="631" height="167"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you register the editor in C#, the &lt;code&gt;dbType&lt;/code&gt; may differ, but that detail is not important for this discussion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data migration (13 → 14+)
&lt;/h2&gt;

&lt;p&gt;The difference between the two approaches becomes visible during migration.&lt;/p&gt;

&lt;p&gt;In one of the later Umbraco 13 versions a &lt;strong&gt;pre-migration&lt;/strong&gt; was introduced that prepares Property Editors for upgrading to Umbraco 14+. During this step, a record is written to the &lt;code&gt;umbracoKeyValue&lt;/code&gt; table describing how each Data Type should be migrated.&lt;/p&gt;

&lt;p&gt;For a manifest-only Property Editor, the record looks like this:&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="nl"&gt;"DataTypeId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1055&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"EditorUiAlias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"proudnerds.videoplayer.editor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"EditorAlias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Umbraco.Plain.String"&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;However, if the editor was defined in C#, both aliases will be the same:&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="nl"&gt;"DataTypeId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1055&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"EditorUiAlias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"proudnerds.videoplayer.editor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"EditorAlias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"proudnerds.videoplayer.editor"&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;After the database upgrade to Umbraco 17, this information is used to populate the new &lt;code&gt;propertyEditorAlias&lt;/code&gt; and &lt;code&gt;propertyEditorUiAlias&lt;/code&gt; columns in the &lt;code&gt;umbracoDataType&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;For the video player editor, the result of the migration looks like this:&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%2F58t3d0gffuy81n6oww8m.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%2F58t3d0gffuy81n6oww8m.png" alt=" " width="772" height="148"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating the code
&lt;/h2&gt;

&lt;p&gt;Once you understand what the migration produced in the database, updating the code becomes much clearer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating and registering the Property Editor UI
&lt;/h3&gt;

&lt;p&gt;With the new backoffice introduced in Umbraco 14+, the UI part of your Property Editor needs to be recreated because AngularJS is no longer supported.&lt;/p&gt;

&lt;p&gt;After creating the UI, you need to register it. I won't go into the details of building a Property Editor UI here (the &lt;a href="https://docs.umbraco.com/umbraco-cms/customizing/property-editors/composition/property-editor-ui" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; covers that), but the aliases you use during registration are important.&lt;/p&gt;

&lt;p&gt;Based on the migration result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;alias&lt;/code&gt; should match &lt;strong&gt;PropertyEditorUiAlias&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;propertyEditorSchemaAlias&lt;/code&gt; should match &lt;strong&gt;PropertyEditorAlias&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;manifests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UmbExtensionManifest&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;propertyEditorUi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;proudnerds.videoplayer.editor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Proud Nerds video player property editor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;js&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./proud-nerds-video-property-editor-ui.element&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Video player&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;icon-play&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Proud Nerds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;propertyEditorSchemaAlias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Umbraco.Plain.Json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;properties&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="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;h3&gt;
  
  
  Updating the DataEditor (if it already exists)
&lt;/h3&gt;

&lt;p&gt;If your editor already had a &lt;code&gt;DataEditor&lt;/code&gt; implementation, you can continue using it, but it needs to be updated for Umbraco 17.&lt;/p&gt;

&lt;p&gt;Most of the implementation remains similar to Umbraco 13. The main difference is that several parameters have been removed from the &lt;code&gt;DataEditor&lt;/code&gt; attribute because of the clearer separation between UI and backend logic.&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;// Umbraco 13&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DataEditor&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;"proudnerds.videoplayer.editor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Video Player"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"/App_Plugins/VideoPlayer/videoplayer.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Proud Nerds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Icon&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"icon-play"&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;VideoPlayerEditor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DataEditor&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;// Umbraco 17&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DataEditor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"proudnerds.videoplayer.editor"&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;VideoPlayerEditor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DataEditor&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Updating the Property Value Converter (if needed)
&lt;/h3&gt;

&lt;p&gt;If you used the manifest-only approach in Umbraco 13, there is a good chance your Property Value Converter no longer works.&lt;/p&gt;

&lt;p&gt;During migration, the &lt;code&gt;EditorAlias&lt;/code&gt; for the video editor changed from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;proudnerds.videoplayer.editor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Umbraco.Plain.Json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if your Property Value Converter checks the editor alias, it will no longer match:&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;override&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsConverter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IPublishedPropertyType&lt;/span&gt; &lt;span class="n"&gt;propertyType&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;propertyType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EditorAlias&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"proudnerds.videoplayer.editor"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The simplest fix is to check the &lt;strong&gt;EditorUiAlias&lt;/strong&gt; instead:&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;override&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsConverter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IPublishedPropertyType&lt;/span&gt; &lt;span class="n"&gt;propertyType&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;propertyType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EditorUiAlias&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"proudnerds.videoplayer.editor"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not entirely semantically correct, but it is often the easiest solution without introducing additional migrations. After this, your Property Editor will work as expected again and the models builder will use your custom model instead of the generic JsonDocument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Done
&lt;/h2&gt;

&lt;p&gt;Once you understand how the Property Editor migration works, it becomes much easier to determine which aliases to use in &lt;code&gt;umbraco-package.json&lt;/code&gt; and what code changes are required after upgrading.&lt;/p&gt;

&lt;p&gt;The concepts themselves are straightforward, but the migration can be confusing the first time you encounter it. Hopefully this overview helps clarify what is happening behind the scenes.&lt;/p&gt;

&lt;h2&gt;
  
  
  EditorUiAlias or not?
&lt;/h2&gt;

&lt;p&gt;Earlier I mentioned that checking &lt;code&gt;EditorUiAlias&lt;/code&gt; inside the &lt;code&gt;IsConverter&lt;/code&gt; method of a Property Value Converter is not entirely correct from a semantic point of view.&lt;/p&gt;

&lt;p&gt;A Property Value Converter operates on the data stored in the database and converts that to something else to put in the cache. A Property Editor UI is only the interface used to edit that data. In fact, multiple UIs could theoretically exist for the same underlying editor.&lt;/p&gt;

&lt;p&gt;Because of that, checking &lt;code&gt;EditorUiAlias&lt;/code&gt; introduces some risk. If a different UI were introduced for the same editor, your converter might no longer match the correct data. Checking &lt;code&gt;EditorAlias&lt;/code&gt; is the safest way to guarantee you are handling the expected data structure.&lt;/p&gt;

&lt;p&gt;That said, in many real-world scenarios a Property Editor has exactly one UI and one DataEditor that always belong together. If you fully control the implementation, checking &lt;code&gt;EditorUiAlias&lt;/code&gt; can be a pragmatic and perfectly workable solution that avoids writing additional migrations.&lt;/p&gt;

&lt;p&gt;Opinions on this tend to differ. Some developers strongly prefer to always check &lt;code&gt;EditorAlias&lt;/code&gt; for correctness, while others consider &lt;code&gt;EditorUiAlias&lt;/code&gt; acceptable when the editor and UI are tightly coupled. In the end, the choice depends on how strictly you want to follow the separation between UI and data.&lt;/p&gt;

</description>
      <category>umbraco</category>
      <category>webdev</category>
    </item>
    <item>
      <title>No, you don’t need Lit, Vite, or TypeScript to Extend the Umbraco Backoffice</title>
      <dc:creator>Luuk Peters</dc:creator>
      <pubDate>Wed, 05 Nov 2025 10:30:59 +0000</pubDate>
      <link>https://forem.com/luukpeters/no-you-dont-need-lit-vite-or-typescript-to-extend-the-umbraco-backoffice-2mg6</link>
      <guid>https://forem.com/luukpeters/no-you-dont-need-lit-vite-or-typescript-to-extend-the-umbraco-backoffice-2mg6</guid>
      <description>&lt;p&gt;One of the biggest misconceptions I see pop up regularly among developers who start working on the new Umbraco "Bellissima" backoffice for the first time is that they think you need a lot of scaffolding and an entire build pipeline to extend the Umbraco 14+ backoffice.  &lt;/p&gt;

&lt;p&gt;This makes it seem like arbitrary extensions to the backoffice are very hard, especially for backend developers who aren’t into npm and such.&lt;br&gt;&lt;br&gt;
I'm here to tell you that you don’t need to use Lit, Vite, or TypeScript if you don’t want to.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why do so many developers think you need all that scaffolding?
&lt;/h2&gt;

&lt;p&gt;For the most part, this is a documentation thing. Umbraco itself uses Lit, Vite, and TypeScript in its codebase, and as a result, most examples in the documentation are based on Lit. Also, the &lt;a href="https://docs.umbraco.com/umbraco-cms/customizing/development-flow" rel="noopener noreferrer"&gt;Setup your development environment&lt;/a&gt; article talks about setting up TypeScript and Vite, which reinforces that impression.&lt;/p&gt;

&lt;p&gt;You might think it’s just a matter of better documentation, and although I agree that the docs could improve, the problem is actually a bit bigger.&lt;/p&gt;




&lt;h2&gt;
  
  
  A well-designed (and overly flexible) architecture
&lt;/h2&gt;

&lt;p&gt;The funny thing is that biggest problem is that the backoffice architecture is really well designed. That sounds like a contradiction, but hear me out.&lt;/p&gt;

&lt;p&gt;The backoffice is built to be flexible, extensible, and framework-agnostic. Because of this, there are actually very few hard requirements for extending it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI extensions (like property editors, buttons, or workspaces) must be web components.&lt;/li&gt;
&lt;li&gt;Other extensions (like repositories or stores) must follow the requirements of their extension type.&lt;/li&gt;
&lt;li&gt;And lastly, you need an &lt;code&gt;umbraco-package.json&lt;/code&gt; file to register the extensions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But that’s practically it.&lt;/p&gt;

&lt;p&gt;This means that as long as you follow these basic rules, you can create your extension however you want. Yes, Umbraco uses TypeScript for their code, Lit for its web components, and Vite to bundle it all, but you don’t have to. You can use your own framework and bundler if you like, as long as your bundles use the ES module syntax.&lt;/p&gt;




&lt;h2&gt;
  
  
  No scaffolding? No problem
&lt;/h2&gt;

&lt;p&gt;You can also use plain JavaScript and skip any compilation or bundling entirely. As long as the requirements are met. That means no npm packages, no build scripts, and no scaffolding.&lt;/p&gt;

&lt;p&gt;So, there are many ways to go about this. And that’s what I mean when I say that the backoffice architecture is both a blessing and a curse: there are so many possibilities that it’s impossible to document every one of them.&lt;/p&gt;

&lt;p&gt;That said, I also believe the documentation could do a better job at showing this flexibility. I’m part of the Umbraco Documentation Community Team, and I try to spread the message of this blog wherever I can.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to use what
&lt;/h2&gt;

&lt;p&gt;There’s no absolute right or wrong, but here’s my opinion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use plain JavaScript for small and simple things, like a &lt;a href="https://forum.umbraco.com/t/custom-block-grid-views-too-complicated/2894/49?u=luuk" rel="noopener noreferrer"&gt;block view&lt;/a&gt; or a small Tiptap extension.&lt;/li&gt;
&lt;li&gt;For anything bigger, it’s worth investing the time to use TypeScript — it elevates the quality of your code and prevents bugs.
Especially if you’re a C# developer, you’ll probably appreciate the strong typing.&lt;/li&gt;
&lt;li&gt;Once you’re already using npm and build scripts, it’s just a small step to introduce Lit and bundle it with Vite.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the end, it’s not that hard. I’m primarily a C# backend developer, and yes, the learning curve exists, but I can honestly say that the quality of my packages has improved significantly since adopting these tools.&lt;/p&gt;

&lt;p&gt;I’d also recommend going with Lit for elements. The Umbraco source code and most examples use it, so unless you have a specific reason not to, it’s a good default.&lt;br&gt;&lt;br&gt;
Lit is lightweight and removes the most annoying parts of writing web components by hand.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want to learn more?
&lt;/h2&gt;

&lt;p&gt;Because I don’t want to bloat this blog with tons of examples, I’ll wrap up with a few useful links if you want to dive deeper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I recently rewrote &lt;a href="https://docs.umbraco.com/umbraco-cms/customizing/extending-overview/extension-registry" rel="noopener noreferrer"&gt;the documentation on the extension registry&lt;/a&gt;, which hopefully makes things a bit clearer.
&lt;/li&gt;
&lt;li&gt;For the Umbraco Autumn Contribution Challenge 2025, I created an example showing how to register a custom condition to an existing extension.
The example illustrates three different approaches (with the same result):
vanilla JavaScript, TypeScript without bundling, and Lit + Vite.
You can find it on &lt;a href="https://github.com/Luuk1983/UmbracoExamples/blob/main/ConditionsToExistingExtensions/README.md" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;In short: you don’t need all the scaffolding to extend Umbraco’s backoffice, but knowing how to use it can make your life a lot easier.&lt;/p&gt;

</description>
      <category>umbraco</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Sync NuGet and Umbraco Package versions automatically in Umbraco 14+</title>
      <dc:creator>Luuk Peters</dc:creator>
      <pubDate>Thu, 09 Oct 2025 10:15:29 +0000</pubDate>
      <link>https://forem.com/luukpeters/how-to-sync-nuget-and-umbraco-package-versions-automatically-in-umbraco-14-8i3</link>
      <guid>https://forem.com/luukpeters/how-to-sync-nuget-and-umbraco-package-versions-automatically-in-umbraco-14-8i3</guid>
      <description>&lt;p&gt;In an Umbraco package, there have always been two versions that matter: the version of the NuGet package and the version of the Umbraco extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  NuGet Package Version
&lt;/h2&gt;

&lt;p&gt;The NuGet package version is displayed in a NuGet feed and is relevant for your .NET solution. It's easy to see the currently installed version and check if any updates are available using the Semantic Versioning (&lt;a href="https://semver.org/" rel="noopener noreferrer"&gt;SemVer&lt;/a&gt;) scheme. &lt;/p&gt;

&lt;p&gt;The version for the NuGet package is set in the project file. This can be done using different properties, like &lt;code&gt;Version&lt;/code&gt;, &lt;code&gt;VersionPrefix&lt;/code&gt;, &lt;code&gt;PackageVersion&lt;/code&gt;, and others, but for this example, I'll stick to the &lt;code&gt;Version&lt;/code&gt; property like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&lt;/span&gt; &lt;span class="na"&gt;Sdk=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Sdk.Razor"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;net9.0&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
        ...
        &lt;span class="nt"&gt;&amp;lt;Version&amp;gt;&lt;/span&gt;16.0.0&lt;span class="nt"&gt;&amp;lt;/Version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, here are some custom packages in their NuGet feed showing their versions in Visual Studio:&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%2Fvjwp58uj0ob80r7kl1og.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%2Fvjwp58uj0ob80r7kl1og.png" alt="Example of a NuGet feed in Visual Studio" width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Umbraco Extension Version
&lt;/h2&gt;

&lt;p&gt;On the other hand, we have the version of the Umbraco extension. This is the version specified in the &lt;code&gt;package.manifest&lt;/code&gt; in Umbraco 13 or the &lt;code&gt;umbraco-package.json&lt;/code&gt; in Umbraco 16. This version number is displayed in the backoffice in the list of installed packages.&lt;/p&gt;

&lt;p&gt;For example, this &lt;code&gt;umbraco-package.json&lt;/code&gt;:&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;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../../umbraco-package-schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ProudNerds.Umbraco.ContentExpiration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ProudNerds Content Expiration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"16.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extensions"&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="err"&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;will be displayed in the backoffice of Umbraco 16 like this:&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%2Fs3lunebvbr414znly07c.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%2Fs3lunebvbr414znly07c.png" alt="Overview example of installed package in Umbraco" width="512" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Keep Them the Same?
&lt;/h2&gt;

&lt;p&gt;I believe these two versions should always be the same. It’s very convenient to check the version of a package directly in the backoffice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version Synchronization in Umbraco 13
&lt;/h2&gt;

&lt;p&gt;In Umbraco 13, we actually had an option to take the assembly version and use that as the version of the Umbraco extension:&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ProudNerds Content Expiration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ProudNerds.Umbraco.ContentExpiration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"versionAssemblyName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ProudNerds.Umbraco.ContentExpiration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&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;By specifying the assembly that holds the NuGet version in the &lt;code&gt;versionAssemblyName&lt;/code&gt; property, the extension would automatically use the same version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version Synchronization in Umbraco 16
&lt;/h2&gt;

&lt;p&gt;In Umbraco 16, this feature no longer exists. Because the backoffice is now a separate entity that has no knowledge of any backend C# code, there’s no easy way to read a version from an assembly.&lt;/p&gt;

&lt;p&gt;The way I currently make sure that my extension version is the same as the NuGet version is by installing a small Umbraco NuGet package. Then I use a task that's in that package in an after-build event to update the version number in the &lt;code&gt;umbraco-package.json&lt;/code&gt; file. In this setup, the NuGet version is leading.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install the Helper Package
&lt;/h3&gt;

&lt;p&gt;Install the NuGet package &lt;code&gt;Umbraco.JsonSchema.Extensions&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its version is &lt;code&gt;0.3.0&lt;/code&gt; at the time of writing, so it doesn’t feel like a finished product, but it does its job well — and Umbraco uses it themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Create an After-Build Event
&lt;/h3&gt;

&lt;p&gt;Add the following target to your project file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Updates the version in the umbraco-package.json file to match the NuGet package version --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Target&lt;/span&gt; &lt;span class="na"&gt;Name=&lt;/span&gt;&lt;span class="s"&gt;"UpdateUmbracoPackageJsonVersion"&lt;/span&gt; &lt;span class="na"&gt;AfterTargets=&lt;/span&gt;&lt;span class="s"&gt;"Build"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Change the path to your umbraco-package.json file --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;UmbracoPackageJsonPath&amp;gt;&lt;/span&gt;$(ProjectDir)wwwroot\App_Plugins\ExamplePlugin\umbraco-package.json&lt;span class="nt"&gt;&amp;lt;/UmbracoPackageJsonPath&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;Message&lt;/span&gt;
        &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"Checking for umbraco-package.json at: $(UmbracoPackageJsonPath)"&lt;/span&gt;
        &lt;span class="na"&gt;Importance=&lt;/span&gt;&lt;span class="s"&gt;"High"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- This will use the Version property from the project file.
         Change it here if you want to use VersionPrefix, for example. --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;JsonPathUpdateValue&lt;/span&gt;
        &lt;span class="na"&gt;JsonFile=&lt;/span&gt;&lt;span class="s"&gt;"$(UmbracoPackageJsonPath)"&lt;/span&gt;
        &lt;span class="na"&gt;Path=&lt;/span&gt;&lt;span class="s"&gt;"$.version"&lt;/span&gt;
        &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"&amp;amp;quot;$(Version)&amp;amp;quot;"&lt;/span&gt;
        &lt;span class="na"&gt;Condition=&lt;/span&gt;&lt;span class="s"&gt;"Exists('$(UmbracoPackageJsonPath)')"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;Message&lt;/span&gt;
        &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"✅ Updated umbraco-package.json version to $(Version)."&lt;/span&gt;
        &lt;span class="na"&gt;Importance=&lt;/span&gt;&lt;span class="s"&gt;"High"&lt;/span&gt;
        &lt;span class="na"&gt;Condition=&lt;/span&gt;&lt;span class="s"&gt;"Exists('$(UmbracoPackageJsonPath)')"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;Message&lt;/span&gt;
        &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"⚠️ Skipped updating umbraco-package.json version - file not found at $(UmbracoPackageJsonPath)."&lt;/span&gt;
        &lt;span class="na"&gt;Importance=&lt;/span&gt;&lt;span class="s"&gt;"High"&lt;/span&gt;
        &lt;span class="na"&gt;Condition=&lt;/span&gt;&lt;span class="s"&gt;"!Exists('$(UmbracoPackageJsonPath)')"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Target&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is all you need! On every build, you will now see the messages appear in your output on build:&lt;br&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%2Fudkdz93sv1p8ttlmtf0r.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%2Fudkdz93sv1p8ttlmtf0r.png" alt="Example of output with the updated package version" width="800" height="35"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;By automatically updating the version in &lt;code&gt;umbraco-package.json&lt;/code&gt; after every build, you can ensure your Umbraco extension version stays in sync with your NuGet package version. This makes it easier to track versions across your solution and in the Umbraco backoffice without manual updates.&lt;/p&gt;

</description>
      <category>umbraco</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Upgrade Umbraco 13 to 16: API controllers</title>
      <dc:creator>Luuk Peters</dc:creator>
      <pubDate>Thu, 02 Oct 2025 10:00:53 +0000</pubDate>
      <link>https://forem.com/luukpeters/umbraco-13-to-16-api-controllers-235n</link>
      <guid>https://forem.com/luukpeters/umbraco-13-to-16-api-controllers-235n</guid>
      <description>&lt;p&gt;This is part four in a series of blogs about common tasks you'll encounter when updating Umbraco 13 to 16. In this part, we'll look into updating Umbraco API controllers to Umbraco 16. We'll cover the &lt;code&gt;UmbracoApiController&lt;/code&gt; and the &lt;code&gt;UmbracoAuthorizedApiController&lt;/code&gt;, because both of them no longer exist in Umbraco 16.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update UmbracoApiControllers
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;UmbracoApiController&lt;/code&gt; is a controller for creating your own Web API endpoints. There really wasn't much special about these controllers, except that they were routed automatically to a certain URL. As an example, take this endpoint:&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;ExampleController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UmbracoApiController&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="nf"&gt;HelloWorld&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="s"&gt;"Hello World!"&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;It would be routed to &lt;code&gt;~/Umbraco/Api/Example/HelloWorld&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Umbraco likely recognized that an &lt;code&gt;UmbracoApiController&lt;/code&gt; isn’t very special, so it was replaced with regular controllers where you are responsible for defining the routing yourself. Umbraco has documentation for &lt;a href="https://docs.umbraco.com/umbraco-cms/reference/routing/umbraco-api-controllers/porting-old-umbraco-apis" rel="noopener noreferrer"&gt;porting over old Umbraco API Controllers&lt;/a&gt;, so I’ll just refer you to that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update UmbracoAuthorizedApiControllers
&lt;/h2&gt;

&lt;p&gt;The changes to the &lt;code&gt;UmbracoAuthorizedApiController&lt;/code&gt; are more significant. These controllers were meant to be used by extensions in the Umbraco backoffice and could only be called in the context of a logged-in user. You could think of them as &lt;code&gt;backoffice API endpoints&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cue the Management API
&lt;/h3&gt;

&lt;p&gt;With the new Umbraco 14+ backoffice, the Management API was introduced as a uniform way for the frontend to communicate with the backend. Compared to Umbraco 13, this is like backoffice API endpoints in steroids! To use custom endpoints for your backoffice extensions, you now add them to the Management API. So we need to change our &lt;code&gt;UmbracoAuthorizedApiControllers&lt;/code&gt; to use the Management API.&lt;/p&gt;

&lt;p&gt;As an example, here’s a small API endpoint in Umbraco 13 that creates a new website in the content tree. We’ll update this to Umbraco 16:&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="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;PluginController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CoreContent"&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;CoreContentDashboardApiController&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UmbracoAuthorizedApiController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;CreateNewWebsite&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;websiteName&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Perform the update
&lt;/h3&gt;

&lt;p&gt;First, change the base class from &lt;code&gt;UmbracoAuthorizedApiController&lt;/code&gt; to &lt;code&gt;ManagementApiControllerBase&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CoreContentDashboardApiController&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ManagementApiControllerBase&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, remove the &lt;code&gt;PluginController&lt;/code&gt; attribute and replace it with &lt;code&gt;VersionedApiBackOfficeRoute&lt;/code&gt; and &lt;code&gt;ApiExplorerSettings&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="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;VersionedApiBackOfficeRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"core-content"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ApiExplorerSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GroupName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ProudNerds Core"&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;CoreContentDashboardApiController&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ManagementApiControllerBase&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So what did we add?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;VersionedApiBackOfficeRoute&lt;/code&gt; determines the last part of the URL. To stay consistent with the rest of the Management API, use lowercase letters and separate words with a dash. In this example, the base URL of the endpoints will be &lt;code&gt;/umbraco/management/api/v1/core-content&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;ApiExplorerSettings&lt;/code&gt; with a &lt;code&gt;GroupName&lt;/code&gt; ensures a separate section with that title will appear in the Swagger docs of the Management API.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The individual endpoints don’t need many changes, but keep these caveats and tips in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Umbraco uses a single responsibility per controller in their source code, so typically you’ll have one endpoint per controller. If you place multiple endpoints in a single controller, you need to supply a template for the &lt;code&gt;HttpGet&lt;/code&gt; (or other HTTP method) to make sure the endpoints are unique, for example &lt;code&gt;[HttpGet("create-new-website")]&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add &lt;code&gt;ProducesResponseType&lt;/code&gt; attributes to enhance the Swagger docs. But avoid adding response types for common codes like 400 and 404, as this will break Swagger because Umbraco already registers those internally.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a complete API could look 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="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;VersionedApiBackOfficeRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"core-content/create-new-website"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ApiExplorerSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GroupName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ProudNerds Core"&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;CoreContentDashboardApiController&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ManagementApiControllerBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Because this controller has only one endpoint,&lt;/span&gt;
    &lt;span class="c1"&gt;// we don’t need to set a template on the HttpGet.&lt;/span&gt;
    &lt;span class="c1"&gt;// The VersionedApiBackOfficeRoute determines the endpoint.&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ProducesResponseType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status200OK&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ProducesResponseType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status400BadRequest&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ProducesResponseType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProblemDetails&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status500InternalServerError&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;CreateNewWebsite&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;websiteName&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will add a section in Swagger of the Management API with our custom endpoint:&lt;br&gt;&lt;br&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%2Fvzt3tws71v93snt2yo73.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%2Fvzt3tws71v93snt2yo73.png" alt="Endpoint as seen in Swagger in the Management API" width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Granular security
&lt;/h3&gt;

&lt;p&gt;By default, Management API endpoints are accessible to any logged-in Umbraco user. However, sometimes you need to restrict access to specific roles or policies. That’s where the &lt;code&gt;Authorize&lt;/code&gt; attribute comes in:&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="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AuthorizationPolicies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequireAdminAccess&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;VersionedApiBackOfficeRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"core-content"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ApiExplorerSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GroupName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ProudNerds Core"&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;CoreContentDashboardApiController&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ManagementApiControllerBase&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;In this example, the endpoint is only accessible to administrators.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.umbraco.com/umbraco-cms/tutorials/creating-a-backoffice-api" rel="noopener noreferrer"&gt;documentation for creating a backoffice API&lt;/a&gt; has more information and a more elaborate example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Updating Umbraco APIs isn’t difficult once you know what to look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace &lt;code&gt;UmbracoApiController&lt;/code&gt; usage with regular controllers and wire up routing yourself.&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;UmbracoAuthorizedApiController&lt;/code&gt; with &lt;code&gt;ManagementApiControllerBase&lt;/code&gt;, add &lt;code&gt;VersionedApiBackOfficeRoute&lt;/code&gt; and &lt;code&gt;ApiExplorerSettings&lt;/code&gt;, and secure endpoints with &lt;code&gt;Authorize&lt;/code&gt; when needed.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>umbraco</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Upgrade Umbraco 13 to 16: Localization</title>
      <dc:creator>Luuk Peters</dc:creator>
      <pubDate>Thu, 04 Sep 2025 09:35:14 +0000</pubDate>
      <link>https://forem.com/luukpeters/upgrade-umbraco-13-to-16-localization-133n</link>
      <guid>https://forem.com/luukpeters/upgrade-umbraco-13-to-16-localization-133n</guid>
      <description>&lt;p&gt;This is part three in a series of blogs about common tasks you'll encounter when updating Umbraco 13 to 16. In this part, we'll look into updating localization to work with Umbraco 16. First, we'll cover updating backoffice localization, then we'll cover changes to localization in C# code, and finally, I'll briefly touch on using localization in Umbraco backoffice extensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backoffice localization (UI Localization)
&lt;/h2&gt;

&lt;p&gt;Probably the most common localization feature is the ability to localize the backoffice. Think of the names and descriptions of content types and their properties, but also things like sections, dashboards, and message popups.&lt;/p&gt;

&lt;p&gt;Backoffice localization works in roughly the same way in Umbraco 13 and 16: you have a file for each language you want to support in the backoffice, and each file defines a set of keys with localized values. You then use those keys in various places in the Umbraco backoffice. Because this blog focuses on upgrading Umbraco, I’ll assume you already know how localization works in the Umbraco 13 backoffice. If not, read the &lt;a href="https://docs.umbraco.com/umbraco-cms/13.latest/extending/language-files" rel="noopener noreferrer"&gt;localization documentation of Umbraco 13&lt;/a&gt; first.&lt;/p&gt;

&lt;p&gt;The main difference between Umbraco 13 and 16 is the file format and how the files are registered, so for the most part, backoffice localization is not difficult to update.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update language files
&lt;/h3&gt;

&lt;p&gt;Let’s start by updating the language files to the new format. In Umbraco 13, language files were XML, but in Umbraco 16 they are — like almost everything in the backoffice — extensions. This means they work with JavaScript and must be registered using a manifest. And because they are JavaScript, you can actually do some useful things with them, but I’ll come back to that later.&lt;/p&gt;

&lt;p&gt;It’s also good to note that XML language files haven’t completely disappeared; they are still used for localization in backend C# code. However, everything in the backoffice now uses JavaScript.&lt;/p&gt;

&lt;p&gt;Here’s an example Umbraco 13 language file, located in &lt;code&gt;wwwroot/App_Plugins/Example/Lang/en.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="utf-8" ?&amp;gt;&lt;/span&gt; 
&lt;span class="nt"&gt;&amp;lt;language&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;area&lt;/span&gt; &lt;span class="na"&gt;alias=&lt;/span&gt;&lt;span class="s"&gt;"exampledocype"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&lt;/span&gt; &lt;span class="na"&gt;alias=&lt;/span&gt;&lt;span class="s"&gt;"doctype_name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Example&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&lt;/span&gt; &lt;span class="na"&gt;alias=&lt;/span&gt;&lt;span class="s"&gt;"doctype_description"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;This is an example doc type&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&lt;/span&gt; &lt;span class="na"&gt;alias=&lt;/span&gt;&lt;span class="s"&gt;"property1_name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Property name&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&lt;/span&gt; &lt;span class="na"&gt;alias=&lt;/span&gt;&lt;span class="s"&gt;"property1_description"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Use this property for something&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/area&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/language&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We now need to convert this file to JavaScript. The idea remains the same, but unlike Umbraco 13 — where language files had to be in a specific folder to be automatically loaded — in Umbraco 16 you can name your file anything and place it anywhere. The only requirement is that you register the file in a manifest. Just make sure to use the same aliases for your keys.&lt;/p&gt;

&lt;p&gt;For example, let's update the Umbraco 13 example and place it at &lt;code&gt;wwwroot/App_Plugins/Example/localization-en.js&lt;/code&gt;:&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;exampledocype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;doctype_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Example&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;doctype_description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is an example doc type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;property1_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Property name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;property1_description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Use this property for something&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that’s it! For large files, simply let AI do the conversion for you — it’s surprisingly good at it. Now we just need to register the localizations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Register localization in 16
&lt;/h3&gt;

&lt;p&gt;As mentioned, localizations are backoffice extensions. This means they need to be registered using an Extension Manifest and are not automatically loaded by convention. There are multiple ways to register manifests, but in this example, we’ll register them directly in the &lt;code&gt;umbraco-package.json&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Create or update &lt;code&gt;wwwroot/App_Plugins/Example/umbraco-package.json&lt;/code&gt; and add localization manifests. You need a manifest for each language:&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;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../../umbraco-package-schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example.Localization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A localization example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"extensions"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"localization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"alias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example.Localization.Localize.En"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"English"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"meta"&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"&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;"js"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/App_Plugins/Example/localization-en.js"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"localization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"alias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example.Localization.Localize.Dk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Danish"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"meta"&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;"dk"&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;"js"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/App_Plugins/Example/localization-dk.js"&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;Now, most backoffice localization should work again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix property description fields
&lt;/h3&gt;

&lt;p&gt;The bulk of the work to make your backoffice localizations from Umbraco 13 work in 16 is now done, but we’re not finished yet. Not all localizations work out of the box. Specifically, description fields on properties do not localize as they did in Umbraco 13. If you use a key in a description field the same way as before, the description will not be localized:&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%2F1wftmod7ou4rp30u3bth.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%2F1wftmod7ou4rp30u3bth.png" alt=" " width="800" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead, you need to wrap the description key in curly brackets:&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%2Fheu4wg7219jte5ue83f3.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%2Fheu4wg7219jte5ue83f3.png" alt=" " width="783" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is because descriptions are passed through the &lt;a href="https://docs.umbraco.com/umbraco-cms/reference/umbraco-flavored-markdown" rel="noopener noreferrer"&gt;Umbraco Flavoured Markdown&lt;/a&gt; pipeline, which requires the correct formatting.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://docs.umbraco.com/umbraco-cms/customizing/foundation/localization" rel="noopener noreferrer"&gt;Umbraco 16 documentation on backoffice localization&lt;/a&gt; for more details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backend localizations (.NET localization)
&lt;/h2&gt;

&lt;p&gt;Backend localizations in .NET are useful when you need to localize texts for things like sending emails, handling errors, or running health checks. &lt;/p&gt;

&lt;h3&gt;
  
  
  No more ILocalizationService
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;ILocalizationService&lt;/code&gt; no longer exists. It was a bit of an odd service with too many responsibilities. You could use it to manage content languages in Umbraco as well as manage the Umbraco dictionary. This functionality has now been split into two services: &lt;code&gt;ILanguageService&lt;/code&gt; and &lt;code&gt;IDictionaryService&lt;/code&gt;. It’s straightforward to use them, so you shouldn’t have much difficulty updating code that uses the &lt;code&gt;IDictionaryService&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ILocalizeTextService&lt;/code&gt; still exists and works the same as in Umbraco 13. It is the backend service for reading localization files using keys. This service still uses XML files and not the JavaScript files from the backoffice.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://docs.umbraco.com/umbraco-cms/extending/language-files/net-localization" rel="noopener noreferrer"&gt;Umbraco 16 documentation on .NET localization&lt;/a&gt; for more details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Localization in your extensions
&lt;/h2&gt;

&lt;p&gt;I also want to briefly touch on localization in custom Umbraco extensions and provide some links to the documentation.&lt;/p&gt;

&lt;p&gt;You can also reference localization keys directly in your extension manifests. For instance, this is an example of the title of a dashboard in the backoffice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UmbExtensionManifest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UmbExtensionManifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dashboard&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="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#dashboardTabs_contentExpirationDashboard&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;p&gt;If you are building a custom Umbraco web component, make sure it inherits from &lt;code&gt;UmbLitElement&lt;/code&gt; or uses the &lt;code&gt;UmbElementMixin&lt;/code&gt; on the &lt;code&gt;LitElement&lt;/code&gt;. If you do, you’ll get a helper for localization in your component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LitElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;customElement&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@umbraco-cms/backoffice/external/lit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UmbElementMixin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@umbraco-cms/backoffice/element-api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyElement&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;UmbElementMixin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LitElement&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;uui-button .label=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;term&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;general_close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;
        &amp;lt;/uui-button&amp;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;p&gt;You can also use the &lt;code&gt;umb-localize&lt;/code&gt; element in your component like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;umb&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;localize&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dialog_myKey&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/umb-localize&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works the same way as the &lt;code&gt;&amp;lt;localize&amp;gt;&lt;/code&gt; element in AngularJS back in Umbraco 13.&lt;/p&gt;

&lt;p&gt;Read the &lt;a href="https://docs.umbraco.com/umbraco-cms/customizing/foundation/localization#using-the-localizations" rel="noopener noreferrer"&gt;documentation on using localization in Umbraco 16&lt;/a&gt; for more information.&lt;/p&gt;

&lt;p&gt;Because localizations are now in JavaScript, this opens up new possibilities. You can use arguments and placeholders in your localizations. For example, you can define a localization for singular and plural using the same key and an argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;section&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;numberOfItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&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="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Showing nothing&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Showing only one item&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Showing &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; items`&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the &lt;a href="https://docs.umbraco.com/umbraco-cms/customizing/foundation/localization#using-the-localizations" rel="noopener noreferrer"&gt;documentation on using arguments and placeholders in Umbraco 16&lt;/a&gt; for more information.&lt;/p&gt;

</description>
      <category>umbraco</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Upgrade Umbraco 13 to 16: First upgrade steps</title>
      <dc:creator>Luuk Peters</dc:creator>
      <pubDate>Tue, 26 Aug 2025 11:18:58 +0000</pubDate>
      <link>https://forem.com/luukpeters/upgrade-umbraco-13-to-16-first-upgrade-steps-5e1l</link>
      <guid>https://forem.com/luukpeters/upgrade-umbraco-13-to-16-first-upgrade-steps-5e1l</guid>
      <description>&lt;p&gt;This is part two in a series of blogs about common things you'll encounter when updating Umbraco 13 to 16. In this part, we'll look into the prerequisites for an update, perform the first upgrade steps and fix a number of errors as a result of the upgrade steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Let's look at what needs to be in place before you can start the update.&lt;/p&gt;

&lt;p&gt;Umbraco 16 requires .NET 9, whereas Umbraco 13 used .NET 8. So you'll need to have .NET 9 installed on your development system. If you're using Visual Studio, you need at least version 17.12, which is the first version with .NET 9 support.&lt;/p&gt;

&lt;p&gt;There are also two notable missing features in Umbraco 14+ that were still present in Umbraco 13 (although deprecated): &lt;strong&gt;Nested Content&lt;/strong&gt; and &lt;strong&gt;Macros&lt;/strong&gt;. You'll need to migrate these to &lt;strong&gt;Block Lists&lt;/strong&gt; and &lt;strong&gt;Blocks&lt;/strong&gt;, respectively. I prefer handling migrations while still on Umbraco 13, so everything is up-to-date before the upgrade and there's one less thing to worry about during the upgrade process. I might cover this in more detail in a future blog.&lt;/p&gt;

&lt;p&gt;Lastly, before any update, always ensure you're on the latest Umbraco 13 version and using the latest compatible versions of all your packages. This gives you the best chance of a smooth upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update .NET Version
&lt;/h2&gt;

&lt;p&gt;The first upgrade step is updating the .NET version. Umbraco 13 uses .NET 8, while Umbraco 16 uses .NET 9. Simply update the &lt;em&gt;Target Framework&lt;/em&gt; in all project files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Original --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;net8.0&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Updated --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;net9.0&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Update NuGet Packages
&lt;/h2&gt;

&lt;p&gt;Next, update all NuGet packages. Start with the Umbraco packages, then move on to the others.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Remove the Umbraco.Cms.Web.Backoffice package. This package no longer exists in Umbraco 16.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Update all Umbraco packages to the latest version (16.1.1 at the time of writing). Due to dependencies, updating via the NuGet Package Manager can be tricky. I usually update the version numbers directly in the project file(s) using find/replace.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Update all other NuGet packages. Be aware that some popular packages, like Contentment and SEOChecker, may not have final versions for Umbraco 16 yet. You might need to check for pre-release versions.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once all packages are updated, you'll likely see a massive amount of build errors—this is expected. Let's fix some common ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix Caching and Especially the Models Builder
&lt;/h2&gt;

&lt;p&gt;Umbraco 15 introduced the HybridCache as a replacement for NuCache. As a result, IPublishedSnapshotAccessor is no longer available. Any code using it needs to be updated.&lt;/p&gt;

&lt;p&gt;Most errors will probably come from models generated by the Models Builder, as every generated model references IPublishedSnapshotAccessor.&lt;/p&gt;

&lt;p&gt;Manually updating all models would be a waste of time. Fortunately, Søren Kottal wrote an excellent blog on how to fix this quickly:&lt;br&gt;
&lt;a href="https://dev.to/skttl/quick-fix-for-ipublishedsnapshotaccessor-issues-when-upgrading-to-umbraco-15-4pa4"&gt;Quick fix for IPublishedSnapshotAccessor issues&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Outside of Models Builder, you can use IPublishedContentCache instead of IPublishedSnapshotAccessor. Updating the code should be straightforward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Small Suggestions and Fixes
&lt;/h2&gt;

&lt;p&gt;Larger topics—like updating UmbracoApiController, UmbracoAuthorizedApiController, and localization—will be covered in separate blogs. But here are a few small things you might run into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You'll notice many more functions are now async. In most cases, it's just a matter of making the wrapping function async and using the await keyword.&lt;br&gt;
If a function name has changed, it's usually obvious what the new name is. For example, &lt;em&gt;Save&lt;/em&gt; functions are now often split into &lt;em&gt;Create&lt;/em&gt; and &lt;em&gt;Update&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It's not perfect yet, but most code now uses GUIDs for identifiers (e.g., for nodes or users).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This is reflected in the fact that UmbracoCore.Constants.Security.SuperUserId is deprecated. You now need to use UmbracoCore.Constants.Security.SuperUserKey.&lt;br&gt;
Since SuperUserKey is not a constant, you can't use it as a default parameter value in functions anymore.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s it for now! Hopefully this gives you a solid start on upgrading to Umbraco 16. I’ll be covering more topics like API controller changes, localization tweaks, and migrating deprecated features in future posts. If you’ve run into anything interesting or hit a snag during your own upgrade, I’d love to hear about it—drop a comment or reach out!&lt;/p&gt;

</description>
      <category>umbraco</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Vibecoding a Mobile-Friendly Umbraco backoffice experience</title>
      <dc:creator>Luuk Peters</dc:creator>
      <pubDate>Mon, 25 Aug 2025 12:13:44 +0000</pubDate>
      <link>https://forem.com/luukpeters/vibecoding-a-mobile-friendly-umbraco-backoffice-experience-occ</link>
      <guid>https://forem.com/luukpeters/vibecoding-a-mobile-friendly-umbraco-backoffice-experience-occ</guid>
      <description>&lt;p&gt;One of the pain points of the new backoffice introduced in Umbraco 14, is that the &lt;strong&gt;backoffice is not mobile-friendly&lt;/strong&gt;. This issue has been raised in &lt;a href="https://github.com/umbraco/Umbraco-CMS/discussions/17603" rel="noopener noreferrer"&gt;this discussion on GitHub&lt;/a&gt;.  &lt;/p&gt;

&lt;p&gt;As of Umbraco 16, the experience on a mobile device is still far from usable. Buttons and navigation elements are either inaccessible or too small to interact with properly. Sure, you can switch your browser to “desktop mode,” but then the entire interface becomes tiny and awkward to use.  &lt;/p&gt;

&lt;p&gt;Now, I don’t expect every complex application to be perfectly optimized for mobile. Some things make sense only in a desktop context. But the lack of mobile usability makes even small edits or urgent fixes on the go impossible. And sometimes, that’s exactly what you need: just a quick update while you’re away from your laptop.  &lt;/p&gt;

&lt;p&gt;That got me thinking:  &lt;/p&gt;

&lt;p&gt;Since Umbraco 14, we have the &lt;strong&gt;Management API&lt;/strong&gt; that technically allows us to build &lt;em&gt;a completely different backoffice experience&lt;/em&gt; if we wanted to. What if we built a &lt;strong&gt;lightweight, mobile-focused backoffice&lt;/strong&gt;—not with every feature, but just the essentials for content editing and publishing?&lt;/p&gt;

&lt;h2&gt;
  
  
  Vibecoding a Proof of Concept
&lt;/h2&gt;

&lt;p&gt;So I decided to experiment. I started &lt;strong&gt;vibecoding with Claude&lt;/strong&gt;, armed with access to an Umbraco MCP, the source code, and the documentation. The goal was clear:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browse the content tree easily
&lt;/li&gt;
&lt;li&gt;Edit content and media
&lt;/li&gt;
&lt;li&gt;Save and publish changes
&lt;/li&gt;
&lt;li&gt;All in a &lt;strong&gt;mobile-friendly layout&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s what came out of 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%2Fg975lc7ubx639eao8snd.gif" 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%2Fg975lc7ubx639eao8snd.gif" alt="POC in action" width="490" height="688"&gt;&lt;/a&gt;  &lt;/p&gt;

&lt;p&gt;Some highlights of the prototype:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browsing and selecting content happens in a &lt;strong&gt;modal&lt;/strong&gt; to have the best overview.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Navigation elements&lt;/strong&gt; are sized and spaced for mobile.
&lt;/li&gt;
&lt;li&gt;The functionality is deliberately minimal: &lt;strong&gt;save&lt;/strong&gt; and &lt;strong&gt;publish&lt;/strong&gt; only.
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;This proof of concept isn’t about replacing the full Umbraco backoffice. It’s about reimagining what’s possible with the Management API and showing how a streamlined “editor on the go” could work.  &lt;/p&gt;

&lt;p&gt;There are still plenty of challenges to solve:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handling complex property types like Blocks or Nested Content
&lt;/li&gt;
&lt;li&gt;Creating new content from scratch
&lt;/li&gt;
&lt;li&gt;Permissions and user flows
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But even in its rough state, this little POC proves that &lt;strong&gt;a mobile-friendly backoffice is possible&lt;/strong&gt;. And more importantly, it sparks inspiration: what if the future of Umbraco included a dedicated “mobile backoffice mode” built right into core?  &lt;/p&gt;

&lt;p&gt;For now, this vibecoded prototype shows that with a bit of creativity—and the power of the Management API—you can start bridging the gap.  &lt;/p&gt;

</description>
      <category>umbraco</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Upgrade Umbraco 13 to 16: introduction</title>
      <dc:creator>Luuk Peters</dc:creator>
      <pubDate>Fri, 08 Aug 2025 09:48:48 +0000</pubDate>
      <link>https://forem.com/luukpeters/upgrade-umbraco-13-to-16-introduction-28n2</link>
      <guid>https://forem.com/luukpeters/upgrade-umbraco-13-to-16-introduction-28n2</guid>
      <description>&lt;p&gt;It feels like only yesterday that the current LTS version of Umbraco (Umbraco 13) was released. Yet by the end of this year, it will already be celebrating its second birthday — and that can only mean one thing: time for a new Umbraco LTS version! At the end of the year, Umbraco 17 will arrive alongside .NET 10, which will also be an LTS release.&lt;/p&gt;

&lt;p&gt;Version 13 will still receive &lt;a href="https://umbraco.com/products/knowledge-center/long-term-support-and-end-of-life/" rel="noopener noreferrer"&gt;security updates&lt;/a&gt; until December 2026, but feature updates, regression fixes, and compatibility tweaks? Those are done. So, if you haven’t already, it’s time to start thinking about your Umbraco 17 upgrade plan.&lt;/p&gt;

&lt;p&gt;The biggest change in Umbraco 17 compared to 13 is, of course, the completely new bellissima backoffice, built with web components instead of AngularJS — first introduced in Umbraco 14. That release also brought us the Management API, giving full control over Umbraco through an API. In the same release, we waved goodbye to Nested Content and Macros (probably for the best 😉). Then Umbraco 15 introduced the HybridCache (replacing NuCache), block-level variants, and the shiny new TipTap rich text editor.&lt;/p&gt;

&lt;p&gt;As of now, Umbraco 17 isn’t out yet — not even a release candidate — but I don’t expect it to be drastically different from Umbraco 16. Version 16 was more of an evolution than a revolution compared to 14 and 15, with a strong focus on stability and performance rather than big new features. I expect Umbraco 17 will follow the same approach.&lt;/p&gt;

&lt;p&gt;In this blog series, I’ll go over some of the most common changes you’ll encounter when upgrading from Umbraco 13 to 16 (and, by extension, 17).&lt;/p&gt;

&lt;p&gt;Over the next few weeks, I’ll cover the following topics based on my experiences upgrading both Umbraco projects and packages to versions 15 and 16 (this list will be updated with links as the posts go live):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/luukpeters/upgrade-umbraco-13-to-16-first-upgrade-steps-5e1l"&gt;First upgrade steps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/luukpeters/upgrade-umbraco-13-to-16-localization-133n"&gt;Localization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/luukpeters/umbraco-13-to-16-api-controllers-235n"&gt;API controllers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ll only lightly touch on the new backoffice in these posts. This series isn’t meant to be an advanced guide to backoffice extensions, so I won’t be covering Lit, Vite, or TypeScript here — those deserve their own blog series in the future.&lt;/p&gt;

</description>
      <category>umbraco</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Use the Umbraco MCP in your IDE with GitHub Copilot</title>
      <dc:creator>Luuk Peters</dc:creator>
      <pubDate>Wed, 09 Jul 2025 13:22:48 +0000</pubDate>
      <link>https://forem.com/luukpeters/use-the-umbraco-mcp-in-your-ide-with-github-copilot-3jmh</link>
      <guid>https://forem.com/luukpeters/use-the-umbraco-mcp-in-your-ide-with-github-copilot-3jmh</guid>
      <description>&lt;p&gt;At Codegarden 2025, we were introduced to the awesome first version of the &lt;a href="https://github.com/Matthew-Wise/umbraco-mcp" rel="noopener noreferrer"&gt;Umbraco MCP&lt;/a&gt; by Matthew and Phil. And while still in development, it already shows great promise. As someone who uses GitHub Copilot daily, I wondered: Can I integrate Copilot with the Umbraco MCP in my IDE? Turns out—it’s totally doable.&lt;/p&gt;

&lt;p&gt;For this example, I’m using Visual Studio on Windows 11, but the general approach should work on other operating systems and IDEs as well. The idea is to run the Umbraco MCP locally and connect GitHub Copilot to it so I can use it in Visual Studio. This way, when my local Umbraco instance is running, Copilot can query Umbraco for more context or even execute actions within Umbraco.&lt;/p&gt;

&lt;p&gt;Before we can actually do anything with the mcp there are a few things we need to do first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install Node.js
&lt;/h2&gt;

&lt;p&gt;The Umbraco MCP is written in Node.js and requires version 22 or higher. So we need to have Node.js installed on the machine that runs the mcp server, in this case my local development machine. You can get Node.js from their &lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create an Umbraco API User
&lt;/h2&gt;

&lt;p&gt;The mcp works with the management API that was introduced in Umbraco 14. To access the management API, you obviously need to be authorized to access it. So we need to create credentials for when the mcp is accessing the management API. This is done by creating an Umbraco API User in the backoffice of the Umbraco instance the Umbraco MCP needs to access.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm just giving the Umbraco API maximum access by making it an administrator because this is just for development. Obviously be careful in other scenarios!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Log into the Umbraco backoffice, go to the member section and create a new API User&lt;br&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%2F3n239g5a82m0xfnza9kd.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%2F3n239g5a82m0xfnza9kd.png" alt="Showing location of the add API User option in the backoffice of Umbraco" width="484" height="257"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An API User can have one or more client credentials. These are the credentials we need for the Umbraco MCP. So when the API User is created, open the details of the user and add new client credentials. Remember these credentials, we need them later. Also Assign access to all content and media.&lt;br&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%2Fz5k93298qp7s17p2eguh.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%2Fz5k93298qp7s17p2eguh.png" alt="Where to add client credentials to an API User in the backoffice of Umbraco" width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now that we have the prerequisites done, it's time to add the mcp server to GitHub Copilot. You need Visual Studio 17.14 or later for this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create an mcp file in the solution
&lt;/h2&gt;

&lt;p&gt;Create a new file that will hold the Umbraco MCP configuration in the root of your solution: .mcp.json. This file will be solution scoped, but you can have other scopes by placing the file in other locations. See the &lt;a href="https://learn.microsoft.com/en-us/visualstudio/ide/mcp-servers?view=vs-2022#file-locations-for-automatic-discovery-of-mcp-configuration" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt; for that.&lt;/p&gt;

&lt;p&gt;Add the following server to the file, update the variables and save the 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;"servers"&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;"umbraco-mcp"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"@umbraco-mcp/umbraco-mcp-cms@alpha"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"UMBRACO_CLIENT_ID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CLIENT_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"UMBRACO_CLIENT_SECRET"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CLIENT_SECRET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"UMBRACO_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"URL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"NODE_TLS_REJECT_UNAUTHORIZED"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0"&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;The CLIENT_ID and CLIENT_SECRET are the client credentials we set up for the API User in Umbraco. Make sure the base URL does not end with a /, so for instance: &lt;a href="https://localhost:44327" rel="noopener noreferrer"&gt;https://localhost:44327&lt;/a&gt;. And because I use a self signed certificate locally, I set NODE_TLS_REJECT_UNAUTHORIZED to 0.&lt;/p&gt;

&lt;p&gt;As an example, I use this in my testproject:&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;"servers"&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;"umbraco-mcp"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"@umbraco-mcp/umbraco-mcp-cms@alpha"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"UMBRACO_CLIENT_ID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"umbraco-back-office-mcpuser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"UMBRACO_CLIENT_SECRET"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test123456!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"UMBRACO_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:44327"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"NODE_TLS_REJECT_UNAUTHORIZED"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0"&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;h2&gt;
  
  
  Getting it up and running in Visual Studio
&lt;/h2&gt;

&lt;p&gt;Open your Umbraco Visual Studio project. In agent mode, you will now see the Umbraco MCP in the tools list.&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%2Fwlh3sqpudbztms53cwkg.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%2Fwlh3sqpudbztms53cwkg.png" alt="The list of Umbraco MCP tools in Github Copilot" width="562" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It will shown an error, because it cannot connect to our Umbraco instance because it's not running. Start your Umbraco instance and reload the tool. It should now show 195 available tools!&lt;br&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%2Fip6um4izstiek4auznlu.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%2Fip6um4izstiek4auznlu.png" alt="Overview of the Umbraco tools" width="571" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As long as your Umbraco instance is running, you can query your Umbraco instance from you GitHub Copilot chat. It will ask for permission the first time a tool is used and you can decide how long you want to grant permission before it will ask for permission again:&lt;br&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%2Fomn5vi8fvt44lez3g8oe.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%2Fomn5vi8fvt44lez3g8oe.png" alt="Example of a permission prompt" width="560" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Visual Studio Code
&lt;/h2&gt;

&lt;p&gt;For Visual Studio Code, the idea is the same. I haven't tried it, but the two main differences are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need to enable MCP support with a setting&lt;/li&gt;
&lt;li&gt;The location of the .mcp.json file differs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See the official documentation for &lt;a href="https://code.visualstudio.com/docs/copilot/chat/mcp-servers" rel="noopener noreferrer"&gt;Visual Studio Code&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Done!
&lt;/h2&gt;

&lt;p&gt;We can now use the Umbraco MCP in Copilot as long as Umbraco is running! This was just an experiment to see if I could get it to work, the next step is to find out how this can help me during Umbraco development.&lt;/p&gt;

&lt;p&gt;But it's already so cool to be able to talk to Copilot and have it access and manage Umbraco:&lt;br&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%2Ffaxgzumt5w8vlx8hqy9j.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%2Ffaxgzumt5w8vlx8hqy9j.png" alt="Example of a chat with Copilot" width="677" height="681"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>umbraco</category>
      <category>visualstudio</category>
      <category>vscode</category>
    </item>
  </channel>
</rss>
