Skip to main content

Overview

Product Listing Pages (PLPs) are curated collections of products, typically used for category pages, collections, or promotional landing pages. PLPs support pinned products, intelligent ranking, and merchandising rules.

Basic Usage

import { Refine } from '@refine-ai/sdk';

const refine = new Refine({
  apiKey: process.env.REFINE_API_KEY,
  organizationId: 'org_abc123',
  catalogId: 'cat_xyz789'
});

// Fetch a PLP by its configuration ID
const plp = await refine.plp.get('plp_summer_collection');

// Get all products
console.log(plp.results);

// Get pinned products specifically
const pinnedProducts = plp.getPinnedProducts();

Creating PLPs

PLPs are created and managed in the Refine dashboard: Dashboard → Product Listing Pages → New PLP Configuration options include:
OptionDescription
NameInternal name for the PLP
SlugURL-friendly identifier
ProductsSelected products for the collection
Pinned ProductsProducts pinned to specific positions
RankingSort order (manual, popularity, newest, etc.)
FiltersDefault filters applied to the collection

Response Structure

interface PLPResponse {
  results: PLPProduct[];
  pinnedPositions: Map<string, number>;
  totalResults: number;
  configId: string;
}

interface PLPProduct {
  productId: string;
  title: string;
  price: number;
  imageUrl: string;
  metadata: Record<string, any>;
  isPinned: boolean;
  pinnedPosition?: number;
}

Pinned Products

Pinned products appear at specific positions regardless of the ranking algorithm:
const plp = await refine.plp.get('plp_summer_collection');

// Get only pinned products
const pinned = plp.getPinnedProducts();
// Returns products sorted by their pinned position

// Check if a specific product is pinned
const product = plp.results[0];
if (product.isPinned) {
  console.log(`Pinned at position ${product.pinnedPosition}`);
}

Tracking PLP Views

Track when users view and interact with PLPs:
const plp = await refine.plp.get('plp_summer_collection');

const plpContext = refine.events.trackItemsServed({
  surface: 'category_page',
  source: 'plp',
  itemIds: plp.results.map(p => p.productId),
  totalResults: plp.totalResults,
  metadata: {
    plpId: 'plp_summer_collection',
    plpName: 'Summer Collection'
  }
});

// Track interactions
plpContext.trackClick(productId, position);
plpContext.trackView(productId, position);
plpContext.trackAddToCart(productId);

With Filters

Apply additional filters at runtime:
const plp = await refine.plp.get('plp_summer_collection', {
  filters: [
    { field: 'price', operator: 'lte', value: 100 },
    { field: 'metadata.size', operator: 'in', value: ['S', 'M', 'L'] }
  ]
});

Complete Implementation

import { Refine } from '@refine-ai/sdk';

const refine = new Refine({
  apiKey: process.env.REFINE_API_KEY,
  organizationId: 'org_abc123',
  catalogId: 'cat_xyz789'
});

class CategoryPage {
  private plpContext: any;

  async load(categorySlug: string) {
    const plp = await refine.plp.get(categorySlug);

    // Track the PLP serve
    this.plpContext = refine.events.trackItemsServed({
      surface: 'category_page',
      source: 'plp',
      itemIds: plp.results.map(p => p.productId),
      totalResults: plp.totalResults,
      metadata: { categorySlug }
    });

    this.render(plp);
    return plp;
  }

  private render(plp: PLPResponse) {
    const container = document.getElementById('products')!;

    container.innerHTML = plp.results.map((product, index) => `
      <div class="product ${product.isPinned ? 'pinned' : ''}"
           data-id="${product.productId}"
           data-position="${index}">
        ${product.isPinned ? '<span class="badge">Featured</span>' : ''}
        <img src="${product.imageUrl}" alt="${product.title}" />
        <h3>${product.title}</h3>
        <p>$${product.price.toFixed(2)}</p>
      </div>
    `).join('');

    this.attachEventHandlers();
  }

  private attachEventHandlers() {
    document.querySelectorAll('.product').forEach(el => {
      const productId = (el as HTMLElement).dataset.id!;
      const position = parseInt((el as HTMLElement).dataset.position!);

      el.addEventListener('click', () => {
        this.plpContext?.trackClick(productId, position);
      });
    });
  }
}

// Usage
const categoryPage = new CategoryPage();
await categoryPage.load('summer-collection');

React Integration

// hooks/usePLP.ts
import { useState, useEffect, useRef, useCallback } from 'react';
import { refine } from '@/lib/refine';

export function usePLP(plpId: string) {
  const [products, setProducts] = useState<any[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  const contextRef = useRef<any>(null);

  useEffect(() => {
    setIsLoading(true);
    
    refine.plp.get(plpId)
      .then(plp => {
        setProducts(plp.results);
        contextRef.current = refine.events.trackItemsServed({
          surface: 'category_page',
          source: 'plp',
          itemIds: plp.results.map(p => p.productId),
          totalResults: plp.totalResults,
          metadata: { plpId }
        });
      })
      .catch(setError)
      .finally(() => setIsLoading(false));
  }, [plpId]);

  const trackClick = useCallback((productId: string, position: number) => {
    contextRef.current?.trackClick(productId, position);
  }, []);

  return { products, isLoading, error, trackClick };
}
// components/CategoryPage.tsx
import { usePLP } from '@/hooks/usePLP';

export function CategoryPage({ slug }: { slug: string }) {
  const { products, isLoading, error, trackClick } = usePLP(slug);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Failed to load category</div>;

  return (
    <div className="product-grid">
      {products.map((product, index) => (
        <div
          key={product.productId}
          className={`product ${product.isPinned ? 'featured' : ''}`}
          onClick={() => {
            trackClick(product.productId, index);
            window.location.href = `/products/${product.productId}`;
          }}
        >
          {product.isPinned && <span className="badge">Featured</span>}
          <img src={product.imageUrl} alt={product.title} />
          <h3>{product.title}</h3>
          <p>${product.price.toFixed(2)}</p>
        </div>
      ))}
    </div>
  );
}

Merchandising Best Practices

Pin strategically:
  • Pin high-margin products at positions 1-3
  • Pin new arrivals to increase visibility
  • Pin products with excess inventory to drive sales
Don’t over-pin:
  • Keep pinned products under 20% of total
  • Let the algorithm work for most positions
  • Rotate pinned products regularly

Next Steps