---
title: "Rendering banner campaigns with the Search API"
description: "Learn how to read the campaigns array from the Search API response and render search banner campaigns in a custom search results page."
slug: quickstart/search/banner-campaigns
docKind: guide
hub: quickstart
---

## Introduction

This guide is for developers who already build a custom search results page with the [Search API](/search/api/) and now want to render banner campaigns in their own search UI.

Search banner campaigns are returned inside the `results.campaigns` array in the standard Search API response. There is no dedicated banners endpoint. Your job is to read those campaign definitions and render them into the appropriate positions in your page.

Conceptually, banner campaigns are additional rendering instructions attached to the same search response you already use for products, facets, and pagination. You still render the normal `results.hits` array as before, but now you also inspect `results.campaigns` and decide whether a banner should appear above the list, in the side panel, inside the list, or below it.

### What you'll learn

- How to read banner campaign data from the Search API response.
- How to render the supported search banner positions.
- How to insert a list banner directly into the search results grid.
- How to keep your existing custom search implementation and extend it with banners.

### Who is this guide for

- Developers already using the Search API directly.
- Teams building custom search pages instead of [Search.js](/search/search-js/).
- Developers who need full control over where search banners appear.

### Prerequisites

Before you start, please ensure you have the following in place:

- A working custom search page that already calls the [Search API](/search/api/).
- The ability to make HTTP `GET` requests and render search results.
- Your Luigi's Box `trackerId`.
- Banner campaigns configured in the Luigi's Box application for at least one search query.

:::caution
This guide assumes you already understand the basics from [Building a custom search UI with the Search API](/quickstart/search/building-custom-ui/). If you do not already render `results.hits`, start there first.
:::

:::note[Runnable example HTML]
Open or download the full standalone sample: [banner-campaigns.html](/examples/search/banner-campaigns.html)
:::

## Step-by-step

### Step 1: keep your normal Search API request

Banner campaigns are returned through the normal search response.

This means your first goal is not to redesign the request, but to understand that the existing request already contains everything you need. If a banner campaign matches the current query, the API simply enriches the response with the `results.campaigns` array.

#### Endpoint

`GET` `https://live.luigisbox.com/search`

#### Required parameters

- **`tracker_id`:** Your unique site identifier.

#### Recommended parameters

- **`q`:** The current search query.
- **`f[]`:** Filters, usually including `type:product`.
- **`hit_fields`:** Only the attributes you want to render, such as `title,url,price_amount,image_link,brand`.
- **`size`:** Number of results per page.

#### Example

The code below follows the same request pattern as a normal custom search UI. The only banner-specific addition is that after reading `response.data.results`, you also read `results.campaigns` and pass that data into a dedicated campaign renderer.

```javascript
const TRACKER_ID = 'YOUR_TRACKER_ID';
const SEARCH_API_URL = 'https://live.luigisbox.com/search';

async function loadSearchResults(query) {
  const response = await axios.get(SEARCH_API_URL, {
    params: {
      tracker_id: TRACKER_ID,
      q: query,
      'f[]': ['type:product'],
      size: 8,
      hit_fields: 'title,url,price_amount,image_link,brand',
    },
  });

  const resultsData = response.data.results || {};
  const hits = resultsData.hits || [];
  const campaigns = resultsData.campaigns || [];

  const listBanner = renderCampaigns(campaigns);
  renderResults(resultsData, listBanner);
}
```

The only addition is that you now read `results.campaigns` alongside the regular search hits.

That separation matters. `results.hits` still drives your core result cards, while `results.campaigns` acts as optional merchandising content that can be rendered into specific layout slots.

### Step 2: understand the search banner response

Search banner campaigns are returned inside `results.campaigns`.

Each item in `results.campaigns` is one campaign definition that matched the current query. In practice, each campaign object answers three questions:

- what campaign matched, via `id`
- where the click should go, via `target_url`
- which search positions have images available, via the `banners` object

The `banners` object is a dictionary keyed by fixed Luigi's Box position names. Those keys are the contract between the application and your frontend. Your UI can decide how those positions look visually, but the keys themselves are stable and should be treated as identifiers for placement.

#### Example response fragment

```json
{
  "results": {
    "query": "guitar",
    "hits": [
      {
        "url": "https://yourshop.com/products/acoustic-guitar",
        "attributes": {
          "title": "Acoustic Guitar",
          "price_amount": 499,
          "image_link": "https://yourshop.com/images/guitar.jpg",
          "brand": ["Fender"]
        },
        "type": "product"
      }
    ],
    "campaigns": [
      {
        "id": 13,
        "target_url": "https://yourshop.com/sale",
        "banners": {
          "search_header": {
            "desktop_url": "https://yourshop.com/banners/header-desktop.jpg",
            "mobile_url": "https://yourshop.com/banners/header-mobile.jpg"
          },
          "search_footer": {
            "desktop_url": "https://yourshop.com/banners/footer-desktop.jpg",
            "mobile_url": "https://yourshop.com/banners/footer-mobile.jpg"
          }
        }
      }
    ]
  }
}
```

Notice that `campaigns` sits inside `results`, next to `hits`, `facets`, and the other search-specific data. That is why this guide reads `response.data.results.campaigns` rather than a top-level `campaigns` array.

Also note that a single campaign can provide banners for multiple positions at once. For example, one campaign may have both `search_header` and `search_footer`, while another query might return only a `search_list` banner.

The supported search banner positions are:

- `search_header`
- `search_panel`
- `search_list`
- `search_footer`

Each position object inside `banners` typically contains:

- `desktop_url` for larger screens
- `mobile_url` for smaller screens

At the campaign level, you will also typically use:

- `target_url` as the click destination
- `id` for debugging or diagnostics

In other words, the campaign object tells you which banner belongs to which slot, while the nested position object tells you which image asset to display there.

### Step 3: extract the first banner for each position

As with autocomplete banners, it is useful to normalize the campaign data before rendering.

The raw API response is campaign-oriented, but your UI is usually slot-oriented. Your page might have a dedicated header banner area, a panel area, and a footer area. A small helper lets you ask a simple question such as "give me the first `search_header` banner" instead of repeatedly scanning the full campaign array in each render branch.

#### Example

The helper below filters the campaigns for the requested position and returns the first match in a simplified structure that is easy to pass into rendering functions.

```javascript
function firstBannerForPosition(campaigns, position) {
  return campaigns
    .filter((campaign) => campaign.banners && campaign.banners[position])
    .map((campaign) => ({
      targetUrl: campaign.target_url,
      banner: campaign.banners[position],
      position,
    }))[0];
}
```

This helper keeps the rendering code simple and position-based.

### Step 4: render header, panel, and footer banners

Most banner positions can be rendered directly into dedicated containers in your layout.

Before looking at the code, it helps to split the responsibility into two layers:

- a small function that turns one banner entry into HTML
- a coordinator function that chooses which campaign belongs in each page slot

That approach keeps each piece narrow and makes it easier to test and debug.

#### Example

In the example below, `bannerMarkup` handles the HTML generation for a single banner entry. `renderCampaigns` then calls it for the header, panel, and footer containers, and separately returns the list banner for use inside the product grid.

```javascript
function isMobileViewport() {
  return window.matchMedia('(max-width: 767px)').matches;
}

function bannerMarkup(entry, altLabel, className = 'campaign-card') {
  if (!entry) {
    return `<div class="placeholder">No ${altLabel} banner returned for this query.</div>`;
  }

  const imageUrl = isMobileViewport()
    ? entry.banner.mobile_url || entry.banner.desktop_url
    : entry.banner.desktop_url || entry.banner.mobile_url;

  if (!imageUrl) {
    return `<div class="placeholder">No usable ${altLabel} image URL returned.</div>`;
  }

  return `
    <a class="${className}" href="${entry.targetUrl}" target="_blank" rel="noreferrer noopener">
      <img src="${imageUrl}" alt="${altLabel} banner campaign">
    </a>
  `;
}

function renderCampaigns(campaigns) {
  document.getElementById('search-header').innerHTML = bannerMarkup(
    firstBannerForPosition(campaigns, 'search_header'),
    'search header'
  );

  document.getElementById('search-panel').innerHTML = bannerMarkup(
    firstBannerForPosition(campaigns, 'search_panel'),
    'search panel'
  );

  document.getElementById('search-footer').innerHTML = bannerMarkup(
    firstBannerForPosition(campaigns, 'search_footer'),
    'search footer'
  );

  return firstBannerForPosition(campaigns, 'search_list');
}
```

This function also returns the `search_list` banner so you can place it inside the results grid.

That extra return value is important because `search_list` behaves differently from the other positions. It does not belong in a static container around the page. It belongs inside the list of rendered results.

### Step 5: insert the list banner into the search results

The `search_list` position is different because it belongs inside the list of rendered results.

Instead of rendering `search_list` above or below the results, you usually inject it into the array of already-rendered product cards. This is best treated as a normal rendering decision: build your product card markup first, then splice the banner markup into the desired index.

#### Example

The example below builds the normal product cards, then inserts the list banner after the first three results if one is available.

```javascript
function renderResults(resultsData, listBanner) {
  const hits = resultsData.hits || [];

  const cards = hits.map((result) => {
    const attributes = result.attributes || {};
    const title = attributes.title || 'Untitled';
    const image = attributes.image_link || '';
    const url = result.url || '#';

    return `
      <article class="product-card">
        <a href="${url}">
          <img src="${image}" alt="${title}">
        </a>
        <div class="product-body">${title}</div>
      </article>
    `;
  });

  if (listBanner) {
    const bannerCard = bannerMarkup(listBanner, 'search list', 'banner-in-list');
    const insertAt = Math.min(3, cards.length);
    cards.splice(insertAt, 0, bannerCard);
  }

  document.getElementById('results-grid').innerHTML = cards.join('');
}
```

The insertion point is entirely your choice. This example inserts the banner after the first three product cards.

In your own implementation, that insertion point can be based on design, merchandising goals, or device type. The important part is that `search_list` should be treated as one more tile in the rendered sequence.

### Step 6: keep your existing analytics approach

Banner campaigns do not replace the normal manual analytics requirements for Search API integrations. Continue sending analytics for the search results page as described in [Building a custom search UI with the Search API](/quickstart/search/building-custom-ui/).

:::note
Banners are purely additional rendering data. You do not need to change your request format to enable them. If a query matches an active campaign, Luigi's Box automatically includes `results.campaigns` in the response.
:::

## Next steps

- **Build the underlying custom search page first:** If you need the full search UI flow with results, filters, and pagination in more detail, continue with the [Building a custom search UI with the Search API](/quickstart/search/building-custom-ui/) guide.
- **Understand broader banner behavior:** For a more general explanation of supported positions, response structure, and image recommendations, see [Banner campaigns](/search/guides/banner-campaigns/).
- **Explore ranking and merchandising context:** If you also want to understand why certain products appear where they do around your banners, continue to [Understanding and influencing result ranking](/quickstart/search/understanding-rankings/).
