How to Create a Product Catalog Rotation Billboard¶
A product catalog rotation billboard cycles through your products automatically — showing a different product every 15–30 seconds, filtered by season, time of day, or a data-driven active flag. No manual creative updates needed when products change: update your Google Sheet, and the billboard reflects the change immediately.
Prerequisites¶
- A Lucit account with access to the Template Designer and Google Sheets integration
- A Google Sheet with your product catalog data
- The Google Sheets app connected to your Lucit account (see Google Sheets App Reference)
What You'll Build¶
A billboard creative that: 1. Reads a product list from a connected Google Sheet 2. Automatically rotates through active products each time the creative renders 3. Optionally filters products by season, time of day, or a manual active/inactive flag 4. Updates immediately when the data source changes — no creative rebuild required
Step 1: Set Up Your Product Google Sheet¶
Create a Google Sheet with the following structure:
| Column | Description | Required | Example |
|---|---|---|---|
product_name |
Display name of the product | Yes | Peppermint Mocha |
product_image_url |
Public URL to the product image | Yes | https://cdn.brand.com/peppermocha.jpg |
tagline |
Short promotional line | No | Back for the Season |
price |
Price to display | No | $5.49 |
active |
Whether to include in rotation | Yes | true / false |
season |
Season filter (optional) | No | winter / summer / all |
start_date |
Date when this product becomes active | No | 2025-11-15 |
end_date |
Date when this product is retired | No | 2026-01-05 |
Minimum required columns: product_name, product_image_url, active
Step 2: Connect the Google Sheet to Lucit¶
- Go to Apps & Data → ADD NEW
- Select Google Sheets
- Authorize your Google account
- Paste the Sheet ID from your URL:
https://docs.google.com/spreadsheets/d/[SHEET_ID]/edit - Select the tab name containing your product data
- Map column headers to Lucit data variables — use the column names from your sheet
Step 3: Design the Template¶
Design a template with the following elements:
- Image element (
#product-image) — displays the product photo - Text element (
#product-name) — displays the product name - Text element (
#product-tagline) — displays the promotional tagline - Text element (
#product-price) — displays the price (optional) - Brand frame — logo, colors, border elements (static across all rotations)
Step 4: Add the Rotation JavaScript¶
The rotation logic selects one product from the active list each time the creative renders. Because digital billboard slots cycle (typically every 8–15 seconds on a rotation loop), the creative will automatically show different products in sequence.
Open the JS tab in the Code Editor and paste:
registerDesignerFormattingFunction(
'applyProductRotation',
function(element, dataValue, dataObject, elementSettings, cssSelector) {
// =====================================================
// CONFIGURATION
// =====================================================
var SEASON_FILTER = 'all'; // 'all' | 'winter' | 'summer' | 'fall' | 'spring'
var USE_DATE_FILTER = true; // filter by start_date / end_date if provided
// =====================================================
var doc = element.ownerDocument;
// dataObject is expected to contain an array of rows from Google Sheets
// Each row: { product_name, product_image_url, tagline, price, active, season, start_date, end_date }
var rows = dataObject['rows'];
if (!rows || !Array.isArray(rows) || rows.length === 0) {
return;
}
var now = new Date();
// Filter to active products
var activeProducts = rows.filter(function(row) {
// Must be active
if (!row.active || row.active.toString().toLowerCase() !== 'true') {
return false;
}
// Season filter
if (SEASON_FILTER !== 'all' && row.season && row.season !== 'all') {
if (row.season !== SEASON_FILTER) {
return false;
}
}
// Date filter
if (USE_DATE_FILTER) {
if (row.start_date) {
var start = new Date(row.start_date);
if (now < start) return false;
}
if (row.end_date) {
var end = new Date(row.end_date);
if (now > end) return false;
}
}
return true;
});
if (activeProducts.length === 0) {
return;
}
// Select which product to show based on current time
// This creates a deterministic rotation that stays consistent
// across renders within the same minute-window
var minuteOfDay = now.getHours() * 60 + now.getMinutes();
var index = minuteOfDay % activeProducts.length;
var product = activeProducts[index];
// Apply product data to template elements
var nameEl = doc.querySelector('#product-name');
var taglineEl = doc.querySelector('#product-tagline');
var priceEl = doc.querySelector('#product-price');
var imageEl = doc.querySelector('#product-image');
if (nameEl && product.product_name) {
nameEl.textContent = product.product_name;
}
if (taglineEl && product.tagline) {
taglineEl.textContent = product.tagline;
taglineEl.style.display = '';
} else if (taglineEl) {
taglineEl.style.display = 'none'; // hide if no tagline
}
if (priceEl && product.price) {
priceEl.textContent = product.price;
priceEl.style.display = '';
} else if (priceEl) {
priceEl.style.display = 'none'; // hide if no price
}
if (imageEl && product.product_image_url) {
imageEl.src = product.product_image_url;
imageEl.style.objectFit = 'cover';
}
},
[]
);
Rotation Logic Explained¶
The minuteOfDay % activeProducts.length approach:
- Each minute of the day maps deterministically to a product index
- This means all screens show the same product at the same time (useful for brand consistency)
- With 5 products, product rotation happens every minute, cycling through all 5 in 5 minutes
- To show products less frequently per cycle, change minuteOfDay to Math.floor(minuteOfDay / 5) for a 5-minute dwell per product
Step 5: Seasonal Auto-Filter¶
To have the billboard automatically show only products appropriate to the current season without manual intervention:
registerDesignerFunction('getCurrentSeason', function(params, data) {
var month = new Date().getMonth() + 1; // 1-12
if (month >= 3 && month <= 5) return 'spring';
if (month >= 6 && month <= 8) return 'summer';
if (month >= 9 && month <= 11) return 'fall';
return 'winter'; // December, January, February
});
Use the return value of getCurrentSeason to set SEASON_FILTER dynamically, or pass it as a parameter.
Step 6: Managing the Rotation Over Time¶
To add a new product:
1. Add a new row to your Google Sheet with active = true
2. The product appears in rotation on the next render — no template changes needed
To retire a product:
1. Set active = false in the sheet — product immediately drops out of rotation
2. Or set end_date to yesterday — automatic date-filter handles removal
To promote a featured product: 1. Add the product row multiple times in the sheet (duplicate rows) 2. Because index selection is by position, it will appear more frequently proportionally
Pattern: "12 Days of" Sequential Reveal¶
A variation of the rotation concept that shows a specific product on a specific day — used for "12 Days of" holiday campaigns:
registerDesignerFunction('get12DaysProduct', function(params, data) {
// Target date: the first day of your 12-day campaign
var CAMPAIGN_START = new Date('2025-12-14T00:00:00');
var CAMPAIGN_END = new Date('2025-12-25T23:59:59');
// Which value from the row to return: 'name' | 'image' | 'tagline'
var returnField = params[0] || 'name';
// Product list — exactly 12 items, one per day
var PRODUCTS = [
{ name: 'Peppermint Mocha', image: 'https://cdn.brand.com/p1.jpg', tagline: 'Day 12' },
{ name: 'Gingerbread Latte', image: 'https://cdn.brand.com/p2.jpg', tagline: 'Day 11' },
{ name: 'Eggnog Latte', image: 'https://cdn.brand.com/p3.jpg', tagline: 'Day 10' },
{ name: 'Toasted White Mocha',image: 'https://cdn.brand.com/p4.jpg', tagline: 'Day 9' },
{ name: 'Caramel Brulée', image: 'https://cdn.brand.com/p5.jpg', tagline: 'Day 8' },
{ name: 'Sugar Cookie Latte', image: 'https://cdn.brand.com/p6.jpg', tagline: 'Day 7' },
{ name: 'Chestnut Praline', image: 'https://cdn.brand.com/p7.jpg', tagline: 'Day 6' },
{ name: 'Irish Cream Cold Brew',image:'https://cdn.brand.com/p8.jpg', tagline:'Day 5' },
{ name: 'Snowman Cookie', image: 'https://cdn.brand.com/p9.jpg', tagline: 'Day 4' },
{ name: 'Cranberry Bliss Bar',image: 'https://cdn.brand.com/p10.jpg',tagline: 'Day 3' },
{ name: 'Gift Card', image: 'https://cdn.brand.com/p11.jpg',tagline: 'Day 2' },
{ name: 'Holiday Blend', image: 'https://cdn.brand.com/p12.jpg',tagline: 'Day 1' }
];
var now = new Date();
if (now < CAMPAIGN_START || now > CAMPAIGN_END) {
return returnField === 'name' ? 'Happy Holidays!' : '';
}
// Calculate which day of the campaign we're on (0-indexed)
var dayIndex = Math.floor((now - CAMPAIGN_START) / 86400000);
dayIndex = Math.min(dayIndex, PRODUCTS.length - 1);
var product = PRODUCTS[dayIndex];
if (returnField === 'image') return product.image;
if (returnField === 'tagline') return product.tagline;
return product.name;
});
Related Guides¶
- Day & Night Time-of-Day Creatives — Combine with time-sensitive product selection
- How to Create a Countdown Ad — Add urgency to product promotions
- Google Sheets App Reference — Data source setup
- JavaScript Guide — Full function reference
- Holiday Season Campaigns — "12 Days of" and seasonal rotation use cases
- Food, Beverage & CPG — Seasonal CPG product rotation
- Retail — Retail product showcase ideas