<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Per Nergård</title><link href="http://world.optimizely.com" /><updated>2026-03-10T21:18:57.0000000Z</updated><id>https://world.optimizely.com/blogs/Per-Nergard/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Missing Properties tool for Optimizely CMS</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/3/missing-properties-tool-for-optimizely-cms/" /><id>&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;</id><updated>2026-03-10T21:18:57.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Resource Editor - A localization management tool for Optimizely CMS</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/2/resource-editor---a-localization-management-tool-for-optimizely-cms/" /><id>&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;</id><updated>2026-02-23T06:58:46.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Scheduled job for deleting content types and all related content</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/scheduledjob-for-deleting-content-types-and-all-related-content/" /><id>&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;</id><updated>2026-01-30T13:09:03.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>ScheduledJob for getting overview of site content usage</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/scheduledjob-for-getting-overview-of-site-content-usage/" /><id>&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;</id><updated>2026-01-27T22:47:29.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>ScheduledJob overview Blazor component</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2026/1/scheduledjob-overview-blazor-component/" /><id>&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;</id><updated>2026-01-15T15:21:39.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>PageCriteriaQueryService builder with Blazor and MudBlazor</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2025/2/dynamic-pagecriteriaqueryservice-builder-with-blazor-and-mudblazor/" /><id>&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;</id><updated>2025-02-10T09:59:23.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Content statistics Blazor component</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2025/1/content-statistics-blazor-components/" /><id>&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;</id><updated>2025-01-28T13:54:43.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Find and delete non used media and blocks</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2025/1/find-and-delete-non-used-media-and-blocks/" /><id>&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;</id><updated>2025-01-21T07:43:26.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Order tabs with drag and drop - Blazor</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2025/1/order-tabs-with-drag-and-drop---revisited/" /><id>&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;</id><updated>2025-01-14T12:56:20.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>ImageFile alt-description validation attribute</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2025/1/imagefile-alt-text-validation-attribute/" /><id>&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;</id><updated>2025-01-07T14:13:03.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Developer meetup i G&#246;teborg</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2020/2/developer-meetup-in-gothenburg/" /><id>&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;
</id><updated>2020-02-14T14:14:42.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Developer meetup in Malm&#246;</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2020/1/developer-meetup-in-malmo/" /><id>&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;</id><updated>2020-01-27T12:45:35.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Use a page type as a  scheduledjob</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2020/1/scheduledjobs-ish-with-input-parameters/" /><id>&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;</id><updated>2020-01-21T09:55:36.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Find and delete content based on type</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2019/6/find-and-delete-content-based-on-type/" /><id>&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;</id><updated>2019-06-17T15:47:46.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Episerver CMS  11 certification - tips and comments</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2019/4/episerver-cms-certification---tips-and-comments/" /><id>&lt;p&gt;This week it what that time again where I needed to renew my Episerver CMS certificate. So here is a post with some thoughts, comments and tips that hopefully is to some use&amp;nbsp; for others preparing for the exam.&lt;/p&gt;
&lt;p&gt;A lot of things have happened in the Episerver world the last couple of years both with the CMS and other products and not least going into the cloud with the DXC.I think this is reflected in the exam. You really need to have a broad knowledge about Episerver both from a developer perspective but also general product knowledge and editorial skills sprinkled with some Find and DXC to pass the exam.&lt;/p&gt;
&lt;p&gt;I&#39;ve been working with Episerver for a long time (maybe too long) and I&#39;m certified in all versions since CMS 5, and although I try to keep myself updated&amp;nbsp; my professional career has drifted away to doing other stuff than coding so I thought that some reading in the development guide probably was a good idea.&lt;/p&gt;
&lt;p&gt;I was pleasently suprised that the &lt;a href=&quot;/link/f3882e248540406fa3bc4a6858a3b244.aspx&quot;&gt;development guide&lt;/a&gt; has been updated and include more code examples than before.Also a big improvement is that new features now are clearly marked from which version that feature is available which is great for seasoned developers that really only need to brush up their knowledge on new stuff. The new improved looks on world also really made reading more easy on the eyes.&lt;/p&gt;
&lt;p&gt;I&#39;d say that the development guide is the main place to go for information but the &lt;a href=&quot;/link/3aa770bc7f0044fb9175c18048c0d2e7.aspx&quot;&gt;user guide&lt;/a&gt; is a great place&amp;nbsp; to gain knowledge about ie personalization of blocks, and you can learn som useful stuff around environments, deployment, optional extras and service and health continuity in the &lt;a href=&quot;https://www.episerver.com/services/descriptions/episerver-dxc-service-description/&quot;&gt;DXC service description&lt;/a&gt;. And if you haven&#39; used Episerver Find it can be well spent time to play around with how to do searches using different Find features like best bets, stemming, filtering etc. And don&#39;t forget to use the Visual Studio extension creating an Alloy sample project and have a look in the code and maybe try out some of the newer features like projects and notifications.&lt;/p&gt;
&lt;p&gt;It&#39;s alot of material to read through but if you have the time I would really recomend going through it all since the documentation is quite good and even super experienced Episerver proffessionals will learn something new.&lt;/p&gt;
&lt;p&gt;Anders G Nordbys &lt;a href=&quot;https://andersnordby.wordpress.com/2018/01/09/knowledge-areas-for-the-episerver-cms-certification/&quot;&gt;blog post&lt;/a&gt; from 2018 is still a very good writeup with general information and hints.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;So with this post and Anders blog posts, in combination with putting in some effort I&#39;m sure that pass you will pass the exam.&lt;/p&gt;
&lt;p&gt;Oh and the exam I took was for CMS version 11.&lt;/p&gt;
&lt;p&gt;Happy certification!&lt;/p&gt;</id><updated>2019-04-13T14:44:20.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>AllowedTypes for LinkItem collection</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2019/3/allowedtypes-for-linkitem-collection/" /><id>&lt;p&gt;The other day I heard a discussion about a possible requirement for only allowing specific pages in a LinkItem collection.&amp;nbsp; My first thought was to use a ContentArea property in combination with the AllowedTypes attribute but for some reason the possiblity to have external links was needed.&lt;/p&gt;
&lt;p&gt;I googled a bit but I only found a post where a custom validation class handled validation of max number of items in a LinkItemCollection, you can read Allan Thraens blog post &lt;a href=&quot;/link/e7723a75c6a24c6a8c5b952386265817.aspx&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It seemed like a clean and simple approach so I decided to do a variant that is similiar to AllowedTypes for ContentArea and ContentReference properties.&lt;/p&gt;
&lt;p&gt;My solution is a simple validation attribute that takes an array of types that should be allowed to add to a LinkItemCollection property. The attribute ignores linkitems that are not Episerver content.If an Epierver content (page,image) is added to the LinkItemCollection and the underlying type is not inte the allowed types array a error message is displayed and publishing of the page is blocked.&lt;/p&gt;
&lt;p&gt;You can find the coder over at my &lt;a href=&quot;https://gist.github.com/PNergard/7df2636b45753a5b9a654447cf64461a&quot;&gt;gist&lt;/a&gt;.&lt;/p&gt;</id><updated>2019-04-02T17:29:30.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Automatically create pages for ContentReference properties</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2019/2/automatically-create-pages-for-contentreference-properties/" /><id>&lt;p&gt;If you like me have tried to take an existing solution but wanted to start from scratch, maybe for a demo and you just gave up due to the amount of required properties on for example a settings page? Most of them are probably strings and ints that should be pretty easy to fix (or really should have had good defaults). But what about those pesky properties of type ContentReference which excpects a page of a certain type? And I bet those new pages have more pesky required properties as well! Gaah my blood pressure!&lt;/p&gt;
&lt;p&gt;So to come closer to my (pipe?) dream of super easy site site setup even from scratch and using a &quot;mature&quot; solution that has developed over some years I decided to go with an attribute and initmodule approach.&lt;/p&gt;
&lt;p&gt;Ok so the attribute is very simple it just takes one parameter with the type we want to get automatically created. So in my example here I have three page types: A startpage that needs a reference to a settingspage which have two references to a searchpage and a newsarchivepage.&lt;/p&gt;
&lt;p&gt;So my page type looks like this (RequiredDependec is my custom attribute)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;         [RequiredDependency(typeof(Settings))]&lt;br /&gt;         [AllowedTypes(typeof(Settings))]&lt;br /&gt;        public virtual ContentReference SettingsPage {get; set;}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The settingspage looks like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;        [RequiredDependency(typeof(NewsArchive))]&lt;/code&gt;&lt;br /&gt;&lt;code&gt;        public virtual ContentReference NewsArchive { get; set; }&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;        [RequiredDependency(typeof(ContactCards))]&lt;/code&gt;&lt;br /&gt;&lt;code&gt;        public virtual ContentReference ContactCards { get; set; }&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;To fix the automagic creating and setting or properties I did a initmodule that on the onpublish event scans the underlying type with reflection for properties decorated with the new attribute, creates a new page of that type and sets the property to that reference. The created pages are not automatically published since I name them &quot;Automatically created - &amp;lt;pagetypename&amp;gt;&quot; so I guess atleast the name should be changed before they are published. The new pages are created below the ContentReference.StartPage so the startpage need to be created and a site and start page needs to be configured in admin mode.&lt;/p&gt;
&lt;p&gt;Ok so what does it look like:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Here we have a unpublished startpage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/ceb6e48cd10844ff8d49f30e7d835939.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;
&lt;p&gt;
&lt;p&gt;&lt;strong&gt;Here we have the automatically created settingspage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/20c9e74d5de941c9bfe2681681983b1c.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;
&lt;p&gt;&lt;img src=&quot;/link/8ba9a5de715044929895224bbb7439ab.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;
&lt;p&gt;&lt;strong&gt;After publishing the settingspage we have a newsarchive and searchpage created&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/5190bbc3968345be88f1ef2aba4d4c13.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The code is what it is and you use it at your own risk but you can find a complete project over at my Pnergard &lt;a href=&quot;https://github.com/PNergard/AutoCreateDependency&quot;&gt;GitHub&lt;/a&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;&lt;/p&gt;&lt;/p&gt;&lt;/p&gt;&lt;/p&gt;</id><updated>2019-02-24T10:56:22.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Warning before deleting a page that is used in a property decorated with a SelectOne or SelectMany attribute</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2019/2/warning-before-deleting-a-page-that-is-used-in-a-property-decorated-with-a-selectone-or-selectmany-attribute/" /><id>&lt;p&gt;Long title and I hope it made some sense.&lt;/p&gt;
&lt;p&gt;The case is that it&#39;s quite common that the underlying data for SelectionFactories which is then used with SelectOne and SelectMany attributes is normal Episerver pages. For example this is used to handle contact-cards in the Alloy site.&lt;/p&gt;
&lt;p&gt;But since it&#39;s pages there is always a risk that someone by mistake deletes such a page causing problems. We would really like to have Episervers built in is this page used somewhere check.&lt;/p&gt;
&lt;p&gt;The fix for a SelectOne attribute is as easy as having the underlying property to be of type ContentReference instead of a string. But if you have a SelectMany the underlying property needs to be a string and the chosen values is just added as a comma separated string.&lt;/p&gt;
&lt;p&gt;To fix this for SelectMany I have created a custom attribute and a validation attribute class. When you add a SelectMany attribute where the SelectionFactory returns ContentReferences as id&#39;s you also add the new custom attribute which specifies an IList&amp;lt;ContentReference&amp;gt; property. The validation attribute then adds / removes references to the IList property automatically. With this when we try to delete a referenced page we get the builtin reference check functionality.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pseudo code:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SelectMany(typeof(factoryclass))&lt;br /&gt;SelectManyListProperty(&quot;ListPropertyName&quot;)&lt;br /&gt;public virtual string SelectManyProp { get; set; }&lt;/p&gt;
&lt;p&gt;public virtual IList ListPropertyName {get; set;}&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/744223f60a83448f8f45485b136a6dc7.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;After&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/b85e3e6027c4481081a50e69849ca8e6.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Note that the ContentReference property isn&#39;t updated until page is published (values are there) and ofcourse it should be hidden under an other tab or atleast disabled for editors.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gist.github.com/PNergard/41d38a7daeb0c92845454d55c630f6f3&quot;&gt;Code over at my Gist.&lt;/a&gt;&lt;/p&gt;</id><updated>2019-02-19T16:02:14.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Plugin for viewing / filtering wastebasket content and delete single content items</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2017/11/plugin-for-viewing--filtering-wastebasket-content-and-delete-single-content-items/" /><id>&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;p&gt;From time to time we have the need to delete single content from the waste basket in Episerver. Saddly there is no built in support for this. A year ago &lt;a href=&quot;https://gregwiechec.com/2016/06/deleting-single-file-from-trash/&quot;&gt;Grzegorz modified&lt;/a&gt; the built in waste basket. It&#39;s great but I wanted to see some more information about the content and doing a Dojo solution is out of my comfort zone I decided to create yet another admin mode plugin.&lt;/p&gt;
&lt;p&gt;It&#39;s very simple and uses datatables.js for search / filtering functionality. You can delete all or selected items. See below for what it looks like. Code over at my &lt;a href=&quot;https://gist.github.com/PNergard/eba36f8c585419f1b65e27c0bd5f43fe&quot;&gt;Gist&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/b5890876a6c142c3b6c4b706adb44cfa.aspx&quot; alt=&quot;Image wastebasket.jpg&quot; /&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</id><updated>2017-11-09T13:47:53.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Plugin for viewing scheduled job run history</title><link href="https://world.optimizely.com/blogs/Per-Nergard/Dates/2017/9/plugin-for-viewing-scheduled-job-run-history/" /><id>&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;p&gt;A couple of times during the years I have had the need to view the scheduled job history log. In all these cases it has been jobs that are running frequently and therefor I need to use the paging since the default view doesn&#39;t display that many rows.&lt;/p&gt;
&lt;p&gt;So I did a small plugin that let you choose between all scheduled jobs and display all the history in one table. Might not be the oste useful plugin but since I built it can as wll share it. You can find it over at my &lt;a href=&quot;https://gist.github.com/PNergard/743e5cb4e0480be7994789b49490cfea&quot;&gt;Gist&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/2a817b35c4c4458f9c0d2c5020fb933a.aspx&quot; alt=&quot;Image logviewer.jpg&quot; /&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</id><updated>2017-09-14T22:21:44.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>