<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Stephan Lonntorp</title><link href="http://world.optimizely.com" /><updated>2018-11-27T21:54:42.0000000Z</updated><id>https://world.optimizely.com/blogs/stephan-lonntorp/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Setting up Continuous Integration with Azure DevOps and Episerver DXC Service</title><link href="https://world.optimizely.com/blogs/stephan-lonntorp/dates/2018/11/setting-up-continuous-integration-with-azure-devops-and-episerver-dxc-service/" /><id>&lt;p&gt;Just so you know, this is going to be a really long post. I wish I could tell you it&#39;s as simple as one, two, three, but it isn&#39;t, and there are quite a few steps to go through. This is one way to do it, but I&#39;m sure there are other people out there that know how to do it better, or at least differently. Some things outlined in this post is how &lt;em&gt;we&lt;/em&gt; do things, and can serve as inspiration, or flame bait, depending on how you choose to view the world.&lt;/p&gt;
&lt;h3&gt;What, How, and Why?&lt;/h3&gt;
&lt;p&gt;Azure DevOps, the artist formerly known as Visual Studio Team Services, is a suite of tools for teams that can be used for managing the entire lifecycle of software projects. It is a fully fledged project management platform, containing functionality for VCS, issue tracking, wiki, dashboards, build- and deploy pipelines, and whatever else teams might need. There are of course alternatives, but I like it because it is cloud hosted, simple to set up, and most importantly, free for small teams. Incidentally, it is also the platform that Episerver uses under the hood for facilitating deploys in their DXC Service Platform.&lt;/p&gt;
&lt;p&gt;Free you say? Well, it&#39;s free for small teams of up to 5 users, and you get 1800 minutes per month of CI/CD. Today, it&#39;s not uncommon for partner companies to have large installations of build servers and deployment facilitators, used for all their clients. Modern development processes often rely on the build and deployment as an integral part of the workflow, but these systems are often considered internal, and not covered by any SLA with the respective clients. This might not be the case for you, and it might not be a problem, but again, it might. I&#39;ll leave that up to you and your organization to decide.&lt;/p&gt;
&lt;p&gt;Continuous Integration is the practice of constantly building and integrating changes made to a piece of software, and paired with Continuous Deployment, means that those changes are deployed to an environment (not neccessarily a production environment). This constant building and deplying is often referred to as Continuous Delivery. Tomato, tomato. What we&#39;re trying to achieve is a constant flow of well tested software, being delivered in a timely fashion, making it easy to push changes, and fixes, quickly. If you want to play with the big boys, break things and move fast like a true httpster, this is a practice you&#39;ll need to adopt.&lt;/p&gt;
&lt;p&gt;We use CI/CD because it helps us sleep at night, and we move the responsibility of delivery to a separate system, instead of relying on a single developer&#39;s computer, like using Visual Studio&#39;s Publish feature.&lt;/p&gt;
&lt;h3&gt;So, what do we need to get started?&lt;/h3&gt;
&lt;p&gt;This guide assumes that you have an active DXC Service subscription, and that you have requested a Tenant ID from Episerver support, so that you can connect your Azure DevOps to that subscription&#39;s Integration environment. While you&#39;re requesting that Tenant ID, also ask for your SendGrid credentials, as those will come in handy later on.&lt;/p&gt;
&lt;p&gt;Go and &lt;a href=&quot;https://azure.microsoft.com/en-us/services/devops/&quot;&gt;sign up for Azure DevOps&lt;/a&gt; and create your project. Then it&#39;s time to set up your build process. This is going to get rather long, so strap in.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Project preparations&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In order for Azure DevOps to do things with out project, that Visual Studio shouldn&#39;t do in our local development environment, we&#39;ll need to add a few lines to our csproj file. You can put them near the end of the file.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;  &amp;lt;ItemGroup&amp;gt;
    &amp;lt;None Include=&quot;AzureDevOps.targets&quot; /&amp;gt;
  &amp;lt;/ItemGroup&amp;gt;
  &amp;lt;Import Project=&quot;AzureDevOps.targets&quot; Condition=&quot;Exists(&#39;AzureDevOps.targets&#39;)&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then create the following file as AzureDevOps.targets in the root of your project. Note: We&#39;re building our prototype as a separate process, as you&#39;ll see later on, some of the steps in the .targets file won&#39;t apply to you, but then again, they might inspire you to do things differently.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;Project DefaultTargets=&quot;Build&quot; xmlns=&quot;http://schemas.microsoft.com/developer/msbuild/2003&quot;&amp;gt;
  &amp;lt;Target Name=&quot;MaintenancePage&quot; AfterTargets=&quot;PipelineCopyAllFilesToOneFolderForMsdeploy&quot;&amp;gt;
    &amp;lt;Copy SourceFiles=&quot;..\ui\dist\maintenance.html&quot; DestinationFiles=&quot;$(_PackageTempDir)\maintenancepage.htm&quot; /&amp;gt;
  &amp;lt;/Target&amp;gt;
  &amp;lt;Target Name=&quot;MaintenanceToAppOffline&quot; AfterTargets=&quot;MaintenancePage&quot;&amp;gt;
    &amp;lt;Copy SourceFiles=&quot;$(_PackageTempDir)\maintenancepage.htm&quot; DestinationFiles=&quot;$(_PackageTempDir)\App_Offline.htm&quot; /&amp;gt;
  &amp;lt;/Target&amp;gt;
  &amp;lt;Target Name=&quot;AssetFilesForPackage&quot; AfterTargets=&quot;PipelineCopyAllFilesToOneFolderForMsdeploy&quot;&amp;gt;
    &amp;lt;ItemGroup&amp;gt;
      &amp;lt;Files Include=&quot;..\ui\public\**\*.*&quot; Exclude=&quot;..\ui\public\mock\**\*.*;..\ui\public\*.htm*&quot; /&amp;gt;
    &amp;lt;/ItemGroup&amp;gt;
    &amp;lt;Copy SourceFiles=&quot;@(Files)&quot; DestinationFolder=&quot;$(_PackageTempDir)\Assets\%(RecursiveDir)&quot; /&amp;gt;
  &amp;lt;/Target&amp;gt;
&amp;lt;/Project&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key here is the &quot;PipelineCopyAllFilesToOneFolderForMsdeploy&quot; task that is only performed by Azure DevOps, and not locally. What these tasks do, is copy some build output from a few directories to certain files. In our project we use a separate process for frontend assets, and the build output from tha tprocess is then copied into the deployment package. The specifics aren&#39;t that important, as I&#39;m sure your process is different and you&#39;ll have to either remove or adapt these steps to suit your needs. But I think you get the gist of it.&lt;/p&gt;
&lt;p&gt;If you don&#39;t like to include separate files, you can paste the contents of the .targets file directly into the csproj file, and that&#39;ll work too.&lt;/p&gt;
&lt;p&gt;Next, we&#39;ll need to add some transformations and parameters.&lt;/p&gt;
&lt;p&gt;The file parameters.xml has a special meaning in Visual Studio and MS Build, as it becomes a SetParameters.xml file after build, that will fit nicely into the process. I&#39;m no expert, but I&#39;m sure there&#39;s some documentation around the feature that you can read up on if you&#39;d like.&lt;/p&gt;
&lt;p&gt;Add this to the parameters.xml file, in the root of the project, and set its type to &quot;Content&quot;. We&#39;ll use it later on.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;parameters&amp;gt;
	&amp;lt;parameter name=&quot;Sendgrid.Username&quot; description=&quot;The username used for Sendgrid authentication&quot; defaultValue=&quot;#{Sendgrid.UserName}#&quot; tags=&quot;&quot;&amp;gt;
		&amp;lt;parameterEntry kind=&quot;XmlFile&quot;
		                scope=&quot;obj\\Release\\Package\\PackageTmp\\Web\.config$&quot;
		                match=&quot;/configuration/system.net/mailSettings/smtp/network/@userName&quot; /&amp;gt;
	&amp;lt;/parameter&amp;gt;
	&amp;lt;parameter name=&quot;Sendgrid.Password&quot; description=&quot;The password used for Sendgrid authentication&quot; defaultValue=&quot;#{Sendgrid.Password}#&quot; tags=&quot;&quot;&amp;gt;
		&amp;lt;parameterEntry kind=&quot;XmlFile&quot;
		                scope=&quot;obj\\Release\\Package\\PackageTmp\\Web\.config$&quot;
		                match=&quot;/configuration/system.net/mailSettings/smtp/network/@password&quot; /&amp;gt;
	&amp;lt;/parameter&amp;gt;
&amp;lt;/parameters&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As for config transforms, Episerver DXC Service will automatically transform Web.Preproduction.config and Web.Production.config, as those are the names of the environments, but since they don&#39;t control deployments into the integration environment, Web.Integration.config won&#39;t be automatically applied. But, we&#39;ll use that name anyway, since it will be a lot less confusing :)&lt;/p&gt;
&lt;p&gt;When using Episerver DXC Service, config transforms should be considered as sequencial, meaing that any changes introduced in Web.Integration.config, should be considered as present when doing transforms with Web.Preproduction.config, and so forth into production. This can be a hassle to troubleshoot after the first deploy, but once you get it right, very few changes are probably needed, so it&#39;s not that big of a deal.&lt;/p&gt;
&lt;p&gt;In my Web.config I have SMTP settings that uses a MailDrop folder for generated emails, but I&#39;d like to change that in Integration, and add some more things not relevant for my local environment, like Azure blob storage and Azure events. Also, I&#39;ll remove my local Find configuration. (Remember, an extra index is included in the DXC Service subscription, but you gotta remember to ask for it!)&lt;/p&gt;
&lt;p&gt;Web.config (snippet)&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;  &amp;lt;system.net&amp;gt;
    &amp;lt;mailSettings&amp;gt;
      &amp;lt;smtp deliveryMethod=&quot;SpecifiedPickupDirectory&quot;&amp;gt;
        &amp;lt;specifiedPickupDirectory pickupDirectoryLocation=&quot;C:\Projects\Client\src\Client.Web\App_Data\MailDrop&quot; /&amp;gt;
      &amp;lt;/smtp&amp;gt;
    &amp;lt;/mailSettings&amp;gt;
  &amp;lt;/system.net&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Web.Integration.config&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;configuration xmlns:xdt=&quot;http://schemas.microsoft.com/XML-Document-Transform&quot;&amp;gt;
  &amp;lt;episerver.find xdt:Transform=&quot;Remove&quot; /&amp;gt;
  &amp;lt;episerver.framework&amp;gt;
    &amp;lt;blob defaultProvider=&quot;AzureBlobs&quot; xdt:Transform=&quot;Insert&quot;&amp;gt;
      &amp;lt;providers&amp;gt;
        &amp;lt;add name=&quot;AzureBlobs&quot; type=&quot;EPiServer.Azure.Blobs.AzureBlobProvider, EPiServer.Azure&quot; connectionStringName=&quot;EPiServerAzureBlobs&quot; container=&quot;blobs&quot; /&amp;gt;
      &amp;lt;/providers&amp;gt;
    &amp;lt;/blob&amp;gt;
    &amp;lt;event defaultProvider=&quot;AzureEvents&quot; xdt:Transform=&quot;Insert&quot;&amp;gt;
      &amp;lt;providers&amp;gt;
        &amp;lt;add name=&quot;AzureEvents&quot; type=&quot;EPiServer.Azure.Events.AzureEventProvider, EPiServer.Azure&quot; connectionStringName=&quot;EPiServerAzureEvents&quot; topic=&quot;events&quot; /&amp;gt;
      &amp;lt;/providers&amp;gt;
    &amp;lt;/event&amp;gt;
  &amp;lt;/episerver.framework&amp;gt;
  &amp;lt;system.net&amp;gt;
    &amp;lt;mailSettings&amp;gt;
      &amp;lt;smtp xdt:Transform=&quot;Replace&quot; from=&quot;no-reply+integration@client.com&quot; deliveryMethod=&quot;Network&quot;&amp;gt;
        &amp;lt;network host=&quot;smtp.sendgrid.net&quot; port=&quot;587&quot; userName=&quot;&quot; password=&quot;&quot; enableSsl=&quot;true&quot; /&amp;gt;
      &amp;lt;/smtp&amp;gt;
    &amp;lt;/mailSettings&amp;gt;
  &amp;lt;/system.net&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Web.Preproduction.config&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;configuration xmlns:xdt=&quot;http://schemas.microsoft.com/XML-Document-Transform&quot;&amp;gt;
  &amp;lt;system.net&amp;gt;
    &amp;lt;mailSettings&amp;gt;
      &amp;lt;smtp xdt:Transform=&quot;SetAttributes(from)&quot; from=&quot;no-reply+preprod@client.com&quot; /&amp;gt;
    &amp;lt;/mailSettings&amp;gt;
  &amp;lt;/system.net&amp;gt;
  &amp;lt;system.web&amp;gt;
    &amp;lt;httpRuntime enableVersionHeader=&quot;false&quot; xdt:Transform=&quot;SetAttributes(enableVersionHeader)&quot; /&amp;gt;
  &amp;lt;/system.web&amp;gt;
  &amp;lt;system.webServer&amp;gt;
    &amp;lt;security&amp;gt;
      &amp;lt;requestFiltering removeServerHeader=&quot;true&quot; xdt:Transform=&quot;SetAttributes(removeServerHeader)&quot; /&amp;gt;
    &amp;lt;/security&amp;gt;
  &amp;lt;/system.webServer&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OK, that should probably be enough. Next we&#39;ll set up the build process.&lt;/p&gt;
&lt;h3&gt;Step one! We can have lots of fun!&lt;/h3&gt;
&lt;p&gt;I think I used a guide for setting this up in the first place, but I&#39;ve forgotten what that looked like. If you&#39;re using a guide, you can look at these screen shots as a reference.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/da5664e4262148f8b06bea5f7dadddeb.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is the general outline of the build process. Since we&#39;re using a hosted VS2017 instance, no caching is done between builds, meaning that the tasks &lt;em&gt;npm install&lt;/em&gt; and &lt;em&gt;nuget restore&lt;/em&gt; will take some time, but if it&#39;s free, we can&#39;t afford to be that picky. Relax and enjoy your coworker&#39;s company for a few minutes.&lt;/p&gt;
&lt;p&gt;For clarification, our project&#39;s directory structure looks something like this:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;root&amp;gt;
  - src
    - Client.Web
    - Client.Core
    - &amp;lt;more projects&amp;gt;
    - ui
      - src
        package.json
        &amp;lt;a gazillion other frontend files I can never get my head around&amp;gt;
    Client.Web.sln
    nuget.config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is what our nuget.config looks like&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;configuration&amp;gt;
  &amp;lt;packageSources&amp;gt;
    &amp;lt;add key=&quot;NuGet.org&quot; value=&quot;https://api.nuget.org/v3/index.json&quot; /&amp;gt;
    &amp;lt;add key=&quot;Episerver&quot; value=&quot;http://nuget.episerver.com/feed/packages.svc/&quot; /&amp;gt;
  &amp;lt;/packageSources&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, here&#39;s what those steps looks like. Pretty straight forward.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/56f20ae9283a4c65a7673447018b2b7c.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/689ba917981a48109a09cd24406bb001.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/4f849f91bb5a4798aaccd38b2cc3b907.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Here&#39;s a reference to the root directory for the UI project, ui/src, given that the build process runs from &amp;lt;root&amp;gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/3fa78f8408074d439a33678742ca3c0a.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Again, specifying the root directory for the UI project, ui/src, and our specific run command that we&#39;ve set up for doing things especially for when we run the ci build. That could be unit testing JavaScript, bundling, linting, minification and so on. Apparently, this week, &lt;a href=&quot;https://webpack.js.org/&quot;&gt;wepback&lt;/a&gt; is what the kids use.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/6eb11eb152134afc970d71ef3c643c6d.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We might not need this, I honestly don&#39;t know :)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/f9a52c9539f146888639c086b945d6dc.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Specifying that src/nuget.config file.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/d1cde623f5a64c0da85eed5ee907a87d.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is where all that stuff with the parameters.xml, and AzureDevops.targets comes into play. Instead of you having to visually decode those arguments, they&#39;re here: /p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:PackageLocation=&quot;$(build.artifactstagingdirectory)\\&quot; /p:AutoParameterizationWebConfigConnectionStrings=false&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/b3ec67fb2122406599764472713cf303.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As we all know, tests are totally optional. If you don&#39;t have any, or use a different tool than xUnit, do your thing. If you&#39;re not using MSTest (who is these days?) you will probalby need a runner, I&#39;m using xunit.runner.visualstudio as a wrapper. It works for me (and not only on my machine!)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/9d31ed6695df45e7b85acbad9efd5e93.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This puts the output artifacts, named &quot;drop&quot;, where we can get at it from our Release step.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/6a5a8c283e574f1687dec31b43dcb81e.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is just what the variables for the build pipeline looks like, so you have something to compare yours too. I know how we all like to compare things.&lt;/p&gt;
&lt;p&gt;On to the release, where we tie things together with the DXC Service, and all our hard work comes to fruition.&lt;/p&gt;
&lt;h3&gt;Step two! There&#39;s so much we can do!&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/link/bb820bf157ed41fbbaa28a99f45e11e1.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is our release, called Continuous Deployment, since it is triggered on every build from the develop branch. Essentially, that will deploy all our changes into the Integration environment in DXC Service. Doing this will require us to do all of our feature work in feature branches, and integrate changes with pull-requests, or live with the fact that things break. In my opinion, either way is fine. Things will break any way, having a process that enables us to fix those breaks fast, is better than living in constant fear of failing. Some people say &quot;Go big, or go home&quot;, &quot;Fail fast&quot;, and things like &quot;Move fast and break things&quot;. I like it better when things don&#39;t break. So having this process in place enables me to fix things before anybode else notices, which is pretty close to not braking things at all. If a tree falls in the forest, and all that jazz.&lt;/p&gt;
&lt;p&gt;I&#39;ve named my environment &quot;Integration&quot;, because, 1. it ties nicely into the whole DXC Service naming of things, and 2. it allows us to have automagic config transforms of Web.Integration.config.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/6ca51b4f842243cd9e13fb97c44d0aa2.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The whole deployment is made up of three steps, one of which is completely voluntary (and probably unnecessary), which is the last one, Annotate Release, which will log the release in Application Insights. The reason it is probably unnecessary, is because it will only annotate it in AI for the Integration environment. That environment is always so painfully slow, it will probably not give me any insights into performance changes in conjuction with the release. But enough ranting about that.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/0c11650bbb4e40d58c217de9e80cf016.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/4b6cea3a1a794261a3f5e3c4a8ac6537.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is an important step, as it will replace tokens in the SetParameters.xml generated by the build, and later applied to Web.config, enabling us to keep our SMTP credentials away from source control, and safely stored as variables in the release. I&#39;m using a custom step from the Marketplace, called &lt;a href=&quot;https://github.com/qetza/vsts-replacetokens-task&quot;&gt;Replace Tokens&lt;/a&gt;, in the root directory &quot;$(System.DefaultWorkingDirectory)/$(Build.DefinitionName)/drop&quot; for the target files of &quot;*.SetParameters.xml&quot;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/9a7a49ffb46b409388a8821bcdad5a7d.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is where that Tenant ID will come in handy. Using it you can connect Azure DevOps to your DXC Service Azure Subscription, and by specifying the folder &quot;$(System.DefaultWorkingDirectory)/$(Build.DefinitionName)/drop/*.zip&quot; files will be picked up correctly. It&#39;s hard of fitting everything in a screenshot, so here&#39;s the rest:&lt;/p&gt;
&lt;p&gt;Under &quot;File Transforms &amp;amp; Variable Substitution Options&quot; select &quot;XML transformation&quot; and &quot;XML variable substitution&quot;.&lt;/p&gt;
&lt;p&gt;Under &quot;Additional Deployment Options&quot; select &quot;Take App Offline&quot;, &quot;Publish using Web Deploy&quot;, &quot;Remove additional files at destination&quot;, and &quot;Exclude files from the App_Data folder&quot;. In the text box for &quot;SetParameters file&quot; input &quot;$(System.DefaultWorkingDirectory)/$(Build.DefinitionName)/drop/Client.Web.SetParameters.xml&quot; where &quot;Client.Web&quot; is the name of your package, in my case, the main web project.&lt;/p&gt;
&lt;p&gt;Under &quot;Post Deployment Action&quot;, select &quot;Inline Script&quot; as the &quot;Deployment script type&quot;, and put this in the text box to do some post-deploy house keeping.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;@echo off
del parameters.xml
del Web.$(Release.EnvironmentName).config&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/link/4ba3548c196240bb936ba1fac13386e7.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If you too want to do unnecessary annotations in AI, get your appliation ID from your azure portal for the DXC Service subscription, paste it in the &quot;Application ID&quot; field, and add &quot;$(ApplicationInsights.ApiKey)&quot; in the API Key field.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/a73d534033814808b7b070b816a6903c.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is the mothership. This is where we store our sensitive information.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ApplicationInsights.ApiKey is an API key that you can generate yourself from the azure portal for the AI account for the integration environment.&lt;/li&gt;
&lt;li&gt;BuildVersion is a value I use and have an appSetting for, it will automagically be given the value of my $(Build.BuildNumber) variable. Appsettings and ConnectionStrings are like that.&lt;/li&gt;
&lt;li&gt;EPiServerDB is the connectionstring for the integration environment, it, like the appsetting will be automagivally replaced.&lt;/li&gt;
&lt;li&gt;Release.EnvironmentName is Integration, beacuse reasons.&lt;/li&gt;
&lt;li&gt;Sendgrid.Password and Sendgrid.Username are given to you, upon request, from Episerver operations, and are tied to your DXC Service subscription. They are the same for Integration through Production, so no need to store those in source control either. &lt;strong&gt;No credentials in source control, ever&lt;/strong&gt;. Note: CPNM is not my real username for SendGrid. No credentials in screen shots either, ever.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step three! It&#39;s just you and me!&lt;/h3&gt;
&lt;p&gt;Well, that was it for this time.&lt;/p&gt;
&lt;p&gt;In conclusion, setting up a CI/CD toolchain isn&#39;t black magic, as you&#39;ve seen. It&#39;s an automated chain of hacks and quirks, that somehow just works&amp;trade;.&lt;/p&gt;
&lt;p&gt;As always, comments, discussions and feedback is greatly appreciated.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Step four! I can give you more!&lt;/em&gt;&lt;br /&gt;&lt;em&gt;Step five! Don&#39;t you know that the time is right! Huh!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;P.S. As Scott mentioned in the comments, there&#39;s a YAML export/import functionality. So here&#39;s the YAML.&lt;/em&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;resources:
- repo: self
  fetchDepth: 10
queue:
  name: Hosted VS2017
  demands: 
  - npm
  - msbuild
  - visualstudio
  - vstest

variables:
  Parameters.solution: &#39;src\Client.sln&#39;
  Parameters.ArtifactName: &#39;drop&#39;

steps:
- task: Npm@1
  displayName: &#39;npm install&#39;
  inputs:
    workingDir: src/ui

    verbose: false


- task: Npm@1
  displayName: &#39;npm run build&#39;
  inputs:
    command: custom

    workingDir: src/ui

    verbose: false

    customCommand: &#39;run ci-build&#39;


- task: NuGetToolInstaller@0
  displayName: &#39;Use NuGet 4.4.1&#39;
  inputs:
    versionSpec: 4.4.1


- task: NuGetCommand@2
  displayName: &#39;NuGet Package Restore&#39;
  inputs:
    restoreSolution: &#39;$(Parameters.solution)&#39;

    feedsToUse: config

    nugetConfigPath: src/nuget.config


- task: VSBuild@1
  displayName: &#39;Build Solution&#39;
  inputs:
    solution: &#39;$(Parameters.solution)&#39;

    msbuildArgs: &#39;/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:PackageLocation=&quot;$(build.artifactstagingdirectory)\\&quot; /p:AutoParameterizationWebConfigConnectionStrings=false&#39;

    platform: &#39;$(BuildPlatform)&#39;

    configuration: &#39;$(BuildConfiguration)&#39;


- task: VSTest@1
  displayName: &#39;Run xUnit Tests&#39;
  inputs:
    testAssembly: &#39;**\bin\$(BuildConfiguration)\*test*.dll;-:**\xunit.runner.visualstudio.testadapter.dll&#39;

    codeCoverageEnabled: true

    vsTestVersion: latest

    otherConsoleOptions: /InIsolation

    platform: &#39;$(BuildPlatform)&#39;

    configuration: &#39;$(BuildConfiguration)&#39;


- task: PublishBuildArtifacts@1
  displayName: &#39;Publish Artifacts&#39;
  inputs:
    ArtifactName: &#39;$(Parameters.ArtifactName)&#39;
&lt;/code&gt;&lt;/pre&gt;</id><updated>2018-11-27T21:54:42.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Localizations in PropertyValueList</title><link href="https://world.optimizely.com/blogs/stephan-lonntorp/dates/2018/11/localizations-in-propertyvaluelist/" /><id>&lt;p&gt;The PropertyValueList property type ca be really useful for things that don&#39;t have a need for reuse, or doesn&#39;t have a view. If you&#39;ve used it you might have noticed that the editor experience, especially in regards to localizations, hasn&#39;t been optimal.&lt;/p&gt;
&lt;p&gt;In &lt;a href=&quot;/link/763efe6588f84cfb8c101358905752f9.aspx&quot;&gt;Per&#39;s blog post from 2015&lt;/a&gt;, the question on how to localize arose, and Kai de Leuw answered with:&lt;/p&gt;
&lt;pre&gt;&lt;span&gt;[Display(Name=&quot;/path/to/lang/resource&quot;)]&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;That worked really well, up until a few weeks ago.&lt;/p&gt;
&lt;p&gt;In Episerver CMS UI 11.12, something was introduced, that broke this functionality. But have no fear, it can still be done, and it&#39;s even cleaner.&lt;/p&gt;
&lt;p&gt;Now, your POCOs are localized in the same way that other content types are localized.&lt;/p&gt;
&lt;p&gt;Provided you have a POCO like this:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class Contact {
    public string Name { get; set; }
    public string Country { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can localize it with an XML like this:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;languages&amp;gt;
  &amp;lt;language name=&quot;English&quot; id=&quot;en&quot;&amp;gt;
    &amp;lt;contenttypes&amp;gt;
      &amp;lt;Contact&amp;gt;
        &amp;lt;properties&amp;gt;
          &amp;lt;Name&amp;gt;
            &amp;lt;caption&amp;gt;Contact name&amp;lt;/caption&amp;gt;
            &amp;lt;help&amp;gt;The name of the contact.&amp;lt;/help&amp;gt;
          &amp;lt;/Name&amp;gt;
          &amp;lt;Country&amp;gt;
            &amp;lt;caption&amp;gt;Country of origin&amp;lt;/caption&amp;gt;
            &amp;lt;help&amp;gt;The country of origin for the contact.&amp;lt;/help&amp;gt;
          &amp;lt;/Country&amp;gt;
        &amp;lt;/properties&amp;gt;
      &amp;lt;/Contact&amp;gt;
    &amp;lt;/contenttypes&amp;gt;
  &amp;lt;/language&amp;gt;
&amp;lt;/languages&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Happy localizing!&lt;/p&gt;</id><updated>2018-11-22T15:02:31.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>A way of working in a multi-developer team with Episerver DXC-Service</title><link href="https://world.optimizely.com/blogs/stephan-lonntorp/dates/2018/11/working-in-a-multi-developer-team-with-episerver-dxc-service/" /><id>&lt;p&gt;There are probably as many ways of collaborating on Episerver projects, as there are teams out there. This is an approach that I have been using in my teams for quite a while now, and it has served us well.&lt;/p&gt;
&lt;p&gt;When working with Episerver DXC-Service, you get three environments, Integration, Pre-production, and Production, and you get access to deploying code into the Integration environment (hence its name). From there you either use Episerver Support, or the PaaS portal, to deploy changes from Integration to Pre-production, and then from Pre-production to Production by requesting a scheduled deploy by Episerver support.&lt;/p&gt;
&lt;p&gt;This blog post attepts to explain how a team of developers can set up their project, so that changes can be integrated, and synchronized between developer machines, with minimal effort and setup. Minimal is of course relative, so I&#39;ll try to explain my reasoningas I go along.&lt;/p&gt;
&lt;p&gt;In my experience, working with a shared database is a nightmare. It was workable in Episerver 6, but in projects using a code-first approach, it quickly became unfeasable. In the teams I have been working in for the past 5 years, we have had a shared &quot;Development Master&quot; environment, and used scripts to duplicate the database and blob files from that environment to local development machines. This approach has served us well, since it leaves us free to work remotely, isolated, and minimizes the risk of a single developer messing things up for the other team members when experimenting or doing YSOD-driven development.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/7c1931deefc245b5ac4d4c0ad0e46310.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In a DXC-Service project, we use the Integration environment as a &quot;Development Master&quot;, and it is kept as lightweight as possible, meaning that it only contains enough content to to test functionality and configure features. This ensures that it is a quick process to synchronize to the local environment. The process isn&#39;t fool proof, and can leave some things to be desired, especially when working with feature branches, but it works in most cases.&lt;/p&gt;
&lt;p&gt;The process itself is one part of the job, another part is making it simple to repeat the process. It can surely be done using visual tools, and pointing and clicking, but that would take forever, and we want this to be fast, and we want to be able to do it several times a day, if needed. In order to keep things secure, simple, and repeatable, there are a few prerequisites.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I use IIS Express, because that removes the need for a License.config file to be present in my solution.&lt;/li&gt;
&lt;li&gt;I use SQL Server Integrated Security, because that removes the need for stored credentials in my web.config.&lt;/li&gt;
&lt;li&gt;I use a local SQL Server instance, because that allows me to work disconnected and remotely.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Provided that these prerequisites are in place, we can do a few things with Powershell. I&#39;m no powershell guru, so consider this a hack-warning. I have created a script that presents a list of options, to synchronize the database, blobs, or both. My script relies on a file called Environment.ps1, a file that is excluded from source control, and contains all the sensitive information required for these tasks. It uses the Sql Server Dac Framework and AzCopy tools to performs the heavy lifting. Environment.ps1 looks something like this.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[CmdletBinding()]
param()

# SqlPackage is part of the Microsoft&amp;reg; SQL Server&amp;reg; Data-Tier Application Framework (DacFramework) which installed with SSMS or with SQL Server Data Tools, and can be found here https://www.microsoft.com/en-us/download/details.aspx?id=56356
$SqlPackagePath			= &quot;C:\Program Files (x86)\Microsoft SQL Server\140\DAC\bin\SqlPackage.exe&quot;

# AzCopy is a command-line utility designed for copying data to/from Microsoft Azure Blob, File, and Table storage, and can be found here: http://aka.ms/downloadazcopy
$AzCopyPath				= &quot;C:\Program Files (x86)\Microsoft SDKs\Azure\AzCopy\AzCopy.exe&quot;

# The name of the Web Application directory
$ApplicationDir			= &quot;&amp;lt;Name of directory and Web project csproj-file&amp;gt;&quot;

# The name of the Web Application .csproj file (located in the $ApplicationDir directory)
$CSProjFile				= &quot;$ApplicationDir.csproj&quot;

# The site hostname of the DXC Service Application, without the protocol identifier.
$DXCSSiteHostName		= &quot;dxc-service-subscriptionname-inte.dxcloud.episerver.net&quot;

# The name of the DXC Service Application, usually something ending in &quot;inte&quot;, &quot;prep&quot; or &quot;prod&quot;.
$DXCSApplicationName 	= &quot;dxc-service-subscriptionname-inte&quot;
 
# The connection string to use for the SQL Azure database&quot;
$DBConnectionString 	= &quot;&amp;lt;ConnectionString for Integration Environment&amp;gt;&quot;

# The key used to access the Azure Blob storage container
$BlobAccountKey 		= &quot;&amp;lt;Blob Storage Key for the Integration Environment&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the Magic script looks like this.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[CmdletBinding()]
param()
$ScriptDir 				= Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$ExitScriptNo			= 4

# Ensure Environment.ps1 exists
if (-not (Test-Path &quot;$ScriptDir\Environment.ps1&quot;)) {
	Write-Host &quot;`r`nEnvironment.ps1 could not be found, please make sure it exists. Check documentation for what it should contain.&quot; -ForegroundColor &quot;Red&quot;
	exit
}
. $ScriptDir\Environment.ps1

if(-not (Get-Variable -Name DXCSSiteHostName -ErrorAction SilentlyContinue)){
    Write-Host &quot;`r`nEnvironment.ps1 does not contain the variable DXCSSiteHostName, please ensure it is declared and contains the correct value.&quot; -ForegroundColor &quot;Red&quot;
	exit
}

# The path to the web application root directory
$AppLocalPath			= &quot;$ScriptDir\..\src\$ApplicationDir&quot;

# The path to where blobs are stored in the Episerver application
$BlobLocalPath			= &quot;$AppLocalPath\App_Data\blobs&quot;

function Get-EPiServerDBConnectionString {
	param([string]$fileName)
	$fileExists = Test-Path -Path $fileName
	if($fileExists){
		$xml = [xml](Get-Content $fileName)
		return $xml.SelectSingleNode(&quot;/configuration/connectionStrings/add[@name=&#39;EPiServerDB&#39;]&quot;).connectionString
	}
	else {
		Write-Host &quot;Could not extract connectionstring from $fileName, exiting.&quot; -ForegroundColor &quot;Red&quot;
		exit
	}
}
function Get-IISExpress-Url {
	param([string]$fileName)
	$fileExists = Test-Path -Path $fileName
	if($fileExists){
		$xml = [xml](Get-Content $fileName)
		$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
		$ns.AddNamespace(&quot;ns&quot;, $xml.DocumentElement.NamespaceURI)
		$iisUrl = $xml.SelectSingleNode(&quot;//ns:ProjectExtensions/ns:VisualStudio/ns:FlavorProperties/ns:WebProjectProperties/ns:IISUrl&quot;, $ns).&#39;#text&#39;
		return $iisUrl.Replace(&quot;http://&quot;, &quot;&quot;).Replace(&quot;https://&quot;, &quot;&quot;).TrimEnd(&quot;/&quot;)
	}
	else {
		Write-Host &quot;Could not extract IIS Express URL from $fileName, exiting.&quot; -ForegroundColor &quot;Red&quot;
		exit
	}
}
function Recreate-Database {
	param([string]$targetServer, [string]$databaseName)
	$srv = New-Object Microsoft.SqlServer.Management.Smo.Server($targetServer)
	if($srv.Databases[$databaseName]){
		$srv.KillAllProcesses($databaseName)
		$srv.KillDatabase($databaseName)
	}
	$db = New-Object Microsoft.SqlServer.Management.Smo.Database($srv, $databaseName)
	$db.Create()
	return
}
function Update-SiteAndHostDefinitions {
	param([string]$targetServer, [string]$databaseName, [string]$oldUrl, [string]$newUrl)
	$query = &quot;UPDATE tblSiteDefinition SET SiteUrl = &#39;http://$newUrl/&#39; WHERE SiteUrl = &#39;https://$oldUrl/&#39;;UPDATE tblHostDefinition SET Name = &#39;$newUrl&#39; WHERE Name = &#39;$oldUrl&#39;;&quot;
	Invoke-Sqlcmd -ServerInstance $targetServer -Query $query -Database $databaseName
}
function Delete-FormPosts {
	param([string]$targetServer, [string]$databaseName)
	$query = &quot;DELETE FROM [tblBigTableReference] WHERE [pkId] IN (SELECT [pkId] FROM [tblBigTable] WHERE [StoreName] LIKE &#39;%FormData_%&#39;);DELETE FROM [tblBigTable] WHERE [StoreName] LIKE &#39;%FormData_%&#39;;DELETE FROM [tblBigTableIdentity] WHERE [StoreName] LIKE &#39;%FormData_%&#39;;DELETE FROM [tblBigTableStoreInfo] WHERE [fkStoreId] IN (SELECT [pkID] FROM [tblBigTableStoreConfig] WHERE [StoreName] LIKE &#39;%FormData_%&#39;);DELETE FROM [tblBigTableStoreConfig] WHERE [StoreName] LIKE &#39;%FormData_%&#39;;&quot;
	Invoke-Sqlcmd -ServerInstance $targetServer -Query $query -Database $databaseName
}

# Ensure SqlPackage exists
if (-not (Test-Path $SqlPackagePath)) {
	Write-Host &quot;`r`nPlease ensure SqlPackage is installed and variable SqlPackagePath in script is correct.&quot; -ForegroundColor &quot;Red&quot;
	exit
}
# Ensure AzCopy exists
if (-not (Test-Path $AzCopyPath)) {
	Write-Host &quot;`r`nPlease ensure AzCopy is installed and variable AzCopyPath in script is correct.&quot; -ForegroundColor &quot;Red&quot;
	exit
}
# Ensure SqlServer PowerShell Module is installed
if (-not (Get-Module -ListAvailable -Name SqlServer)) {
    Write-Host &quot;`r`nSqlServer Powershell Module is not available, please install using command: &#39;Install-Module -Name SqlServer -AllowClobber&#39;&quot; -ForegroundColor &quot;Red&quot;
	exit
}
# Ensure local path for BLOBs exists
if (-not (Test-Path $BlobLocalPath)) {
	New-Item -ItemType directory -Path $BlobLocalPath | Out-Null
}

Import-Module SqlServer

while(-Not ($Step -eq $ExitScriptNo) -Or ($Step -eq $NULL)) {
	Write-Host &quot;`r`nAvailable options:`r`n&quot; -ForegroundColor cyan
	Write-Host &quot;1. Copy Database from SQL Azure to instance specified in Web.config&quot; -ForegroundColor white
	Write-Host &quot;2. Copy BLOBs from Azure Blob Storage to local machine&quot; -ForegroundColor gray
	Write-Host &quot;3. Full restore (1 &amp;amp; 2)&quot; -ForegroundColor white
	Write-Host &quot;4. Exit`r`n&quot; -ForegroundColor gray

	$Step = Read-Host &quot;Please choose option&quot;

	if(($Step -eq 1) -Or ($Step -eq 3)) {
		Write-Host &quot;`r`nBeginning backup of SQL Azure database`r`n&quot; -ForegroundColor yellow
		$bacpacFilename = &quot;$ScriptDir\$DatabaseName&quot; + (Get-Date).ToString(&quot;yyyy-MM-dd-HH-mm&quot;) + &quot;.bacpac&quot;
		&amp;amp; $SqlPackagePath /Action:Export /TargetFile:$bacpacFilename /SourceConnectionString:$DBConnectionString /Quiet:True
		Write-Host &quot;`r`nFinished backup of SQL Azure database`r`n&quot; -ForegroundColor green

		Write-Host &quot;`r`nBeginning restore of SQL Azure database to instance specified in Web.config`r`n&quot; -ForegroundColor yellow
		$webConfig = &quot;$AppLocalPath\Web.config&quot;
		$webConnectionString = Get-EPiServerDBConnectionString $webConfig
		$connString = New-Object System.Data.Common.DbConnectionStringBuilder
		$connString.set_ConnectionString($webConnectionString)
		$targetDatabaseName = $connString[&quot;initial catalog&quot;]
		$targetServer = $connString[&quot;server&quot;]
		Recreate-Database $targetServer $targetDatabaseName
		&amp;amp; $SqlPackagePath /Action:Import /SourceFile:$bacpacFilename /TargetDatabaseName:$targetDatabaseName /TargetServerName:$targetServer /Quiet:True
		Remove-Item $bacpacFilename
		Write-Host &quot;`r`nFinished restore of SQL Azure database to instance specified in Web.config`r`n&quot; -ForegroundColor green

		Write-Host &quot;`r`nUpdating site- and host definitions`r`n&quot; -ForegroundColor yellow
		$iisExpressUrl = Get-IISExpress-Url &quot;$AppLocalPath\$CSProjFile&quot;
		Update-SiteAndHostDefinitions $targetServer $targetDatabaseName $DXCSSiteHostName $iisExpressUrl
		Write-Host &quot;`r`nFinished updating site- and host definitions`r`n&quot; -ForegroundColor green

		Write-Host &quot;`r`nRemoving any stored Episerver Forms submissions`r`n&quot; -ForegroundColor yellow
		Delete-FormPosts $targetServer $targetDatabaseName
		Write-Host &quot;`r`nFinished removing Episerver Forms submissions`r`n&quot; -ForegroundColor green

		Write-Host &quot;`r`nTouching web.config to force application pool recycle`r`n&quot; -ForegroundColor green
		(dir $webConfig).LastWriteTime = Get-Date

		Write-Host &quot;`r`nDone with step 1.`r`n&quot; -ForegroundColor green

		if(($Step -eq 1)){
			Write-Host &quot;Bye&quot; -ForegroundColor green
			exit
		}
	}

	if(($Step -eq 2) -Or ($Step -eq 3)) {
		Write-Host &quot;`r`nBeginning copying BLOBs to local directory`r`n&quot; -ForegroundColor yellow
		&amp;amp; $AzCopyPath /Source:https://$DXCSApplicationName.blob.core.windows.net/blobs /Dest:$BlobLocalPath /SourceKey:$BlobAccountKey /S /MT /XO /Y
		Write-Host &quot;`r`nFinished copying BLOBs to local directory`r`n&quot; -ForegroundColor green

		Write-Host &quot;`r`nDone with step 2.`r`n&quot; -ForegroundColor green
		if(($Step -eq 2)) {
			Write-Host &quot;Bye&quot; -ForegroundColor green
			exit
		}
	}

	if(($Step -eq 3)) {
		Write-Host &quot;`r`nDone with step 3.`r`n&quot; -ForegroundColor green
		Write-Host &quot;Bye&quot; -ForegroundColor green
		exit
	}

	if($Step -eq $ExitScriptNo) {
		Write-Host &quot;Bye&quot; -ForegroundColor green
		exit
	}

	if([string]::IsNullOrEmpty($Step) -Or ($Step -lt 1) -Or -Not ($Step -lt ($ExitScriptNo + 1))) {
		Write-Host &quot;`r`nPlease enter a valid option&quot; -ForegroundColor &quot;Red&quot;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What happens when you run it? Besides pretty colors, it does the following:&lt;/p&gt;
&lt;p&gt;The database copy step creates a .dacpac file from the database in the integration environment, copies it to your local machine and then restores it to the database specified in the Web.config file. After it has restored the database, it ensures that the siteUrl in for the configured site matches the configured domain in the project settings in the csproj file, so that routing works locally, and then continues to delete any Episerver Forms posted form data, so that any sensitive posts won&#39;t sully your local dev environment (GDPR all the things!).&lt;/p&gt;
&lt;p&gt;The blob file step simply copies all the blob files in the blob container and puts them in your App_Data/blobs directory.&lt;/p&gt;
&lt;p&gt;Please note, that depending on your specific project, and how your project is set up, you might need to change or remove things. This has worked well for me, but doesn&#39;t necessarily guarantee success for you.&lt;/p&gt;
&lt;p&gt;This process, combined with a clean CI/CD process of getting your code into the Integration environment, can greatly reduce the time spent on boring stuff like copying databases and blob files. It has shortened our startup time for new developers on the team from half a day, to about 5-10 minutes, depending on connection speed. Time that can be spent on adding value instead!&lt;/p&gt;
&lt;p&gt;Good luck, have fun, and most importantly, don&#39;t be afraid to break things.&lt;/p&gt;</id><updated>2018-11-19T16:29:23.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Building a content-backed option list for your editors</title><link href="https://world.optimizely.com/blogs/stephan-lonntorp/dates/2018/11/building-a-content-backed-option-list-for-your-editors/" /><id>&lt;p&gt;So here&#39;s the premise, you want to let your editors select an option in a list, but, you also know that that list could change over time. A common approach is to use an enum, or a list of strings in an app-settings key. While both of these approaches work, they often leave editors wanting, and are limited to simple property types, like, enums, ints or strings, and require a deploy to update.&lt;/p&gt;
&lt;p&gt;Here&#39;s an approach of putting your editors in the driver&#39;s seat, while showing them something that puts content in context in a more visual way.&lt;/p&gt;
&lt;p&gt;This is a view of the end result for the editor.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/7cfdea1217704846b383bd5413a3d664.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This list of icons will probably change over time, and we don&#39;t want to have to do a deploy every time we add an icon. Granted, there are a few other aspects that will need to be considered, like how to handle values that no longer exist, localization, and probably how these should be handled in your front-end, but I&#39;ll leave most of that up to you as a reader and implementor to decide. Here&#39;s &lt;em&gt;a&lt;/em&gt; way to do this, not &lt;em&gt;the&lt;/em&gt; way.&lt;/p&gt;
&lt;h3&gt;First, let&#39;s get the data source into the CMS.&lt;/h3&gt;
&lt;p&gt;I&#39;ve opted for the use of a PropertyValueList, but if you&#39;re more of a blocks-for-all-the-things kind of developer, that is certainly doable too.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    public class CustomIcon
    {
        [Display(Name = &quot;/CustomIcon/Icon&quot;, Order = 1), AllowedTypes(typeof(SvgFile)), UIHint(UIHint.Image)]
        public ContentReference Icon { get; set; }

        [Display(Name = &quot;/CustomIcon/Label&quot;, Order = 2)]
        public string Label { get; set; }
    }

    /// &amp;lt;remarks&amp;gt;
    /// This class must exist so that Episerver can store the property list data.
    /// &amp;lt;/remarks&amp;gt;
    [PropertyDefinitionTypePlugIn]
    public class CustomIconCollectionProperty : PropertyList&amp;lt;CustomIcon&amp;gt; { }

    [EditorDescriptorRegistration(TargetType = typeof(IList&amp;lt;CustomIcon&amp;gt;))]
    public class CustomIconCollectionEditorDescriptor : CollectionEditorDescriptor&amp;lt;CustomIcon&amp;gt;
    {
        public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable&amp;lt;Attribute&amp;gt; attributes)
        {
            ClientEditingClass = &quot;CustomIconPicker/CollectionFormatter.js&quot;;
            base.ModifyMetadata(metadata, attributes);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OK, so what&#39;s going on here? This is just a simple list of a simple POCO, containing a ContentReference and a string property. I always define specific Media typesper extension, instead of having a generic ImageFile media type, since that allows me to do restrictions like above, where I only allow SVG&#39;s as icons.&lt;/p&gt;
&lt;p&gt;And here&#39;s the JavaScript for showing the icon, instead of the ID, in the grid editor for the PropertyValueList (the contents of the CollectionFormatter.js file)&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;define([
    &quot;dojo/_base/declare&quot;,
    &quot;epi/dependency&quot;,
    &quot;epi-cms/contentediting/editors/CollectionEditor&quot;
],
    function (
        declare,
        dependency,
        collectionEditor
    ) {
        return declare([collectionEditor], {
            _resolveContentData: function(id) {
                if (!id) {
                    return null;
                }
                var registry = dependency.resolve(&quot;epi.storeregistry&quot;);
                var store = registry.get(&quot;epi.cms.content.light&quot;);
                var contentData;
                dojo.when(store.get(id), function (returnValue) {
                    contentData = returnValue;
                });
                return contentData;
            },
            _getGridDefinition: function () {
                var that = this;
                var result = this.inherited(arguments);
                result.icon.formatter = function (value) {
                    var content = that._resolveContentData(value);
                    if (content) {
                        return `&amp;lt;img style=&quot;width:16px;display:inline-block;margin-right:4px;&quot; src=&quot;${content.publicUrl}&quot; alt=&quot;${content.name}&quot; title=&quot;${content.name} (${content.contentLink})&quot; /&amp;gt;`;
                    }
                    return value;
                };
                return result;
            }
        });
    });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What the above code produces, is something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/6107eaebbf8748f7b6efcb7ca28a06fc.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;OK, so now we have a property editor, but we&#39;ll need somewhere to store the data too. I&#39;ve used a block, and an abstracted interface, and for brevity I&#39;ve left the registering and resolving of it out, but you&#39;ll need to have a way if registering it in the IoC container. A simple way could be to have it as a property on your start page, and resolve the property from there, and register it as a scoped instance, but I think that could be a blog post in its own right. If you want me to explain that in detail, let me know in the comments.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    [ContentType(GUID = Guid)]
    public class IconConfiguration : BlockData, ICustomIconConfiguration
    {
        public const string Guid = &quot;52C7D11A-E1CB-4E8F-847F-40DA095F1234&quot;;

        [Display(Order = 10), CultureSpecific]
        public virtual IList&amp;lt;CustomIcon&amp;gt; AvailableIcons { get; set; }
    }

    public interface ICustomIconConfiguration
    {
        IList&amp;lt;CustomIcon&amp;gt; AvailableIcons { get; }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OK, now that we have that object to store our configured icons, and we can resolve it from the IoC container, how do we use it? On to part 2.&lt;/p&gt;
&lt;h3&gt;Using content in our editor&lt;/h3&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    [EditorDescriptorRegistration(TargetType = typeof(ContentReference), UIHint = &quot;CustomIconPicker&quot;)]
    public class CustomIconPickerEditorDescriptor : EditorDescriptor
    {
        public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable&amp;lt;Attribute&amp;gt; attributes)
        {
            ClientEditingClass = &quot;CustomIconPicker/Editor.js&quot;;
            base.ModifyMetadata(metadata, attributes);
            metadata.EditorConfiguration.Add(&quot;data&quot;, ServiceLocator.Current.GetInstance&amp;lt;IIconConfiguration&amp;gt;().AvailableIcons);
            metadata.EditorConfiguration.Add(&quot;noneSelectedName&quot;, LocalizationService.Current.GetString(&quot;/CustomIcon/Picker/NoneSelected&quot;));
        }
    }
	
	[ContentType(GUID = Guid)]
    public class MyBlockWithCustomIcon : BlockData
    {
        public const string Guid = &quot;5E2556B3-3071-43E6-B4CE-CDD34B13E4DE&quot;;

        [Display(Order = 10), UIHint(&quot;CustomIconPicker&quot;)]
        public virtual ContentReference Icon { get; set; }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So I have a block with a ContentReference property, and a UIHint that makes it use my custom editor. Just a quick note on why I&#39;m using the &lt;a href=&quot;http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/&quot;&gt;despicable anti-pattern&lt;/a&gt; that is the ServiceLocator: Using constructor injection in an editordescriptor works, but will effectively turn the instance injected into a singleton in the editor descriptor scope, meaning that any changes to the IIconConfiguration instance won&#39;t show up until app restart. Thus, using constructor injection in editor descriptors is generally a bad idea, unless you&#39;re only using singleton dependencies, but as a consumer you shouldn&#39;t have to care about that.&lt;/p&gt;
&lt;p&gt;But enough about that, here&#39;s the code in the Editor.js file.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;define([
    &quot;dojo/on&quot;,
    &quot;dojo/_base/declare&quot;,
    &quot;dojo/aspect&quot;,
    &quot;dijit/registry&quot;,
    &quot;dijit/WidgetSet&quot;,
    &quot;dijit/_Widget&quot;,
    &quot;dijit/_TemplatedMixin&quot;,
    &quot;dijit/_WidgetsInTemplateMixin&quot;,
    &quot;dijit/form/Select&quot;,
    &quot;epi/dependency&quot;,
    &quot;epi/i18n!epi/cms/nls/customicon.picker&quot;,
    &quot;xstyle/css!./WidgetTemplate.css&quot;
],
    function (
        on,
        declare,
        aspect,
        registry,
        WidgetSet,
        _Widget,
        _TemplatedMixin,
        _WidgetsInTemplateMixin,
        Select,
        dependency,
        localization) {
        return declare([_Widget, _TemplatedMixin, _WidgetsInTemplateMixin], {
            templateString: dojo.cache(&quot;customicon.picker&quot;, &quot;WidgetTemplate.html&quot;),
            intermediateChanges: false,
            value: null,
            picker: null,
            _resolveContentData: function(id) {
                if (!id) {
                    return null;
                }
                var registry = dependency.resolve(&quot;epi.storeregistry&quot;);
                var store = registry.get(&quot;epi.cms.content.light&quot;);
                var contentData;
                dojo.when(store.get(id), function (returnValue) {
                    contentData = returnValue;
                });
                return contentData;
            },
            onChange: function (value) {
            },
            postCreate: function () {
                this.inherited(arguments);
                this.initializePicker(this.value);
            },
            startup: function () {
            },
            destroy: function () {
                this.inherited(arguments);
            },
            _setValueAttr: function (value) {
                if (value === this.value) {
                    return;
                }
                this._set(&quot;value&quot;, value);
                this.setSelected(value);
            },
            setSelected: function (value) {
                var self = this;
                self.picker.attr(&quot;value&quot;, value);
            },
            initializePicker: function (initialValue) {
                var self = this;
                var data = self.data;
                if (data != null) {
                    var options = [{ label: self.noneSelectedName, value: &quot; &quot;, selected: initialValue === null }];
                    for (var index = 0; index &amp;lt; data.length; index++) {
                        var item = data[index];
                        var content = self._resolveContentData(item.icon);
                        if (content) {
                            options.push({ label: `&amp;lt;div class=&quot;customiconpicker--icon&quot;&amp;gt;&amp;lt;img style=&quot;width:32px;display:inline-block;margin-right:4px;&quot; src=&quot;${content.publicUrl}&quot; alt=&quot;${item.label}&quot; title=&quot;${item.label}&quot; /&amp;gt;&amp;lt;span class=&quot;customiconpicker--label&quot;&amp;gt;${item.label}&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;`, value: item.icon, selected: item.icon === initialValue });
                        }
                    }
                    var select = new Select({
                        name: &quot;customiconpicker&quot;,
                        options: options,
                        maxHeight: -1
                    }, &quot;customiconpicker&quot;);
                    select.on(&quot;change&quot;, function () {
                        self._onChange(this.get(&quot;value&quot;));
                    });
                    this.picker = select;
                    this.container.appendChild(select.domNode);
                }
            },
            _onChange: function (value) {
                this._set(&quot;value&quot;, value);
                this.onChange(value);
            }
        });
    });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&#39;s the WidgetTemplate.html referenced in the javascript.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;dijitInline customiconpicker-editor&quot;&amp;gt;
    &amp;lt;div data-dojo-attach-point=&quot;container&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&#39;s the WidgetTemplate.css also referenced from the JavaScipt, they are placed beside the Editor.js file.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot;&gt;&lt;code&gt;.customiconpicker-editor .dijitButtonText {
    height: 40px;
}

.customiconpicker--icon {
    margin-left: 0px;
    display: flex;
    align-items: center;
    min-width: 40px;
}

    .customiconpicker--icon .customiconpicker--label {
        height: 40px;
        line-height: 40px;
        padding-left: 4px;
    }

.dijitSelectLabel .customiconpicker--icon {
    margin-left: 0px;
    display: flex;
    align-items: center;
}

    .dijitSelectLabel .customiconpicker--icon .customiconpicker--label {
        height: 40px;
        line-height: 40px;
        padding-left: 4px;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And, if you&#39;re a &quot;don&#39;t-hardcode-things&quot; kind of person like me, here&#39;s the XML that&#39;ll give your editors editing controls in a language they can understand.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;languages&amp;gt;
  &amp;lt;language name=&quot;English&quot; id=&quot;en&quot;&amp;gt;
    &amp;lt;contenttypes&amp;gt;
      &amp;lt;IconConfiguration&amp;gt;
        &amp;lt;name&amp;gt;Icon Configuration&amp;lt;/name&amp;gt;
        &amp;lt;description&amp;gt;A block used to configure the icon feature on the site.&amp;lt;/description&amp;gt;
        &amp;lt;properties&amp;gt;
          &amp;lt;AvailableIcons&amp;gt;
            &amp;lt;caption&amp;gt;Available Icons&amp;lt;/caption&amp;gt;
            &amp;lt;help&amp;gt;The icons available for editors to select from.&amp;lt;/help&amp;gt;
          &amp;lt;/AvailableIcons&amp;gt;
        &amp;lt;/properties&amp;gt;
      &amp;lt;/IconConfiguration&amp;gt;
      &amp;lt;MyBlockWithCustomIcon&amp;gt;
        &amp;lt;name&amp;gt;My block with an icon&amp;lt;/name&amp;gt;
        &amp;lt;description&amp;gt;A block with an icon used for this demo.&amp;lt;/description&amp;gt;
        &amp;lt;properties&amp;gt;
          &amp;lt;Icon&amp;gt;
            &amp;lt;caption&amp;gt;Icon&amp;lt;/caption&amp;gt;
            &amp;lt;help&amp;gt;An optional icon to use for this block.&amp;lt;/help&amp;gt;
          &amp;lt;/Icon&amp;gt;
        &amp;lt;/properties&amp;gt;
      &amp;lt;/MyBlockWithCustomIcon&amp;gt;
    &amp;lt;/contenttypes&amp;gt;
    &amp;lt;CustomIcon&amp;gt;
      &amp;lt;Label&amp;gt;Label&amp;lt;/Label&amp;gt;
      &amp;lt;Icon&amp;gt;Icon&amp;lt;/Icon&amp;gt;
      &amp;lt;Picker&amp;gt;
        &amp;lt;NoneSelected&amp;gt;Default icon&amp;lt;/NoneSelected&amp;gt;
      &amp;lt;/Picker&amp;gt;
    &amp;lt;/CustomIcon&amp;gt;
  &amp;lt;/language&amp;gt;
&amp;lt;/languages&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hopefully, following these steps should leave you with a smoother editor experience, that will give your editors more control over their site, and help them not have to memorize what icon a text string corresponds to, and also be able to change that icon. Or image, or whatever your imagination can come up with.&lt;/p&gt;
&lt;p&gt;Remember, Episerver is a tool built to suit everyone, but that doesn&#39;t mean that you can&#39;t make it suit your clients even better. Put your editors in the driver&#39;s seat, and give them a tool they love to use, not one that they tolerate.&lt;/p&gt;</id><updated>2018-11-16T13:23:32.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Performance Optimization: Preloading required client resources and pushing them to the browser with HTTP/2 Server Push</title><link href="https://world.optimizely.com/blogs/stephan-lonntorp/dates/2018/10/preloading-required-client-resources/" /><id>&lt;p&gt;Today, a colleague of mine asked me if we could utilize the new HTTP/2&amp;nbsp;Server Push feature in Cloudflare, for one of our DXC-Service clients. I thought it wouldn&#39;t be too hard to accomplish, and I was right.&lt;/p&gt;
&lt;p&gt;The HTTP/2 Server Push feature in Cloudflare relies on the Link HTTP Header, that can be used instead of&amp;nbsp;a regular &amp;lt;link rel=&quot;preload&quot;&amp;gt; element in the &amp;lt;head&amp;gt; of your page, and works like this: Any resources you specify in the Links header, will be checked if it is local, and if it is, pushed along with the page directly to the client, and removed from the HTTP header. Resources that aren&#39;t local are left in the header value, so your clients can still benefit from preloading. Read more about this &lt;a href=&quot;https://www.cloudflare.com/website-optimization/http2/serverpush/&quot;&gt;here&lt;/a&gt;. Please, note, that even if you don&#39;t use DXC Service, you can still use Cloudflare, and even if you don&#39;t use Cloudflare, your visitors can still benefit from asset preloading.&lt;/p&gt;
&lt;p&gt;If you aren&#39;t using ClientResources for your own styles and scripts, either implement that, or just add them manually in the module.&lt;/p&gt;
&lt;p&gt;Step 1: If you are using DXC-Service, email &lt;a href=&quot;mailto:support@episerver.com&quot;&gt;support@episerver.com&lt;/a&gt;&amp;nbsp;and kindly ask them to enable the HTTP/2 Server Push feature in Cloudflare for your subscription.&lt;/p&gt;
&lt;p&gt;Step 2: Implement an IHttpModule like this:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using System.Collections.Generic;
using System.Linq;
using System.Web;
using EPiServer.Framework.Web.Resources;
using EPiServer.ServiceLocation;

namespace My.Fully.Qualified.Namespace
{
    public class AssetPreloadModule : IHttpModule
    {
        public void Init(HttpApplication context)
        {
            context.PreSendRequestHeaders += SendLinkHeaderForRequiredResources;
        }

        private static void SendLinkHeaderForRequiredResources(object sender, System.EventArgs e)
        {
            if (application.Context.Response.ContentType == &quot;text/html&quot;)
            {
                var requiredResources = ServiceLocator.Current.GetInstance&amp;lt;IRequiredClientResourceList&amp;gt;().GetRequiredResourcesSettings().ToArray();
                var requiredResourceService = ServiceLocator.Current.GetInstance&amp;lt;IClientResourceService&amp;gt;();
                var assets = requiredResources.SelectMany(
                    setting =&amp;gt; requiredResourceService
                        .GetClientResources(setting.Name, new[] { ClientResourceType.Script, ClientResourceType.Style })
                        .Where(x =&amp;gt; x.IsStatic), (setting, clientResource) =&amp;gt; GetPreloadLink(clientResource)).ToList();
                if (assets.Any())
                {
                    AddLinkTag(application.Context, assets);
                }
            }
        }

        private static AssetPreloadLink GetPreloadLink(ClientResource clientResource)
        {
            var link = new AssetPreloadLink
            {
                Type = ConvertType(clientResource.ResourceType),
                Url = clientResource.Path
            };
            return link;
        }

        private static AssetType ConvertType(ClientResourceType resourceType)
        {
            switch (resourceType)
            {
                case ClientResourceType.Script:
                    return AssetType.Script;
                case ClientResourceType.Style:
                    return AssetType.Style;
                default:
                    return AssetType.Unknown;
            }
        }

        private static void AddLinkTag(HttpContext context, IEnumerable&amp;lt;AssetPreloadLink&amp;gt; links)
        {
            context.Response.AppendHeader(&quot;Link&quot;, string.Join(&quot;,&quot;, links));
        }

        public void Dispose()
        {
        }
    }

    internal class AssetPreloadLink
    {
        private const string Format = &quot;&amp;lt;{0}&amp;gt;; rel=preload; as={1}&quot;;
        public string Url { get; set; }
        public AssetType Type { get; set; }
        public bool NoPush { get; set; }

        public override string ToString()
        {
            if (Type != AssetType.Unknown)
            {
                var output = string.Format(Format, Url, Type.ToString().ToLowerInvariant());
                if (NoPush)
                {
                    return output + &quot;; nopush&quot;;
                }
                return output;
            }
            return string.Empty;
        }
    }

    internal enum AssetType
    {
        Unknown = 0,
        Script = 100,
        Style = 200,
        Image = 300
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Step 3: Add the module to your web.config&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;configuration&amp;gt;
  &amp;lt;system.webServer&amp;gt;
    &amp;lt;modules runAllManagedModulesForAllRequests=&quot;true&quot;&amp;gt;
      &amp;lt;add name=&quot;AssetPreloadModule&quot; type=&quot;My.Fully.Qualified.Namespace.AssetPreloadModule, My.Assemblyname&quot; /&amp;gt;
    &amp;lt;/modules&amp;gt;   
  &amp;lt;/system.webServer&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Step 4: Build, run, and watch your conversion rates soar!&lt;/p&gt;
&lt;p&gt;Note: Thanks to Johan Petersson for alerting me to the fact that I appended the header to every request. Although my assumption was correct, in that requests for resources other than html requests won&#39;t have any required resources in the IRequiredClientResourceList, executing the lookup for those requests&amp;nbsp;was unnecessary. If you manually add resources that are not in the list, the&amp;nbsp;check&amp;nbsp;with assets.Any() can be removed.&lt;/p&gt;</id><updated>2018-10-26T21:03:30.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Clean and unique media URLs with ImageResizer.NET presets</title><link href="https://world.optimizely.com/blogs/stephan-lonntorp/dates/2018/1/clean-and-unique-image-urls/" /><id>&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;p&gt;For a while I&#39;ve been working on cleaning up the urls generated for images, to ensure that they are clean, cacheable, and preferrably querystring-free. Previously, I have been using IIS URL Rewrites to achieve this, but I have always known there are better ways, but just haven&#39;t gotten around to it yet. Better, meaning&amp;nbsp;fewer moving parts, and a drop-in solution.&lt;/p&gt;
&lt;p&gt;Yes, I am well aware of the built-in functionality of naming blob properties on media items and tacking on an attribute to get a scaled image, but image optimization is far more than just resizing, and a component like ImageResizer is a really great tool to have in your site. It really is worth the cost.&lt;/p&gt;
&lt;p&gt;So, without further ado, here&#39;s &quot;&lt;a href=&quot;http://nuget.episerver.com/en/OtherPages/Package/?packageId=ImageResizer.Plugins.UniqueUrlFolderPresets&quot;&gt;UniqueUrlFolderPresets&lt;/a&gt;&quot;, I didn&#39;t know what to name it, but this name is as good as any :)&lt;/p&gt;
&lt;p&gt;What does it do?&lt;/p&gt;
&lt;p&gt;It takes a media URL, like &quot;/globalassets/my-images/my-image.jpg&quot; and prepends it with a 8 character hash based off of the media item&#39;s last saved date. Out of the box, it also adds a max-age header, that caches it for a year. Yes, that&#39;s configurable. I know, this isn&#39;t a very new concept, but AFAIK no one has done it in conjunction with resizing images. This is the &quot;UniqueUrl&quot; part of the add-on.&lt;/p&gt;
&lt;p&gt;The second part, the &quot;FolderPresets&quot; part, has support for imageresizing.net&#39;s presets as part of the segments of the URL, as opposed to tacking on an ugly querystring. I&#39;ve added a couple of really simple UrlHelper extensions to help with generating the URLs, but in essence, if you prepend your image URL with &quot;/optimized/&amp;lt;preset&amp;gt;&quot;, it will apply that preset, if it exists, to the image request. If the preset doesn&#39;t exist, it&#39;ll give you a 404. Yes, the &quot;optimized&quot; part is also configurable.&lt;/p&gt;
&lt;p&gt;It &lt;em&gt;should&lt;/em&gt;&amp;nbsp;work with any other ImageResizer plugins, but I haven&#39;t tested it that much, please help with that. If you have clientcaching configured for imageresizer, this plugin&amp;nbsp;replaces&amp;nbsp;that, so you can remove it.&lt;/p&gt;
&lt;p&gt;If there&#39;s no hash in the URL, you&#39;ll get a 301 redirect to the latest version (hash), and if there&#39;s an old hash in the url, you&#39;ll also get a 301 redirect.&lt;/p&gt;
&lt;p&gt;Hopefully, someone besides me will find this useful. And as usual, I&#39;m open to feedback and suggestions. Use &lt;a href=&quot;https://github.com/defsteph/UniqueUrlFolderPresets&quot;&gt;github&lt;/a&gt; for that.&lt;/p&gt;
&lt;p&gt;There&#39;s also a back-port for CMS 10, if you have yet to upgrade, you&#39;ll &lt;a href=&quot;http://nuget.episerver.com/en/OtherPages/Package/?packageId=ImageResizer.Plugins.UniqueUrlFolderPresets.Cms10&quot;&gt;find it here&lt;/a&gt;.&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</id><updated>2018-02-01T07:58:50.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>All I want for christmas is updated addons and an automated approval process.</title><link href="https://world.optimizely.com/blogs/stephan-lonntorp/dates/2017/12/update-all-the-things/" /><id>&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;p&gt;Today I decided to make a little effort in getting things ready for the inevitable CMS 11 upgrade storm. Eventually, we&#39;ll be asked to upgrade our client sites to the latest and greates version of EPiServer. This process usually goeas a little something like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Upgrade site&lt;/li&gt;
&lt;li&gt;Fix compilation errors&lt;/li&gt;
&lt;li&gt;Identify obsoleted methods&lt;/li&gt;
&lt;li&gt;Find out how to do it the new way&lt;/li&gt;
&lt;li&gt;Do it the new way&lt;/li&gt;
&lt;li&gt;Huge sigh of relief for&amp;nbsp;your code not being nearly as broken as you thought&amp;nbsp;it was going to be.&lt;/li&gt;
&lt;li&gt;Stop everything because an add-on you&#39;re using hasn&#39;t been upgraded yet.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This time around, EPiServer has been fast to fix things, but there are still a few things missing.&lt;/p&gt;
&lt;p&gt;I have taken the time to ensure that the &lt;a href=&quot;https://github.com/CreunaAB/EPiFocalPoint&quot;&gt;FocalPoint&lt;/a&gt;&amp;nbsp;and &lt;a href=&quot;https://github.com/CreunaAB/EPi.UrlTransliterator&quot;&gt;URL Transliteration&lt;/a&gt;&amp;nbsp;plugins are updated accordingly, beacuse we use them in quite a few projects.&lt;/p&gt;
&lt;p&gt;I just submitted a PR to get &lt;a href=&quot;https://github.com/episerver/PowerSlice&quot;&gt;PowerSlice&lt;/a&gt; up to speed, being that PowerSlice is open source.&lt;/p&gt;
&lt;p&gt;OK, so why the blog post?&lt;/p&gt;
&lt;p&gt;Well, a small part is obviously self promotion, (hi mom!), but mostly it&#39;s because I feel that the workflow for submitting updated nuget packages is broken.&lt;/p&gt;
&lt;p&gt;I get how PR&#39;s will take time to process, but&amp;nbsp;the packages I manage could easily be deployed via the NuGet API, instead of being manually processed. I think that we as a developer community should speak up about what we need, and this is my way of doing just that.&lt;/p&gt;
&lt;p&gt;So, as soon as someone at EPiServer has approved the aforementioned packages and PR, I will have done my part in getting us all one step closer to upgrading all our sites.&lt;/p&gt;
&lt;p&gt;Also, Alans automated IFTTT script will tweet this, and that will make for a funny tweet.&lt;/p&gt;
&lt;p&gt;Merry X-Mas!&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</id><updated>2017-12-07T14:52:42.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Ensuring image extension for content URLs</title><link href="https://world.optimizely.com/blogs/stephan-lonntorp/dates/2016/11/ensuring-image-extension-for-content/" /><id>&lt;p&gt;When using an image resizing component, like we do in the Focal Point plugin, it&#39;s really important that the file extension is there. If it&#39;s not, the resizing engine won&#39;t kick in for the request, and that enormous image is sent straight to your mobile users, and they will forever hate your guts.&lt;/p&gt;
&lt;p&gt;I&#39;ve implemented a solution using the new (in CMS 10) IUrlSegmentCreator events, and a regular InitializationModule. Code below. It uses the first registered extension for your media type as the default extension for the URL segment. This works for me, since I have separate Media types for all image files, like JpegImage, PngImage etc, to be able to more granularly control their usage, through the AllowedTypes attribute. If you have a single MediaData for all your images, I recommend altering the extension mapping to maybe use the MimeType property and get an appropriate extension based on that.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;/*using System;
using System.IO;
using System.Linq;

using EPiServer;
using EPiServer.Core;
using EPiServer.Framework;
using EPiServer.Framework.DataAnnotations;
using EPiServer.Framework.Initialization;
using EPiServer.Web;
*/

[InitializableModule, ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
	public class ImageDataInitialization : IInitializableModule {
		private bool eventsAttached;
		public void Initialize(InitializationEngine context) {
			if(!this.eventsAttached) {
				var creator = context.Locate.Advanced.GetInstance&amp;lt;IUrlSegmentCreator&amp;gt;();
				creator.Created += CreatedMediaSegment;
				this.eventsAttached = true;
			}
		}
		private static void CreatedMediaSegment(object sender, UrlSegmentEventArgs e) {
			var content = e.Content as ImageData;
			if(content != null) {
				var extension = Path.GetExtension(content.RouteSegment);
				if(string.IsNullOrWhiteSpace(extension)) {
					var type = content.GetOriginalType();
					var fileExtension = GetExtensionForType(type);
					if(!string.IsNullOrWhiteSpace(fileExtension)) {
						content.RouteSegment = content.RouteSegment + &quot;.&quot; + fileExtension;
					}
				}
			}
		}
		private static string GetExtensionForType(Type contentType) {
			var mediaDescriptorAttribute = contentType?.GetCustomAttributes(typeof(MediaDescriptorAttribute), false).OfType&amp;lt;MediaDescriptorAttribute&amp;gt;().FirstOrDefault();
			return mediaDescriptorAttribute?.Extensions?.FirstOrDefault();
		}
		public void Uninitialize(InitializationEngine context) {
			var creator = context.Locate.Advanced.GetInstance&amp;lt;IUrlSegmentCreator&amp;gt;();
			creator.Created -= CreatedMediaSegment;
			this.eventsAttached = false;
		}
	}&lt;/code&gt;&lt;/pre&gt;</id><updated>2016-11-18T14:35:57.0770000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>URL Transliteration for EPiServer CMS 10</title><link href="https://world.optimizely.com/blogs/stephan-lonntorp/dates/2016/10/url-transliteration-for-episerver-cms-10/" /><id>&lt;p&gt;A while back we built a website that had a chinese language version, and we had a few issues with URL Segments not looking very nice. I took me a while to figure out that what I was looking for is called Transliteration. I implemented a really hacky way of modifying the URL Segment that EPiServer produces, so that I could inject my transliterated page name, instead of the page name in chinese.&lt;/p&gt;
&lt;p&gt;Now in CMS 10, the old UrlSegment class has been removed, and instead we have the IUrlSegmentGenerator, IUrlSegmentCreator and IUrlSegmentLocator, you can read more about that in the release note &lt;a href=&quot;/link/76974ad8d2a84c1b989ad0ac453ab663.aspx?releaseNoteId=CMS-3824&amp;amp;epsremainingpath=ReleaseNote/&quot;&gt;CMS-3824&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The default implementations for these interfaces are all internal, so it&#39;s still a bit hacky to extend, but, I&#39;ve implemented a Transliterating UrlSegmentGenerator, and swaped out the implementation, so you don&#39;t have to.&lt;/p&gt;
&lt;h2&gt;OK, so what is transliteration, and why is this important?&lt;/h2&gt;
&lt;p&gt;Let&#39;s say we have a page named&amp;nbsp;&quot;伤寒论 勘误&quot; (I don&#39;t know what that means, it&#39;s just some chinese text that I copied). The default UrlSegmentGenerator would produce&amp;nbsp;the url &quot;-&quot;, since everything but alphanumeric chars are&amp;nbsp;stripped out, so the only thing that remains is the whitespace character in the name.&lt;/p&gt;
&lt;p&gt;Using transliteration, the chinese characters are converted to their alphanumeric versions, so the same input string&amp;nbsp;&quot;伤寒论 勘误&quot; is converted to&amp;nbsp;&quot;Shang&amp;nbsp;Han&amp;nbsp;Lun&amp;nbsp;Kan&amp;nbsp;Wu&quot;, and the Transliterating UrlSegmentGenerator then produces the url &quot;shang-han-lun-kan-wu&quot;.&lt;/p&gt;
&lt;p&gt;Granted, I don&#39;t know chinese, so I can&#39;t verify that this is 100% correct. But I do know that&amp;nbsp;&lt;span&gt;&quot;shang-han-lun-kan-wu&quot; is a better representation than &quot;-&quot;, since three&amp;nbsp;pages in chinese, in the same location, would have the urls &quot;-&quot;, &quot;-1&quot;&amp;nbsp;and&quot;-2&quot; using the default generator.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;This approach should work for all languages, not just chinese, but you&#39;ll have to test it for yourself, if you find any bugs, please let us&amp;nbsp;know by sending a pull request.&lt;/p&gt;
&lt;p&gt;The code is available at&amp;nbsp;&lt;a href=&quot;https://github.com/creunaab/EPi.UrlTransliterator&quot;&gt;https://github.com/creunaab/EPi.UrlTransliterator&lt;/a&gt;, and a package with the same name should be available in the EPiServer NuGet feed shortly.&lt;/p&gt;</id><updated>2016-10-28T10:56:41.1470000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Focal point based image cropping for EPiServer using ImageResizing.NET</title><link href="https://world.optimizely.com/blogs/stephan-lonntorp/dates/2016/9/focal-point-based-image-cropping-for-episerver-using-imageresizing-net/" /><id>&lt;p&gt;There are a lot of solutions out there to ensure that the important information in an image is always visible, regardless of how you crop it. Now, we can do it in EPiServer, too.&lt;/p&gt;
&lt;p&gt;For a long time, me and my team mates have been discussing image optimizations and how to make it as effortless as possible for editors to work with image content. We&#39;ve been using ImageResizer to do it for quite a while, but we haven&#39;t had a solution for how to modify the cropping area based on width and height. This could sometimes lead to the unwanted effect of cropping faces, and/or other parts of images that were important.&lt;/p&gt;
&lt;p&gt;We procrastinated a lot, and kept hoping for someone else to solve the problem for us, but finally we gave in and sat down together to get our hands dirty.&lt;/p&gt;
&lt;p&gt;After discussing a lot of different use cases, we settled on having one crop point, and store that, instead of saving a lot of different settings based on different usages. The main reason for this was that a single crop point gives us the flexibility to use the image in many different scenarios. It gives us the freedom to change the UI of our website, without having to involve the editor again if we choose to change or add usage scenarios.&lt;/p&gt;
&lt;h2&gt;Ok, so how does it work?&lt;/h2&gt;
&lt;p&gt;First, the editor sets a focal point in the image. It is done in EPiServer by clicking on the image in the Focal Point Editor.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://www.creuna.se/static/media/sizes/1000/uploads/Ideer/Blogg/2016/epifocalpoint_editor_first.jpg&quot; alt=&quot;Setting the focal point to the far left in the image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;When the image is loaded by the website visitor, the focal point is used to determine what part of the image should be included in the crop.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://www.creuna.se/static/media/sizes/1000/uploads/Ideer/Blogg/2016/epifocalpoint_crop_first.jpg&quot; alt=&quot;The image is cropped using the focal point&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If the focal point is instead placed in the center of the pizza, the crop using the same parameter is altered. (The focal point is red, but it is there, in the center of the pie, trust me)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://www.creuna.se/static/media/sizes/1000/uploads/Ideer/Blogg/2016/epifocalpoint_editor_second.jpg&quot; alt=&quot;The focal point is moved to the center of the pizza&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://www.creuna.se/static/media/sizes/1000/uploads/Ideer/Blogg/2016/epifocalpoint_crop_second.jpg&quot; alt=&quot;The crop is altered based on the focal point&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The plugin has support for both querystring parameters, and presets. We&#39;ve been using presets together with IIS UrlRewrite Module to make for really pretty image URLs, but that&#39;s another blog post :)&lt;/p&gt;
&lt;p&gt;The NuGet package is available from the&amp;nbsp;&lt;a href=&quot;http://nuget.episerver.com/en/OtherPages/Package/?packageId=ImageResizer.Plugins.EPiFocalPoint&quot;&gt;EPiServer NuGet Feed&lt;/a&gt;, or you can download the source code from&amp;nbsp;&lt;a href=&quot;https://github.com/CreunaAB/EPiFocalPoint&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This blog post was &lt;a href=&quot;http://www.creuna.se/blogg/focal-point-based-image-cropping-for-episerver-using-imageresizingnet/&quot;&gt;originally published&amp;nbsp;on creuna.se&lt;/a&gt;&lt;/p&gt;</id><updated>2016-09-13T20:39:03.4070000Z</updated><summary type="html">Blog post</summary></entry></feed>