Let's Talk

Build a Lightning-Fast, Filterable Car Inventory Page in HubSpot CMS

By Tomislaw Dalic October 31, 2025

200+ cars. Real-time search. Dynamic IMAGIN images. Zero backend.
One HubL template. One JavaScript file. Done.

I built a leasing platform with 370+ vehicles — all filterable, searchable, and powered by IMAGIN.studio images. No Node.js. No database. Just HubDB + HubL + vanilla JS.

This is the complete blueprint — from CSV to live page.


The Full Architecture

CSV File (Car Data)
    ↓
HubDB (Structured Database)
    ↓
HubL Template (Server-Side Rendering)
    ↓
JavaScript (Client-Side Filtering)
    ↓
Beautiful Car Inventory Page

No external APIs. No React. No jQuery. Just HubSpot.


Step 1: The Perfect HubDB Schema

Field Type Example Why?
hs_name Text bmw-3-series-2024-001 Unique ID
brand Text BMW Filterable
model Text 3 Series Touring Full name
body_style Text Station Wagon Filter + IMAGIN variant
fuel_type Text Hybrid Filter + IMAGIN powerTrain
price_per_month Number 499 Sorting + range filter
api_make Text bmw IMAGIN-ready
api_modelfamily Text series-3 IMAGIN-ready

Pro Tip: Keep api_* fields separate — your display name can be “BMW 3 Series Touring”, but IMAGIN needs series-3.


Step 2: Import CSV → HubDB (Automated)

const hubspot = require('@hubspot/api-client');

async function importCars(csvData) {
  const client = new hubspot.Client({ accessToken: 'YOUR_TOKEN' });
  const tableId = '12345';

  for (const car of csvData) {
    await client.cms.hubdb.rowsApi.createTableRow(tableId, {
      path: car.slug,
      name: car.hs_name,
      values: { /* all fields */ }
    });
  }

  await client.cms.hubdb.tablesApi.publishDraftTable(tableId);
  console.log('Imported & published!');
}

Step 3: The HubL Template (Copy-Paste Ready)

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">

<div class="container-fluid py-5">
  <div class="row">
    <!-- Filters -->
    <div class="col-lg-3">
      <div class="filters-sidebar">
        <h4>Filter Cars</h4>
        <input type="text" id="searchInput" class="form-control" placeholder="Search...">
        <select id="brandFilter" class="form-select mt-3">...</select>
        <select id="fuelFilter" class="form-select mt-3">...</select>
        <select id="bodyStyleFilter" class="form-select mt-3">...</select>
        <input type="range" id="priceRange" class="form-range mt-3">
        <button id="clearFilters" class="btn btn-outline-secondary w-100 mt-3">Clear</button>
      </div>
    </div>

    <!-- Car Grid -->
    <div class="col-lg-9">
      <div id="carCardsContainer" class="row g-4">
        
      </div>
    </div>
  </div>
</div>

Step 4: Client-Side Filtering (Vanilla JS)

<script>
document.addEventListener('DOMContentLoaded', () => {
  const cards = document.querySelectorAll('.car-card');
  const applyFilters = () => { /* full logic */ };
  document.getElementById('searchInput').addEventListener('input', applyFilters);
  // ... all event listeners
  applyFilters();
});
</script>

Mobile-First UX

  • Collapsible filters on mobile
  • Touch-friendly range slider
  • Lazy-loaded images
  • URL sync for sharing filters

Performance Checklist

  • Lazy load images
  • Debounce search
  • Batch DOM updates
  • Cache filter state
  • No jQuery

Final Result

  • 200+ cars
  • Real-time filters
  • Dynamic IMAGIN images
  • Mobile-ready
  • Zero backend

Drop your HubDB schema in the comments — I’ll build your IMAGIN mapping for free.

Tomislaw Dalic

Join the Conversation

Share your thoughts and connect with other readers