Quickstart: Rendering banner campaigns with the Search API
Introduction
This guide is for developers who already build a custom search results page with the 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. See what you'll build »
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.
- 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.
- The ability to make HTTP
GETrequests and render search results. - Your Luigi's Box
trackerId. - Banner campaigns configured in the Luigi's Box application for at least one search query.
Important
This guide assumes you already understand the basics from Quickstart: Building a custom Search UI with the Search API. If you do not already render results.hits, start there first.
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 includingtype:product. -
hit_fields: Only the attributes you want to render, such astitle,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.
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
bannersobject
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
{
"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_headersearch_panelsearch_listsearch_footer
Each position object inside banners typically contains:
-
desktop_urlfor larger screens -
mobile_urlfor smaller screens
At the campaign level, you will also typically use:
-
target_urlas the click destination -
idfor 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.
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.
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.
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 Quickstart: Building a custom Search UI with the Search API.
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.
Finished example
You can see the complete working example here: