> ## Documentation Index
> Fetch the complete documentation index at: https://docs.joinrefine.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Product Listing Pages

> Display curated, merchandised product collections with pinned products

## 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

```typescript theme={null}
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:

| Option              | Description                                   |
| ------------------- | --------------------------------------------- |
| **Name**            | Internal name for the PLP                     |
| **Slug**            | URL-friendly identifier                       |
| **Products**        | Selected products for the collection          |
| **Pinned Products** | Products pinned to specific positions         |
| **Ranking**         | Sort order (manual, popularity, newest, etc.) |
| **Filters**         | Default filters applied to the collection     |

## Response Structure

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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

```typescript theme={null}
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

```tsx theme={null}
// 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 };
}
```

```tsx theme={null}
// 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

<Tip>
  **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
</Tip>

## Next Steps

<CardGroup cols={2}>
  <Card title="Event Tracking" icon="chart-line" href="/sdk/events/overview">
    Track PLP interactions
  </Card>

  <Card title="Filters" icon="filter" href="/sdk/search/filters">
    Apply filters to PLPs
  </Card>
</CardGroup>
