<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><language>en</language><title>Blog posts by Per Nergård</title> <link>https://world.optimizely.com/blogs/Per-Nergard/</link><description></description><ttl>60</ttl><generator>Optimizely World</generator><item> <title>Four database surprises when upgrading from CMS 11 to CMS 13</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/6/four-database-surprises-when-upgrading-from-cms-11-to-cms-13/</link>            <description>&lt;div&gt;
&lt;div&gt;We&#39;re in the middle of migrating a fairly large site from CMS 11 / .NET Framework to CMS 13 / .NET 10. The code migration is one thing, but the database is where the real surprises live. Everything worked fine against our dev and stage databases &amp;mdash; and then we pointed the upgrade at a copy of the production database.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;Here are four things that bit us. Two of them are reported to Optimizely (one verified as a bug and fixed in the latest release, one in triage), so depending on when you read this you might be spared. The other two are by design in CMS 13 and you could hit them if your solution is old enough.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&lt;strong&gt;1. NULL values in tblSiteDefinition stop the site from booting&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;CMS 13 replaces `SiteDefinition` with the new `Application` model, and on first boot a schema migration moves your rows from `tblSiteDefinition` to `tblApplication` with a plain `INSERT ... SELECT`. The catch: `Saved` and `SavedBy` are `NOT NULL` in the new table, and in our production database some of the old rows had NULL in those columns. The migration rolls back with an INSERT NULL violation and the site doesn&#39;t start at all.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;The kicker is that our stage databases didn&#39;t have this problem &amp;mdash; only production did. So if your upgrade rehearsals all run against stage copies, you can sail through every test and still faceplant on go-live night.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;The fix is a one-off backfill before the first CMS 13 boot:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;UPDATE tblSiteDefinition
SET Saved   = ISNULL(Saved, GETUTCDATE()),
    SavedBy = ISNULL(SavedBy, &#39;system&#39;)
WHERE Saved IS NULL OR SavedBy IS NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;&lt;strong&gt;2. Tab names must be plain ASCII&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;CMS 13 validates tab definition names at model sync, and it&#39;s strict: no non-ASCII characters, no spaces, no hyphens. Being a Swedish project we had tab names like &quot;S&amp;ouml;kinst&amp;auml;llningar&quot;, &quot;SEO-Settings&quot; and &quot;Externa l&amp;auml;nkar&quot; &amp;mdash; and the app refused to boot with:&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;```&lt;/div&gt;
&lt;div&gt;ValidationException: The Name &#39;X&#39; is not a valid format.&lt;/div&gt;
&lt;div&gt;```&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;So all tab names need to be plain ASCII identifiers. They&#39;re internal keys though &amp;mdash; if you want nicer editor-facing labels (or anything with &amp;aring;&amp;auml;&amp;ouml;), that&#39;s handled with tab translations in your localization files.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&lt;strong&gt;3. &quot;Content&quot; and &quot;Settings&quot; are reserved tab names&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;This one is sneaky. CMS 13 reserves the tab names `Content` and `Settings` for its built-in tabs, and if you have those exact strings as your own tab names &amp;mdash; which we did, in a perfectly innocent-looking GroupNames class &amp;mdash; model sync throws a `ConflictingResourceException` against a freshly upgraded database.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;The confusing part is that `SystemTabNames.Content` isn&#39;t the string &quot;Content&quot; at all &amp;mdash; it&#39;s &quot;Information&quot;. And `SystemTabNames.Settings` is &quot;Advanced&quot;. So the constants never collide, only the literals do. The fix is simply to point your own constants at `SystemTabNames`:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public const string Content  = SystemTabNames.Content;   // &quot;Information&quot;
public const string Settings = SystemTabNames.Settings;  // &quot;Advanced&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;
&lt;div&gt;
&lt;div&gt;&lt;strong&gt;4. NULLs in tblSynchedUser break edit mode for everyone&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;The site authenticates with WS-Federation against an external ADFS, and not every identity comes with `email`, `givenname` or `surname` claims. After user sync those rows end up with NULL in the corresponding columns in `tblSynchedUser` &amp;mdash; including one literally named `Metadata`. The columns are nullable, so we assumed that was fine.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;It isn&#39;t. As soon as anyone opens edit mode, the CMS shell tries to subscribe the current user to feature notifications, reads through the user table, and dies:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;System.Data.SqlTypes.SqlNullValueException: Data is Null.
   at EPiServer.DataAccess.Internal.SynchronizeUsersDB.&amp;lt;FindUsersAsync&amp;gt;g__CreateUser|18_0
   ...
   at FeatureNotificationService.SubscribeAsync&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;A single NULL row anywhere in the table is enough, and it doesn&#39;t have to belong to the user logging in. The public site keeps working &amp;mdash; only edit mode breaks &amp;mdash; which makes it genuinely confusing to troubleshoot.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;The workaround is backfilling the nullable string columns with empty strings. The good news: we reported this one, Optimizely verified it as a bug, and a fix ships in the latest release. If you&#39;re on an earlier 13.x, the backfill is:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;UPDATE tblSynchedUser SET Email            = &#39;&#39; WHERE Email IS NULL;
UPDATE tblSynchedUser SET GivenName        = &#39;&#39; WHERE GivenName IS NULL;
UPDATE tblSynchedUser SET LoweredGivenName = &#39;&#39; WHERE LoweredGivenName IS NULL;
UPDATE tblSynchedUser SET Surname          = &#39;&#39; WHERE Surname IS NULL;
UPDATE tblSynchedUser SET LoweredSurname   = &#39;&#39; WHERE LoweredSurname IS NULL;
UPDATE tblSynchedUser SET Metadata         = &#39;&#39; WHERE Metadata IS NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&lt;strong&gt;Wrapping up&lt;/strong&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;The common thread: none of this shows up on a clean database. It&#39;s the years of accumulated production data &amp;mdash; old site definitions, legacy users synced before claims were complete, tab names from a more liberal era &amp;mdash; that trip the upgrade. So rehearse against a fresh copy of *production*, not stage, before the real cutover.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/6/four-database-surprises-when-upgrading-from-cms-11-to-cms-13/</guid>            <pubDate>Fri, 12 Jun 2026 10:02:44 GMT</pubDate>           <category>Blog post</category></item><item> <title>Automatic assign of content reference properties on content creation</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/6/automatic-assign-of-content-reference-properties-on-content-creation/</link>            <description>&lt;p&gt;A long time ago I was tired of configuring setting properties on a settings page when deploying new functionality that needed global settings, so I did a way of accomplishaing that with attributes from the settings page.&lt;br /&gt;&lt;br /&gt;Earlier this week I got tired of it again and did another version for CMS 13 but this time I did it from the other way around with an attribute on the content type.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;Old blog post here: &lt;a href=&quot;/link/1c51a56e85ba40a1885e7c27c4400234.aspx&quot;&gt;2019&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So the idea is that when building a new function and we need to configure a settings property to point to a new content type for some shared functionality you could configure that via an attribute on the content type so after deploy you just create your new page and it automatically wires up the setting for you.&amp;nbsp; Done in an Alloy site so thats why I use StartPage.&lt;br /&gt;&lt;br /&gt;Maybe not a super big problem if you just delploying a new features but if setting up a boilerplate solution of some sort from scratch it will make your life much easier.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;// Auto-match: the settings property whose [AllowedTypes] most specifically accepts ContactPage is used.
[RegisterIn(typeof(StartPage))]
public class ContactPage : SitePageData { }

// Explicit property: recommended when the settings property allows a broad base type (e.g. PageData).
[RegisterIn(typeof(StartPage), nameof(StartPage.ContactsPageLink))]
public class ContactPage : SitePageData { }

// React on creation instead of publish.
[RegisterIn(typeof(StartPage), Trigger = RegisterTrigger.Created)]
public class ContactPage : SitePageData { }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br /&gt;Code over at my GitHub. &lt;a href=&quot;https://github.com/PNergard/Nergard.Opti.AutoRegister&quot;&gt;Pnergard AutoRegister&lt;/a&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/6/automatic-assign-of-content-reference-properties-on-content-creation/</guid>            <pubDate>Fri, 05 Jun 2026 11:42:50 GMT</pubDate>           <category>Blog post</category></item><item> <title>Extending the Optimizely 11 Link Validation job with custom exclude patterns</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/5/extending-the-optimizely-11-link-validation-job-with-custom-exclude-patterns/</link>            <description>&lt;p&gt;This might be common knowledge but I have never done this in all my years working with Optimizely solutions.&lt;/p&gt;
&lt;p&gt;On a customer I noticed that the link validation scheduled job had been failing for quite some time with the error: The scheme for the url &quot;tel:00 000&quot; in not http or https. So I needed to figure out if and how the configure the job to ignore certain patterns.&lt;br /&gt;&lt;br /&gt;It turned out to be quite easy to just create a initialization module that hooked into the options for the job. Code snippet below and complete example on my &lt;a href=&quot;https://gist.github.com/PNergard/1d60247461e486e53ad64722c9656ca1&quot;&gt;Gist&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;So adding that init module fixed the error and made the job to successfully run.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class LinkValidatorConfiguration : IConfigurableModule
    {
        public void ConfigureContainer(ServiceConfigurationContext context)
        {
            var options = new LinkValidatorOptions();
            options.ExcludePatterns.Add(@&quot;^\s*tel:&quot;);
            options.ExcludePatterns.Add(@&quot;^\s*mailto:&quot;);
            options.ExcludePatterns.Add(@&quot;^\s*callto:&quot;);
            options.ExcludePatterns.Add(@&quot;^\s*sms:&quot;);
            options.ExcludePatterns.Add(@&quot;^\s*javascript:&quot;);
            options.ExcludePatterns.Add(@&quot;^\s*skype:&quot;);
            context.Services.AddSingleton(options);
        }

        public void Initialize(InitializationEngine context) { }
        public void Uninitialize(InitializationEngine context) { }
    }&lt;/code&gt;&lt;/pre&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/5/extending-the-optimizely-11-link-validation-job-with-custom-exclude-patterns/</guid>            <pubDate>Thu, 28 May 2026 10:49:20 GMT</pubDate>           <category>Blog post</category></item><item> <title>Hide built in scheduled job from the admin UI</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/4/hide-built-in-scheduled-job-from-the-admin-ui/</link>            <description>&lt;p&gt;Ok so this probably goes into the not so useful section but late last night I got a veery strong feeling that all projects I am&amp;nbsp; involved with have alot of builtin scheduled jobs that are never used and only add noice to the admin ui scheduled job list. So there must be a way to hide them I thought...&lt;/p&gt;
&lt;p&gt;And it turns out that you can, even though that in retrospect this isn&#39;t such a big problem maybe :). But I created a&amp;nbsp; RCL project which you just register / configure in your solution.&lt;/p&gt;
&lt;p&gt;You can get the code over at &lt;a href=&quot;https://github.com/PNergard/Nergard.ScheduledJobVisibility&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Usage&lt;br /&gt;1. Add the below line to startup and then start in debug mode and check the output for a list of scheduledjobs and their guid / name&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;services.HideScheduledJobs(_ =&amp;gt; { });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2. Add the Guids you want to hide&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;services.HideScheduledJobs(opts =&amp;gt; opts
    .Hide(new Guid(&quot;e652f3bd-f550-40e8-8743-2c39cda651dc&quot;), &quot;Remove unrelated content assets&quot;)
    .Hide(new Guid(&quot;656e747e-b2cb-4930-83dc-5d8d97aeaabb&quot;), &quot;Trim content versions&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Results&amp;nbsp;&lt;br /&gt;&lt;br /&gt;All jobs visible:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/a8fd58add1374f2fae6aa44ff47e3f54.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;572&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Here I have hidden the bottow two&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/c3ddca0c1c5e459d9b1b0045a68e87ef.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;523&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/4/hide-built-in-scheduled-job-from-the-admin-ui/</guid>            <pubDate>Wed, 06 May 2026 08:52:48 GMT</pubDate>           <category>Blog post</category></item><item> <title>Scheduled jobs with parameters</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/4/scheduled-job-admin-plugin/</link>            <description>&lt;p&gt;Scheduled jobs is an integral part of most Optimizely solution but the UI has, in my opinon, always been lacking in usability and features. Earlier this year I did a small tool that tried to give a better overview&amp;nbsp; and make it easier digging into log messages when troubleshooting errors.&lt;br /&gt;&lt;br /&gt;It kinda solved some issues but the look and feel was quite bad so I have been thinking about how to make it better now and then since then. Link to old blog post&amp;nbsp;&lt;a title=&quot;Link to a blog post about an attempt to do a better UI for scheduled jobs and searching status messages&quot; href=&quot;/link/fe977e7ffca64f4a88419dcf3ea36449.aspx&quot;&gt; &quot;Scheduled job overview&quot; &lt;/a&gt;So this lead up to this blog post.&lt;/p&gt;
&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;Nergard.ScheduledJobsAdmin is my new attemt on a complement or replacment for the built in scheduled jobs admin UI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;!Important! I have not been able to test it out fully in a production environment so use at your own risk. If you try it and run inte issues feedback or pull request are welcome.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Features&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Searchable job roster&lt;/strong&gt; - give a good overview of builtin and custom scheduled jobs&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Health state indicator&lt;/strong&gt; - at a glance you can see which jobs are healthy, failing, or overdue&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Typed parameters&lt;/strong&gt; - declare a strongly-typed parameters class on your job and the tool renders a form for it automatically. No more hardcoding values in job logic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sleep window&lt;/strong&gt; - define quiet hours for a job in code or let operators configure them per-job in the UI. Manual runs always bypass them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;History drill down&lt;/strong&gt; - click any run in the history timeline to see the full log output, which parameters were active, and how long it took&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/8fd07937e7e5448fa2604f0d15f2fbee.aspx&quot; alt=&quot;Overview image&quot; width=&quot;1200&quot; height=&quot;384&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Feature details&lt;/h2&gt;
&lt;div&gt;
&lt;h3&gt;Job roster&lt;/h3&gt;
&lt;br /&gt;
&lt;div&gt;The left panel shows all your scheduled jobs in a scrollable list with a search box at the top. Jobs are split into two groups: your own custom jobs at the top, and Optimizely&#39;s built-in system jobs in a collapsed section below. The built-in section is collapsed by default so it does not clutter the view, but you can expand it if you need to manage one.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;Each job in the list shows a colour-coded health state: succeeded, failed, overdue (has not run within twice its configured interval), never run, or manual-only. You can also exclude specific jobs entirely via configuration if there are system jobs you never want to see.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h3&gt;History tab&lt;/h3&gt;
&lt;br /&gt;
&lt;div&gt;The History tab shows a paginated timeline of all past runs for the selected job, newest first. You can filter by keyword to find specific runs quickly.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;Click any row to drill into that run: you see the start time, duration, whether it succeeded or failed, how it was triggered (scheduled or manual), and which parameter values were active when it ran. The full log output is shown below with its own search box so you can hunt for specific lines. Prev/Next buttons let you step through runs without going back to the list.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;/link/d7552a2505f84ab3b1cc05cffdf2feb8.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;339&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;/link/dcc5ddeb12f540cb956b33d774b5c768.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;377&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h3&gt;Parameters tab&lt;/h3&gt;
&lt;br /&gt;
&lt;div&gt;This is the feature I been wanting forever for my self but never took the time to build it until now. The built-in scheduled jobs admin has no concept of job parameters &amp;mdash; you either hardcode values in the job or manage your own settings page somewhere. With this tool you can declare a typed parameters class, decorate it with a couple of attributes, and the tool renders a form for it automatically.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;Supported field types: `&lt;em&gt;string&lt;/em&gt;` (single line or multiline), `int`, `&lt;em&gt;decimal&lt;/em&gt;`, `&lt;em&gt;bool&lt;/em&gt;` (toggle), `&lt;em&gt;ContentReference&lt;/em&gt;` (numeric ID with live name resolution), `&lt;em&gt;enum&lt;/em&gt;` (dropdown), `[&lt;em&gt;Flags&lt;/em&gt;] enum` (checkboxes), `&lt;em&gt;DateTime&lt;/em&gt;`, and `&lt;em&gt;TimeSpan&lt;/em&gt;`.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class MyJobParams
{
    [JobParam(&quot;Content root&quot;, Description = &quot;The page to process.&quot;)]
    public ContentReference? ContentRoot { get; set; }

    [JobParam(&quot;Batch size&quot;)]
    public int BatchSize { get; set; } = 100;

    [JobParam(&quot;Dry run&quot;, Group = &quot;Safety&quot;)]
    public bool DryRun { get; set; }

    [JobParam(&quot;Run mode&quot;)]
    public RunMode Mode { get; set; } = RunMode.Incremental;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;Parameters are saved to DDS per job and loaded automatically before each execution &amp;mdash; whether the job runs on schedule or is triggered manually.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;/link/759fb2fd45104b2daef665b116f91a4f.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;426&quot; /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;h3&gt;Schedule tab&lt;/h3&gt;
&lt;br /&gt;
&lt;div&gt;The Schedule tab lets you enable or disable a job and change its run interval without going through the built-in admin. It also exposes the sleep window configuration (see below).&lt;/div&gt;
A warning is shown if you select a built-in Optimizely job, since schedule changes for those may get reset by CMS updates.
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;h3&gt;Sleep windows&lt;/h3&gt;
&lt;br /&gt;
&lt;div&gt;Sleep windows let you define a time range during which a job should not execute &amp;mdash; useful for jobs that should not run during business hours or during nightly maintenance windows.&lt;/div&gt;
&lt;br /&gt;You can declare a default sleep window directly on your job class:&lt;br /&gt;&lt;br /&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[ScheduledPlugIn(DisplayName = &quot;My nightly import&quot;)]
[SleepWindow(&quot;07:00&quot;, &quot;22:00&quot;, &quot;Europe/Stockholm&quot;)]
public class MyImportJob : ParameterizedScheduledJob&amp;lt;MyImportParams&amp;gt; { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;Operators can override the sleep window per-job in the Schedule tab without touching code. The UI always shows what the code default is so it is clear what the fallback is if the override is cleared. Manual runs always bypass the sleep window regardless.&lt;/div&gt;
&lt;br /&gt;Sleep window enforcement only applies to jobs that extend `ParameterizedScheduledJob&amp;lt;TParams&amp;gt;`.&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;/link/7a627793f00b4d59b0fe8370c60d6a56.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;333&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h3&gt;Run tab&lt;/h3&gt;
&lt;br /&gt;
&lt;div&gt;The Run tab has one job: start the selected job right now. While the job is running a progress bar and live status log are shown, updating every 500 ms as the job reports progress. A Stop button appears next to the Run button during execution.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2&gt;Adding a parameterized job&lt;/h2&gt;
&lt;br /&gt;
&lt;div&gt;To take advantage of typed parameters your job extends `ParameterizedScheduledJob&amp;lt;TParams&amp;gt;` instead of `ScheduledJobBase`, and you implement `ExecuteInternal()` instead of `Execute()`.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[ScheduledPlugIn(DisplayName = &quot;My job&quot;)]
[JobParameters(typeof(MyJobParams))]
public class MyJob : ParameterizedScheduledJob&amp;lt;MyJobParams&amp;gt;
{
    public MyJob(JobConfigRepository cfg, IManualRunFlagService flag)
        : base(cfg, flag) { }

    protected override void ExecuteInternal()
    {
        Status.Add($&quot;Starting with batch size {Parameters.BatchSize}&quot;);

        // your logic here

        Status.AddCount(&quot;Items processed&quot;, processedCount);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;`&lt;em&gt;Status&lt;/em&gt;` is a `&lt;em&gt;JobStatusBuilder&lt;/em&gt;` &amp;mdash; a simple fluent builder that handles Optimizely&#39;s 2048-character limit on the status message field gracefully (it appends a truncation notice rather than silently cutting off). Before `ExecuteInternal()` runs, the base class pre-seeds `Status` with a compact summary of the current parameter values, so every run in the history log shows exactly what configuration was active.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;br /&gt;
&lt;div&gt;Register the tool in your `Startup.cs`:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;services.AddJobMonitor(options =&amp;gt;
{
    options.DisplayTimeZoneId = &quot;Europe/Stockholm&quot;; // or any TimeZoneInfo.GetSystemTimeZones() Id
    options.ShowDetailedErrors = true; // set false in production
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;Create a page in the CMS tree using the included `ScheduledJobsAdminPage` content type (or adapt it to your own base page class). The page just needs a controller that renders the Blazor `JobMonitorShell` component.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;That is it.&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2&gt;Source code&lt;/h2&gt;
&lt;br /&gt;
&lt;div&gt;The full source is on GitHub:&lt;a href=&quot;https://github.com/PNergard/Nergard.ScheduledJobsAdmin&quot;&gt; Nergard.ScheduledJobAdmin&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;The source contains a readme with more details.&lt;/div&gt;
&lt;br /&gt;
&lt;div&gt;As mentioned above &amp;mdash; this has not been fully tested in a production environment. If you try it and hit something broken, or have ideas for improvements, issues and pull requests are very welcome.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/4/scheduled-job-admin-plugin/</guid>            <pubDate>Tue, 14 Apr 2026 07:35:47 GMT</pubDate>           <category>Blog post</category></item><item> <title>Searchable settings page</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/4/searchable-settings-page/</link>            <description>&lt;p&gt;In my current project which has been actively developed for quite some time we have a big classic settings page. Unfortunately the placement and naming of properties have not always been the best so I tend to end up searching for a property with ctrl+f in the browser when I know it should be there somewhere but i cant find it.&lt;/p&gt;
&lt;p&gt;So today I thought of why not add an actual view to the page with a simple search so I atleast can figure out on what tab the property is on. I guess everyone has been using settings pages for ages but atleast I have nerver actually created a view for it.&lt;br /&gt;&lt;br /&gt;It&#39;s a simple searchbox that filters on the property name and localized caption and descripton values. And the default listing view is just a long list grouped by tab.&lt;br /&gt;&lt;br /&gt;It looks like below (a bit of a crappy image but I couldnt display that much of the properties) and you can find it over at my &lt;a href=&quot;https://gist.github.com/PNergard/6ff6e9cb01a8240be4d41c65ca7670c0&quot;&gt;Gist&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/4bd4251e974242ae804a2527124e103f.aspx&quot; alt=&quot;&quot; width=&quot;1126&quot; height=&quot;366&quot; /&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/4/searchable-settings-page/</guid>            <pubDate>Mon, 06 Apr 2026 17:24:09 GMT</pubDate>           <category>Blog post</category></item><item> <title>Missing Properties tool for Optimizely CMS</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/3/missing-properties-tool-for-optimizely-cms/</link>            <description>&lt;p&gt;If you have been working with Optimizely CMS for a while you have probably accumulated some technical debt in your property definitions. When you refactor your models removing properties, renaming content types, or cleaning up after a migration&amp;nbsp;the old property definitions often stick around in the database. They are harmless for the most part, but they add noise and can be confusing when inspecting content types.&lt;/p&gt;
&lt;p&gt;&amp;nbsp; I actually built a tool for this like 10 years ago and I recently had a need to revisit it during a large migration project where we ended up with quite a few orphaned properties that &amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;nbsp; needed cleaning up.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;strong&gt; What it does&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp; The tool scans all your code-defined content types (PageTypes and BlockTypes) and looks for property definitions in the database that no longer have a corresponding attribute in code. Manually created content types are intentionally skipped it&amp;nbsp; only checks types backed by a C# model.&lt;/p&gt;
&lt;p&gt;&amp;nbsp; The results are grouped by content type so you can see exactly what is affected. From there you have a few options:&lt;/p&gt;
&lt;p&gt;&amp;nbsp; - Select individual properties and delete them&lt;br /&gt;&amp;nbsp; - Select all properties for a specific content type&lt;br /&gt;&amp;nbsp; - Delete everything at once&lt;/p&gt;
&lt;p&gt;&amp;nbsp; Both deletion paths require a confirmation dialog because this operation is permanent and cannot be undone.&lt;/p&gt;
&lt;p&gt;&amp;nbsp; There is also a Rescan button if you want to re-check after deploying model changes.&lt;/p&gt;
&lt;p&gt;&amp;nbsp; &lt;strong&gt;Installation&lt;br /&gt;&lt;/strong&gt;&lt;br /&gt;&amp;nbsp; Configure blazor and decide on how you want to render the component. I just created a content type specific for the tool. Example layout included on github.&lt;/p&gt;
&lt;p&gt;&amp;nbsp; Add the tool to your project and register it in Startup.cs:&lt;/p&gt;
&lt;p&gt;&amp;nbsp; services.AddMissingProperties();&lt;/p&gt;
&lt;p&gt;&amp;nbsp; &lt;img src=&quot;/link/bbd1f90ab6bc4b6cacc86984dd1a48ea.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;861&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp; The source code is on &lt;a href=&quot;https://github.com/PNergard/Nergard.MissingProperties&quot;&gt;https://github.com/pernergard/NergardToolsAndUtilities&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/3/missing-properties-tool-for-optimizely-cms/</guid>            <pubDate>Tue, 10 Mar 2026 21:18:57 GMT</pubDate>           <category>Blog post</category></item><item> <title>Resource Editor - A localization management tool for Optimizely CMS</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/2/resource-editor---a-localization-management-tool-for-optimizely-cms/</link>            <description>&lt;div&gt;If you have worked with Optimizely CMS for any amount of time you know that managing localization through XML files can be tedious. Content type names, property captions, tab names, editor hints - they all need translations, and keeping those XML files organized across multiple languages is not fun.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;I am a firm believer that translations should be owned by the partner and managed in source control. That said, I fully understand the need to fix localization quickly in a DXP environment without having to deploy code. The Resource Editor was built to handle both scenarios - edit and save to XML files during development, and apply runtime overrides via DDS (Dynamic Data Store) when you need a quick fix in production.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;This tool started as a smaller idea but grew quite a bit as I kept adding features. It is built with Blazor Server and MudBlazor, packaged as a Razor Class Library that you can reference in your Optimizely project.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;TL;DR: The Resource Editor is a Blazor-based admin tool for Optimizely CMS that replaces manual XML editing for localization. It provides visual editors for content type &amp;nbsp; names, properties, tabs, display channels, editor hints, and frontend translations &amp;mdash; all with multi-language support. Features include a completeness dashboard, runtime &amp;nbsp;overrides via DDS (no deploy needed), DeepL-powered automated translation at field/item/bulk level, CSV import/export, and a guided migration from legacy XML formats.&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;The Resource Editor is distributed as a Razor Class Library. Add a project reference and register the services in your `Startup.cs`:&lt;br /&gt;&lt;br /&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public void ConfigureServices(IServiceCollection services)
{
    services.AddServerSideBlazor();
    services.AddMudServices();

    // Optional: Add DeepL translation (must be registered before AddResourceEditor)
    services.AddDeepLTranslation(_configuration);

    // Register Resource Editor services
    services.AddResourceEditor(_configuration);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;Configuration is done through `appsettings.json`:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;{
  &quot;ResourceEditor&quot;: {
    &quot;TranslationFolder&quot;: &quot;Resources/Translations&quot;,
    &quot;EnableFileSaving&quot;: true,
    &quot;EnableOverrides&quot;: true,
    &quot;ShowOverridesUI&quot;: true,
    &quot;EnableAutomatedTranslation&quot;: true,
    &quot;DeepL&quot;: {
      &quot;ApiKey&quot;: &quot;your-api-key&quot;,
      &quot;UseFreeApi&quot;: true
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;Depending on how you want to render the tool you also need a CSHTML page to host the Blazor component. The tool runs as a full-page Blazor application without the standard Optimizely layout:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code&gt;@using Nergard.ResourceEditor.Components
@using Microsoft.AspNetCore.Components.Web
@model PageViewModel&amp;lt;BlazorToolResourceEditor&amp;gt;

@{ Layout = null; }

&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot; /&amp;gt;
    &amp;lt;base href=&quot;~/&quot; /&amp;gt;
    &amp;lt;title&amp;gt;Resource Editor&amp;lt;/title&amp;gt;
    &amp;lt;link href=&quot;https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;amp;display=swap&quot;
          rel=&quot;stylesheet&quot; /&amp;gt;
    &amp;lt;link href=&quot;_content/MudBlazor/MudBlazor.min.css&quot; rel=&quot;stylesheet&quot; /&amp;gt;
    &amp;lt;component type=&quot;typeof(HeadOutlet)&quot; render-mode=&quot;Server&quot; /&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    @Html.AntiForgeryToken()

    &amp;lt;component type=&quot;typeof(ResourceEditorHost)&quot;
               render-mode=&quot;Server&quot;
               param-IsDarkMode=&quot;false&quot; /&amp;gt;

    &amp;lt;script src=&quot;_framework/blazor.server.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;_content/MudBlazor/MudBlazor.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;Create a page type in Optimizely, set up a controller and view, and you are good to go.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2&gt;Dashboard&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;When you open the Resource Editor you land on the dashboard. It gives you an overview of translation completeness across all your configured languages. Each language card shows progress for content types, properties and tabs so you can quickly see where translations are missing.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;The dashboard also shows your view/frontend translation files with their own completion status.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;Clicking a language card takes you directly to the Language Overview for that language. More on that further down in the post.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;/link/705a63ef3c6c42adaa31b3bd0fc1efa1.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;651&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2&gt;Content type editor&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;This is where you probably will spend most of your time. The editor lets you manage translations for all your content types, organized in three sections: Pages, Blocks and Media.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;For each content type you can edit the name and description across all languages. Expand a content type and you get access to all its properties where you can edit both the caption (label) and help text.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;The editor detects shared properties - properties that exist on multiple content types - and highlights them. This is useful because shared properties in Optimizely use a fallback mechanism through `icontentdata`, so changing a shared property translation can affect multiple content types. The base type in the saved xml-files will be icontentdata since I believe that same named properties should be consistent throughout the solution.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;You can filter by language if you only want to focus on one language at a time. The tool has support for automatic translations but not for the master language which must be managed manually.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;/link/8e91301e30924d8ebdc05c1594c4c79e.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;500&quot; /&gt;&lt;/div&gt;
&lt;h2&gt;&lt;strong&gt;Tab editor&lt;/strong&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;The Tab Editor handles localization of property group names (tabs). These are the tab labels editors see in the content editing UI.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;It works the same way as the content type editor - select a tab, edit the name across all configured languages, save.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;/link/1cbb177b94214c149fb719ed71fb2e4b.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;455&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;h2&gt;Display Channels and Editor Hints&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;Two smaller editors handle display channel names and editor hint names. Same pattern - select an item, edit translations, save. These are less commonly needed but nice to have covered.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;/link/69a222d0653144c88840df08ccdf1abf.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;484&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;h2&gt;&lt;strong&gt;View / Frontend Translations&lt;/strong&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;The Resource Editor also handles view translation files. &amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;These files use a multi-language format where all languages are in a single XML file. The editor presents them as a navigable tree structure where you can edit values per language, add new sections and add new keys.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;The file pattern is configurable via the `ViewFilePattern` option (defaults to `views_*.xml`).&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;/link/2f9b109aeadc43d8a6714fe984a2205a.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;477&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2&gt;Language overview&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;The Language Overview gives you a focused view of a single language. It shows all content types and their properties with translation status indicators so you can work through missing translations systematically.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;This is also where the bulk translate button lives - translate all incomplete fields for a language in one click.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;Note! The automatic translations are only for the non master languages. So put in the effort for good localization texts for the master language you can reap the benefits for the other languages.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;/link/5dcdfcb88976474ab7071cc50d8ba0c1.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;557&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2&gt;Overrides&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;This is the feature that makes the Resource Editor work well in DXP environments. Overrides are stored in the Dynamic Data Store and take precedence over XML translations at runtime - no deployment needed.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;The override system works through a custom `LocalizationProvider` that is automatically registered at position 0 in the provider chain. When Optimizely resolves a localization key, the override provider checks DDS first. If no override exists, it falls through to the standard XML-based providers.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&lt;em&gt;**Known issue:** There is currently a known issue where overridden content type captions (names) may not be picked up correctly by Optimizely, while help text overrides work as expected. I have an ongoing support ticket with Optimizely for investigation.&lt;/em&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h3&gt;Inline overrides&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;When `ShowOverridesUI` is enabled, every text field in the editors gets a small override button. Click it and you can create an override for that specific field and language. The override is stored in DDS and takes effect immediately.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;/link/cace30955a8f4c108bb4b53060c428f5.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;479&quot; /&gt;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h3&gt;Override manager&lt;/h3&gt;
&lt;div&gt;- View all active overrides with language and value&lt;/div&gt;
&lt;div&gt;- Filter by language and search by property name&lt;/div&gt;
&lt;div&gt;- Add, edit and delete individual overrides&lt;/div&gt;
&lt;div&gt;- **Export to CSV** for documentation or backup&lt;/div&gt;
&lt;div&gt;- **Import from CSV** for bulk operations&lt;/div&gt;
&lt;div&gt;- **Save to XML** to migrate an override into a permanent translation file&lt;/div&gt;
&lt;div&gt;- Clear all overrides&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;/link/102e96bc602444a39dbeff5f6b6800ab.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;435&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2&gt;Automated translations with DeepL&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;If you enable automated translation and register the DeepL service, the Resource Editor gives you translation assistance at three levels:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;1. **Field level** - Each text field gets a translate button that translates from the default language&lt;/div&gt;
&lt;div&gt;2. **Item level** - A &quot;Translate Missing&quot; button in each editor header translates all empty fields for the current item&lt;/div&gt;
&lt;div&gt;3. **Bulk level** - In the Language Overview, translate all incomplete fields for an entire language&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;The translations populate the fields but are not saved automatically - you review and save when you are happy with the results.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;To enable it, signup for the free tier of DeepL and then register the DeepL service before the Resource Editor:&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;services.AddDeepLTranslation(_configuration);
services.AddResourceEditor(_configuration);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;The translation service is pluggable. If you want to use a different provider than DeepL, implement `ITranslationService` and register it before calling `AddResourceEditor()`.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;/link/e26f1f3099774d7faa4a1046fe6a6c14.aspx&quot; alt=&quot;&quot; width=&quot;1200&quot; height=&quot;281&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2&gt;XML Migration&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;If you have an existing site with translations in the classic Alloy XML format (separate files like `ContentTypeNames.xml`, `PropertyNames.xml`, `GroupNames.xml`), the Resource Editor detects this automatically and offers a guided migration to its own format. The migration runs step by step with progress tracking and error reporting.&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;img src=&quot;/link/9a7c8780c3824bd6a8933176e7e5e6cc.aspx&quot; alt=&quot;&quot; width=&quot;818&quot; height=&quot;654&quot; /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2&gt;Configuration options&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;Here is a quick overview of all the configuration options:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;| Option | Default | Description |
|--------|---------|-------------|
| `TranslationFolder` | `Resources/Translations` | Path to translation XML files |
| `EnableFileSaving` | `true` | Allow saving to XML files. Set to `false` on DXP |
| `EnableOverrides` | `true` | Enable DDS-based runtime overrides |
| `ShowOverridesUI` | `true` | Show inline override buttons in editors |
| `EnableAutomatedTranslation` | `false` | Enable automated translation features |
| `ViewFilePattern` | `views_*.xml` | Glob pattern for view translation files |&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;
&lt;div&gt;
&lt;div&gt;A typical DXP configuration would look like this:&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;{
  &quot;ResourceEditor&quot;: {
    &quot;EnableFileSaving&quot;: false,
    &quot;EnableOverrides&quot;: true,
    &quot;ShowOverridesUI&quot;: true
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;h2&gt;Source code and license&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;The full source code is available on GitHub: &lt;a href=&quot;https://github.com/PNergard/Resource-Editor&quot;&gt;Resource Editor GitHub&lt;/a&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;This tool grew organically and while I have been testing it using a Alloy demo template, I would still recommend using it with some care - especially the save operations. Back up your XML files before making large changes. I am happy to receive feedback, bug reports and pull requests.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/2/resource-editor---a-localization-management-tool-for-optimizely-cms/</guid>            <pubDate>Mon, 23 Feb 2026 06:58:46 GMT</pubDate>           <category>Blog post</category></item><item> <title>Scheduled job for deleting content types and all related content</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/scheduledjob-for-deleting-content-types-and-all-related-content/</link>            <description>&lt;p&gt;In my previous blog post which was about getting an overview of your sites content &lt;a href=&quot;/link/e89ad1059bc047499d1ed92fe36b1726.aspx&quot;&gt;https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/scheduledjob-for-getting-overview-of-site-content-usage/&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;I actually found a couple of content types that was related to decomission functionality that actually hade quite a lot of created content. So what to do? ContentTypeObliteratorScheduledJob to the rescue.&lt;br /&gt;&lt;br /&gt;It&#39;s very simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Get the contenttype ids you want to remove all content for and deleting the actual content type as well.&lt;/li&gt;
&lt;li&gt;Take a database backup&lt;/li&gt;
&lt;li&gt;Add the contenttype ids into a comma separated list&lt;/li&gt;
&lt;li&gt;Run the job and enjoy less code and a less bloated content tree.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I have only done a test run on one content type but for that it worked.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Link to my Gist: &lt;a title=&quot;Gist link to the ContenttypeObliteratorScheduledJob&quot; href=&quot;https://gist.github.com/PNergard/818677955ae0e2c933f30a3186e0fe37&quot;&gt;ContentTypeObliteratorScheduledJob&lt;/a&gt;.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/scheduledjob-for-deleting-content-types-and-all-related-content/</guid>            <pubDate>Fri, 30 Jan 2026 13:09:03 GMT</pubDate>           <category>Blog post</category></item><item> <title>ScheduledJob for getting overview of site content usage</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/scheduledjob-for-getting-overview-of-site-content-usage/</link>            <description>&lt;p&gt;In one of my current project which we are going to upgrade from Optimizely 11 I needed to get an overview of the content and which content types we have, are some unused etc.&lt;/p&gt;
&lt;p&gt;In order to do that I did a scheduled job that will go through the sites content types, check for usage and if a content type id is specified render links to edit mode for all content instances for that type.&lt;/p&gt;
&lt;p&gt;Everything is wrapped up into a CSV file that are easily imported into excell for easy filtering and send that as an attached file via SMTP.&lt;br /&gt;&lt;br /&gt;In the code you can change sender, recipients and after an initial run and you also want to get the links to the content instances for one or more content types that can be set as well.&lt;br /&gt;&lt;br /&gt;The collumns you get is the following:&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;/link/2d7cd960b8a64d3b87e165ef09d482bd.aspx&quot; alt=&quot;&quot; width=&quot;1731&quot; height=&quot;36&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I expected some content types with zero content instances but to my suprise the number was bigger than I expected.&lt;br /&gt;&lt;br /&gt;Nothing advanced but a simple way to identify content types that could be removed or find all instances of a content type that you might be able to remove after you actually check the content.&lt;br /&gt;&lt;br /&gt;You can find the content over at my gist. &lt;a title=&quot;Link to tool gist&quot; href=&quot;https://gist.github.com/PNergard/f3e2e363bbee0e9c1781348de1550b8a&quot;&gt;ScheduledJob content usage Gist&lt;/a&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/scheduledjob-for-getting-overview-of-site-content-usage/</guid>            <pubDate>Tue, 27 Jan 2026 22:47:29 GMT</pubDate>           <category>Blog post</category></item><item> <title>ScheduledJob overview Blazor component</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/scheduledjob-overview-blazor-component/</link>            <description>&lt;p&gt;I have always felt that the admin UI for scheduled jobs in Optimizely is harder to use than it needs to be. To check a job you have to go into admin, filter to find the right job, click to see the history, and then click again to view status messages. When you are troubleshooting, this quickly becomes frustrating.&lt;/p&gt;
&lt;p&gt;Back in 2017 I tried to improve this by creating a small plugin for viewing scheduled job run history. I wrote about it here:&lt;br /&gt;&lt;a class=&quot;decorated-link&quot; href=&quot;/link/1bd5f39a76cf4152b8bd1054b27f99e9.aspx&quot;&gt;https://world.optimizely.com/blogs/Per-Nergard/Dates/2017/9/plugin-for-viewing-scheduled-job-run-history/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;During the Christmas break I decided to revisit the idea. The result is a Blazor component that gives a better overview of scheduled jobs and their status messages, with fewer clicks and faster access to the information you usually care about. I am using MudBlazor for UI components.&amp;nbsp; It&#39;s a Razor Class Library so you need to configure Blazor in you CMS solution and see that the MudBlazor css and js are loaded.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The component includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Separate tabs for custom jobs and built-in jobs, since custom jobs are often the ones causing issues&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Filtering on job names&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Direct access to the latest run and status message when selecting a job&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A list view of up to 1000 status message excerpts with keyword filtering&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Drill-down into a specific status message with additional filtering&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Image 1: Overview of the component.&lt;br /&gt;&lt;img src=&quot;/link/6e9aa90e7cdb4d389a044d29889ae04a.aspx&quot; alt=&quot;&quot; width=&quot;1242&quot; height=&quot;418&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Image 2: A selected job showing all status messages.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/61d90647a4e04383ab98a2c5cf4c4181.aspx&quot; alt=&quot;&quot; width=&quot;1248&quot; height=&quot;565&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Image 3: Drill-down view of a single status message with filtering.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/91173c34077645e6ac19714cc1eca483.aspx&quot; alt=&quot;&quot; width=&quot;1255&quot; height=&quot;565&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The full source code is available in a public GitHub repository:&lt;/p&gt;
&lt;p&gt;https://github.com/PNergard/ScheduledJob-overview-Blazor-component&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/scheduledjob-overview-blazor-component/</guid>            <pubDate>Thu, 15 Jan 2026 15:21:39 GMT</pubDate>           <category>Blog post</category></item><item> <title>PageCriteriaQueryService builder with Blazor and MudBlazor</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2025/2/dynamic-pagecriteriaqueryservice-builder-with-blazor-and-mudblazor/</link>            <description>&lt;p&gt;This might be a stupid idea but my new years resolution was to do / test more stuff so here goes. This razor component allows users to build and execute queries against your CMS site, providing a semi user-friendly interface for searching for pages based on content type, properties.&lt;br /&gt;&lt;br /&gt;You do need to have knowledge about your content types and the data but the thought is that when the need arises to identify what pages / how many pages you have with a certain combination of data you should be able to find those without any coding. If the results are not that many maybe manually editing them and adjusting what is needed is enough. If the number is to high or more complex changes needs to be performed a action-button can be added to do the work instead of possible doing a scheduled job or something.&lt;br /&gt;&lt;br /&gt;You can find the code over at my &lt;a href=&quot;https://gist.github.com/PNergard/31e8ab4ad39ddb2bfc123fc4d540ad64&quot;&gt;Gist&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Criteria:&lt;/strong&gt;&lt;br /&gt;So you can create criteria parameters based on content type , propertes and condition and make them required or not. Required parameters will be shown in green.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Search:&lt;/strong&gt;&lt;br /&gt;When youre happy with your criterias you select a startnode (Root or any of the defined sites) and hit search. If you are not happy with the results you can delete individual parameters or all.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Results:&lt;/strong&gt;&lt;br /&gt;Simple list with a link to edit mode for each content that opens in a new tab.&lt;br /&gt;&lt;br /&gt;&lt;strong&gt;Actions:&lt;/strong&gt;&lt;br /&gt;I actually haven&#39;t implemented any actions. But the idea is that it&#39;s easy to add any action need to perform actions on the search result.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Looks like this:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;img src=&quot;/link/fd7c2b688d67403eba3c505ce978a8dd.aspx&quot; alt=&quot;&quot; width=&quot;1018&quot; height=&quot;449&quot; /&gt;&lt;/strong&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2025/2/dynamic-pagecriteriaqueryservice-builder-with-blazor-and-mudblazor/</guid>            <pubDate>Mon, 10 Feb 2025 09:59:23 GMT</pubDate>           <category>Blog post</category></item><item> <title>Content statistics Blazor component</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2025/1/content-statistics-blazor-components/</link>            <description>&lt;p&gt;Another week and another MudBlazor component to explore. I wanted to test the charts components so I created a small Blazor component that displays som charts based on information we can get from the pages of a site.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How many pages are created / updated on a monthly basis&lt;/li&gt;
&lt;li&gt;Whats the most used content type&lt;/li&gt;
&lt;li&gt;When are pages created during the day&lt;/li&gt;
&lt;li&gt;When are pages being updated during the day&lt;/li&gt;
&lt;li&gt;Who is the most productive content creator&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Not life changing stuff but could be a bit fun to get an overview. Code over at my &lt;a href=&quot;https://gist.github.com/PNergard/85b2417ecf3b64c069623b340edae10a&quot;&gt;Gist&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;Looks like this in a Alloy solution:&lt;br /&gt;&lt;br /&gt;&lt;img src=&quot;/link/f0dedefc578a476eb6aea1a73faa58f1.aspx&quot; alt=&quot;&quot; width=&quot;609&quot; height=&quot;303&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2025/1/content-statistics-blazor-components/</guid>            <pubDate>Tue, 28 Jan 2025 13:54:43 GMT</pubDate>           <category>Blog post</category></item><item> <title>Find and delete non used media and blocks</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2025/1/find-and-delete-non-used-media-and-blocks/</link>            <description>&lt;p&gt;On my new quest to play around with Blazor and MudBlazor I&#39;m going back memory lane and porting some previously plugins. So this time up is my plugin for finding non used images in the global assets folder and you can read the old blog post from 2017 over &lt;a href=&quot;/link/127945d0edab4aa2aeafde6581e907e6.aspx&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I decided I wanted to update it a bit so it could look for blocks as well. It&#39;s not that much to tell about it but you can choose if you want to look for media and / or blocks and look for orphans and then optionally delete selected or all.&lt;/p&gt;
&lt;p&gt;You can grab the code over at my &lt;a href=&quot;https://gist.github.com/PNergard/704913111bed24b6a9fdc11fe18f4567&quot;&gt;Gist.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I have in my tests used the component in a page type I have setup for Blazor testing but if you want to have it as a plugin have a look at Ove Lartelius excellent post about how to run you components in different areas of the Optimizely UI &lt;a href=&quot;https://www.epinova.se/en/blog/2024/use-blazor-components-in-optimizely-cms-adminedit-interface/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And it looks like this&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/1185ebe3f615426cb749edf4a98b8432.aspx&quot; alt=&quot;&quot; width=&quot;878&quot; height=&quot;490&quot; /&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2025/1/find-and-delete-non-used-media-and-blocks/</guid>            <pubDate>Tue, 21 Jan 2025 07:43:26 GMT</pubDate>           <category>Blog post</category></item><item> <title>Order tabs with drag and drop - Blazor</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2025/1/order-tabs-with-drag-and-drop---revisited/</link>            <description>&lt;p&gt;I have started to play around a little with Blazor and the best way to learn is to reimplement some old stuff for CMS12.&lt;/p&gt;
&lt;p&gt;So I took a look at my old blog posts and decided to redo my plugin for drag and drop sorting of tabs since I still don&#39;t like that using the [GroupDefinition] attribute forces you do manage the index order in code. &lt;a href=&quot;/link/5a6818bd02074b9ea8473dd0ef5711f3.aspx&quot;&gt;Old blog post here.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;My thought with this Blazor component is to have a tools content type with it&#39;s own layout which is only for tools and stuff done with Blazor. If feels easier than doing menuproviders or plugins in admin mode.&lt;/p&gt;
&lt;p&gt;It&#39;s very simple, just a list of the tabs which you can reorder with drang and drop and then save or reset (only currently unsaved order). In the example it&#39;s wrapped into another component for a header and navigation but it&#39;s not included in the component.&lt;/p&gt;
&lt;p&gt;You can find the code and a small readme over at my&lt;a href=&quot;https://gist.github.com/PNergard/865a402c0fb41fdc04bac8f67e79e66f&quot;&gt; Gist&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/4537b6d723bf4814870f44eb822bab25.aspx&quot; alt=&quot;&quot; width=&quot;738&quot; height=&quot;493&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2025/1/order-tabs-with-drag-and-drop---revisited/</guid>            <pubDate>Tue, 14 Jan 2025 12:56:20 GMT</pubDate>           <category>Blog post</category></item><item> <title>ImageFile alt-description validation attribute</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2025/1/imagefile-alt-text-validation-attribute/</link>            <description>&lt;p&gt;A customer wanted to improve their quality of having meta descriptive texts on all their published images but it was ok that it could take some time to get it into place (they are forced to review and republish pages on a regular basis) so thought that a validation attribute would do the trick.&lt;br /&gt;&lt;br /&gt;I vaguely remebered that I did something like this a long time a go which I had (&lt;a href=&quot;/link/0c07f80212834b7ab46f0180ee181c8c.aspx&quot;&gt;nov 2015&lt;/a&gt;) but that didn&#39;t handle images added in Xhtml-properties which was needed in this case. I found a solution that had solved a similiar problem but did handle it on actual rendering which used HtmlAgilitypack to handle the check for xthml-editors.&lt;br /&gt;&lt;br /&gt;So I updated my validation attribute and I have published it as a Gist. &lt;a href=&quot;https://gist.github.com/PNergard/d0accbbc46cc756b04c309257b69c902&quot;&gt;ImageValidatorGis&lt;/a&gt;t.&lt;br /&gt;&lt;br /&gt;The validation attribute checks contentreference, ContentArea and XhtmlEditor properties for images and check if they have a description property with value and if not returns an error. Since a image-file added to a XhtmlEditor gets the filename name as alt-text we need to compare the alt-text to the file-name and if equal we interpret that as not set even if the image file it self might have a descripton set.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2025/1/imagefile-alt-text-validation-attribute/</guid>            <pubDate>Tue, 07 Jan 2025 14:13:03 GMT</pubDate>           <category>Blog post</category></item><item> <title>Developer meetup i G&#246;teborg</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2020/2/developer-meetup-in-gothenburg/</link>            <description>&lt;p&gt;Knowit och Episervers meetup i Malm&amp;ouml; var en succ&amp;eacute; s&amp;aring; nu k&amp;ouml;r vi den &amp;auml;ven i G&amp;ouml;teborg den 3e mars. S&amp;aring; om man &amp;auml;r intresserad av att h&amp;ouml;ra mer om vad som &amp;auml;r p&amp;aring; g&amp;aring;ng inom .Net core eller l&amp;auml;ra sig mer om Episervers nya referens arkitektur &quot;Foundation&quot; ska man genast klicka sig vidare till meetup sidan och anm&amp;auml;la sig. Kan ni inte g&amp;aring; just detta tillf&amp;auml;lle passa p&amp;aring; att g&amp;aring; med i gruppen s&amp;aring; att ni inte missar kommande events!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.meetup.com/EPiServer-utvecklare-Goteborg/events/268705786/&quot;&gt;https://www.meetup.com/EPiServer-utvecklare-Goteborg/events/268705786/&lt;/a&gt;&lt;/p&gt;
</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2020/2/developer-meetup-in-gothenburg/</guid>            <pubDate>Fri, 14 Feb 2020 14:14:42 GMT</pubDate>           <category>Blog post</category></item><item> <title>Developer meetup in Malm&#246;</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2020/1/developer-meetup-in-malmo/</link>            <description>&lt;p&gt;After a long slumber in the Episerver Developers &amp;Ouml;resund group Knowit and Episerver is kicking off 2020 with a developer meetup in Malm&amp;ouml; february 6.&lt;/p&gt;
&lt;p&gt;Episerver .Net Core and the new reference project Episerver Foundation is the topics of the evening.&lt;/p&gt;
&lt;p&gt;So if you are in the &amp;Ouml;resund area come and join us.&lt;/p&gt;
&lt;p&gt;See below link to sign up!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.meetup.com/EPiServer-utvecklare-Oresund/events/267933749/&quot;&gt;https://www.meetup.com/EPiServer-utvecklare-Oresund/events/267933749/&lt;/a&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2020/1/developer-meetup-in-malmo/</guid>            <pubDate>Mon, 27 Jan 2020 12:45:35 GMT</pubDate>           <category>Blog post</category></item><item> <title>Use a page type as a  scheduledjob</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2020/1/scheduledjobs-ish-with-input-parameters/</link>            <description>&lt;p&gt;Scheduledjobs have evolved a bit over the years but they still lack a nice way to have input parameters. If you don&#39;t hard code everything I see the following alternatives.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Appsettings&lt;br /&gt;Works but can only be done by a developer with access to the server environment. Causes a restart of the website.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Properties on a page&lt;br /&gt;I really like using a builtuin way to get a nice editorial experience when setting parameters. But having to view logs / status in admin mode and change settings in edit mode feels a bit disconnected. And I think that admin-mode access for non developers should be kept to an absolute minimum.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Extending the builtin views in admin mode&lt;br /&gt;Mathias Kunto did a nice implemantation where you can extend the builtin views with the help of control adapters. You can read his blog post &lt;a href=&quot;https://blog.mathiaskunto.com/2014/03/21/scheduled-jobs-with-input-parameters-in-episerver-7-5/&quot;&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So hardcoding and using appsettings is out of questions but I really like using builtin editorial features for a smooth experience but mixing edit and admin mode together makes is hard to understand. Mathias way is for sure the most &quot;integrated&quot; way of doing it, but is in admin mode which is still in webforms and for some jobs I would like a more granular control over who can handle specific jobs without giving access to everything in admin mode.&lt;/p&gt;
&lt;p&gt;So here is a proof of concept of the solution I came up with: A page type that after initial publish it automatically sets it self for scheduled publishing according to the preffered interval and all work is done during the publishing event.&lt;/p&gt;
&lt;p&gt;To get everything up and running we need a specific page type which have all the settings we want and the model will contain all code that do the actual scheduledwork and handling logging. We also need an initalizationmodule hooking up to&amp;nbsp; the PublishingContent and PublishedContent events. And of course the schedulejob handling future publishing of content must be up and running.&lt;/p&gt;
&lt;p&gt;The page type is lika any normal page type but it contains one method that will perform all work and log a status message to a IList property:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public void DoTheMagic()
{
  if (this.HistoryLog == null) this.HistoryLog = new List&amp;lt;Log&amp;gt;();

     var message = new Log
     {
       Date = DateTime.Now.ToString(),
       Message = &quot;We did som magic!&quot;,
       ExecutedBy = &quot;The logged in user&quot;
      };

      this.HistoryLog.Add(message);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the initaliziationmodule PublishingContent event if the content is of the correct type we call the DoTheMagic method. We need to do it in the publishing event because we are creating a log message and adds it to the logging IList property and we need to have the object in a writable state.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;private void ToolPageInitializationModule1_PublishingContent(object sender, EPiServer.ContentEventArgs e)
{
    var page = e.Content as ToolPage;

    if (page != null)
    {
        page.DoTheMagic();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To hande setting the new scheduled publish date we need to to it in the PublishedContent event. Trying to change the date in the same event as the DoTheMagic don&#39;t work.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;private void ToolPageInitializationModule1_PublishedContent(object sender, ContentEventArgs e)
{
    var page = e.Content as ToolPage;
    var writable = page.CreateWritableClone() as ToolPage;

    if (page != null)
    {
        if (page.Interval == &quot;Minute&quot;)
        {
            var date = writable.StartTime = page.StartTime.AddMinutes(page.IntervalValue);
            writable.StartPublish = date;

            ServiceLocator.Current.GetInstance&amp;lt;IContentRepository&amp;gt;().Save(writable, SaveAction.CheckIn | SaveAction.Schedule, EPiServer.Security.AccessLevel.NoAccess);
        }

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So in the editorial interface it looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/fc72859129bc49a4ae9a90764f2feb2e.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/ae02845bb3474216b1e6f1ef7b62b407.aspx&quot; /&gt;&lt;/p&gt;

&lt;p&gt;So this is only a simple proof of concept but it does what I set up to do so at this stage Im happy :)&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2020/1/scheduledjobs-ish-with-input-parameters/</guid>            <pubDate>Tue, 21 Jan 2020 09:55:36 GMT</pubDate>           <category>Blog post</category></item><item> <title>Find and delete content based on type</title>            <link>https://world.optimizely.com/blogs/Per-Nergard/Dates/2019/6/find-and-delete-content-based-on-type/</link>            <description>&lt;p&gt;Another blog post and another admin mode plugin..&lt;/p&gt;
&lt;p&gt;From time to time I have had the need to delete content for specific types. It could be a cleanup case before using a dev database as UAT or maybe we wan&#39;t to discard some content type.&lt;/p&gt;
&lt;p&gt;In general content of a specific types tend to be all over the place especially with images and blocks so this can be a tedious process. So I whipped together a small admin mode plugin that lets you select a type and get a list of all content-instances and lets you delete all or selected instances of that type. Nothing fancy but it does it&#39;t job.&lt;/p&gt;
&lt;p&gt;You can find the code over at my &lt;a href=&quot;https://gist.github.com/PNergard/8a1037dcea6918dfc558676bded4e7b9&quot;&gt;Gist&lt;/a&gt; and it looks lite this.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/def33cea04e64ea2add53e51da715fb8.aspx&quot; /&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/Per-Nergard/Dates/2019/6/find-and-delete-content-based-on-type/</guid>            <pubDate>Mon, 17 Jun 2019 15:47:46 GMT</pubDate>           <category>Blog post</category></item></channel>
</rss>