Pages

Wednesday 10 April 2019

Umbraco - Upload SVG as Image

In Umbraco, you can upload multiple images into the media library by simply dragging and dropping them all together unto a Media Folder.


This works fine for JPG, GIF, PNG and the like, but when you drop an SVG, it is uploaded as a File instead of an Image.

When you subsequently want to use a MediaPicker to select that image in one of your content items, you cannot select the SVG, because only Images are allowed, not Files.

It's perfectly possible to click Create > Image, then drop the SVG on the Upload image pane, but as a developer, I pride myself on my laziness. I will not upload 28 images one by one, if I can solve this once and for all.

"Hidden" setting

The list of file extensions that is recognized as an Image (everything else is a File) is defined in settings/content/imaging/imageFileTypes.

For some reason, however, this setting is not present in config/umbracoSettings.config by default.
When not present, the code falls back to jpeg, jpg, gif, bmp, png, tiff, tif. We just want to add svg to the list.

There are many settings omitted like this. Maybe they do not want to overload the configs with settings that are falling back to default anyway? I've seen the same in other CMS's.

So, all you need to do, is go to config/umbracoSettings.config and add the following :

<?xml version="1.0" encoding="utf-8" ?>
<settings>   

  <content>
    <imaging>
      <!-- what file extension that should cause umbraco to create thumbnails -->
      <imageFileTypes>jpeg,jpg,gif,bmp,png,tiff,tif,svg</imageFileTypes>     
    </imaging>

    ......

  </content>
</settings>

Now, when you drop one or more SVG's on a folder, it will be recognized as an Image.

Monday 8 April 2019

Umbraco Courier - Azure Storage

When you use Umbraco Courier to transfer items from development to production or vice-versa, you may run into a small problem when you store your media in AzureStorage, as is highly recommended.

The problem

When transferring media items with Courier, the data and the blob (= the actual image) are transferred to the target environment, but at the target, Umbraco is saving the blob to the default location on disk, not checking its FileSystemProviders, telling it to transfer the blob to AzureStorage.

The solution

I made a small modification to Hugo's solution on the Umbraco forum, where he raises the issue, then solves it himself.

You'll need to provide 3 EventHandlers.

The first one will run on the sending environment, telling the target to queue a postprocessor for the image.

public class MediaEventTrigger : ApplicationEventHandler
{
  protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
  {
    ExtractionManagerEvents.ExtractedItemResources += ExtractedItemResources; ;
  }

  private void ExtractedItemResources(object sender, ItemEventArgs e)
  {
    var provider = sender as RevisionExtraction;

    //make sure the media event only fires for media items, not other resources
    if (e.Item.ItemId.ProviderId == ItemProviderIds.mediapropertyDataItemProviderGuid)
    {
      var media = e.Item.Dependencies.Single(i => i.ItemId.ProviderId == ItemProviderIds.mediaItemProviderGuid);
      provider.Context.ExecuteEvent("QueueCourierMediaEvent", e.ItemId, new SerializableDictionary<string, string>());
    }
  }
}

The second one will be running at the receiving end. It will queue the third, instructing it to move the blob to Azure Storage.

public class MediaEventListener : ItemEventProvider
{
  public override string Alias => "QueueCourierMediaEvent";

  public override void Execute(ItemIdentifier itemId, SerializableDictionary<string, string> parameters)
  {
    ExecutionContext.QueueEvent("UpdateMediaPath", itemId, parameters, EventManagerSystemQueues.DeploymentComplete);
  }
}

And now for the actual moving of the blob.

public class MediaEventQueueListener : ItemEventProvider
{
  public override string Alias => "UpdateMediaPath";

  public override void Execute(ItemIdentifier itemId, SerializableDictionary<string, string> parameters)
  {
    var fs = FileSystemProviderManager.Current.GetFileSystemProvider<MediaFileSystem>();
    var ms = ApplicationContext.Current.Services.MediaService;

    var media = ms.GetById(Guid.Parse(itemId.Id));
    var oldPath = GetMediaPath(media);
    if (!string.IsNullOrEmpty(oldPath))
    {
      var absolutePath = HttpContext.Current.Server.MapPath(oldPath); //path to file on filesystem
      using (var fileStream = System.IO.File.OpenRead(absolutePath))
      {
        fs.AddFile(oldPath, fileStream); //will be picked up by FileSystemProvider and saved to AzureStorage
      }
     Directory.Delete(Path.GetDirectoryName(absolutePath), true);
     }
  }

  private string GetMediaPath(IMedia media)
  {
    //Image may use ImageCropper (or not), so test presence of Json
    var umbracoFile = media.GetValue<string>(Constants.Conventions.Media.File);
    if (!string.IsNullOrWhiteSpace(umbracoFile))
    {
      if (umbracoFile.StartsWith("{"))
      {
        //the umbracofile property contains the src for the image, which at the beginning is the location of the file on the filesystem.
        var umbracoFileJson = JsonConvert.DeserializeObject<ImageCropDataSet>(umbracoFile);
        return $"~{umbracoFileJson.Src}";
      }
      else
      {
        //Contains the path directly
        return umbracoFile;
      }
    }

    return string.Empty;
  }
}

Shouldn't we rename?

No. Or, not really.

Let's say we are transferring media/1028/flower.jpg from Dev to Prod. The path will remain media/1028/flower.jpg, because that just makes life easy.

You could argue that we need to rename each item's folder with a Guid or something, so it becomes unique, but then you would have to write another handler, parsing and updating all your content, as you'd need to update all media-paths in grid-content referencing your media.

If you always migrate in one direction, this becomes a non-issue, as their will be (almost) no name-collisions on the media folder.

Name-collisions?

Suppose someone manually added tree.jpg to Prod directly.
It is possible that - after transfer from Dev - we now have media/1028/flower.jpg and media/1028/tree.jpg in the same folder on Azure storage. I can live with that.

The only scenario where this becomes a problem is, if you uploaded a different flower.jpg and the blob is overwritten when you transfer the one from Dev.

In my case, this is extremely unlikely, but it's good to at least be aware of this possibility.

Umbraco Courier - Https

This is going to be a short one, but one that took me far too long to figure out.

Install Courier

First off, when you install Courier in your Umbraco, make sure you install the package in each environment. Don't just add the configs and dlls to your source and deploy it. It requires database changes that are only deployed through the package installer.

Configure Courier

I'd suggest giving the documentation a quick run-through. It's rather thorough.

The most important is configuring your repositories (i.e. the environments you want to be able to send data to).

<repository alias="DEV" name="Development" type="CourierWebserviceRepositoryProvider" visible="true">
  <url>https://xxx-dev.azurewebsites.net</url>
  <user>0</user>
</repository>
<repository alias="ACC" name="Acceptance" type="CourierWebserviceRepositoryProvider" visible="true">
  <url>https://xxx-acc.azurewebsites.net</url>
  <user>0</user>
</repository>
<repository alias="PRD" name="Production" type="CourierWebserviceRepositoryProvider" visible="true">
  <url>https://xxx-prd.azurewebsites.net</url>
  <user>0</user>
</repository>

Https not working

Out of the box, post-install, this should just work.

If you're unlucky - like I always am - when trying to use Courier to transfer a few items, you may get

Cannot connect to the destination
"https://xxxx//umbraco/plugins/courier/webservices/repository.asmx"

Take note of the https. There may be other reasons why it's not working for you, but if it works over http and not over https, the solution is simple.

According to Umbraco Support, https is not supported by Courier, but I found that a bit hard to believe. Comparing with other projects - where it WAS working over https - I found the culprit to be the SecurityProtocol.

The only thing you need to do to get this working, is change the default Securityprotocl to TLS1.2 (or higher, if you have this option and are reading this a few years in the future).

Find a startup-class in your project (ContainerInitializer or WebApiConfig or whatever, you'll find something) and add the following line:

ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

Friday 1 February 2019

Winning the Google Hackathon!

On January 16th, I participated (with a lot of my colleagues) in the first and only European Google Mobile Speed Hackathon. We spent the day learning how to improve the speed of some of our projects.
To motivate is further, the winning team would win a small prize and eternal worship of our peers.

Long version of this post : https://www.the-reference.com/en/blog/jeroenvantroyen/2019/google-hackathon

Expectations

I'm used to using PageSpeed Insights. Most of our projects already score in the 80 range. So, to be honest, I didn't anticipate huge improvement in score. 

Boy, was I in for a surprise!

Turns out Google recently changed the scoring system and is now expecting a lot more from us.
The tool we were to measure speed with was Lighthouse, built into Chrome DevTools (F12).

Lighthouse immediately makes a bunch of suggestions on how to improve your score. 

Techniques

If you're interested in the techniques we applied to our project, you can read the technical details here : https://www.the-reference.com/en/blog/jeroenvantroyen/2019/google-hackathon

The result?

The resulting score at the end of the day was very satisfying. We now scored 90% on performance.


Putting theory to practice

After all we'd learned, I was itching to put my newfound knowledge to good use, so I revisited my last project (Skyn) and checked it's initial score and suggestions.


Wait, 17%? AND I forgot to check minimization of Javascript AND css?


With an effort of less than 2 hours, the result is below. The site also feels more snappy than it used to.

When there's more time, I plan to split scripts in a critical and a non-critical bundle, then defer the last one. Split CSS in critical inline and loadCSS the rest.

Play with it!

How about you? Let me know if a few simple tricks, a couple of hours well-spent, can improve your pagespeed. 
Good luck!