Smart Banner Implementation (Personalized Ads) on Website
Smart banners are ad blocks with dynamically generated content based on user behavior, browsing history, and product catalog data. Unlike static banners, they show exactly what user already viewed or what algorithm predicts as relevant.
How It Works
System has three parts: view tracking, recommendation engine, and banner rendering. Viewed product data accumulates in browser and/or server, engine ranks positions, banner built from template.
View Tracking
class ViewHistoryTracker {
private readonly KEY = 'view_history';
private readonly MAX_ITEMS = 50;
track(item: ViewedItem): void {
const history = this.get();
// Remove duplicate item, add to start
const filtered = history.filter(i => i.id !== item.id);
const updated = [
{ ...item, viewed_at: Date.now() },
...filtered,
].slice(0, this.MAX_ITEMS);
localStorage.setItem(this.KEY, JSON.stringify(updated));
this.syncToServer(item); // async
}
get(): ViewedItem[] {
try {
return JSON.parse(localStorage.getItem(this.KEY) ?? '[]');
} catch {
return [];
}
}
getRecent(count = 10): ViewedItem[] {
return this.get().slice(0, count);
}
private async syncToServer(item: ViewedItem): Promise<void> {
if (!getAuthToken()) return; // sync only for authorized
await fetch('/api/views', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item),
});
}
}
// Example on product page
const tracker = new ViewHistoryTracker();
tracker.track({
id: '123',
type: 'product',
category: 'laptops',
price: 89900,
title: 'MacBook Pro 14',
image: '/images/mbp14.jpg',
url: '/catalog/laptops/macbook-pro-14',
});
Recommendation Engine
Basic algorithm—collaborative filtering based on view history with recency weights:
function rankItems(
history: ViewedItem[],
candidates: CatalogItem[]
): CatalogItem[] {
const categoryWeights: Record<string, number> = {};
const viewedIds = new Set(history.map(i => i.id));
// Calculate category weights from history
history.forEach((item, index) => {
const recencyWeight = 1 / (index + 1); // first views more important
categoryWeights[item.category] = (categoryWeights[item.category] ?? 0) + recencyWeight;
});
return candidates
.filter(c => !viewedIds.has(c.id)) // remove already viewed
.map(candidate => ({
...candidate,
score: (categoryWeights[candidate.category] ?? 0) * (candidate.popularity ?? 1),
}))
.sort((a, b) => b.score - a.score)
.slice(0, 6);
}
For serious projects, move engine to server—PHP/Python—using user×product matrix.
Smart Banner Rendering
interface SmartBannerProps {
placement: 'sidebar' | 'inline' | 'sticky-bottom';
title?: string;
}
function SmartBanner({ placement, title = 'You viewed' }: SmartBannerProps) {
const [items, setItems] = useState<CatalogItem[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const history = tracker.getRecent();
if (history.length === 0) {
setLoading(false);
return;
}
fetch('/api/recommendations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
viewed_ids: history.map(i => i.id),
categories: [...new Set(history.map(i => i.category))],
limit: placement === 'sidebar' ? 4 : 6,
}),
})
.then(r => r.json())
.then(data => setItems(data.items))
.finally(() => setLoading(false));
}, [placement]);
if (loading) return <BannerSkeleton count={4} />;
if (items.length === 0) return null; // don't show empty banner
return (
<div className={`smart-banner smart-banner--${placement}`}>
<h3 className="smart-banner__title">{title}</h3>
<div className="smart-banner__grid">
{items.map(item => (
<a
key={item.id}
href={item.url}
className="smart-banner__item"
onClick={() => trackBannerClick(item, placement)}
>
<img src={item.image} alt={item.title} loading="lazy" />
<span className="smart-banner__name">{item.title}</span>
<span className="smart-banner__price">{formatPrice(item.price)}</span>
</a>
))}
</div>
</div>
);
}
function trackBannerClick(item: CatalogItem, placement: string): void {
gtag('event', 'smart_banner_click', {
item_id: item.id,
item_name: item.title,
placement,
item_category: item.category,
});
}
Server Recommendations Endpoint
// RecommendationsController.php
class RecommendationsController extends Controller
{
public function index(Request $request): JsonResponse
{
$viewedIds = $request->input('viewed_ids', []);
$categories = $request->input('categories', []);
$limit = min($request->input('limit', 6), 12);
$items = Product::query()
->whereNotIn('id', $viewedIds)
->where('is_active', true)
->where(function ($q) use ($categories) {
$q->whereIn('category_slug', $categories)
->orWhere('is_bestseller', true);
})
->orderByRaw('
CASE WHEN category_slug = ANY(?) THEN 1 ELSE 2 END,
popularity DESC
', ['{' . implode(',', $categories) . '}'])
->limit($limit)
->get(['id', 'title', 'price', 'image', 'url', 'category_slug']);
return response()->json(['items' => $items]);
}
}
Personalization via External Platforms
For e-commerce with large catalog (10k+ products), consider specialized engines:
- Retail Rocket — Russian personalization service, integrates via JS pixel
- Mindbox — CDP with recommendation module, API integration
- Dynamic Yield — enterprise solution with ML recommendations
Basic Retail Rocket integration:
// Track product view
rrApi.view(123456); // Product ID in Retail Rocket
// Track add to cart
rrApi.addToBasket(123456);
// Recommendation block renders via callback
rrApiOnReady(function() {
rrApi.recommend('block_id_from_rr_panel', {
callback: function(items) {
renderRecommendations(items);
}
});
});
Timeline
Own tracking + recommendation engine + banner component: 3-5 days. Retail Rocket or similar integration: 1-2 days. Server recommendation engine with user×product matrix on PostgreSQL: 3-5 days.







