Cloudinary Blog

Building creative content on the fly for A/B testing and 1:1 personalization

Build A/B Testing & Personalization creative on the fly

Content Optimization and Personalization programs can deliver tremendous ROI to an organization but tend to be very resource intensive, requiring developers to build the code for alternate experiences and creative folks to generate the content. Many of the content optimization/personalization tools out there today (Maxymiser, Optimizely, Adobe Target, Ensighten etc.) have created WYSIWYG (What You See Is What You Get) editors to help relieve the code/development bottleneck but the creative bottleneck stubbornly remains.

Often you’ll have a great personalization or A/B test idea but can’t execute it because the creative team doesn’t have the time or resources to create what’s needed. Anybody on an optimization/testing/personalization team has dealt with this before and it’s been an intractable problem for years.  

It’s a bit of a catch 22: the more successful you are in your optimization program, the harder and more resource intensive it is going to be to maintain, especially as you get into personalization which requires more and more content catered to the individual user. How does one scale-up such initiatives going forward without hiring an army of creative folks?  

Webinar
How to Optimize for Page Load Speed

Transforming your media to fit your requirements

Often there’s already creative content on your website somewhere or images from past campaigns that you can reuse; it’s only the wrong size, needs rounded corners, or just needs specific text overlayed on top of it. Let’s take Williams-Sonoma’s website (a retailer I love) as an example.

If you go to williams-sonoma.com and scroll down on the homepage you’ll see a section titled “Just for You!”. This section is used for displaying personalized content based on your browsing behavior. If this is your first time at Williams-Sonoma and you’ve yet to browse, you’ll see the default content. At the time of writing, it looks like this:

Williams Sonoma products sample

But if I start navigating the site, Williams Sonoma learns what sort of products I’m interested in and will show me relevant content, making my experience better. I navigated around the “Cutlery” section and now on the homepage I’m presented with the following:

Williams Sonoma products sample

This is very cool, now I can see relevant content with less clutter.

But how does this look from Williams Sonoma’s end? Clearly they have to maintain additional content, about a dozen or so different versions for each main product category. This of course puts an additional strain on both the development and creative teams.

This might be why we only see the dozen or so different versions and then only on one spot on one page. If they wanted to expand their personalization initiatives they would have to weigh the benefits against the additional effort, and while the benefits are linear, the effort becomes exponential.

When I was navigating through the cutlery category to build up a category affinity, I was actually looking at the “Rösle Cheese Knives”. But when I returned to the homepage I was shown “Shun Cutlery” content. Same category (cutlery) but still probably irrelevant to me, perhaps not personalized enough. As we discussed, it’s probably impossible to get to this level of personalization (true 1:1) as it’s just too much effort to create all the resources that would be needed as I’d guess Williams-Sonoma has in the area of 1000 products or more.

Can you imagine what the response would be if you went to your creative team and asked them to create 1000+ different images for each spot you’d like to try personalized content (on top of their normal job)? I’m guessing there would be laughter followed by an invitation to get out of their office.

With Cloudinary we can actually create all of these images dynamically, simply by changing the url of the image, applying custom parameters right in the image URL and building an image on the fly. The following images were created with absolutely zero use of image software and built to mirror the creative style of the images currently on williams-sonoma.com:

Creative content with text overlay

Creating content personalized

Here are the details showing how these images were created:

Ruby:
Copy to clipboard
cl_image_tag("https://s3.amazonaws.com/blog-images-origin/cheese_knives.jpg", :type=>"fetch", :transformation=>[
  {:width=>320, :height=>400, :border=>"1px_solid_gray", :crop=>"fill"},
  {:overlay=>"white-bar", :width=>300, :height=>90, :gravity=>"south", :y=>20},
  {:overlay=>{:font_family=>"Helvetica", :font_size=>14, :text=>"ROSLE%20CHEESE%20KNIVES%20%3E"}, :gravity=>"south", :y=>58}
  ])
PHP v1:
Copy to clipboard
cl_image_tag("https://s3.amazonaws.com/blog-images-origin/cheese_knives.jpg", array("type"=>"fetch", "transformation"=>array(
  array("width"=>320, "height"=>400, "border"=>"1px_solid_gray", "crop"=>"fill"),
  array("overlay"=>"white-bar", "width"=>300, "height"=>90, "gravity"=>"south", "y"=>20),
  array("overlay"=>array("font_family"=>"Helvetica", "font_size"=>14, "text"=>"ROSLE%20CHEESE%20KNIVES%20%3E"), "gravity"=>"south", "y"=>58)
  )))
PHP v2:
Copy to clipboard
(new ImageTag('https://s3.amazonaws.com/blog-images-origin/cheese_knives.jpg'))
  ->border(Border::solid(1, Color::GRAY))
  ->resize(Resize::fill()->width(320)->height(400))
  ->overlay(
      Overlay::source(Source::image('white-bar')
        ->transformation((new ImageTransformation())
          ->resize(Resize::scale()->width(300)->height(90))))
      ->position((new Position())
        ->gravity(Gravity::compass(Compass::south()))
        ->offsetY(20)))
    ->overlay(Overlay::source(Source::text('ROSLE CHEESE KNIVES >', (new TextStyle('Helvetica', 14))))
      ->position((new Position())
        ->gravity(Gravity::compass(Compass::south()))
        ->offsetY(58)))
    ->deliveryType('fetch');
Python:
Copy to clipboard
CloudinaryImage("https://s3.amazonaws.com/blog-images-origin/cheese_knives.jpg").image(type="fetch", transformation=[
  {'width': 320, 'height': 400, 'border': "1px_solid_gray", 'crop': "fill"},
  {'overlay': "white-bar", 'width': 300, 'height': 90, 'gravity': "south", 'y': 20},
  {'overlay': {'font_family': "Helvetica", 'font_size': 14, 'text': "ROSLE%20CHEESE%20KNIVES%20%3E"}, 'gravity': "south", 'y': 58}
  ])
Node.js:
Copy to clipboard
cloudinary.image("https://s3.amazonaws.com/blog-images-origin/cheese_knives.jpg", {type: "fetch", transformation: [
  {width: 320, height: 400, border: "1px_solid_gray", crop: "fill"},
  {overlay: "white-bar", width: 300, height: 90, gravity: "south", y: 20},
  {overlay: {font_family: "Helvetica", font_size: 14, text: "ROSLE%20CHEESE%20KNIVES%20%3E"}, gravity: "south", y: 58}
  ]})
Java:
Copy to clipboard
cloudinary.url().transformation(new Transformation()
  .width(320).height(400).border("1px_solid_gray").crop("fill").chain()
  .overlay(new Layer().publicId("white-bar")).width(300).height(90).gravity("south").y(20).chain()
  .overlay(new TextLayer().fontFamily("Helvetica").fontSize(14).text("ROSLE%20CHEESE%20KNIVES%20%3E")).gravity("south").y(58)).type("fetch").imageTag("https://s3.amazonaws.com/blog-images-origin/cheese_knives.jpg");
JS:
Copy to clipboard
cloudinary.imageTag('https://s3.amazonaws.com/blog-images-origin/cheese_knives.jpg', {type: "fetch", transformation: [
  {width: 320, height: 400, border: "1px_solid_gray", crop: "fill"},
  {overlay: new cloudinary.Layer().publicId("white-bar"), width: 300, height: 90, gravity: "south", y: 20},
  {overlay: new cloudinary.TextLayer().fontFamily("Helvetica").fontSize(14).text("ROSLE%20CHEESE%20KNIVES%20%3E"), gravity: "south", y: 58}
  ]}).toHtml();
jQuery:
Copy to clipboard
$.cloudinary.image("https://s3.amazonaws.com/blog-images-origin/cheese_knives.jpg", {type: "fetch", transformation: [
  {width: 320, height: 400, border: "1px_solid_gray", crop: "fill"},
  {overlay: new cloudinary.Layer().publicId("white-bar"), width: 300, height: 90, gravity: "south", y: 20},
  {overlay: new cloudinary.TextLayer().fontFamily("Helvetica").fontSize(14).text("ROSLE%20CHEESE%20KNIVES%20%3E"), gravity: "south", y: 58}
  ]})
React:
Copy to clipboard
<Image publicId="https://s3.amazonaws.com/blog-images-origin/cheese_knives.jpg" type="fetch">
  <Transformation width="320" height="400" border="1px_solid_gray" crop="fill" />
  <Transformation overlay="white-bar" width="300" height="90" gravity="south" y="20" />
  <Transformation overlay={{fontFamily: "Helvetica", fontSize: 14, text: "ROSLE%20CHEESE%20KNIVES%20%3E"}} gravity="south" y="58" />
</Image>
Vue.js:
Copy to clipboard
<cld-image publicId="https://s3.amazonaws.com/blog-images-origin/cheese_knives.jpg" type="fetch">
  <cld-transformation width="320" height="400" border="1px_solid_gray" crop="fill" />
  <cld-transformation :overlay="white-bar" width="300" height="90" gravity="south" y="20" />
  <cld-transformation :overlay="{fontFamily: 'Helvetica', fontSize: 14, text: 'ROSLE%20CHEESE%20KNIVES%20%3E'}" gravity="south" y="58" />
</cld-image>
Angular:
Copy to clipboard
<cl-image public-id="https://s3.amazonaws.com/blog-images-origin/cheese_knives.jpg" type="fetch">
  <cl-transformation width="320" height="400" border="1px_solid_gray" crop="fill">
  </cl-transformation>
  <cl-transformation overlay="white-bar" width="300" height="90" gravity="south" y="20">
  </cl-transformation>
  <cl-transformation overlay="text:Helvetica_14:ROSLE%20CHEESE%20KNIVES%20%3E" gravity="south" y="58">
  </cl-transformation>
</cl-image>
.NET:
Copy to clipboard
cloudinary.Api.UrlImgUp.Transform(new Transformation()
  .Width(320).Height(400).Border("1px_solid_gray").Crop("fill").Chain()
  .Overlay(new Layer().PublicId("white-bar")).Width(300).Height(90).Gravity("south").Y(20).Chain()
  .Overlay(new TextLayer().FontFamily("Helvetica").FontSize(14).Text("ROSLE%20CHEESE%20KNIVES%20%3E")).Gravity("south").Y(58)).Action("fetch").BuildImageTag("https://s3.amazonaws.com/blog-images-origin/cheese_knives.jpg")
Android:
Copy to clipboard
MediaManager.get().url().transformation(new Transformation()
  .width(320).height(400).border("1px_solid_gray").crop("fill").chain()
  .overlay(new Layer().publicId("white-bar")).width(300).height(90).gravity("south").y(20).chain()
  .overlay(new TextLayer().fontFamily("Helvetica").fontSize(14).text("ROSLE%20CHEESE%20KNIVES%20%3E")).gravity("south").y(58)).type("fetch").generate("https://s3.amazonaws.com/blog-images-origin/cheese_knives.jpg");
iOS:
Copy to clipboard
imageView.cldSetImage(cloudinary.createUrl().setType( "fetch").setTransformation(CLDTransformation()
  .setWidth(320).setHeight(400).setBorder("1px_solid_gray").setCrop("fill").chain()
  .setOverlay("white-bar").setWidth(300).setHeight(90).setGravity("south").setY(20).chain()
  .setOverlay("text:Helvetica_14:ROSLE%20CHEESE%20KNIVES%20%3E").setGravity("south").setY(58)).generate("https://s3.amazonaws.com/blog-images-origin/cheese_knives.jpg")!, cloudinary: cloudinary)
Dynamic text overlay

Ruby:
Copy to clipboard
cl_image_tag("https://s3.amazonaws.com/blog-images-origin/blender.jpg", :type=>"fetch", :transformation=>[
  {:width=>320, :height=>400, :border=>"1px_solid_gray", :crop=>"fill"},
  {:overlay=>"white-bar", :width=>300, :height=>90, :gravity=>"south", :y=>10},
  {:overlay=>{:font_family=>"Helvetica", :font_size=>14, :text=>"VITAMIX%20780%20BLENDER%20%3E"}, :gravity=>"south", :y=>48}
  ])
PHP v1:
Copy to clipboard
cl_image_tag("https://s3.amazonaws.com/blog-images-origin/blender.jpg", array("type"=>"fetch", "transformation"=>array(
  array("width"=>320, "height"=>400, "border"=>"1px_solid_gray", "crop"=>"fill"),
  array("overlay"=>"white-bar", "width"=>300, "height"=>90, "gravity"=>"south", "y"=>10),
  array("overlay"=>array("font_family"=>"Helvetica", "font_size"=>14, "text"=>"VITAMIX%20780%20BLENDER%20%3E"), "gravity"=>"south", "y"=>48)
  )))
PHP v2:
Copy to clipboard
(new ImageTag('https://s3.amazonaws.com/blog-images-origin/blender.jpg'))
  ->border(Border::solid(1, Color::GRAY))
  ->resize(Resize::fill()->width(320)->height(400))
  ->overlay(
      Overlay::source(Source::image('white-bar')
        ->transformation((new ImageTransformation())
          ->resize(Resize::scale()->width(300)->height(90))))
      ->position((new Position())
        ->gravity(Gravity::compass(Compass::south()))
        ->offsetY(10)))
    ->overlay(Overlay::source(Source::text('VITAMIX 780 BLENDER >', (new TextStyle('Helvetica', 14))))
      ->position((new Position())
        ->gravity(Gravity::compass(Compass::south()))
        ->offsetY(48)))
    ->deliveryType('fetch');
Python:
Copy to clipboard
CloudinaryImage("https://s3.amazonaws.com/blog-images-origin/blender.jpg").image(type="fetch", transformation=[
  {'width': 320, 'height': 400, 'border': "1px_solid_gray", 'crop': "fill"},
  {'overlay': "white-bar", 'width': 300, 'height': 90, 'gravity': "south", 'y': 10},
  {'overlay': {'font_family': "Helvetica", 'font_size': 14, 'text': "VITAMIX%20780%20BLENDER%20%3E"}, 'gravity': "south", 'y': 48}
  ])
Node.js:
Copy to clipboard
cloudinary.image("https://s3.amazonaws.com/blog-images-origin/blender.jpg", {type: "fetch", transformation: [
  {width: 320, height: 400, border: "1px_solid_gray", crop: "fill"},
  {overlay: "white-bar", width: 300, height: 90, gravity: "south", y: 10},
  {overlay: {font_family: "Helvetica", font_size: 14, text: "VITAMIX%20780%20BLENDER%20%3E"}, gravity: "south", y: 48}
  ]})
Java:
Copy to clipboard
cloudinary.url().transformation(new Transformation()
  .width(320).height(400).border("1px_solid_gray").crop("fill").chain()
  .overlay(new Layer().publicId("white-bar")).width(300).height(90).gravity("south").y(10).chain()
  .overlay(new TextLayer().fontFamily("Helvetica").fontSize(14).text("VITAMIX%20780%20BLENDER%20%3E")).gravity("south").y(48)).type("fetch").imageTag("https://s3.amazonaws.com/blog-images-origin/blender.jpg");
JS:
Copy to clipboard
cloudinary.imageTag('https://s3.amazonaws.com/blog-images-origin/blender.jpg', {type: "fetch", transformation: [
  {width: 320, height: 400, border: "1px_solid_gray", crop: "fill"},
  {overlay: new cloudinary.Layer().publicId("white-bar"), width: 300, height: 90, gravity: "south", y: 10},
  {overlay: new cloudinary.TextLayer().fontFamily("Helvetica").fontSize(14).text("VITAMIX%20780%20BLENDER%20%3E"), gravity: "south", y: 48}
  ]}).toHtml();
jQuery:
Copy to clipboard
$.cloudinary.image("https://s3.amazonaws.com/blog-images-origin/blender.jpg", {type: "fetch", transformation: [
  {width: 320, height: 400, border: "1px_solid_gray", crop: "fill"},
  {overlay: new cloudinary.Layer().publicId("white-bar"), width: 300, height: 90, gravity: "south", y: 10},
  {overlay: new cloudinary.TextLayer().fontFamily("Helvetica").fontSize(14).text("VITAMIX%20780%20BLENDER%20%3E"), gravity: "south", y: 48}
  ]})
React:
Copy to clipboard
<Image publicId="https://s3.amazonaws.com/blog-images-origin/blender.jpg" type="fetch">
  <Transformation width="320" height="400" border="1px_solid_gray" crop="fill" />
  <Transformation overlay="white-bar" width="300" height="90" gravity="south" y="10" />
  <Transformation overlay={{fontFamily: "Helvetica", fontSize: 14, text: "VITAMIX%20780%20BLENDER%20%3E"}} gravity="south" y="48" />
</Image>
Vue.js:
Copy to clipboard
<cld-image publicId="https://s3.amazonaws.com/blog-images-origin/blender.jpg" type="fetch">
  <cld-transformation width="320" height="400" border="1px_solid_gray" crop="fill" />
  <cld-transformation :overlay="white-bar" width="300" height="90" gravity="south" y="10" />
  <cld-transformation :overlay="{fontFamily: 'Helvetica', fontSize: 14, text: 'VITAMIX%20780%20BLENDER%20%3E'}" gravity="south" y="48" />
</cld-image>
Angular:
Copy to clipboard
<cl-image public-id="https://s3.amazonaws.com/blog-images-origin/blender.jpg" type="fetch">
  <cl-transformation width="320" height="400" border="1px_solid_gray" crop="fill">
  </cl-transformation>
  <cl-transformation overlay="white-bar" width="300" height="90" gravity="south" y="10">
  </cl-transformation>
  <cl-transformation overlay="text:Helvetica_14:VITAMIX%20780%20BLENDER%20%3E" gravity="south" y="48">
  </cl-transformation>
</cl-image>
.NET:
Copy to clipboard
cloudinary.Api.UrlImgUp.Transform(new Transformation()
  .Width(320).Height(400).Border("1px_solid_gray").Crop("fill").Chain()
  .Overlay(new Layer().PublicId("white-bar")).Width(300).Height(90).Gravity("south").Y(10).Chain()
  .Overlay(new TextLayer().FontFamily("Helvetica").FontSize(14).Text("VITAMIX%20780%20BLENDER%20%3E")).Gravity("south").Y(48)).Action("fetch").BuildImageTag("https://s3.amazonaws.com/blog-images-origin/blender.jpg")
Android:
Copy to clipboard
MediaManager.get().url().transformation(new Transformation()
  .width(320).height(400).border("1px_solid_gray").crop("fill").chain()
  .overlay(new Layer().publicId("white-bar")).width(300).height(90).gravity("south").y(10).chain()
  .overlay(new TextLayer().fontFamily("Helvetica").fontSize(14).text("VITAMIX%20780%20BLENDER%20%3E")).gravity("south").y(48)).type("fetch").generate("https://s3.amazonaws.com/blog-images-origin/blender.jpg");
iOS:
Copy to clipboard
imageView.cldSetImage(cloudinary.createUrl().setType( "fetch").setTransformation(CLDTransformation()
  .setWidth(320).setHeight(400).setBorder("1px_solid_gray").setCrop("fill").chain()
  .setOverlay("white-bar").setWidth(300).setHeight(90).setGravity("south").setY(10).chain()
  .setOverlay("text:Helvetica_14:VITAMIX%20780%20BLENDER%20%3E").setGravity("south").setY(48)).generate("https://s3.amazonaws.com/blog-images-origin/blender.jpg")!, cloudinary: cloudinary)
Alternative text overlay

Let’s take a look at what’s going on here a bit more closely. There are really two key parts to the URL. What the background image should be, denoted by

'https://res.cloudinary.com/demo/image/fetch/https://s3.amazonaws.com/blog-images-origin/cheese_knives.jpg'.

And what the text block should say, denoted by 'l_text:Helvetica_14:ROSLE CHEESE KNIVES '.

All the other parameters define the size of the image, the text style, and things like that.

You can read up about all of our on the fly image (and video!) transformations in our documentation.

You can play with this yourself, use this widget to build your own images!

Generated Image URL:

Changing only those two parameters in the image url allows you to build an image for ANY product (or banner/hero image/whatever you can dream up) on the fly. The only thing you need to do is track the last product (or page/category/article/etc) your visitor viewed and dynamically replace the values in the image URL. This is simple enough to do with any Optimization tool (Maxymiser, Optimizely, Adobe Target), and even some Tag Management Systems (Ensighten, Adobe DTM).

We’re truly talking about 1:1 personalization here with only a single URL. Also, I don’t even have to go through the work of uploading my images, as Cloudinary can automatically pull in your current images, transform them as you see fit, and place them out across the CDN using our 'fetch' functionality used in the examples above.

The same ideas can be applied to other areas as well: site banners, creative emails, display ads, as well as managing all of your site’s assets (which we do for thousands of customers). There is also a WYSIWYG image editor in the UI:

Media library

Cloudinary automatically places all images on a worldwide CDN for extremely fast performance, alongside image quality optimization and dynamic image formatting to reduce bandwidth and speed up your site’s load time.

Conclusion

Using Cloudinary for your A/B Testing and Personalization program can help you reduce your exposure to creative bottlenecks, accelerate the number of tests you can do, and open up new possibilities. With Cloudinary you can dynamically resize images and videos, overlay custom text and other images/videos on top of them, add filters, change colors, create on the fly banners, and much more. In order to test this out for yourself, sign up for a free account here.

Recent Blog Posts

Automatically Translating Videos for an International Audience

No matter your business focus—public service, B2B integration, recruitment—multimedia, in particular video, is remarkably effective in communicating with the audience. Before, making video accessible to diverse viewers involved tasks galore, such as eliciting the service of production studios to manually dub, transcribe, and add subtitles. Those operations were costly and slow, especially for globally destined content.

Read more
Cloudinary Helps Minted Manage Its Image-Generation Pipeline at Scale

David first shared his thoughts at our ImageCon coverence last October and this case study is an abbreviated version of Minted’s success using Cloudinary.

Over time, Faithful renderings of the creations of the illustrators, textile designers, painters, packaging designers, marketers, and stay-at-home moms, all of whom are core contributors of the Minted world, was getting harder and harder. Legacy technology wasn’t cutting it any more—and it was time for Cloudinary to step in.

Read more
Highlights on ImageCon 2021 and a Preview of ImageCon 2022

New year, same trend! Visual media will continue to play a monumental role in driving online conversions. To keep up with visual-experience trends and best practices, Cloudinary holds an annual conference called ImageCon, a one-of-a-kind event that helps attendees create the most engaging visual experiences possible.

Read more

New for DAM: Media Library Extension for Chrome

By Sharon Yelenik
A New Media Library Chrome Extension for Cloudinary DAM

With the introduction of the Media Library Extension, a Chrome-browser add-on that streamlines the access to, search for, and management of images and videos, Cloudinary offers yet another effective tool for its Digital Asset Management (DAM) solution. Let’s have a look at how most teams are currently working with media assets and how the new add-on not only boosts efficiency, but also renders the process a pleasure to work with.

Read more
New Features Supercharge Cloudinary’s Digital Asset Management Solution.

Today, I’m thrilled to announce the launch of Apps for Digital Asset Management and a Media Library Extension for the Chrome browser, which enables easy, flexible integration with all web-based applications in addition to making asset discovery more robust and accessible to all.

Read more
Scale and Automate Workflows With Modern Digital Asset Management Systems

With building, growing, and maintaining a strong digital presence being a top priority for all brands, high-quality visual content is paramount. In fact, consumers are 40 times more likely to share visual content on social networks than on other forums. Plus, a recent study from Wyzowl found that 84% of consumers made purchase decisions after watching a video, which explains why many brands are adding more and more visual media to their sites.

Read more