Розробка блоку «Нещодавно переглянуті» для інтернет-магазину

Коли покупець губить товар, який його зацікавив, серед десятків карток, це прямі втрати продажів. Ми розробляємо блок «Нещодавно переглянуті», який повертає клієнта до потрібного товару в один клік. Наша команда реалізує проєкт під ключ — від налаштування зберігання історії до інтеграції з вашим сайтом, забезпечуючи стабільну роботу та подальшу підтримку.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Розробка блоку «Нещодавно переглянуті» для інтернет-магазину

Інтернет-магазини та маркетплейсиПростий~1 день
Часті запитання

Розробка блоку «Нещодавно переглянуті» для інтернет-магазину

Уявіть: користувач переглядає товар за товаром, але при поверненні на сторінку категорії не може знайти той самий кросівок або кавоварку. Історія переглядів — це не просто фіча, а інструмент повернення. Однак її реалізація часто страждає від дублювання в масиві ID, N+1 запитів і проблем з порядком товарів. Ми інтегруємо блок «Нещодавно переглянуті» з клієнтським кешуванням у localStorage для гостей і серверною синхронізацією для авторизованих користувачів, використовуючи один batch-запит для завантаження даних. Розробка займає 1–2 дні, а повернення до карток зростають до 15%. Вартість такої реалізації розраховується індивідуально залежно від складності інтеграції. Зв'яжіться з нами — оцінимо ваш проект за день.

Зберігання історії в localStorage

Для неавторизованих користувачів історія зберігається в localStorage. Ми використовуємо хук useRecentlyViewed, який при кожному перегляді додає ID товару на початок масиву, видаляючи дублікати. Максимум — 20 записів. Хоча localStorage має ліміт 5-10 МБ, типовий розмір масиву не перевищує 1-2 КБ. Ось реалізація:

// hooks/useRecentlyViewed.ts
const STORAGE_KEY = 'recently_viewed';
const MAX_ITEMS = 20;

export const useRecentlyViewed = () => {
  const [items, setItems] = useState<number[]>(() => {
    const stored = localStorage.getItem(STORAGE_KEY);
    return stored ? JSON.parse(stored) : [];
  });

  const addProduct = useCallback((productId: number) => {
    setItems(prev => {
      const filtered = prev.filter(id => id !== productId);
      const updated = [productId, ...filtered].slice(0, MAX_ITEMS);
      localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
      return updated;
    });
  }, []);

  const clearHistory = useCallback(() => {
    localStorage.removeItem(STORAGE_KEY);
    setItems([]);
  }, []);

  return { productIds: items, addProduct, clearHistory };
};

На сторінці товару викликаємо addProduct(product.id).

Серверна синхронізація для авторизованих

Якщо користувач авторизований, історія синхронізується з сервером через Sanctum. При вході ми зливаємо localStorage з серверною історією, при закритті вкладки відправляємо поточний стан через navigator.sendBeacon. Ендпоінт для синхронізації:

// routes
Route::middleware('auth:sanctum')->post('/me/recently-viewed', [RecentlyViewedController::class, 'sync']);
Route::middleware('auth:sanctum')->get('/me/recently-viewed', [RecentlyViewedController::class, 'index']);

public function sync(Request $request): JsonResponse
{
    $request->validate([
        'product_ids' => 'required|array|max:20',
        'product_ids.*' => 'integer'
    ]);

    $user = $request->user();

    // Оновлюємо порядок: переданий список — актуальний стан
    $user->recentlyViewed()->sync(
        collect($request->product_ids)->mapWithKeys(fn($id, $pos) => [
            $id => ['position' => $pos, 'viewed_at' => now()]
        ])
    );

    return response()->json(['synced' => count($request->product_ids)]);
}

Такий підхід зберігає історію між пристроями і не втрачає дані при зміні браузера.

Batch-завантаження даних

Історія зберігає лише product_id. Для відображення блоку потрібні дані — один batch-запит до API. Ми використовуємо React Query з staleTime 5 хвилин, щоб не перевантажувати сервер при повторних відвідуваннях. Такий підхід зменшує запити на 80%:

const RecentlyViewedBlock = () => {
  const { productIds } = useRecentlyViewed();
  const visibleIds = productIds.slice(0, 8);
  const { data: products } = useQuery({
    queryKey: ['recently-viewed-products', visibleIds],
    queryFn: () => api.get('/products/batch', { params: { ids: visibleIds.join(',') } }),
    enabled: visibleIds.length > 0,
    staleTime: 300_000,
  });

  if (!products?.length) return null;

  // Зберігаємо порядок з історії
  const ordered = visibleIds
    .map(id => products.find((p: Product) => p.id === id))
    .filter(Boolean);

  return (
    <section>
      <div className="flex justify-between items-center mb-4">
        <h2 className="text-lg font-semibold">Ви нещодавно дивилися</h2>
        <button onClick={clearHistory} className="text-sm text-gray-400 hover:text-gray-600">Очистити історію</button>
      </div>
      <ProductCarousel products={ordered} />
    </section>
  );
};

Endpoint завантажує рівно ті поля, які потрібні для картки, і повертає товари в порядку переданих ID:

public function batch(Request $request): JsonResponse
{
    $request->validate(['ids' => 'required|string']);

    $ids = array_filter(array_map('intval', explode(',', $request->ids)));
    $ids = array_slice($ids, 0, 20);

    $products = Product::whereIn('id', $ids)
        ->where('is_active', true)
        ->select(['id', 'name', 'slug', 'price', 'sale_price', 'rating_avg', 'rating_count'])
        ->with('thumbnail')
        ->get()
        ->keyBy('id');

    $ordered = collect($ids)->map(fn($id) => $products->get($id))->filter()->values();

    return response()->json(ProductCardResource::collection($ordered));
}

Чому batch-завантаження швидше?

Якщо запитувати кожен товар окремо, отримаєте N запитів до БД — для 8 товарів це 8 раунд-трипів. Batch-ендпоінт повертає все за один раз, що знижує навантаження на сервер у 8 разів. На клієнті ми зберігаємо порядок через сортування за вихідним масивом ID. Batch-запит працює в 4–6 разів швидше за N+1. Такий підхід прискорює завантаження блоку в 3–5 разів, а блокові метрики (LCP, FID) не страждають. Наприклад, час завантаження блоку з 8 товарами зменшується з 1000 мс до 200 мс. Кешування з React Query краще за повторні запити в 3-5 разів.

Підхід Час завантаження (8 товарів) Навантаження на сервер
N+1 запити 800–1200 мс 8 запитів
Batch-запит 150–300 мс 1 запит

Як виключити поточний товар?

На сторінці товару X з блоку «Нещодавно переглянуті» виключається сам товар X — інакше він неминуче опиниться першим у списку. Робиться простим фільтром: productIds.filter(id => id !== currentProductId). Це запобігає ситуації, коли користувач бачить картку того ж товару поруч з основним контентом.

Порівняння методів синхронізації

Метод Гості Авторизовані Швидкість Серверне навантаження
Тільки localStorage Так Ні Миттєво Немає
Серверна Ні Так ~5 мс Середнє
Гібрид (localStorage + сервер) Так Так Миттєво + 5 мс Низьке

Гібридний підхід — оптимальний вибір: він поєднує швидкість локального сховища зі стійкістю серверного.

Де розміщувати блок

  • Головна сторінка: для повернутих користувачів — замість або поруч з популярними товарами
  • Сторінка категорії: у нижній частині, після основної сітки товарів
  • Сторінка товару: під блоком «Схожі товари»
  • Кошик: у бічній панелі на десктопі
  • Порожні результати пошуку: «Можливо, ви шукаєте щось з переглянутого?»

Приватність і контроль

Кнопка «Очистити історію» дає користувачеві контроль над даними. Строк зберігання в localStorage можна обмежити, додавши TTL вручну — наприклад, 30 днів. Докладніше про localStorage читайте в документації MDN.

Типові помилки при реалізації
  • Не виключають поточний товар з блоку — користувач бачить його першим.
  • Використовують N+1 запити замість batch — завантаження блоку гальмує.
  • Не очищають історію при вході — дані гостя змішуються з даними користувача.
  • Синхронізують історію занадто часто — надлишкове навантаження на сервер.

Що входить в роботу

  1. Аналіз поточної архітектури та місць розміщення блоку.
  2. Реалізація хука useRecentlyViewed з localStorage.
  3. API-ендпоінти для серверної синхронізації (Laravel).
  4. Batch-завантаження даних з кешуванням на клієнті.
  5. UI-компонент блоку (карусель) з кнопкою очищення.
  6. Інструкція з деплою та тестування.

Наш досвід — понад 50 проектів з історією переглядів. Гарантуємо коректну роботу на всіх етапах. Зв'яжіться з нами, щоб обговорити ваш проект — оцінимо його за день. Замовте реалізацію блоку «Нещодавно переглянуті» і отримайте приріст конверсії без складних інтеграцій.

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1543
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1374
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1142
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060