Skip to content

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

  1. Go to Apps & Data → ADD NEW
  2. Select Google Sheets
  3. Authorize your Google account
  4. Paste the Sheet ID from your URL: https://docs.google.com/spreadsheets/d/[SHEET_ID]/edit
  5. Select the tab name containing your product data
  6. 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;
});