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 mode = ['backend', 'frontend'].includes(req.query.mode)
? req.query.mode
: 'backend';
let adData = null;
if (mode === 'backend') {
try {
const bidResult = await bidder.fetchBid({
site: 'demo_page',
tagId: formatConfig.tagId,
containerId: formatConfig.containerId,
networkId: formatConfig.networkId,
keyvalues: {
manufacturer_id: '12345678',
npa: 0
}
});
adData = bidResult?.hasAd ? bidResult.adData : null;
if (formatSlug === 'branded-products' && adData) {
console.log('[Server] Using mock product service for branded-products...');
const mockProducts = productService.getProductsByIds(
IdealoClient.getDemoProductIds().slice(0, 5)
);
const mockOffers = productService.toOfferFormat(mockProducts);
const originalOffers = adData.offers || [];
adData.offers = mockOffers.map((mockOffer, index) => ({
...mockOffer,
product_url: mockOffer.offer_url,
offer_url: originalOffers[index]?.offer_url || ''
}));
adData.mockProductsUsed = true;
console.log('[Server] Mock products integrated:', mockProducts.length);
}
} catch (error) {
console.error(`[Server] Error fetching bid for ${formatSlug}:`, error.message);
adData = null;
}
}
console.log(`[Server] Rendering ${formatSlug} in ${mode.toUpperCase()} mode`);
res.render(`formats/${formatSlug}`, {
adData,
mode,
format: formatConfig,
formats: FORMATS,
sourceCode: SOURCE_CODE_BY_FORMAT[formatSlug] || SOURCE_CODE_BY_FORMAT['brand-store']
});
});
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 = {} }) {
return {
id: generateRequestId(),
imp: [{
id: 'imp-1',
tagid: tagId,
secure: 1,
native: { request: "{}" },
ext: {
offers_limit: 10
}
}],
site: {
id: site
},
user: {
ext: {
npa: false
}
},
ext: {
network: networkId,
keyvalues: {
kvIV: generateRequestId(),
kvIP: generateRequestId(),
...keyvalues
},
is_non_prebid_request: true
},
at: 1,
tmax: 1000,
regs: {
gdpr: 1,
gpp: '',
ext: {
dsa: 1
}
}
};
}
parseResponse(response, containerId) {
try {
const bid = response?.seatbid?.[0]?.bid?.[0];
if (!bid) {
console.log('[BidderClient] No bid in response');
return null;
}
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 && fieldsClick)
? metaAdclick + encodeURIComponent(fieldsClick)
: null;
const bannerImage = adm?.fields?.bannerImage || null;
const offers = adm?.fields?.feed?.offers || [];
const impressionUrl = bid?.ext?.ems_link;
return {
hasAd: true,
adData: {
containerId,
rawResponse: response,
bannerImage,
offers,
dsaInfo,
clickUrl,
metaAdclick,
impressionUrl,
adId: adm?.meta?.adid,
templateCode: adm?.tplCode,
...getBrandStoreShopData()
}
};
} catch (error) {
console.error('[BidderClient] Error parsing response:', error.message);
return null;
}
}
async fetchBid({ site, tagId, containerId, networkId, keyvalues = {} }) {
const effectiveNetworkId = networkId || this.networkId;
const bidUrl = `https://csr.onet.pl/${effectiveNetworkId}/bid`;
const requestBody = this.buildRequestBody({
site,
tagId,
networkId: effectiveNetworkId,
keyvalues
});
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));
try {
const encodedData = encodeURIComponent(JSON.stringify(requestBody));
const response = await fetch(`${bidUrl}?data=${encodedData}`);
console.log('[BidderClient] Response status:', response.status);
if (!response.ok) {
console.error('[BidderClient] HTTP error:', response.status, response.statusText);
return null;
}
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] - 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;
} catch (error) {
console.error('[BidderClient] Fetch error:', error.message);
return null;
}
}
}
module.exports = {
BidderClient
};
<%
/**
* 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:
* bannerImage: string, // Brand logo URL
* metaAdclick: string, // Base URL for click tracking
* offers: Array<{ // Product offers with tracking URLs
* product_id,
* offer_url // Click tracking parameter (append to metaAdclick)
* }>,
*
* // FROM SHOP (simulated for demo with offer data):
* products: Array<{
* productId: string,
* image: string,
* name: string,
* price: number,
* url: string, // Shop's product URL (destination)
* trackingUrl: string // Ad server tracking URL (metaAdclick + offer_url)
* }>,
*
* // TRACKING:
* dsaInfo: { advertiser, payer }, // DSA transparency info
* rawResponse: object, // Full response for viewability
* 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);
}
/**
* DEMO: Simulate fetching product details from shop's database
* In real implementation, this happens in server.js before rendering
*
* IMPORTANT: In production:
* - url: comes from YOUR shop's product database (destination)
* - trackingUrl: metaAdclick + encodeURIComponent(offer_url) (click tracking)
*/
function getProductsFromShop(offers, metaAdclick) {
// For demo: use offer data directly (simulating shop lookup)
// Note: In real implementation, 'url' would be fetched from shop's database
return (offers || []).map(offer => ({
productId: offer.product_id || offer.offer_id,
image: offer.offer_image,
name: offer.offer_name,
price: offer.offer_price,
url: offer.product_url || offer.offer_url, // Product URL (destination) - prefer product_url if available
// Construct tracking URL: base URL + encoded offer_url parameter
// If metaAdclick is empty, use offer_url directly (without encoding)
trackingUrl: metaAdclick
? metaAdclick + encodeURIComponent(offer.offer_url || '')
: (offer.offer_url || '')
}));
}
// Get products (from shop's database, simulated for demo)
const products = adData ? getProductsFromShop(adData.offers, adData.metaAdclick) : [];
%>
<% if (adData) { %>
<div class="branded-products-container" id="<%= adData.containerId || 'branded-products-container' %>">
<div class="sponsored-header">
<span class="sponsored-badge">Sponsored</span>
<% if (adData.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"><%= adData.dsaInfo.advertiser || 'N/A' %></span>
</div>
<div class="dsa-info-row">
<span class="dsa-info-label">Paid by:</span>
<span class="dsa-info-value"><%= adData.dsaInfo.payer || 'N/A' %></span>
</div>
</div>
</div>
<% } %>
</div>
<div class="brand-section">
<% if (adData.bannerImage) { %>
<img class="brand-logo" src="<%= adData.bannerImage %>" alt="Brand Logo">
<% } else { %>
<div class="brand-logo-placeholder">Brand Logo</div>
<% } %>
</div>
<div class="products-section">
<% if (products.length > 0) { %>
<%
// Shop decides how many products to display
const displayProducts = products.slice(0, MAX_PRODUCTS);
%>
<% displayProducts.forEach(function(product) { %>
<a href="<%= product.url %>" class="product-tile"
target="_blank" rel="noopener nofollow sponsored"
<% if (product.trackingUrl) { %>onclick="new Image().src='<%= product.trackingUrl %>';"<% } %>>
<img class="product-image"
src="<%= product.image %>"
alt="<%= product.name %>">
<div class="product-name"><%= product.name %></div>
<div class="product-price"><%= formatPrice(product.price) %></div>
</a>
<% }); %>
<% } else { %>
<% for (let i = 0; i < 3; i++) { %>
<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>
<% } %>
<% } %>
</div>
</div>
<script>
dlApi.cmd.push(function() {
var rawResponse = <%- JSON.stringify(adData.rawResponse || {}) %>;
if (rawResponse && Object.keys(rawResponse).length > 0) {
dlApi.registerBidResponse(rawResponse, '<%= adData.containerId || 'branded-products-container' %>');
console.log('[Viewability] Bid response registered');
}
});
</script>
<% } else { %>
<div class="no-ad-state">
No ad data available (Backend mode)
</div>
<% } %>
<div id="branded-products-container" class="branded-products-container">
<div class="loading-state">
<div class="spinner"></div>
<span>Loading branded products...</span>
</div>
</div>
<script>
(function() {
'use strict';
const CONFIG = {
containerId: 'branded-products-container',
maxProducts: 5,
tplCode: '1746213/Sponsored-Product-Plus',
productApiUrl: '/api/products'
};
const MOCK_PRODUCT_IDS = [
'1370489',
'6065629',
'6610284',
'206006876',
'203337086',
'6724390',
'920965',
'201783101',
'206254161',
'206190211'
];
function escapeHtml(text) {
if (!text) return '';
const 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>
`;
}
async function fetchProductsFromMockService(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 = await fetch(url);
if (!response.ok) {
throw new Error(`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 [];
}
}
async function fetchProductDetailsFromShop() {
return await fetchProductsFromMockService(MOCK_PRODUCT_IDS);
}
function buildProductTileHtml(product) {
const 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>
`;
}
function buildPlaceholderHtml(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;
}
function renderBrandedProducts(bannerImage, products, dsaInfo) {
const container = document.getElementById(CONFIG.containerId);
if (!container) {
console.error('[Render] Container not found');
return;
}
const brandHtml = bannerImage
? `<img class="brand-logo" src="${escapeHtml(bannerImage)}" alt="Brand Logo">`
: '<div class="brand-logo-placeholder">Brand Logo</div>';
const displayProducts = products.slice(0, CONFIG.maxProducts);
const productsHtml = displayProducts.length > 0
? displayProducts.map(buildProductTileHtml).join('')
: buildPlaceholderHtml(3);
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');
}
function renderEmptyState(message) {
const container = document.getElementById(CONFIG.containerId);
if (container) {
container.innerHTML = `<div class="no-ad-state">${escapeHtml(message)}</div>`;
}
}
dlApi.cmd.push(function(dlApi) {
dlApi.fetchNativeAd({
slot: 'branded_products',
div: CONFIG.containerId,
opts: {
offers_limit: CONFIG.maxProducts
},
tplCode: CONFIG.tplCode,
asyncRender: true
}).then(async function(ad) {
if (!ad) {
console.log('[Ad Server] No ad available');
renderEmptyState('No branded products available');
return;
}
console.log('[Ad Server] Response received:', ad);
const bannerImage = ad.fields?.bannerImage || '';
const offers = ad.fields?.feed?.offers || [];
const 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'}`);
});
console.log('[Ad Server] Note: Product IDs from ad server are ignored in this demo');
console.log('[Ad Server] Using mock product IDs instead:', MOCK_PRODUCT_IDS.slice(0, CONFIG.maxProducts).join(', '));
const products = await fetchProductDetailsFromShop();
const productsWithTracking = products.map((product, index) => ({
...product,
trackingUrl: ad.meta.adclick
? ad.meta.adclick + encodeURIComponent(offers[index]?.offer_url || '')
: (offers[index]?.offer_url || '')
}));
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'}`);
});
renderBrandedProducts(bannerImage, productsWithTracking, dsaInfo);
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');
});
dlApi.fetch();
});
})();
</script>