כיצד לבנות תכונת מוצרים שנצפו לאחרונה לחוויית משתמש טובה יותר

כאשר לקוח מאבד עניין במוצר מסוים בין עשרות כרטיסי מוצר, הדבר פוגע ישירות במכירות. אנו בונים בלוק 'Recently Viewed' שמחזיר את הלקוח למוצר הרצוי בלחיצה אחת. הצוות שלנו מספק את הפרויקט במפתח מלא—מהגדרת אחסון ההיסטוריה ועד לשילוב באתר שלך—תוך הבטחת ביצועים אמינים ותמיכה מתמשכת.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
כיצד לבנות תכונת מוצרים שנצפו לאחרונה לחוויית משתמש טובה יותר
פשוט
~1 יום

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1365
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1330
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1077
  • פיתוח CRM עבור Chasseurs
    פיתוח CRM עבור Chasseurs
    1113
  • פיתוח אתר עבור SBH Partners
    פיתוח אתר עבור SBH Partners
    1201
  • פיתוח אתר עבור Red Pear
    פיתוח אתר עבור Red Pear
    619

בניית תכונת היסטוריית צפייה ללקוחות בחנות המסחר האלקטרוני שלך

תארו לעצמכם משתמש שגולש בין מוצרים, אבל כשהם חוזרים לדף הקטגוריה, הם לא מוצאים את נעלי הספורט או מכונת הקפה ההיא. היסטוריית צפייה היא לא רק תכונה—זה כלי לשימור לקוחות. עם זאת, היישום שלה סובל לעיתים קרובות ממזהים כפולים במערכים, שאילתות N+1, ובעיות סדר. אנו משלבים בלוק 'נצפה לאחרונה' עם שמירה במטמון בצד הלקוח ב-localStorage עבור אורחים וסנכרון שרת עבור משתמשים מחוברים, תוך שימוש בבקשה אחת מרוכזת לטעינת נתונים. הפיתוח אורך 1–2 ימים, עם עלות אופיינית של $500–$1,500 ליישום, והחזרות לדפי מוצר עולות בעד 15%. צרו קשר — נבחן את הפרויקט שלכם תוך יום.

תהליך היישום שלב אחר שלב

  1. הקמת הוק localStorage — צרו הוק React המאחסן מזהי מוצרים ב-localStorage, עם מקסימום 20 רשומות והסרת כפילויות.
  2. יצירת נקודת קצה API מרוכזת — ב-Laravel, בנו נקודת קצה useRecentlyViewed המקבלת מזהים מופרדים בפסיקים ומחזירה את נתוני המוצר המינימליים בשאילתה אחת.
  3. בניית רכיב UI — פתחו רכיב קרוסלה המציג עד 8 מוצרים, תוך שמירה על הסדר מהמערך ההיסטורי.
  4. סנכרון בעת התחברות — כשהמשתמש מתחבר, מיזגו את נתוני ה-localStorage עם ההיסטוריה בצד השרת באמצעות Sanctum.
  5. בדיקה ואופטימיזציה — ודאו ביצועים: טעינה מרוכזת מהירה פי 3–5 מבקשות בודדות.

אחסון היסטוריה ב-localStorage

עבור משתמשים לא מחוברים, אנו מאחסנים היסטוריה ב-localStorage. אנו משתמשים בהוק // 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 }; }; הדוחף את מזהה המוצר לתחילת המערך בכל צפייה, תוך הסרת כפילויות. מקסימום 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)]);
}

גישה זו שומרת על היסטוריה בין מכשירים ומונעת אובדן נתונים בעת מעבר בין דפדפנים. שיטת הסנכרון ההיברידית מהירה פי 10 עבור אורחים בהשוואה לסנכרון שרת בלבד, ומשפרת את חוויית המשתמש באמצעות התאמה אישית ואופטימיזציית ביצועים.

טעינה מרוכזת של נתונים

ההיסטוריה מאחסנת רק // 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)]); } . כדי להציג את הבלוק, אנו זקוקים לנתוני מוצר מלאים — בקשת 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>
  );
};

נקודת הקצה טוענת בדיוק את השדות הדרושים לכרטיס ומחזירה מוצרים בסדר המזהים שהועברו:

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));
}

למה טעינה מרוכזת מהירה יותר?

אם תבקשו כל מוצר בנפרד, תקבלו N שאילתות מסד נתונים — עבור 8 מוצרים, זה 8 סבבים. נקודת קצה מרוכזת מחזירה הכל בבת אחת, ומפחיתה את עומס השרת פי 8. בצד הלקוח, אנו שומרים על הסדר על ידי מיון מול מערך המזהים המקורי. גישה זו מאיצה את טעינת הבלוק פי 3–5 ואינה פוגעת ב-Core Web Vitals (LCP, FID).

גישה זמן טעינה (8 מוצרים) עומס שרת
שאילתות N+1 800–1200 אלפיות השנייה 8 שאילתות
בקשה מרוכזת 150–300 אלפיות השנייה שאילתה אחת

איך להוציא את המוצר הנוכחי?

בדף של מוצר X, אנו מוציאים את מוצר X מהבלוק 'נצפה לאחרונה' — אחרת הוא יופיע בהכרח ראשון. אנו מיישמים פילטר פשוט: product_id. זה מונע מהמשתמש לראות את אותו כרטיס מוצר ממש ליד התוכן הראשי.

השוואת שיטות סנכרון

שיטה אורחים מחוברים מהירות עומס שרת
localStorage בלבד כן לא מיידי ללא
שרת בלבד לא כן ~5 אלפיות השנייה בינוני
היברידי (localStorage + שרת) כן כן מיידי + 5 אלפיות השנייה נמוך

הגישה ההיברידית היא אופטימלית: היא משלבת את המהירות של אחסון מקומי עם העמידות של אחסון בצד השרת.

היכן למקם את הבלוק

  • דף הבית: עבור משתמשים חוזרים — במקום או לצד מוצרים פופולריים
  • דף קטגוריה: בתחתית, אחרי רשת המוצרים הראשית
  • דף מוצר: מתחת לבלוק 'מוצרים דומים'
  • עגלת קניות: בסרגל צדדי במחשב
  • תוצאות חיפוש ריקות: 'אולי אתם מחפשים משהו שצפיתם בו לאחרונה?'

פרטיות ושליטה

כפתור 'נקה היסטוריה' נותן למשתמשים שליטה על הנתונים שלהם. אנו יכולים להוסיף TTL ידני ב-localStorage — למשל, 30 יום. למידע נוסף על localStorage ב-תיעוד MDN.

טעויות יישום נפוצות
  • אי הוצאת המוצר הנוכחי — המשתמש רואה אותו ראשון.
  • שימוש בשאילתות N+1 במקום בקשה מרוכזת — טעינת הבלוק איטית.
  • אי ניקוי היסטוריה בעת התחברות — נתוני אורח מתערבבים עם נתוני משתמש.
  • סנכרון היסטוריה בתדירות גבוהה מדי — עומס שרת מיותר.

מה כלול בעבודה

  • ניתוח הארכיטקטורה הנוכחית ואפשרויות המיקום
  • יישום הוק 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> ); }; עם localStorage
  • נקודות קצה API לסנכרון שרת (Laravel)
  • טעינת נתונים מרוכזת עם שמירה במטמון בצד הלקוח
  • רכיב UI לבלוק (קרוסלה) עם כפתור ניקוי
  • הוראות פריסה ובדיקות

הניסיון שלנו כולל מעל 50 פרויקטים עם היסטוריית צפייה. אנו מבטיחים פעולה תקינה בכל השלבים. צרו קשר כדי לדון בפרויקט שלכם — נבחן אותו תוך יום. הזמינו יישום של בלוק 'נצפה לאחרונה' וקבלו שיפור בהמרות ללא אינטגרציות מורכבות.