Реалізація AI-персоналізації контенту на сайті
Персоналізація — це показувати різним користувачам різний контент на одній сторінці: інший порядок блоків, іншу заголовок, інший CTA, інші товари. AI тут управляє вибором варіантів на основі профілю та контексту користувача.
Рівні персоналізації
Поверхневий — змінні в тексті (ім'я користувача, місто), динамічні заголовки. Без ML.
Сегментний — контент для сегментів (новичок/досвідчений, B2B/B2C, регіон). Правила на основі атрибутів.
Поведінковий — контент на основі історії дій: що переглядав, купував, читав.
Предиктивний — AI передбачає наступну дію та оптимізує контент під конверсію.
Профіль користувача
// Накопиченням профіля в real-time
class UserProfileManager {
constructor(userId) {
this.userId = userId;
this.profileKey = `profile:${userId}`;
}
async trackEvent(event) {
const updates = {};
switch (event.type) {
case 'page_view':
updates[`categories.${event.category}`] = { increment: 1 };
updates['total_sessions'] = { increment: 1 };
break;
case 'purchase':
updates['purchases_count'] = { increment: 1 };
updates['total_spent'] = { increment: event.amount };
updates['last_purchase'] = event.timestamp;
break;
case 'content_read':
updates['read_count'] = { increment: 1 };
updates[`topics.${event.topic}`] = { increment: event.readTime };
break;
}
await redis.hIncrBy(this.profileKey, updates);
await redis.expire(this.profileKey, 86400 * 30); // 30 днів
}
async getProfile() {
const raw = await redis.hGetAll(this.profileKey);
return {
topCategories: getTopN(raw.categories, 5),
topTopics: getTopN(raw.topics, 5),
purchasesCount: parseInt(raw.purchases_count || 0),
totalSpent: parseFloat(raw.total_spent || 0),
segment: this.classifySegment(raw),
};
}
classifySegment(profile) {
if (profile.purchases_count > 10) return 'loyal';
if (profile.purchases_count > 0) return 'buyer';
if (profile.total_sessions > 5) return 'engaged';
return 'new';
}
}
Персоналізована головна сторінка
// API ендпоінт для персоналізованої головної
async function getHomepageContent(userId, context) {
const profile = await getUserProfile(userId);
const geo = context.country || 'UA';
const device = context.device || 'desktop';
// Паралельно отримуємо всі блоки
const [hero, featured, recommendations, cta] = await Promise.all([
getPersonalizedHero(profile, geo),
getFeaturedContent(profile.topCategories),
getPersonalizedProducts(userId, profile, 8),
getPersonalizedCTA(profile),
]);
return { hero, featured, recommendations, cta };
}
async function getPersonalizedHero(profile, geo) {
const variants = await getHeroVariants(); // A/B варіанти з CMS
// Правила вибору варіанту
if (profile.segment === 'loyal') {
return variants.find(v => v.segment === 'loyal') || variants[0];
}
if (geo === 'UA') {
return variants.find(v => v.geo === 'UA') || variants[0];
}
if (profile.topCategories.includes('sale')) {
return variants.find(v => v.theme === 'deals') || variants[0];
}
return variants[0]; // default
}
LLM-генерація персоналізованого тексту
Для висок оцінених користувачів — динамічні заголовки та описи:
async function generatePersonalizedHeadline(product, userProfile) {
const cacheKey = `headline:${product.id}:${userProfile.segment}`;
const cached = await redis.get(cacheKey);
if (cached) return cached;
const prompt = `
Згенеруй заголовок карточки товару (до 10 слів) для користувача.
Товар: ${product.name}, категорія: ${product.category}
Профіль: сегмент=${userProfile.segment}, інтереси=${userProfile.topTopics.join(',')}
Тон: професійний, без клішеїв.
Поверни тільки текст заголовка.
`;
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
max_tokens: 30,
temperature: 0.7,
});
const headline = response.choices[0].message.content.trim();
await redis.setex(cacheKey, 3600 * 6, headline); // кеш 6 годин
return headline;
}
Динамічні CTA
const CTA_VARIANTS = {
new: {
text: 'Почати безплатно',
subtext: 'Без кредитної карти',
color: 'blue',
},
engaged: {
text: 'Спробувати Pro',
subtext: '14 днів безплатно',
color: 'green',
},
buyer: {
text: 'Оновити тариф',
subtext: 'Розблокуй всі функції',
color: 'purple',
},
loyal: {
text: 'Реферальна програма',
subtext: 'Заробляй за друзів',
color: 'orange',
},
};
function PersonalizedCTA({ userId }) {
const { profile } = useUserProfile(userId);
const variant = CTA_VARIANTS[profile.segment] || CTA_VARIANTS.new;
return (
<button
className={`cta-button cta-${variant.color}`}
onClick={() => {
trackCTAClick(userId, profile.segment);
navigate(getCtaDestination(profile.segment));
}}
>
{variant.text}
<span>{variant.subtext}</span>
</button>
);
}
Контекстна персоналізація (без авторизації)
Для анонімних користувачів — сигнали з поточної сесії:
function getContextualSignals(request) {
return {
referrer: request.headers.referer, // звідки прийшов
utm_source: request.query.utm_source, // рекламний канал
utm_campaign: request.query.utm_campaign,
geo: request.headers['cf-ipcountry'], // Cloudflare geo
device: detectDevice(request.headers['user-agent']),
timeOfDay: getTimeOfDay(request.headers['x-forwarded-for']),
entryPage: request.url,
};
}
function getPersonalizationForAnonymous(signals) {
// Прийшов з реклами "знижки" → показати sale-банер
if (signals.utm_campaign?.includes('sale')) {
return { hero: 'sale', cta: 'discount' };
}
// Мобільний + вечір → показати app download
if (signals.device === 'mobile' && signals.timeOfDay === 'evening') {
return { hero: 'mobile-app', cta: 'download' };
}
// B2B сигнал з LinkedIn
if (signals.referrer?.includes('linkedin')) {
return { hero: 'b2b', cta: 'demo' };
}
return { hero: 'default', cta: 'default' };
}
Edge Персоналізація (Cloudflare Workers / Vercel Edge)
Для максимальної швидкості — персоналізація прямо на Edge, до Origin:
// Cloudflare Worker
export default {
async fetch(request, env) {
const url = new URL(request.url);
const userId = getCookie(request, 'user_id');
const segment = userId
? await env.KV.get(`segment:${userId}`)
: 'anonymous';
// Модифікуємо запит до Origin з сегментом
const newRequest = new Request(request.url, {
...request,
headers: {
...Object.fromEntries(request.headers),
'X-User-Segment': segment || 'new',
'X-User-Geo': request.cf.country,
},
});
return fetch(newRequest);
}
};
Вимірювання ефекту
-- Конверсія за сегментами та варіантами персоналізації
SELECT
p.variant,
p.segment,
COUNT(DISTINCT p.user_id) AS shown,
COUNT(DISTINCT c.user_id) AS converted,
ROUND(COUNT(DISTINCT c.user_id)::numeric / COUNT(DISTINCT p.user_id) * 100, 2) AS cvr
FROM personalization_events p
LEFT JOIN conversion_events c
ON c.user_id = p.user_id
AND c.created_at BETWEEN p.created_at AND p.created_at + INTERVAL '7 days'
WHERE p.created_at >= NOW() - INTERVAL '30 days'
GROUP BY p.variant, p.segment
ORDER BY cvr DESC;
Терміни
- Сегментна персоналізація (правила) — 3–4 дні
- Поведінковий профіль + персоналізація рекомендацій — плюс 3–4 дні
- LLM-генерація динамічних заголовків — плюс 2 дні
- Edge персоналізація на Cloudflare Workers — плюс 2 дні
- Повна система з аналітикою, A/B, 5+ варіантами — 3–4 тижні







