<%
/**
* Branded Products - Backend (SSR) Partial
*
* DATA FLOW (Hybrid Format):
* ─────────────────────────────────────────────────────────────────────────────
* 1. Ad Server provides: bannerImage (brand logo) + offers[].product_id
* 2. Shop fetches: product details (image, price, name, URL) for each product_id
* 3. Shop renders: combined data (logo from ad server + product details from shop)
*
* For this demo, the server.js fetches from bidder and passes adData here.
* In real implementation, shop would fetch product details from its database.
* ─────────────────────────────────────────────────────────────────────────────
*
* Expected adData structure:
* {
* // FROM AD SERVER (never mutated):
* bannerImage: string, // Brand logo URL
* urlRedirect: string|null, // Brand logo click destination URL (optional)
* metaAdclick: string, // Base URL for click tracking
* offers: Array<{ // Original offers from ad server
* product_id,
* offer_url, // Click tracking parameter (append to metaAdclick)
* offer_image, offer_name, offer_price // Fallback display data
* }>,
*
* // FROM SHOP (separate from offers, keyed by product_id):
* shopDataMap: {
* [productId]: {
* url: string, // Shop's product URL (destination)
* name: string, // Product name from shop catalog
* image: string, // Product image from shop catalog
* price: number // Product price from shop catalog
* }
* },
*
* // TRACKING:
* dsaInfo: { advertiser, payer }, // DSA transparency info
* adm: string, // adm for viewability tracking
* containerId: string
* }
*
* NOTE: Number of products is configurable by shop (not limited to 5).
*/
// Configuration - shop decides how many products to display
const MAX_PRODUCTS = 5;
/**
* Format price in EUR currency
*/
function formatPrice(price) {
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(price);
}
/**
* Build display-ready product list by merging shop data with ad server offers.
* Ad server offers are never mutated — shop data comes from a separate shopDataMap.
*
* @param {Object} shopDataMap - Shop product data keyed by product_id: { url, name, image, price }
* @param {Array} offers - Original offers from ad server (never mutated)
* @returns {Array} Products ready for display with tracking URLs
*/
function getProductsForDisplay(shopDataMap, offers) {
return (offers || []).map(offer => {
var shop = shopDataMap[offer.product_id] || {};
return {
productId: offer.product_id || offer.offer_id,
image: shop.image || offer.offer_image,
name: shop.name || offer.offer_name,
price: shop.price || offer.offer_price,
url: shop.url || offer.offer_url,
// Per-offer click tracking URL provided directly by the ad server
trackingUrl: offer.adclick || ''
};
});
}
// Get products for display (shop data from shopDataMap, offers from ad server)
const products = adData ? getProductsForDisplay(adData.shopDataMap || {}, adData.offers) : [];
%>
<% if (adData) { %>
<divclass="branded-products-container"id="<%= adData.containerId || 'branded-products-container' %>"><!-- ═══════════════════════════════════════════════════════════════════
HEADER: Sponsored badge + DSA info (from AD SERVER)
═══════════════════════════════════════════════════════════════════ --><divclass="sponsored-header"><spanclass="sponsored-badge">Sponsored</span>
<% if (adData.dsaInfo) { %>
<divclass="dsa-info-wrapper"><spanclass="dsa-info-icon"title="Ad transparency info">i</span><divclass="dsa-info-tooltip"><divclass="dsa-info-row"><spanclass="dsa-info-label">Advertiser:</span><spanclass="dsa-info-value"><%= adData.dsaInfo.advertiser || 'N/A' %></span></div><divclass="dsa-info-row"><spanclass="dsa-info-label">Paid by:</span><spanclass="dsa-info-value"><%= adData.dsaInfo.payer || 'N/A' %></span></div></div></div>
<% } %>
</div><!-- ═══════════════════════════════════════════════════════════════════
BRAND SECTION: Logo from AD SERVER
═══════════════════════════════════════════════════════════════════ --><divclass="brand-section">
<% if (adData.bannerImage) { %>
<% if (adData.urlRedirect) { %>
<!-- Brand logo with click-through URL from ad server --><ahref="<%= adData.urlRedirect %>"target="_blank"rel="noopener nofollow sponsored"><imgclass="brand-logo"src="<%= adData.bannerImage %>"alt="Brand Logo"></a>
<% } else { %>
<!-- Brand logo URL provided by ad server --><imgclass="brand-logo"src="<%= adData.bannerImage %>"alt="Brand Logo">
<% } %>
<% } else { %>
<divclass="brand-logo-placeholder">Brand Logo</div>
<% } %>
</div><!-- ═══════════════════════════════════════════════════════════════════
PRODUCTS SECTION: Details from SHOP (fetched using product_id)
═══════════════════════════════════════════════════════════════════ --><divclass="products-section">
<% if (products.length > 0) { %>
<%
// Shop decides how many products to display
const displayProducts = products.slice(0, MAX_PRODUCTS);
%>
<% displayProducts.forEach(function(product) { %>
<!--
Product data comes from SHOP's database
(looked up using product_id from ad server)
Click tracking URL (trackingUrl) comes from AD SERVER
--><ahref="<%= product.url %>"class="product-tile"target="_blank"rel="noopener nofollow sponsored"
<% if (product.trackingUrl) { %>onclick="new Image().src='<%= product.trackingUrl %>';"<% } %>>
<imgclass="product-image"src="<%= product.image %>"alt="<%= product.name %>"><divclass="product-name"><%= product.name %></div><divclass="product-price"><%= formatPrice(product.price) %></div></a>
<% }); %>
<% } else { %>
<!-- Placeholder when no products available -->
<% for (let i = 0; i < 3; i++) { %>
<divclass="product-tile"><divclass="product-image-placeholder">Product <%= i + 1 %></div><divclass="product-name">Sample Product Name</div><divclass="product-price">€99.00</div></div>
<% } %>
<% } %>
</div></div><!-- ═══════════════════════════════════════════════════════════════════════
TRACKING: Register bid response for viewability measurement
═══════════════════════════════════════════════════════════════════════ --><script>
dlApi.cmd.push(function() {
dlApi.registerBidResponse(<%- JSON.stringify(adData.adm) %>, '<%= adData.containerId || 'branded-products-container' %>');
});
</script>
<% } else { %>
<!-- No ad data available --><divclass="no-ad-state">
No ad data available (Backend mode)
</div>
<% } %>
<!-- Branded Products - Frontend Mode (CSR) --><!--
DATA FLOW (Hybrid Format):
1. Ad Server provides: bannerImage (brand logo) + offers[] with product_id and tracking URLs
2. Shop fetches: product details (image, price, name, URL) from its catalog using product_ids
3. Shop renders: combined data (logo from ad server + product details from shop + tracking URLs)
If a product_id is not found in the shop's catalog, the offer data from the ad server is used directly.
--><divid="branded-products-container"class="branded-products-container"><divclass="loading-state"><divclass="spinner"></div><span>Loading branded products...</span></div></div><script>
(function() {
'use strict';
// =========================================================================// CONFIGURATION// =========================================================================constCONFIG = {
containerId: 'branded-products-container',
maxProducts: 5, // Shop decides how many products to displaytplCode: '1746213/Sponsored-Product-Plus',
// Mock product service endpoint (simulates shop's product catalog)productApiUrl: '/api/products'
};
// =========================================================================// HELPER FUNCTIONS// =========================================================================/**
* Escape HTML special characters to prevent XSS
*/functionescapeHtml(text) {
if (!text) return'';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Format price in EUR currency
*/functionformatPrice(price) {
returnnewIntl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(price);
}
/**
* Build DSA (Digital Services Act) transparency info HTML
*/functionbuildDsaHtml(dsa) {
if (!dsa) return'';
return`
<div class="dsa-info-wrapper">
<span class="dsa-info-icon" title="Ad transparency info">i</span>
<div class="dsa-info-tooltip">
<div class="dsa-info-row">
<span class="dsa-info-label">Advertiser:</span>
<span class="dsa-info-value">${escapeHtml(dsa.behalf) || 'N/A'}</span>
</div>
<div class="dsa-info-row">
<span class="dsa-info-label">Paid by:</span>
<span class="dsa-info-value">${escapeHtml(dsa.paid) || 'N/A'}</span>
</div>
</div>
</div>
`;
}
// =========================================================================// SHOP'S PRODUCT DATA FETCHING (MOCK SERVICE)// =========================================================================/**
* Fetch products from shop's product catalog (mock service in this demo)
*
* @param {Array<string>} productIds - Product IDs to fetch
* @returns {Promise<Array>} - Product details from shop catalog
*/asyncfunctionfetchProductsFromMockService(productIds) {
console.log('[MockService] ─────────────────────────────────────────');
console.log('[MockService] Fetching products from mock service...');
console.log('[MockService] Requested IDs:', productIds.join(', '));
try {
const ids = productIds.slice(0, CONFIG.maxProducts).join(',');
const url = `${CONFIG.productApiUrl}?ids=${ids}`;
console.log('[MockService] Request URL:', url);
const response = awaitfetch(url);
if (!response.ok) {
thrownewError(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('[MockService] Response source:', data.source);
console.log('[MockService] Products received:', data.count);
data.products.forEach((p, i) => {
console.log(`[MockService] ${i + 1}. ${p.name} - ${p.formattedPrice}`);
});
console.log('[MockService] ─────────────────────────────────────────');
return data.products;
} catch (error) {
console.error('[MockService] Error:', error.message);
console.log('[MockService] ─────────────────────────────────────────');
return [];
}
}
// =========================================================================// RENDERING// =========================================================================/**
* Build HTML for a single product tile
* @param {Object} product - Product with shop data + trackingUrl from ad server
*/functionbuildProductTileHtml(product) {
// trackingUrl from ad server for click trackingconst trackingOnclick = product.trackingUrl
? `onclick="new Image().src='${product.trackingUrl}';"`
: '';
return`
<a href="${escapeHtml(product.url)}" class="product-tile"
target="_blank" rel="noopener nofollow sponsored"
${trackingOnclick}>
<img class="product-image" src="${escapeHtml(product.image)}"
alt="${escapeHtml(product.name)}">
<div class="product-name">${escapeHtml(product.name)}</div>
<div class="product-price">${formatPrice(product.price)}</div>
</a>
`;
}
/**
* Build placeholder HTML when no products available
*/functionbuildPlaceholderHtml(count) {
let html = '';
for (let i = 0; i < count; i++) {
html += `
<div class="product-tile">
<div class="product-image-placeholder">Product ${i + 1}</div>
<div class="product-name">Sample Product Name</div>
<div class="product-price">€99.00</div>
</div>
`;
}
return html;
}
/**
* Render the complete Branded Products banner
*
* @param {string} bannerImage - Brand logo URL (from AD SERVER)
* @param {Array} products - Product details (from SHOP)
* @param {Object} dsaInfo - DSA transparency info (from AD SERVER)
* @param {string} urlRedirect - Logo click destination URL (from AD SERVER, optional)
*/functionrenderBrandedProducts(bannerImage, products, dsaInfo, urlRedirect) {
const container = document.getElementById(CONFIG.containerId);
if (!container) {
console.error('[Render] Container not found');
return;
}
// Build brand section (data from AD SERVER)const brandHtml = bannerImage
? (urlRedirect
? `<a href="${escapeHtml(urlRedirect)}" target="_blank" rel="noopener nofollow sponsored">
<img class="brand-logo" src="${escapeHtml(bannerImage)}" alt="Brand Logo">
</a>`
: `<img class="brand-logo" src="${escapeHtml(bannerImage)}" alt="Brand Logo">`)
: '<div class="brand-logo-placeholder">Brand Logo</div>';
// Build products section (data from SHOP)const displayProducts = products.slice(0, CONFIG.maxProducts);
const productsHtml = displayProducts.length > 0
? displayProducts.map(buildProductTileHtml).join('')
: buildPlaceholderHtml(3);
// Render complete banner
container.innerHTML = `
<div class="sponsored-header">
<span class="sponsored-badge">Sponsored</span>
${buildDsaHtml(dsaInfo)}
</div>
<div class="brand-section">
<!-- Brand logo from AD SERVER -->
${brandHtml}
</div>
<div class="products-section">
<!-- Product details from SHOP -->
${productsHtml}
</div>
`;
console.log('[Render] Banner rendered with', displayProducts.length, 'products');
}
/**
* Show empty state when no ad is available
*/functionrenderEmptyState(message) {
const container = document.getElementById(CONFIG.containerId);
if (container) {
container.innerHTML = `<div class="no-ad-state">${escapeHtml(message)}</div>`;
}
}
// =========================================================================// MAIN: SDK INTEGRATION// =========================================================================
dlApi.cmd.push(function(dlApi) {
// Step 2: Fetch ad using fetchNativeAd
dlApi.fetchNativeAd({
slot: 'display', // fixed value for Branded Productsdiv: CONFIG.containerId, // container ID for viewability trackingopts: {
offers_limit: CONFIG.maxProducts// maximum number of products returned from ad server
},
tplCode: CONFIG.tplCode, // fixed value for Branded ProductsasyncRender: true// manually count impression with ad.render()
}).then(asyncfunction(ad) {
if (!ad) {
console.log('[Ad Server] No ad available');
renderEmptyState('No branded products available');
return;
}
console.log('[Ad Server] Response received:', ad);
// ─────────────────────────────────────────────────────────────// STEP A: Extract data from AD SERVER// ─────────────────────────────────────────────────────────────const bannerImage = ad.fields?.bannerImage || ''; // Brand logoconst urlRedirect = ad.fields?.urlRedirect || ''; // Logo click URL (optional)const offers = ad.fields?.feed?.offers || []; // Product offers with tracking URLsconst dsaInfo = ad.dsa;
console.log('[Ad Server] Brand logo:', bannerImage ? 'Yes' : 'No');
console.log('[Ad Server] Offers received:', offers.length);
offers.forEach((o, i) => {
console.log(`[Ad Server] ${i + 1}. product_id: ${o.product_id}, offer_url: ${o.offer_url ? 'Yes' : 'No'}`);
});
// ─────────────────────────────────────────────────────────────// STEP B: Fetch product details from SHOP using ad server IDs// ─────────────────────────────────────────────────────────────const productIds = offers.map(o => o.product_id).filter(Boolean);
console.log('[Shop] Looking up product IDs from ad server:', productIds.join(', '));
let products = awaitfetchProductsFromMockService(productIds);
// Fallback: if shop catalog doesn't have these IDs, use offer data directlyif (products.length === 0 && offers.length > 0) {
console.log('[Shop] Products not found in catalog, using ad server offer data as fallback');
products = offers.map(o => ({
productId: o.product_id,
name: o.offer_name,
price: o.offer_price,
image: o.offer_image,
url: o.offer_url
}));
}
// ─────────────────────────────────────────────────────────────// STEP C: Combine shop data with ad server tracking URLs// ─────────────────────────────────────────────────────────────// href = shop URL (destination), onclick = per-offer adclick URL from ad serverconst productsWithTracking = products.map((product, index) => ({
...product,
trackingUrl: offers[index]?.adclick || ''
}));
console.log('[Data Merge] Products with tracking URLs:');
productsWithTracking.forEach((p, i) => {
console.log(`[Data Merge] ${i + 1}. ${p.name} - tracking: ${p.trackingUrl ? 'Yes' : 'No'}`);
});
// ─────────────────────────────────────────────────────────────// STEP D: Render combined data// ─────────────────────────────────────────────────────────────renderBrandedProducts(bannerImage, productsWithTracking, dsaInfo, urlRedirect);
// ─────────────────────────────────────────────────────────────// STEP E: Count impression// ─────────────────────────────────────────────────────────────
ad.render();
console.log('[Ad Server] Impression counted');
}).catch(function(err) {
console.error('[Ad Server] Ad could not be loaded:', err);
renderEmptyState('Error loading branded products');
});
// Step 3: Trigger ad request
dlApi.fetch();
});
})();
</script>