app.get('/formats/:format', async (req, res) => {
const formatSlug = req.params.format;
const formatConfig = FORMATS.find(f => f.slug === formatSlug);
if (!formatConfig) {
return res.status(404).render('404', { requestedFormat: formatSlug });
}
const frontendOnlyFormats = ['sponsored-products-offers'];
const defaultMode = frontendOnlyFormats.includes(formatConfig.slug) ? 'frontend' : 'backend';
const mode = ['backend', 'frontend'].includes(req.query.mode)
? req.query.mode
: defaultMode;
const debugParams = {
adbeta: req.query.adbeta || null,
test_area: req.query.test_area || null,
test_site: req.query.test_site || null,
test_kwrd: req.query.test_kwrd || null,
test_tid: req.query.test_tid || null,
};
const incomingHeaders = req.headers;
if (Object.values(debugParams).some(Boolean)) {
console.log('[Server] Debug params:', debugParams);
}
if (incomingHeaders['x-adp-event-track-id']) {
console.log('[Server] Debug tracking ID:', incomingHeaders['x-adp-event-track-id']);
}
let adData = null;
let adData2 = null;
let bidderDebugUrl = null;
if (mode === 'backend') {
try {
if (formatSlug === 'sponsored-single-tile') {
const { slots: slotResults, bidderDebugUrl: debugUrl } = await bidder.fetchMultiSlotBid({
site: 'demo_page',
slots: [
{ id: 'sponsored-tile', tagId: formatConfig.tagId, containerId: formatConfig.containerId },
{ id: 'sponsored-tile-2', tagId: 'product-tile2', containerId: 'sponsored-product-container-2' }
],
networkId: formatConfig.networkId,
keyvalues: {
manufacturer_id: '12345678',
npa: 0
},
debugParams,
incomingHeaders
});
bidderDebugUrl = debugUrl;
const slot1 = slotResults['sponsored-tile'];
const slot2 = slotResults['sponsored-tile-2'];
adData = slot1?.hasAd ? slot1.adData : null;
adData2 = slot2?.hasAd ? slot2.adData : null;
[adData, adData2].forEach(data => {
if (!data) return;
if (typeof data.redirectToOffers === 'undefined') {
data.redirectToOffers = true;
}
data.shopDataMap = {};
if (data.redirectToOffers === false && data.offers && data.offers.length > 0) {
console.log('[Server] FLOW: Sponsored Product (shop data needed)');
const productId = data.offers[0].product_id;
if (productId) {
const shopProducts = productService.getProductsByIds([productId]);
if (shopProducts.length > 0) {
const p = shopProducts[0];
data.shopDataMap[productId] = {
url: p.url, name: p.name, image: p.image, price: p.price
};
}
}
} else {
console.log('[Server] FLOW: Sponsored Offer (ad server data only)');
}
});
}
else if (formatSlug === 'sponsored-product-slider') {
const { slots: slotResults, bidderDebugUrl: debugUrl } = await bidder.fetchMultiSlotBid({
site: 'demo_page',
slots: [
{ id: 'slider-slot-1', tagId: 'product-tile-slider', containerId: 'slider-sponsored-1' },
{ id: 'slider-slot-2', tagId: 'product-tile-slider2', containerId: 'slider-sponsored-2' }
],
networkId: formatConfig.networkId,
keyvalues: {
main_category_id: '101',
npa: 0
},
debugParams,
incomingHeaders
});
bidderDebugUrl = debugUrl;
const slot1 = slotResults['slider-slot-1'];
const slot2 = slotResults['slider-slot-2'];
adData = slot1?.hasAd ? slot1.adData : null;
adData2 = slot2?.hasAd ? slot2.adData : null;
[adData, adData2].forEach(data => {
if (!data) return;
if (typeof data.redirectToOffers === 'undefined') {
data.redirectToOffers = true;
}
data.shopDataMap = {};
if (data.redirectToOffers === false && data.offers && data.offers.length > 0) {
console.log('[Server] FLOW: Sponsored Product (shop data needed)');
const productId = data.offers[0].product_id;
if (productId) {
const shopProducts = productService.getProductsByIds([productId]);
if (shopProducts.length > 0) {
const p = shopProducts[0];
data.shopDataMap[productId] = {
url: p.url, name: p.name, image: p.image, price: p.price
};
}
}
} else {
console.log('[Server] FLOW: Sponsored Offer (ad server data only)');
}
});
}
else {
const { result: bidResult, bidderDebugUrl: debugUrl } = await bidder.fetchBid({
site: 'demo_page',
tagId: formatConfig.tagId,
containerId: formatConfig.containerId,
networkId: formatConfig.networkId,
keyvalues: {
manufacturer_id: '12345678',
npa: 0
},
debugParams,
incomingHeaders
});
bidderDebugUrl = debugUrl;
adData = bidResult?.hasAd ? bidResult.adData : null;
if (formatSlug === 'branded-products' && adData) {
const originalOffers = adData.offers || [];
const productIds = originalOffers.map(o => o.product_id).filter(Boolean);
console.log('[Server] Looking up product IDs from ad server:', productIds.join(', '));
const shopProducts = productService.getProductsByIds(productIds);
adData.shopDataMap = {};
if (shopProducts.length > 0) {
shopProducts.forEach(p => {
adData.shopDataMap[p.productId] = { url: p.url, name: p.name, image: p.image, price: p.price };
});
console.log('[Server] Shop data loaded for', shopProducts.length, 'products');
} else {
console.log('[Server] Product IDs not in shop catalog, shopDataMap empty');
}
}
}
} catch (error) {
console.error(`[Server] Error fetching bid for ${formatSlug}:`, error.message);
adData = null;
adData2 = null;
}
}
console.log(`[Server] Rendering ${formatSlug} in ${mode.toUpperCase()} mode`);
res.render(`formats/${formatSlug}`, {
adData,
adData2,
mode,
format: formatConfig,
formats: FORMATS,
sourceCode: SOURCE_CODE_BY_FORMAT[formatSlug] || SOURCE_CODE_BY_FORMAT['brand-store'],
bidderDebugUrl: bidderDebugUrl || null
});
});
function getBrandStoreShopData() {
return {
shopName: 'AD Partner Shop',
price: '€899.00',
label: 'Ad'
};
}
function generateRequestId() {
return Math.random().toString(16).substring(2, 15) +
Math.random().toString(16).substring(2, 15);
}
class BidderClient {
constructor(options = {}) {
this.networkId = options.networkId || '1746213';
this.baseUrl = `https://csr.onet.pl/${this.networkId}/bid`;
}
buildRequestBody({ site, tagId, networkId, keyvalues = {}, debugParams = {} }) {
const effectiveSiteId = debugParams.test_site || site;
const effectiveNetworkId = debugParams.test_tid || networkId;
const siteExt = {};
if (debugParams.test_area) siteExt.area = debugParams.test_area;
if (debugParams.test_kwrd) siteExt.kwrd = debugParams.test_kwrd;
return {
id: generateRequestId(),
imp: [{
id: 'imp-1',
tagid: tagId,
secure: 1,
native: { request: "{}" },
ext: {
offers_limit: 10,
no_redirect: true
}
}],
site: {
id: effectiveSiteId,
...(Object.keys(siteExt).length > 0 && { ext: siteExt })
},
user: {
ext: {
npa: false
}
},
ext: {
network: effectiveNetworkId,
keyvalues: {
kvIV: generateRequestId(),
kvIP: generateRequestId(),
...keyvalues
},
is_non_prebid_request: true,
...(debugParams.adbeta && { adbeta: debugParams.adbeta })
},
at: 1,
tmax: 1000,
regs: {
gdpr: 1,
gpp: '',
ext: {
dsa: 1
}
}
};
}
parseSingleBid(bid, containerId) {
try {
const adm = JSON.parse(bid.adm);
const dsaData = bid?.ext?.dsa || null;
const dsaInfo = dsaData ? {
advertiser: dsaData.behalf || null,
payer: dsaData.paid || null,
adrender: dsaData.adrender || 0
} : null;
const metaAdclick = adm?.meta?.adclick;
const fieldsClick = adm?.fields?.click;
const clickUrl = metaAdclick
? metaAdclick + encodeURIComponent(fieldsClick || '')
: (fieldsClick || null);
const bannerImage = adm?.fields?.bannerImage || null;
const urlRedirect = adm?.fields?.urlRedirect || null;
const offers = adm?.fields?.feed?.offers || [];
let redirectToOffers = undefined;
if (typeof adm?.fields?.redirectToOffers !== 'undefined') {
redirectToOffers = adm.fields.redirectToOffers;
} else if (typeof adm?.fields?.feed?.redirectToOffers !== 'undefined') {
redirectToOffers = adm.fields.feed.redirectToOffers;
}
const impressionUrl = bid?.ext?.ems_link;
return {
hasAd: true,
adData: {
containerId,
adm: bid.adm,
bannerImage,
urlRedirect,
offers,
redirectToOffers,
dsaInfo,
clickUrl,
metaAdclick,
impressionUrl,
adId: adm?.meta?.adid,
templateCode: adm?.tplCode,
...getBrandStoreShopData()
}
};
} catch (error) {
console.error('[BidderClient] Error parsing bid:', error.message);
return null;
}
}
parseResponse(response, containerId) {
const bid = response?.seatbid?.[0]?.bid?.[0];
if (!bid) {
console.log('[BidderClient] No bid in response');
return null;
}
return this.parseSingleBid(bid, containerId);
}
async fetchBid({ site, tagId, containerId, networkId, keyvalues = {}, debugParams = {}, incomingHeaders = {} }) {
const effectiveNetworkId = debugParams.test_tid || networkId || this.networkId;
const bidUrl = `https://csr.onet.pl/${effectiveNetworkId}/bid`;
const requestBody = this.buildRequestBody({
site,
tagId,
networkId: effectiveNetworkId,
keyvalues,
debugParams
});
console.log('[BidderClient] ─────────────────────────────────────────');
console.log('[BidderClient] Fetching bid from:', bidUrl);
console.log('[BidderClient] Tag ID:', tagId);
console.log('[BidderClient] Request:', JSON.stringify(requestBody, null, 2));
const encodedData = encodeURIComponent(JSON.stringify(requestBody));
const bidderDebugUrl = `${bidUrl}?data=${encodedData}`;
const bidderHeaders = {};
const trackId = incomingHeaders['x-adp-event-track-id'];
if (trackId) bidderHeaders['X-ADP-EVENT-TRACK-ID'] = trackId;
try {
const response = await fetch(bidderDebugUrl, { headers: bidderHeaders });
console.log('[BidderClient] Response status:', response.status);
if (response.status === 204) {
console.log('[BidderClient] ✗ No bid (204)');
console.log('[BidderClient] ─────────────────────────────────────────');
return { result: null, bidderDebugUrl };
}
if (!response.ok) {
console.error('[BidderClient] HTTP error:', response.status, response.statusText);
return { result: null, bidderDebugUrl };
}
const data = await response.json();
console.log('[BidderClient] Response received');
const result = this.parseResponse(data, containerId);
if (result) {
console.log('[BidderClient] ✓ Ad found');
console.log('[BidderClient] - Banner image:', result.adData.bannerImage ? 'Yes' : 'No');
console.log('[BidderClient] - URL redirect:', result.adData.urlRedirect ? 'Yes' : 'No');
console.log('[BidderClient] - Products:', result.adData.offers?.length || 0);
console.log('[BidderClient] - DSA info:', result.adData.dsaInfo ? 'Yes' : 'No');
} else {
console.log('[BidderClient] ✗ No ad available');
}
console.log('[BidderClient] ─────────────────────────────────────────');
return { result, bidderDebugUrl };
} catch (error) {
console.error('[BidderClient] Fetch error:', error.message);
return { result: null, bidderDebugUrl };
}
}
async fetchMultiSlotBid({ site, slots, networkId, keyvalues = {}, debugParams = {}, incomingHeaders = {} }) {
const effectiveNetworkId = debugParams.test_tid || networkId || this.networkId;
const bidUrl = `https://csr.onet.pl/${effectiveNetworkId}/bid`;
const effectiveSiteId = debugParams.test_site || site;
const siteExt = {};
if (debugParams.test_area) siteExt.area = debugParams.test_area;
if (debugParams.test_kwrd) siteExt.kwrd = debugParams.test_kwrd;
const requestBody = {
id: generateRequestId(),
imp: slots.map((slot, index) => ({
id: slot.id,
tagid: slot.tagId,
secure: 1,
native: { request: "{}" },
ext: {
pos: String(index + 1),
swidth: 220,
no_redirect: true
}
})),
site: {
id: effectiveSiteId,
...(Object.keys(siteExt).length > 0 && { ext: siteExt })
},
user: { ext: { npa: false } },
ext: {
network: effectiveNetworkId,
keyvalues: {
kvIV: generateRequestId(),
kvIP: generateRequestId(),
...keyvalues
},
is_non_prebid_request: true,
...(debugParams.adbeta && { adbeta: debugParams.adbeta })
},
at: 1,
tmax: 1000,
regs: {
gdpr: 1,
gpp: '',
ext: { dsa: 1 }
}
};
console.log('[BidderClient] ─────────────────────────────────────────');
console.log('[BidderClient] Multi-slot bid request:', slots.length, 'slots');
console.log('[BidderClient] Fetching from:', bidUrl);
console.log('[BidderClient] Slots:', slots.map(s => s.id).join(', '));
const encodedData = encodeURIComponent(JSON.stringify(requestBody));
const bidderDebugUrl = `${bidUrl}?data=${encodedData}`;
const bidderHeaders = {};
const trackId = incomingHeaders['x-adp-event-track-id'];
if (trackId) bidderHeaders['X-ADP-EVENT-TRACK-ID'] = trackId;
try {
const response = await fetch(bidderDebugUrl, { headers: bidderHeaders });
console.log('[BidderClient] Response status:', response.status);
if (response.status === 204) {
console.log('[BidderClient] ✗ No bid (204)');
console.log('[BidderClient] ─────────────────────────────────────────');
return { slots: {}, bidderDebugUrl };
}
if (!response.ok) {
console.error('[BidderClient] HTTP error:', response.status, response.statusText);
return { slots: {}, bidderDebugUrl };
}
const data = await response.json();
const bidsById = {};
for (const seatbid of (data.seatbid || [])) {
for (const bid of (seatbid.bid || [])) {
bidsById[bid.impid] = bid;
}
}
const results = {};
for (const slot of slots) {
const bid = bidsById[slot.id];
if (bid) {
const parsed = this.parseSingleBid(bid, slot.containerId);
if (parsed) {
results[slot.id] = parsed;
console.log('[BidderClient] Slot', slot.id, '→ ad found, offers:', parsed.adData.offers?.length || 0);
}
} else {
console.log('[BidderClient] Slot', slot.id, '→ no bid');
}
}
console.log('[BidderClient] ─────────────────────────────────────────');
return { slots: results, bidderDebugUrl };
} catch (error) {
console.error('[BidderClient] Multi-slot fetch error:', error.message);
return { slots: {}, bidderDebugUrl: bidUrl };
}
}
}
module.exports = {
BidderClient
};
<%
/**
* Sponsored Product Slider - Backend (SSR) Partial
*
* TWO INDEPENDENT SLIDER SLOTS in a horizontal carousel layout.
* Slot 1: product-tile-slider (position 2 in carousel)
* Slot 2: product-tile-slider2 (position 4 in carousel)
*
* Carousel layout (6 tiles total):
* [Organic 1] [SPONSORED 1] [Organic 2] [SPONSORED 2] [Organic 3] [Organic 4]
*
* NOTE: The container ID is placed directly on the .tile div so it remains
* a direct flex child and keeps its flex: 0 0 180px width.
*
* TWO DISTINCT FLOWS per tile based on redirectToOffers:
*
* +-------------------------------------------------------------+
* | FLOW A: SPONSORED PRODUCT (redirectToOffers === false) |
* | Data: Shop catalog (image, name, price, URL) |
* | Link: Tile -> shop product page |
* | Tracking: onclick pixel via offer_url |
* | Badge: "Sponsored Product" |
* +-------------------------------------------------------------+
* | FLOW B: SPONSORED OFFER (redirectToOffers === true) |
* | Data: Ad server only (image, name, price, offer_url) |
* | Link: CTA button -> offer_url (shop offer page) |
* | Badge: "Sponsored Offer" |
* +-------------------------------------------------------------+
*
* Template receives:
* adData — slot 1 (slider-sponsored-1, position 2)
* adData2 — slot 2 (slider-sponsored-2, position 4)
*/
/**
* Format price in EUR currency
*/
function formatPrice(price) {
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(price);
}
/**
* Get shop name from offer data.
* Primary source: offer_custom_fields.shop_name
* Fallback: extract domain from offer_url redirect parameter
*/
function getShopName(offer) {
var customFields = offer.offer_custom_fields;
if (customFields) {
try {
var parsed = (typeof customFields === 'string') ? JSON.parse(customFields) : customFields;
if (parsed.shop_name) return parsed.shop_name;
} catch (e) { /* ignore */ }
}
try {
var urlObj = new URL(offer.offer_url);
var urlParam = urlObj.searchParams.get('URL');
if (urlParam) {
var shopDomain = new URL(urlParam).hostname;
return shopDomain.replace(/^www\./, '').split('.')[0];
}
} catch (e) { /* ignore */ }
return 'Shop';
}
/**
* Resolve all display variables for a single slot's adData.
* Each slot is fully independent — has its own offers, redirectToOffers, DSA, etc.
*/
function resolveSlot(slotData) {
if (!slotData || !slotData.offers || slotData.offers.length === 0) return null;
const offerObj = slotData.offers[0];
const rto = slotData.redirectToOffers;
const sdm = slotData.shopDataMap || {};
const pid = offerObj.product_id || (offerObj.offer_custom_fields && offerObj.offer_custom_fields.product_id);
const shop = pid ? (sdm[pid] || {}) : {};
// Per-offer click tracking URL provided directly by the ad server
const tracking = offerObj.adclick || '';
const fallbackUrl = pid ? ('https://www.idealo.de/produkt/' + encodeURIComponent(pid)) : '#';
return {
offer: offerObj,
redirectToOffers: rto,
dsaInfo: slotData.dsaInfo,
containerId: slotData.containerId,
shopData: shop,
trackingUrl: tracking,
idealoUrl: fallbackUrl,
shopName: getShopName(offerObj)
};
}
// Resolve each slot independently
const tile1 = resolveSlot(adData);
const tile2 = resolveSlot(typeof adData2 !== 'undefined' ? adData2 : null);
%>
<% if (tile1 || tile2) { %>
<div class="carousel-wrapper">
<button class="carousel-nav carousel-nav--left hidden" id="slider-nav-left" aria-label="Scroll left">‹</button>
<div class="carousel" id="slider-carousel">
<div class="tile product-placeholder-tile">
<div class="placeholder-image">Organic Image</div>
<div class="tile-brand">BrandA</div>
<div class="tile-name">Organic Product A</div>
<div class="tile-prices">
<div class="current-price">49,99 €</div>
</div>
</div>
<% if (tile1) { %>
<% if (tile1.redirectToOffers === false) { %>
<div class="tile" id="<%= tile1.containerId || 'slider-sponsored-1' %>">
<% if (tile1.dsaInfo) { %>
<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"><%= tile1.dsaInfo.advertiser || 'N/A' %></span>
</div>
<div class="dsa-info-row">
<span class="dsa-info-label">Paid by:</span>
<span class="dsa-info-value"><%= tile1.dsaInfo.payer || 'N/A' %></span>
</div>
</div>
</div>
<% } %>
<span class="sponsored-badge">Sponsored Product</span>
<a class="tile-product-link" href="<%= tile1.shopData.url || tile1.idealoUrl %>"
target="_blank" rel="noopener nofollow sponsored"
<% if (tile1.trackingUrl) { %>onclick="new Image().src='<%= tile1.trackingUrl %>';"<% } %>>
<img class="tile-image"
src="<%= tile1.shopData.image || tile1.offer.offer_image %>"
alt="<%= tile1.shopData.name || tile1.offer.offer_name %>">
<div class="tile-brand"><%= tile1.offer.offer_brand || '' %></div>
<div class="tile-name"><%= tile1.shopData.name || tile1.offer.offer_name %></div>
<div class="tile-prices">
<% if (tile1.offer.offer_old_price) { %>
<div class="old-price"><%= formatPrice(tile1.offer.offer_old_price) %></div>
<% } %>
<div class="current-price"><%= formatPrice(tile1.shopData.price || tile1.offer.offer_price) %></div>
</div>
</a>
</div>
<% } else { %>
<div class="tile" id="<%= tile1.containerId || 'slider-sponsored-1' %>">
<% if (tile1.dsaInfo) { %>
<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"><%= tile1.dsaInfo.advertiser || 'N/A' %></span>
</div>
<div class="dsa-info-row">
<span class="dsa-info-label">Paid by:</span>
<span class="dsa-info-value"><%= tile1.dsaInfo.payer || 'N/A' %></span>
</div>
</div>
</div>
<% } %>
<span class="sponsored-badge">Sponsored Offer</span>
<img class="tile-image"
src="<%= tile1.offer.offer_image %>"
alt="<%= tile1.offer.offer_name %>">
<div class="tile-brand"><%= tile1.offer.offer_brand || '' %></div>
<div class="tile-name"><%= tile1.offer.offer_name %></div>
<div class="tile-prices">
<% if (tile1.offer.offer_old_price) { %>
<div class="old-price"><%= formatPrice(tile1.offer.offer_old_price) %></div>
<% } %>
<div class="current-price"><%= formatPrice(tile1.offer.offer_price) %></div>
</div>
<a class="tile-cta" href="<%= tile1.offer.offer_url %>"
target="_blank" rel="noopener nofollow sponsored"
<% if (tile1.trackingUrl) { %>onclick="new Image().src='<%= tile1.trackingUrl %>';"<% } %>><%= tile1.shopName %></a>
</div>
<% } %>
<% } else { %>
<div class="tile" id="slider-sponsored-1">
<div class="tile-error">No sponsored product available</div>
</div>
<% } %>
<div class="tile product-placeholder-tile">
<div class="placeholder-image">Organic Image</div>
<div class="tile-brand">BrandB</div>
<div class="tile-name">Organic Product B</div>
<div class="tile-prices">
<div class="current-price">79,99 €</div>
</div>
</div>
<% if (tile2) { %>
<% if (tile2.redirectToOffers === false) { %>
<div class="tile" id="<%= tile2.containerId || 'slider-sponsored-2' %>">
<% if (tile2.dsaInfo) { %>
<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"><%= tile2.dsaInfo.advertiser || 'N/A' %></span>
</div>
<div class="dsa-info-row">
<span class="dsa-info-label">Paid by:</span>
<span class="dsa-info-value"><%= tile2.dsaInfo.payer || 'N/A' %></span>
</div>
</div>
</div>
<% } %>
<span class="sponsored-badge">Sponsored Product</span>
<a class="tile-product-link" href="<%= tile2.shopData.url || tile2.idealoUrl %>"
target="_blank" rel="noopener nofollow sponsored"
<% if (tile2.trackingUrl) { %>onclick="new Image().src='<%= tile2.trackingUrl %>';"<% } %>>
<img class="tile-image"
src="<%= tile2.shopData.image || tile2.offer.offer_image %>"
alt="<%= tile2.shopData.name || tile2.offer.offer_name %>">
<div class="tile-brand"><%= tile2.offer.offer_brand || '' %></div>
<div class="tile-name"><%= tile2.shopData.name || tile2.offer.offer_name %></div>
<div class="tile-prices">
<% if (tile2.offer.offer_old_price) { %>
<div class="old-price"><%= formatPrice(tile2.offer.offer_old_price) %></div>
<% } %>
<div class="current-price"><%= formatPrice(tile2.shopData.price || tile2.offer.offer_price) %></div>
</div>
</a>
</div>
<% } else { %>
<div class="tile" id="<%= tile2.containerId || 'slider-sponsored-2' %>">
<% if (tile2.dsaInfo) { %>
<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"><%= tile2.dsaInfo.advertiser || 'N/A' %></span>
</div>
<div class="dsa-info-row">
<span class="dsa-info-label">Paid by:</span>
<span class="dsa-info-value"><%= tile2.dsaInfo.payer || 'N/A' %></span>
</div>
</div>
</div>
<% } %>
<span class="sponsored-badge">Sponsored Offer</span>
<img class="tile-image"
src="<%= tile2.offer.offer_image %>"
alt="<%= tile2.offer.offer_name %>">
<div class="tile-brand"><%= tile2.offer.offer_brand || '' %></div>
<div class="tile-name"><%= tile2.offer.offer_name %></div>
<div class="tile-prices">
<% if (tile2.offer.offer_old_price) { %>
<div class="old-price"><%= formatPrice(tile2.offer.offer_old_price) %></div>
<% } %>
<div class="current-price"><%= formatPrice(tile2.offer.offer_price) %></div>
</div>
<a class="tile-cta" href="<%= tile2.offer.offer_url %>"
target="_blank" rel="noopener nofollow sponsored"
<% if (tile2.trackingUrl) { %>onclick="new Image().src='<%= tile2.trackingUrl %>';"<% } %>><%= tile2.shopName %></a>
</div>
<% } %>
<% } else { %>
<div class="tile" id="slider-sponsored-2">
<div class="tile-error">No sponsored product available</div>
</div>
<% } %>
<div class="tile product-placeholder-tile">
<div class="placeholder-image">Organic Image</div>
<div class="tile-brand">BrandC</div>
<div class="tile-name">Organic Product C</div>
<div class="tile-prices">
<div class="current-price">34,99 €</div>
</div>
</div>
<div class="tile product-placeholder-tile">
<div class="placeholder-image">Organic Image</div>
<div class="tile-brand">BrandD</div>
<div class="tile-name">Organic Product D</div>
<div class="tile-prices">
<div class="current-price">59,99 €</div>
</div>
</div>
</div>
<button class="carousel-nav carousel-nav--right" id="slider-nav-right" aria-label="Scroll right">›</button>
</div>
<script>
dlApi.cmd.push(function() {
<% if (tile1) { %>
dlApi.registerBidResponse(<%- JSON.stringify(adData.adm) %>, '<%= tile1.containerId || 'slider-sponsored-1' %>');
<% } %>
<% if (tile2) { %>
dlApi.registerBidResponse(<%- JSON.stringify(adData2.adm) %>, '<%= tile2.containerId || 'slider-sponsored-2' %>');
<% } %>
});
</script>
<script>
(function() {
var carousel = document.getElementById('slider-carousel');
var navLeft = document.getElementById('slider-nav-left');
var navRight = document.getElementById('slider-nav-right');
var tileWidth = 196;
function updateNavButtons() {
navLeft.classList.toggle('hidden', carousel.scrollLeft <= 0);
navRight.classList.toggle('hidden', carousel.scrollLeft + carousel.clientWidth >= carousel.scrollWidth - 1);
}
navLeft.addEventListener('click', function() {
carousel.scrollBy({ left: -tileWidth * 2, behavior: 'smooth' });
});
navRight.addEventListener('click', function() {
carousel.scrollBy({ left: tileWidth * 2, behavior: 'smooth' });
});
carousel.addEventListener('scroll', updateNavButtons);
updateNavButtons();
})();
</script>
<% } else { %>
<div class="info-message">
<p>No sponsored ad data available from the ad server.</p>
<p>The ad server returned no bids for <code>product-tile-slider</code> / <code>product-tile-slider2</code> tag IDs.</p>
</div>
<% } %>