You spend budget on ad banners, but their CTR doesn't reach 1%. The problem is that everyone sees the same offer. We solve this with smart banners: dynamic blocks that show each user exactly the products they viewed or that the algorithm considers relevant. The result is a 2-3x increase in CTR and a 15-30% boost in conversion. The customer acquisition cost (CAC) also decreases by 20-30% thanks to more relevant targeting. Contact us for a free engineering consultation to get a preliminary estimate.
How View Tracking Works
Personalization starts with data collection. We use a JavaScript class ViewHistoryTracker that saves view history in localStorage and syncs with the server for authorized users. This is a minimally invasive solution — no cookie banner or GDPR consent required for anonymous sessions.
class ViewHistoryTracker { private readonly KEY = 'view_history'; private readonly MAX_ITEMS = 50; track(item: ViewedItem): void { const history = this.get(); 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); } 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; await fetch('/api/views', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(item), }); } } What Collaborative Filtering Provides
Simply showing the last viewed products gives low CTR — the user has already seen them. We use ranking with category weights and popularity, as well as collaborative filtering to build a user-item matrix. Recent views get higher weight, duplicates are excluded. This approach increases CTR by 2-3 times in our measurements.
function rankItems(history: ViewedItem[], candidates: CatalogItem[]): CatalogItem[] { const categoryWeights: Record<string, number> = {}; const viewedIds = new Set(history.map(i => i.id)); history.forEach((item, index) => { const recencyWeight = 1 / (index + 1); categoryWeights[item.category] = (categoryWeights[item.category] ?? 0) + recencyWeight; }); return candidates .filter(c => !viewedIds.has(c.id)) .map(candidate => ({ ...candidate, score: (categoryWeights[candidate.category] ?? 0) * (candidate.popularity ?? 1), })) .sort((a, b) => b.score - a.score) .slice(0, 6); } For large projects (from 10k products) we move the engine to the server on PHP/Laravel with a PostgreSQL matrix. We cache recommendations in Redis — API response time stays under 50 ms.
Case study: electronics e-commerce store with a catalog of 5000 products. After implementing the smart banner, CTR grew from 0.8% to 2.4%, conversion from banner to purchase was 12%. Average order value increased by 8% due to cross-sells.
Comparison of Implementation Approaches
| Parameter | Client-side tracking + server engine | External platform (Retail Rocket) |
|---|---|---|
| Implementation time | 3–5 days | 1–2 days |
| Recommendation accuracy | High (own algorithms) | High (ML models) |
| Maintenance cost | Low (own server) | Monthly subscription |
| Data control | Full | Partial (data transfer) |
| Scalability | Up to 100k products | From 10k to 1M+ products |
Comparison of Recommendation Algorithms
| Criterion | Client-side ranking | Server-side collaborative filtering |
|---|---|---|
| Data | History in localStorage | Purchase and view matrix |
| Accuracy | Medium | High |
| Server load | None | Moderate (with cache) |
| Cold start | Not an issue | Requires data |
Common Mistakes During Implementation
- Showing products the user just saw. Always exclude viewed items.
- Ignoring product popularity — category weight without popularity gives weak recommendations.
- Not caching server requests. Without Redis, each view generates an SQL query — with 10k users the database will crash.
- Not tracking banner clicks. Without analytics, you won't know real CTR.
- Forgetting mobile adaptation — the banner must display correctly on all devices.
How We Implement Smart Banner Rendering
A React (or Vue) component is embedded in the desired location: sidebar, inline, or sticky-bottom. It requests recommendations via API and displays products with image, name, and price. On click, an event is sent to Google Analytics.
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; 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 Endpoint for Recommendations
We use a Laravel controller that excludes viewed products and ranks by category and popularity. For speed we add Redis cache.
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]); } } When to Connect External Platforms
For stores with a catalog from 10k products or when a ready ML model is needed, we integrate Retail Rocket or Mindbox. Basic Retail Rocket integration takes 1-2 days:
rrApi.view(123456); rrApi.addToBasket(123456); rrApiOnReady(function() { rrApi.recommend('block_id_from_rr_panel', { callback: function(items) { renderRecommendations(items); } }); }); What's Included
- Designing tracking and data storage schema
- Developing client-side banner component with support for different placements
- Server endpoint for recommendations with caching
- Analytics integration (GA/Yandex.Metrica)
- API documentation and embedding instructions
- Handover of repository and hosting access
- Training your developer (1 hour online)
- 3-month warranty on correct algorithm operation
Timeline and Pricing
Implementation timeline: from 3 to 8 days depending on complexity. Pricing is calculated individually — we will evaluate the project after a brief. Over our work, we have implemented smart banners for dozens of e-commerce stores, with an average CTR increase of 180%. Our team has 5+ years of experience in e-commerce personalization.
If you want to increase conversion from ad blocks without extra cost, contact us — we will discuss your task and offer the best solution. Get a free engineering consultation for a preliminary estimate.







