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 Single Tile - Backend (SSR) Partial
*
* TWO INDEPENDENT AD SLOTS (mirrors frontend: one bid request, two imp objects).
* Each slot resolves independently based on its own redirectToOffers flag.
*
* 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 (position 1)
* adData2 — slot 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 (each comes from its own imp in the bid request)
const tile1 = resolveSlot(adData);
const tile2 = resolveSlot(typeof adData2 !== 'undefined' ? adData2 : null);
%>
<% if (tile1) { %>
<div class="product-listing-grid">
<div class="single-tile-container" id="<%= tile1.containerId || 'sponsored-single-tile-container' %>">
<% if (tile1.redirectToOffers === false) { %>
<div class="tile">
<% 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">
<% 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>
<% } %>
</div>
<div class="product-placeholder">
<div class="placeholder-image">Organic Product Image</div>
<div class="placeholder-brand">SampleBrand</div>
<div class="placeholder-name">Example Product - Organic offer</div>
<div class="placeholder-price">49,99 €</div>
</div>
<div class="product-placeholder">
<div class="placeholder-image">Organic Product Image</div>
<div class="placeholder-brand">AnotherBrand</div>
<div class="placeholder-name">Another Product - Organic offer</div>
<div class="placeholder-price">79,99 €</div>
</div>
<% if (tile2) { %>
<div class="single-tile-container" id="<%= tile2.containerId || 'sponsored-product-container-2' %>">
<% if (tile2.redirectToOffers === false) { %>
<div class="tile">
<% 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">
<% 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>
<% } %>
</div>
<% } else { %>
<div class="single-tile-container">
<div class="tile">
<div class="tile-error">No sponsored product available</div>
</div>
</div>
<% } %>
<div class="product-placeholder">
<div class="placeholder-image">Organic Product Image</div>
<div class="placeholder-brand">BrandC</div>
<div class="placeholder-name">Product C - Organic offer</div>
<div class="placeholder-price">34,99 €</div>
</div>
<div class="product-placeholder">
<div class="placeholder-image">Organic Product Image</div>
<div class="placeholder-brand">BrandD</div>
<div class="placeholder-name">Product D - Organic offer</div>
<div class="placeholder-price">59,99 €</div>
</div>
<div class="product-placeholder">
<div class="placeholder-image">Organic Product Image</div>
<div class="placeholder-brand">BrandE</div>
<div class="placeholder-name">Product E - Organic offer</div>
<div class="placeholder-price">89,99 €</div>
</div>
<div class="product-placeholder">
<div class="placeholder-image">Organic Product Image</div>
<div class="placeholder-brand">BrandF</div>
<div class="placeholder-name">Product F - Organic offer</div>
<div class="placeholder-price">119,99 €</div>
</div>
</div>
<script>
dlApi.cmd.push(function() {
dlApi.registerBidResponse(<%- JSON.stringify(adData.adm) %>, '<%= tile1.containerId || 'sponsored-single-tile-container' %>');
});
</script>
<% } else { %>
<div class="info-message">
<p>No sponsored ad data available from the ad server.</p>
<p>
Try <a href="/formats/sponsored-single-tile?mode=frontend">Frontend (CSR) mode</a>
which fetches ads client-side via <code>fetchNativeAd</code>.
</p>
</div>
<% } %>
<script>
(function() {
'use strict';
var CONFIG = {
slot: 'product-tile',
tplCode: '1746213/Sponsored-Product',
containerId: 'sponsored-product-container',
tileId: 'sponsored-tile',
productApiUrl: '/api/products'
};
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatPrice(price) {
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(price);
}
function buildDsaHtml(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>';
}
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) {
}
}
try {
var urlParam = new URL(offer.offer_url).searchParams.get('URL');
if (urlParam) {
var shopDomain = new URL(urlParam).hostname;
return shopDomain.replace(/^www\./, '').split('.')[0];
}
} catch (e) {
}
return 'Shop';
}
async function fetchProductsFromMockService(productIds) {
console.log('[MockService] ─────────────────────────────────────────');
console.log('[MockService] Fetching products from mock service...');
console.log('[MockService] Requested IDs:', productIds.join(', '));
try {
var ids = productIds.join(',');
var url = CONFIG.productApiUrl + '?ids=' + ids;
console.log('[MockService] Request URL:', url);
var response = await fetch(url);
if (!response.ok) {
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
}
var data = await response.json();
console.log('[MockService] Response source:', data.source);
console.log('[MockService] Products received:', data.count);
data.products.forEach(function(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 [];
}
}
function renderOffer(el, offer, dsa, redirectToOffers, shopData) {
var shop = shopData || {};
var oldPriceHtml = offer.offer_old_price
? '<div class="old-price">' + formatPrice(offer.offer_old_price) + '</div>'
: '';
var productId = (offer.offer_custom_fields && offer.offer_custom_fields.product_id) || '';
var idealoUrl = productId
? 'https://www.idealo.de/produkt/' + encodeURIComponent(productId)
: '#';
var productPageUrl = shop.url || idealoUrl;
var clickTrackingUrl = offer.adclick || '';
var badgeText = redirectToOffers ? 'Sponsored Offer' : 'Sponsored Product';
if (redirectToOffers === false) {
var displayImage = shop.image || offer.offer_image;
var displayName = shop.name || offer.offer_name;
var displayPrice = shop.price || offer.offer_price;
el.innerHTML = ''
+ buildDsaHtml(dsa)
+ '<span class="sponsored-badge">' + badgeText + '</span>'
+ '<a class="tile-product-link" href="' + escapeHtml(productPageUrl) + '" target="_blank" rel="noopener nofollow sponsored"'
+ (clickTrackingUrl ? ' onclick="new Image().src=\'' + escapeHtml(clickTrackingUrl) + '\';"' : '')
+ '>'
+ '<img class="tile-image" src="' + escapeHtml(displayImage) + '" alt="' + escapeHtml(displayName) + '" />'
+ '<div class="tile-brand">' + escapeHtml(offer.offer_brand || '') + '</div>'
+ '<div class="tile-name">' + escapeHtml(displayName) + '</div>'
+ '<div class="tile-prices">'
+ oldPriceHtml
+ '<div class="current-price">' + formatPrice(displayPrice) + '</div>'
+ '</div>'
+ '</a>';
} else {
var shopName = getShopName(offer);
el.innerHTML = ''
+ buildDsaHtml(dsa)
+ '<span class="sponsored-badge">' + badgeText + '</span>'
+ '<img class="tile-image" src="' + escapeHtml(offer.offer_image) + '" alt="' + escapeHtml(offer.offer_name) + '" />'
+ '<div class="tile-brand">' + escapeHtml(offer.offer_brand || '') + '</div>'
+ '<div class="tile-name">' + escapeHtml(offer.offer_name) + '</div>'
+ '<div class="tile-prices">'
+ oldPriceHtml
+ '<div class="current-price">' + formatPrice(offer.offer_price) + '</div>'
+ '</div>'
+ '<a class="tile-cta" href="' + escapeHtml(offer.offer_url) + '" target="_blank" rel="noopener nofollow sponsored"'
+ (clickTrackingUrl ? ' onclick="new Image().src=\'' + escapeHtml(clickTrackingUrl) + '\';"' : '')
+ '>' + escapeHtml(shopName) + '</a>';
}
}
dlApi.cmd.push(function(dlApi) {
dlApi.fetchNativeAd({
slot: CONFIG.slot,
div: CONFIG.tileId,
tplCode: CONFIG.tplCode,
asyncRender: true
}).then(async function(ad) {
var el = document.getElementById(CONFIG.tileId);
el.classList.remove('loading');
if (!ad) {
console.log('[fetchNativeAd] No ad available');
el.innerHTML = '<div class="tile-error">No sponsored product available</div>';
return;
}
console.log('[fetchNativeAd] Full ad response:', ad);
console.log('[fetchNativeAd] ad.fields:', ad.fields);
console.log('[fetchNativeAd] ad.fields.feed:', ad.fields && ad.fields.feed);
var offers = (ad.fields && ad.fields.feed && ad.fields.feed.offers) || [];
if (offers.length === 0) {
console.log('[fetchNativeAd] No offers in response');
el.innerHTML = '<div class="tile-error">No offer data in response</div>';
return;
}
var offer = offers[0];
var dsa = ad.dsa || null;
var redirectToOffers = undefined;
if (ad.fields && typeof ad.fields.redirectToOffers !== 'undefined') {
redirectToOffers = ad.fields.redirectToOffers;
console.log('[fetchNativeAd] redirectToOffers found in ad.fields:', redirectToOffers);
} else if (ad.fields && ad.fields.feed && typeof ad.fields.feed.redirectToOffers !== 'undefined') {
redirectToOffers = ad.fields.feed.redirectToOffers;
console.log('[fetchNativeAd] redirectToOffers found in ad.fields.feed:', redirectToOffers);
} else if (typeof ad.redirectToOffers !== 'undefined') {
redirectToOffers = ad.redirectToOffers;
console.log('[fetchNativeAd] redirectToOffers found on ad root:', redirectToOffers);
} else {
redirectToOffers = true;
console.log('[fetchNativeAd] redirectToOffers NOT found in response, defaulting to true (show CTA)');
console.log('[fetchNativeAd] Checked: ad.fields.redirectToOffers, ad.fields.feed.redirectToOffers, ad.redirectToOffers');
}
console.log('[fetchNativeAd] Offer:', offer.offer_name);
console.log('[fetchNativeAd] redirectToOffers (resolved):', redirectToOffers);
console.log('[fetchNativeAd] offer_url:', offer.offer_url);
var customFields = offer.offer_custom_fields;
var productId = (customFields && customFields.product_id) || offer.product_id;
var shopDataMap = {};
if (redirectToOffers === false && productId) {
console.log('[fetchNativeAd] Fetching shop data for product:', productId);
var shopProducts = await fetchProductsFromMockService([productId]);
if (shopProducts.length > 0) {
var p = shopProducts[0];
shopDataMap[productId] = { url: p.url, name: p.name, image: p.image, price: p.price };
console.log('[fetchNativeAd] Shop data loaded for product', productId);
}
}
var shopData = shopDataMap[productId] || null;
renderOffer(el, offer, dsa, redirectToOffers, shopData);
var titleEl = document.getElementById('single-tile-title');
if (titleEl) {
titleEl.textContent = redirectToOffers
? 'Sponsored Offer (Single Tile)'
: 'Sponsored Product (Single Tile)';
}
ad.render();
console.log('[fetchNativeAd] Impression counted');
}).catch(function(err) {
console.error('[fetchNativeAd] Failed:', err);
var el = document.getElementById(CONFIG.tileId);
if (el) {
el.classList.remove('loading');
el.innerHTML = '<div class="tile-error">Error loading sponsored product</div>';
}
});
dlApi.fetch();
});
})();
</script>
<script>
(function() {
'use strict';
var CONFIG = {
slot: 'product-tile2',
tplCode: '1746213/Sponsored-Product',
containerId: 'sponsored-product-container-2',
tileId: 'sponsored-tile-2',
productApiUrl: '/api/products'
};
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatPrice(price) {
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(price);
}
function buildDsaHtml(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>';
}
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) {}
}
try {
var urlParam = new URL(offer.offer_url).searchParams.get('URL');
if (urlParam) {
var shopDomain = new URL(urlParam).hostname;
return shopDomain.replace(/^www\./, '').split('.')[0];
}
} catch (e) {}
return 'Shop';
}
async function fetchProductsFromMockService(productIds) {
console.log('[MockService Slot2] Fetching products:', productIds.join(', '));
try {
var ids = productIds.join(',');
var url = CONFIG.productApiUrl + '?ids=' + ids;
var response = await fetch(url);
if (!response.ok) {
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
}
var data = await response.json();
console.log('[MockService Slot2] Products received:', data.count);
return data.products;
} catch (error) {
console.error('[MockService Slot2] Error:', error.message);
return [];
}
}
function renderOffer(el, offer, dsa, redirectToOffers, shopData) {
var shop = shopData || {};
var oldPriceHtml = offer.offer_old_price
? '<div class="old-price">' + formatPrice(offer.offer_old_price) + '</div>'
: '';
var productId = (offer.offer_custom_fields && offer.offer_custom_fields.product_id) || '';
var idealoUrl = productId
? 'https://www.idealo.de/produkt/' + encodeURIComponent(productId)
: '#';
var productPageUrl = shop.url || idealoUrl;
var clickTrackingUrl = offer.adclick || '';
var badgeText = redirectToOffers ? 'Sponsored Offer' : 'Sponsored Product';
if (redirectToOffers === false) {
var displayImage = shop.image || offer.offer_image;
var displayName = shop.name || offer.offer_name;
var displayPrice = shop.price || offer.offer_price;
el.innerHTML = ''
+ buildDsaHtml(dsa)
+ '<span class="sponsored-badge">' + badgeText + '</span>'
+ '<a class="tile-product-link" href="' + escapeHtml(productPageUrl) + '" target="_blank" rel="noopener nofollow sponsored"'
+ (clickTrackingUrl ? ' onclick="new Image().src=\'' + escapeHtml(clickTrackingUrl) + '\';"' : '')
+ '>'
+ '<img class="tile-image" src="' + escapeHtml(displayImage) + '" alt="' + escapeHtml(displayName) + '" />'
+ '<div class="tile-brand">' + escapeHtml(offer.offer_brand || '') + '</div>'
+ '<div class="tile-name">' + escapeHtml(displayName) + '</div>'
+ '<div class="tile-prices">'
+ oldPriceHtml
+ '<div class="current-price">' + formatPrice(displayPrice) + '</div>'
+ '</div>'
+ '</a>';
} else {
var shopName = getShopName(offer);
el.innerHTML = ''
+ buildDsaHtml(dsa)
+ '<span class="sponsored-badge">' + badgeText + '</span>'
+ '<img class="tile-image" src="' + escapeHtml(offer.offer_image) + '" alt="' + escapeHtml(offer.offer_name) + '" />'
+ '<div class="tile-brand">' + escapeHtml(offer.offer_brand || '') + '</div>'
+ '<div class="tile-name">' + escapeHtml(offer.offer_name) + '</div>'
+ '<div class="tile-prices">'
+ oldPriceHtml
+ '<div class="current-price">' + formatPrice(offer.offer_price) + '</div>'
+ '</div>'
+ '<a class="tile-cta" href="' + escapeHtml(offer.offer_url) + '" target="_blank" rel="noopener nofollow sponsored"'
+ (clickTrackingUrl ? ' onclick="new Image().src=\'' + escapeHtml(clickTrackingUrl) + '\';"' : '')
+ '>' + escapeHtml(shopName) + '</a>';
}
}
dlApi.cmd.push(function(dlApi) {
dlApi.fetchNativeAd({
slot: CONFIG.slot,
div: CONFIG.tileId,
opts: {
pos: 1
},
tplCode: CONFIG.tplCode,
asyncRender: true
}).then(async function(ad) {
var el = document.getElementById(CONFIG.tileId);
el.classList.remove('loading');
if (!ad) {
console.log('[fetchNativeAd Slot2] No ad available');
el.innerHTML = '<div class="tile-error">No sponsored product available</div>';
return;
}
console.log('[fetchNativeAd Slot2] Ad response received');
var offers = (ad.fields && ad.fields.feed && ad.fields.feed.offers) || [];
if (offers.length === 0) {
el.innerHTML = '<div class="tile-error">No offer data in response</div>';
return;
}
var offer = offers[0];
var dsa = ad.dsa || null;
var redirectToOffers = undefined;
if (ad.fields && typeof ad.fields.redirectToOffers !== 'undefined') {
redirectToOffers = ad.fields.redirectToOffers;
} else if (ad.fields && ad.fields.feed && typeof ad.fields.feed.redirectToOffers !== 'undefined') {
redirectToOffers = ad.fields.feed.redirectToOffers;
} else if (typeof ad.redirectToOffers !== 'undefined') {
redirectToOffers = ad.redirectToOffers;
} else {
redirectToOffers = true;
}
console.log('[fetchNativeAd Slot2] Offer:', offer.offer_name, '| redirectToOffers:', redirectToOffers);
var customFields = offer.offer_custom_fields;
var productId = (customFields && customFields.product_id) || offer.product_id;
var shopDataMap = {};
if (redirectToOffers === false && productId) {
console.log('[fetchNativeAd Slot2] Fetching shop data for product:', productId);
var shopProducts = await fetchProductsFromMockService([productId]);
if (shopProducts.length > 0) {
var p = shopProducts[0];
shopDataMap[productId] = { url: p.url, name: p.name, image: p.image, price: p.price };
console.log('[fetchNativeAd Slot2] Shop data loaded for product', productId);
}
}
var shopData = shopDataMap[productId] || null;
renderOffer(el, offer, dsa, redirectToOffers, shopData);
ad.render();
console.log('[fetchNativeAd Slot2] Impression counted');
}).catch(function(err) {
console.error('[fetchNativeAd Slot2] Failed:', err);
var el = document.getElementById(CONFIG.tileId);
if (el) {
el.classList.remove('loading');
el.innerHTML = '<div class="tile-error">Error loading sponsored product</div>';
}
});
dlApi.fetch();
});
})();
</script>