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

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

Разработка и обслуживание любых видов сайтов:

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

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

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

Интернет-магазины и маркетплейсыПростой~1 день
Часто задаваемые вопросы

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

Представьте: пользователь просматривает товар за товаром, но при возврате на страницу категории не может найти тот самый кед или кофеварку. История просмотров — это не просто фича, а инструмент возврата. Однако её реализация часто страдает от дублирования в массиве ID, N+1 запросов и проблем с порядком товаров. Мы интегрируем блок «Недавно просмотренные» с клиентским кешированием в localStorage для гостей и серверной синхронизацией для авторизованных пользователей, используя один batch-запрос для загрузки данных. Разработка занимает 1–2 дня, а возвраты к карточкам вырастают до 15%. Свяжитесь с нами — оценим ваш проект за день.

Хранение истории в localStorage

Для неавторизованных пользователей история хранится в localStorage. Мы используем хук useRecentlyViewed, который при каждом просмотре добавляет ID товара в начало массива, удаляя дубликаты. Максимум — 20 записей. Вот реализация:

// 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 минут, чтобы не перегружать сервер при повторных посещениях:

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. Такой подход ускоряет загрузку блока в 3–5 раз, а блокочие метрики (LCP, FID) не страдают.

Подход Время загрузки (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 — загрузка блока тормозит.
  • Не очищают историю при логине — данные гостя смешиваются с данными пользователя.
  • Синхронизируют историю слишком часто — избыточная нагрузка на сервер.

Что входит в работу

  • анализ текущей архитектуры и мест размещения блока
  • реализация хука useRecentlyViewed с localStorage
  • API-эндпоинты для серверной синхронизации (Laravel)
  • batch-загрузка данных с кешированием на клиенте
  • UI-компонент блока (карусель) с кнопкой очистки
  • инструкция по деплою и тестированию

Наш опыт — более 50 проектов с историей просмотров. Гарантируем корректную работу на всех этапах. Свяжитесь с нами, чтобы обсудить ваш проект — оценим его за день. Закажите реализацию блока «Недавно просмотренные» и получите прирост конверсии без сложных интеграций.

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

Последние работы

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