Adaptive Images for Optimizely

Developer documentation

See the web editor documentation on how to use the add-on in edit mode.

Introduction

This add-on adds two new image property types: SingleImage and AdaptiveImage.

SingleImage allows for a single image, while AdaptiveImage allows for different images for three different form factors, i.e. screen sizes.

Developers define image constraints, such as minimum size and proportions, and web editors can easily crop images without risk of violating those constraints.

Supports images uploaded in Optimizely as well as images from digital asset management (DAM) systems via image providers.

Note: Additional plugins are available, such as the AdaptiveImages.Cloudinary package for image optimization, AI cropping, and CDN delivery, and the AdaptiveImages.Unsplash package for making millions of Unsplash images searchable from within the Optimizely UI.

Installation

Add a single image property

public virtual SingleImage MyImage { get; set; }

Setting minimum size

The following ensures the final image (original or cropped) is at least 1920x1080 px:

[Size(1920, 1080)]
public virtual SingleImage MyImage { get; set; }

Setting proportions

The following ensures the final image (original or cropped) is in widescreen (16:9) format:

[Proportions(16, 9)]
public virtual SingleImage MyImage { get; set; }

Defining multiple formats

You can add multiple Proportions attributes to give editors a list of formats to choose from:

[Proportions(4, 3, Name = "Portrait")]
[Proportions(16, 9, Name = "Widescreen", IsDefault = true)]
[Proportions(1, 1, Name = "Square")]
public virtual SingleImage MyImage { get; set; }

Combining constraints

The following allows both widescreen and square images, but regardless of proportions the final image must be at least 720 px wide:

[Size(720)]
[Proportions(16, 9)]
[Proportions(1, 1)]
public virtual SingleImage MyImage { get; set; }

Add an adaptive image property

public virtual AdaptiveImage HomePageHero { get; set; }

Set minimum image size for one or more form factors

[Size(1024, 768, FormFactor.Small | FormFactor.Medium)] // Mobile and tablet images must be at least 1024x768 pixels
[Size(1920, 1080, FormFactor.Large)] // Desktop image must be at least 1920x1080 pixels
public virtual AdaptiveImage HomePageHero { get; set; }

Set proportion constraints for one or more form factors

[Proportions(16, 9, FormFactor.Large)]  // Desktop image should have widescreen proportions
[Proportions(1, 1, FormFactor.Small | FormFactor.Medium)]  // Square images for mobile and tablet
public virtual AdaptiveImage HomePageHero { get; set; }

Customize form factor display names

The default display names are "Desktop", "Tablet", and "Mobile" for the Large, Medium, and Small form factors, respectively.

You can customize form factor display names on a per-property basis by using the DisplayNames attribute:

[DisplayNames(Large = "Widescreen", Medium = "Standard", Small = "Square")]
public virtual AdaptiveImage MyAdaptiveImage { get; set; }

You can also use localization keys for translated display names:

[DisplayNames(Large = "/some/translation")]
public virtual AdaptiveImage MyAdaptiveImage { get; set; }

You can also change and/or translate form factor display names site-wide by adding appropriate translations, for example through an XML language file like the following:

<?xml version="1.0" encoding="utf-8" ?>
<languages>
    <language name="English" id="en">
        <ContentTypes>
            <AdaptiveImage>
                <properties>
                    <Large>
                        <caption>Desktop</caption>
                    </Large>
                    <Medium>
                        <caption>Tablet</caption>
                    </Medium>
                    <Small>
                        <caption>Mobile</caption>
                    </Small>
                </properties>
            </AdaptiveImage>
        </ContentTypes>
    </language>
</languages>

Rendering adaptive images

Default rendering

This will render the image(s) using default breakpoints, automatically cropped according to any constraints:

@Html.PropertyFor(x => x.HomePageHero)

See the Configuration section on how to change the default breakpoints and other settings.

Override breakpoints, image widths, and other default rendering settings

You can specify desired image widths and/or breakpoints, if different from the defaults:

@Html.PropertyFor(m => m.HomePageHero, new { 
    largeBreakpoint = 1280, // Desktop breakpoint
    mediumBreakpoint = 768, // Tablet breakpoint (767 and below is considered mobile)
    largeWidth = 1920, // Desktop image width
    mediumWidth = 1279, // Tablet image width
    smallWidth = 500, // Mobile image width
    ignoreSizeConstraints = true, // Do not render according to size constraints by default (not applicable when widths are specified)
    altText = "An adaptive image", // Overrides any alt text specified by web editor
    cssClass = "hero-image", // CSS class applied to the picture element
    lazyLoad = true, // Image should be lazy-loaded by browser
    quality = 75, // Use explicit quality value (0-100) for all form factors (default is auto)
    smallQuality = 100 // Use maximum quality for the small form factor only
})

Apply image transformations

For other custom rendering scenarios, apply transformations using a fluent syntax:

// Get the large ("desktop") image, cropped according to constraints and resized to a width of 1280 pixels
var largeImage = currentContent.GetImageRenderSettings(x => x.HomePageHero.Large)
                               .ResizeToWidth(1280);
// Create a 300x300 square version of the medium ("tablet") image, overriding any proportions constraints
var squareImage = currentContent.GetImageRenderSettings(x => x.HomePageHero.Medium)
                                .Crop(new Proportions(1, 1))
                                .ResizeToWidth(300);

Note: When retrieving render settings for an AdaptiveImage form factor or a SingleImage property, the expression needs to include the parent instance (e.g. the page or block that defines the property) in order to be able to resolve any constraints (since that's where the attributes are).

You can also create an ImageRenderSettings instance manually to circumvent any image constraints, for example to render an image with its original proportions, regardless of constraints:

// Create a 300px wide image with its original proportions preserved, ignoring any proportions constraints
var imageWithoutConstraints = new ImageRenderSettings(currentContent.HomePageHero.Large).ResizeToWidth(300);

Render a specific image

You can render an <img> element based on above render settings:

@Html.RenderSingleImage(squareImage)

Get image URLs

If you need to get the final URL of an image, for example to set a background image, first apply any transformations and then call GetUrl():

@{
// Get the large ("desktop") image scaled to a width of 1920 pixels
var backgroundImageUrl = startPage.GetImageRenderSettings(x => x.HomePageHero.Large)
                                  .ResizeToWidth(1920)
                                  .GetUrl();
}

<div style="background-image: url(@backgroundImageUrl)"></div>

Resolving property image constraints and settings

When creating a view with a model type of AdaptiveImage, size and proportion constraints are not accessible as they're defined through attributes on the parent content, i.e. block or page, instance. However, constraints and crop settings can be retrieved like:

// "currentContent" below is a page or block instance with an AdaptiveImage property:

// Get all applicable constraints for all form factors
var imageConstraints = currentContent.GetImageConstraints(x => x.HomePageHero);

// Get crop settings for the large, i.e. "desktop", image
var largeImageCropSettings = currentContent.GetCropSettings(x => x.HomePageHero.Large);

Make images required

To make a property required, use the RequiredImage property:

[RequiredImage]
public virtual AdaptiveImage Logotype { get; set; }

To only make specific form factors required, specify the FormFactor attribute property (not applicable for SingleImage properties):

[RequiredImage(FormFactor = FormFactor.Large | FormFactor.Medium)]
public virtual AdaptiveImage Logotype { get; set; }

To also make the image description (i.e. alt text) required, you set the AlternateText property on the attribute:

[RequiredImage(AlternateText = true)]
public virtual AdaptiveImage Logotype { get; set; }

Make images localizable

To make a property culture-specific, use the CultureSpecificImage property:

[CultureSpecificImage]
public virtual AdaptiveImage Banner { get; set; }

You can choose to only make images or alternate text localizable by specifying the Images and/or AlternateText attribute properties. For example to use the same images for all languages but be able to translate the description:

[CultureSpecificImage(Images = false, AlternateText = true)]
public virtual AdaptiveImage Banner { get; set; }

Rendering single images

Default rendering

This will render a SingleImage property, automatically cropped according to any constraints:

@Html.PropertyFor(x => x.OpenGraphImage)

You can optionally specify desired image width and CSS classes:

@Html.PropertyFor(m => m.OpenGraphImage, new { 
    width = 1920,
    cssClass = "og-image" // CSS class applied to the img element
    altText = "A single image", // Overrides any alt text specified by web editor
})

PropertyList, or IList<T>, properties

There are a few simple steps to follow to implement PropertyList properties where the item type contains one or more image properties.

1. Create your item type

If your item type should have just one image property, you can inherit AdaptiveImagePropertyListItem or SingleImagePropertyListItem.

You can then override the Image property, for example to apply size and/or proportions constraint attributes.

If you prefer to create your own POCO type from scratch, you need to ensure you add JSON attributes like so:

public class MyListItem
{
    // Adaptive image property
    [JsonProperty]
    [JsonConverter(typeof(AdaptiveImageConverter))]
    public virtual AdaptiveImage MyAdaptiveImage { get; set; }
    
    // Single image property
    [JsonProperty]
    [JsonConverter(typeof(SingleImageConverter))]
    public virtual SingleImage MySingleImage { get; set; }
}

2. Create IList<T> property definition

[PropertyDefinitionTypePlugIn]
public class MyListProperty : ImagePropertyList<MyListItem>
{
}

3. Create an editor descriptor for the property type

[EditorDescriptorRegistration(TargetType = typeof(IList<MyListItem>), EditorDescriptorBehavior = EditorDescriptorBehavior.OverrideDefault)]
public class MyListItemEditorDescriptor : ImagePropertyListEditorDescriptor<MyListItem>
{
}

4. Add property to your content type

public virtual IList<MyListItem> Items { get; set; }

Configuration and settings

Add-on settings

Global add-on settings, such as default breakpoints, can be configured by passing an AddonSettings instance when initialization:

services.AddAdaptiveImages(new AddonSettings 
{
    // Default breakpoints unless specified through rendering arguments
    LargeBreakpoint = 1200,
    MediumBreakpoint = 800,

    // Default rendering widths unless specified through rendering arguments (defaults to breakpoint widths if not specified)
    LargeWidth = 1170,
    MediumWidth = 940,
    SmallWidth = 727,

    // The default maximum width of rendered images
    MaxWidth = 2560 
});

Note: If using very large original images, it might be sensible to reduce the maximum image size rendered in the crop dialog to improve UI performance. This is done by setting the MaxCropDialogImageWidth property on the AddonSettings instance. This will not affect the actual rendering on the website.

TinyMCE settings

TinyMCE settings, such as behavior when images are drag-and-dropped from Media, are configured through ITinyMceAddonSettings.

You can either pass an ITinyMceAddonSettings instance to AddAdaptiveImages() during startup...

services.AddCms()
        .AddAdaptiveImages(new AddonSettings(), new TinyMceAddonSettings
        {
            // Specify behavior when image is drag-and-dropped to TinyMCE
            ImageConversionBehavior = ImageConversionBehavior.AlwaysConvert
        });                

...or make changes to the current ITinyMceAddonSettings instance:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // Change TinyMCE settings if Adaptive Images has been enabled
    if (AddonSettings.Enabled)
    {
        var tinyMceSettings = app.ApplicationServices.GetService<ITinyMceAddonSettings>();

        var contentTypeRepository = app.ApplicationServices.GetService<IContentTypeRepository>();

        // Use custom block type for wrapping adaptive images in TinyMCE
        tinyMceSettings.BlockContentTypeId = contentTypeRepository.Load(typeof(CustomTinyMceAdaptiveImageBlock)).ID;
    }
}
Note: If specifying a custom block type, it needs to implement ITinyMceAdaptiveImage.

Web app configuration

You should set an appropriate maximum content length to allow large enough requests if you're using one or more image providers supporting uploads:

<location path="AdaptiveImages">
    <!-- Max upload file size 10 MB-->
    <system.webServer>
        <security>
            <requestFiltering>
                <requestLimits maxAllowedContentLength="10485760" /><!-- Note: bytes-->
            </requestFiltering>
        </security>
    </system.webServer>
    <system.web>
        <httpRuntime maxRequestLength="10240" /><!-- Note: KB -->
    </system.web>
</location>

Migrating Optimizely image properties

If you're adding the Adaptive Images add-on to an existing Optimizely website you may want to migrate existing image properties to AdaptiveImage or SingleImage properties.

One way to do this is to create a SingleImage instance by passing a ContentReference to the constructor:

// Content reference to ImageData content with ID 123
var contentReference = new ContentReference(123);

var optimizelyImage = new SingleImage(contentReference); 

// Assign to SingleImage property
optimizelyImage.AssignTo(currentPage.MySingleImage);

// Assign to AdaptiveImage property
optimizelyImage.AssignTo(currentPage.MyAdaptiveImage);
            

Note: When saving the content, you may need to use SaveAction.SkipValidation if your image properties have size or proportions constraint attributes which the original Optimizely image violates.

If you do skip validation, you may want to consider setting LaxValidationOfPublishedImages to treat validation errors as warnings for unmodified image properties:

services.AddAdaptiveImages(new AddonSettings { 
    LaxValidationOfPublishedImages = true
});

Content Delivery API support

This add-on is designed to work out-of-the-box with Optimizely's Content Delivery API.

However, you may want to include additional data such as image URLs and constraints in the API payload.

You'll need one custom PropertyModel for AdaptiveImage properties and one for SingleImage properties to get ImageConstraints included in the serialized data from the API.

Creating a custom property model to include constraints in serialized JSON data

Note: The code below is a proof-of-concept for AdaptiveImage properties and is merely intended as guidance.
public class AdaptiveImageConverter : PropertyModel<AdaptiveImage, PropertyBlock<AdaptiveImage>>
{
    // The image constraints to include in serialized JSON
    public ImageConstraints ImageConstraints { get; set; }

    public AdaptiveImageConverter(PropertyBlock<AdaptiveImage> adaptiveImage) : base(adaptiveImage)
    {
        if (adaptiveImage.Block.IsSet())
        {
            var parentLink = adaptiveImage.Parent["ContentLink"]?.Value ?? adaptiveImage.Parent["PageLink"]?.Value;

            if (parentLink is ContentReference parentContentLink)
            {
                var parentContent = ServiceLocator.Current.GetInstance<IContentLoader>().Get<IContent>(parentContentLink);

                ImageConstraints = parentContent.GetImageConstraints(adaptiveImage.Name);
            }
            else // Property exists on local block
            {
                // Get the property definition of the local block property
                var propertyDefinitionRepository = ServiceLocator.Current.GetInstance<IPropertyDefinitionRepository>();

                var propertyDefinition = propertyDefinitionRepository.Load(adaptiveImage.PropertyDefinitionID);

                // Get the content type (i.e. local block type) associated with the property definition
                var contentTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository>();

                var contentType = contentTypeRepository.Load(propertyDefinition.ContentTypeID);

                // Get constraints from the AdaptiveImage property attributes
                ImageConstraints = contentType.ModelType.GetProperty(adaptiveImage.Name).GetImageConstraints();
            }
        }
    }
}

Image providers

The add-on supports multiple image providers.

There are ready-made image providers for some digital asset management (DAM) systems. Please contact us if you want to integrate an existing DAM or looking for a suitable one.

Add/remove image providers

Image providers are types implenting the IImageProvider interface.

To add an image provider:

var customProvider = new MyCustomProvider();

ImageProviderFactory.Instance.Register(customProvider);

To remove an image provider:

ImageProviderFactory.Instance.Remove(customProvider.Name);

The user interface lists image providers in the order they appear in ImageProviderFactory:

Dropdowns are displayed for the selected image provider's options. For hierarchical options, multiple dropdowns are displayed.

Note: Only image providers with at least one searchable option (ImageProviderOptionCapability.Search capability) are displayed.

Security

The add-on uses fixed group names for authorization of REST store endpoints etc. Regular users must belong to either the standard CmsEditors virtual role, or a role called AdaptiveImagesEditors. Administrator plugins require users to belong to either the standard CmsAdmins role, or a role called AdaptiveImagesAdmins.

Troubleshooting

To troubleshoot errors in the Optimizely UI, enable UI debugging for more verbose console logging:

public void ConfigureServices(IServiceCollection services)
{
    if (_webHostingEnvironment.IsDevelopment())
    {
        services.Configure<ClientResourceOptions>(x => x.Debug = true);
    }   
}