By combining recommendations and ads into the same widget, a publisher can avoid the user completely overlooking blocks of ads, and in most cases get a higher CTR for the ads because they are actually in the view of the user, as he browses down the list of article recommendations.
This code example is based on the approach decribed in the Cxense Content Integration Guide .
Produces an widget with output like this:
Tag (in a test page):
recs_and_ads_mockup.html
<!DOCTYPE HTML>
<html>
<head>
<title>Recs &: Ads widget test page</title>
<meta charset='utf-8'>
</head>
<body>
<!-- Recs&ads widget start -->
<!-- Recs will be inserted just before this div -->
<div id="recsAndAdsTarget" style="display:none"></div>
<script type="text/javascript">
var cX = cX || {}; cX.callQueue = cX.callQueue || [];
cX.callQueue.push(['insertWidget',{
width: 305, height: 840, // The initial size
resizeToContentSize: true, // Allow size override from template, in pixels
insertBeforeElementId: 'recsAndAdsTarget',
renderTemplateUrl: 'recs_and_ads_template.html'
}]);
</script>
<!-- Async load cx.js -->
<script type="text/javascript">
(function() { try { var scriptEl = document.createElement('script'); scriptEl.type = 'text/javascript'; scriptEl.async = 'async';
scriptEl.src = ('https:' == document.location.protocol) ? 'https://scdn.cxense.com/cx.js' : 'http://cdn.cxense.com/cx.js';
var targetEl = document.getElementsByTagName('script')[0]; targetEl.parentNode.insertBefore(scriptEl, targetEl); } catch (e) {};} ());
</script>
<!-- Recs&ads widget end -->
</body>
</html>
The content-widget configuration needs at least this resultFields to make the following example work:
"resultFields": [
"url",
"title", <== or "og-title"
"description",
"dominantthumbnail", <== or "thumbnail"
"dominantthumbnaildimensions",
],
Render template ('recs_and_ads_template.html'):
(replace all "TODOs" with your own identifiers)
The main flow of this code:
Setup and send the insertAdSpace() request to Cxense Advertising
Setup and send the insertWidget() request to Cxense Content (in parallel)
Wait for both requests to have completed
Render the content by iterating over the two result sets, selecting an ad at the positions 4, 7 and 10.
recs_and_ads_template.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Recs & Ads widget</title>
<style type="text/css" media="all">
html, body, div { margin: 0; padding: 0; border: 0; outline: 0; }
.recsHeading { margin-bottom: 6px; height: 20px; padding-top: 5px; padding-bottom: 3px; padding-left:5px; color: #000000;
text-transform: uppercase;
background-image: url("http://cdn.cxense.com/images/bg.jpg"); background-repeat: repeat-x;
font-family:'PT Sans Narrow','Tahoma','Arial',sans-serif;font-size:18px;font-weight:700;
}
.recsItem { min-height: 91px; border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #cacaca; padding-top:10px;}
.recsText { margin-left: 90px; font-size: 12px; line-height: 14px; font-weight: normal; font-family: 'Arial',sans-serif;margin-bottom:10px;}
.recsImageContainer { float: left; margin: 0px 5px 5px 0px; background-color: #f0f0f0; /* border: 1px solid #ccc; */ }
.recsTitle { color: #333333; font-size: 16px; line-height: 16px; font-weight: bold; font-family: 'PT Sans','Arial',sans-serif; text-decoration: none; }
.recsTeaser { font-size: 12px; line-height: 15px; font-weight: normal; font-family: 'Arial',sans-serif; color: #000; }
.poweredBy { text-align: right; }
.poweredBy a { font-size: 9px; font-family: Arial,Verdana,Tahoma,sans-serif; color: #777777; }
a, a:visited, a:active { text-decoration: none; }
a:hover { text-decoration:underline; color:#5F6F7F; }
.ad { background-color: #f0f0f0; }
.recs { background-color: White; }
.advertisement { font-size: 8px; text-transform: uppercase; color: #b0b0b0; text-align: right; margin: 2px; font-family: 'Arial',sans-serif; }
.recsText br{line-height:20px}
.template { display: none; }
</style>
</head>
<body>
<div id="widgetElement">
<!-- Include our cx.js script -->
<script type="text/javascript" src="http://cdn.cxense.com/cx.js"></script>
<div class="recsHeading">Also of interest:</div>
<div id="targetElement">
<!-- Template (invisible) -->
<div id="resultsTemplate" class="template">
<!--%
var adIdx = 0;
var recsIdx = 0;
for (var i = 0; i < 12; i++) {
var itemClass = 'recsItem recs';
var imageSrc = '';
var imageDimensions = '0x0';
var title = '';
var teaser = '';
var isAd = false;
if (i === 4 || i === 7 || i=== 10) {
//
// Ads at these positions
//
isAd = true;
itemClass = 'recsItem ad';
if (adData.searchResult.spaces && adIdx >= adData.searchResult.spaces[0].ads.length) {
continue;
}
var ads = adData.searchResult.spaces[0].ads;
var ad = ads[adIdx];
if (ad && ad.creative && ad.creative.content && ad.creative.content.length && ad.creative.images && ad.creative.images.length) {
var displayUrlText = '';
var trackingImageSrc = null;
var content = ad.creative.content;
for (var j = 0; j < content.length; j++) {
var contentItem = content[j];
if (contentItem.key.toLowerCase() === 'title')
title = cX.library.getAllText(contentItem.value);
if (contentItem.key.toLowerCase() === 'content')
teaser = cX.library.getAllText(contentItem.value);
if (contentItem.key.toLowerCase() === 'display url')
displayUrlText = cX.library.getAllText(contentItem.value);
}
// get the image
var images = ad.creative.images;
var imageSrc = '';
for (var k = 0; k < images.length; k++) {
var image = images[k];
if (image.key.toLowerCase() === 'rectangle image') {
imageSrc = image.source;
imageDimensions = '' + image.width + 'x' + image.height;
}
}
var clickUrl = ad.clickUrl;
}
adIdx ++;
} else {
//
// Recs at all other positions
//
if (recsIdx >= recsData.response.items.length) {
continue;
}
var item = recsData.response.items[recsIdx];
//
// populate the fields for rendering
//
var teaser = item.description || '';
if (teaser.length > 100) {
teaser = teaser.substr(0,98) + '...';
}
var title = item['og-title'] || item.title || "No title";
// try first the dominantthumbnail and fallback to thumbnail
if (item.dominantthumbnaildimensions && item.dominantthumbnail) {
imageSrc = item.dominantthumbnail;
imageDimensions = item.dominantthumbnaildimensions;
} else {
imageSrc = item.thumbnail;
imageDimensions = '200x150';
}
var displayUrlText = item.url;
var clickUrl = item.click_url;
// check next one
recsIdx ++;
}
if (title && title.length > 0) {
%-->
<div tmp:class="{{itemClass}}">
<!--% if (imageSrc) { %-->
<a target="_top" tmp:id="{{cX.clickTracker(clickUrl)}}" tmp:href="{{displayUrlText}}">
<!--%
// Cxense image scaling support will generate the img-tag within a div container
cX.renderContainedImage({
container: { width: 80, height: 60, cssClass: 'recsImageContainer' },
image: { src: imageSrc, dimensions: imageDimensions }
});
%-->
</a>
<!--% } %-->
<div class="recsText">
<a target="_top" class="recsTitle" tmp:href="{{cX.clickTracker(clickUrl)}}">{{title}}</a><br />
<a target="_top" class="recsTeaser" tmp:id="{{cX.clickTracker(clickUrl)}}" tmp:href="{{displayUrlText}}">{{teaser}}</a><br />
<!--% if (isAd) { %-->
<div class="advertisement">Advertisement</div>
<!--% } %-->
</div>
</div>
<!--%
}
} %-->
</div>
</div>
<div class="poweredBy"><a href="http://www.cxense.com/" target="_blank">Powered by cXense</a></div>
<!-- Callback to parent window to resize this IFRAME now that ad content metrics are known: -->
<script type="text/javascript">
function sendParentResizeMessage(context) {
if (context && context.parentElementId) {
if (parent && parent.postMessage) {
// Measure content width and height
var contentWidth = 305;
var contentHeight = 840;
try {
var adsDivEl = document.getElementById('widgetElement');
contentWidth = adsDivEl.clientWidth ? adsDivEl.clientWidth : adsDivEl.offsetWidth;
contentHeight = adsDivEl.clientHeight ? adsDivEl.clientHeight : adsDivEl.offsetHeight;
} catch (e) {
contentWidth = 305;
contentHeight = 840;
}
// Add the search result count
var parentElementId = context.parentElementId;
var message = 'method=updateAdSpace'
+ '&contentWidth=' + contentWidth
+ '&contentHeight=' + contentHeight
+ '&elementId=' + encodeURIComponent(parentElementId);
// It's safe to use '*' as targetOrigin here, because the message content is not secret.
window.parent.postMessage(message, '*');
}
}
}
var myHashArguments = cX.library.parseHashArgs();
var adData;
var recsData;
function renderWidget() {
cX.library.renderTemplate('resultsTemplate', 'targetElement');
var context = myHashArguments;
sendParentResizeMessage(myHashArguments );
}
//
// Ads renderFunction
//
function onGotAdData(data, context) {
adData = data;
if (typeof recsData !== 'undefined') {
renderWidget();
}
}
//
// Recs renderFunction
//
function onGotRecsData(data) {
recsData = data;
if (typeof adData !== 'undefined') {
renderWidget();
}
}
// Ads setup
cX.library.insertAdSpace({
adSpaceId: '1234', // TODO: <-- Insert your Cxense Advertising adspace id here!
backend:'production',
forwardHashArgs: true,
renderFunction: onGotAdData
});
//
// Recs setup based on new API style, skipped all the legacy request setup
// this is a direct call
//
cX.library.insertWidget({
widgetId: '4567', // TODO: <-- Insert your Cxense Content widget id here!
renderFunction: onGotRecsData
});
</script>
</div>
</body>
</html>
Last updated: December 28, 2021