Posted by

HubDB in HubSpot CMS: Build Data-Driven Pages Without Hardcoding Content

Hardcoding lists of team members, locations, events or products into HubSpot modules works until the third time someone asks you to "just add one more row". HubDB is HubSpot's built-in relational-style data store for CMS content: you keep the data in a table, and your templates read it with HubL. Editors update rows, and pages update without touching code.

When HubDB is a good fit

  • Repeating structured content: team directories, store locators, pricing rows, event lists, resource libraries.
  • Content that needs filtering or sorting on the page.
  • Generating many similar pages (one per row) from a single template.

If the data is a one-off set of fields on a single page, a normal module with a repeater field group is simpler. HubDB pays off when the data is shared across pages or is large.

1. Create the table

In HubSpot, open Content → HubDB (in some accounts the menu appears as Marketing → Files and Templates → HubDB) and create a table. Give it a clear label and name, then add columns with suitable types (text, number, select, date, image, URL and so on). Add a few rows. Remember to publish the table: new tables start as drafts and cannot output data through HubL until published, so edits stay invisible on live pages until you publish.

Note the table's ID (or use its name) because you reference it from HubL. Every table also has four default columns: hs_id, hs_created_at, hs_path and hs_name. The last two (page path and page title) matter when dynamic pages are enabled.

2. Query rows in a module or template

The hubdb_table_rows() function takes a table ID or name and an optional query string:

{% set rows = hubdb_table_rows("team_members", "orderBy=name&limit=12") %}

<ul class="team">
  {% for row in rows %}
    <li>
      <strong>{{ row.name }}</strong>
      <span>{{ row.job_title }}</span>
    </li>
  {% endfor %}
</ul>

Each column is available as row.column_name. Rows also expose built-in values such as row.hs_id and, for dynamic pages, row.hs_path and row.hs_name.

3. Filter, sort and paginate

The query string supports column filters using operators appended to the column name with a double underscore, plus orderBy, limit and offset:

{# Rows where years >= 3, highest first #}
{% set senior = hubdb_table_rows("team_members", "years__gte=3&orderBy=-years") %}

{# Text columns: rows whose name contains "anna" #}
{% set matches = hubdb_table_rows("team_members", "name__contains=anna") %}

HubSpot's HubL reference lists these operators: __eq (the default), __neq, __gt, __gte, __lt, __lte, __is_null, __not_null, __in and __contains. Supported operators depend on the column type, so check HubSpot's HubDB documentation for the current list. If a column is named range, distinct, ndistinct or startswith, add the __eq suffix when filtering on it.

A prefix of - on orderBy sorts descending. A call returns up to 1,000 rows by default, and 1,000 is also the maximum; use limit and offset to page through larger tables. For a paginated listing, read a query parameter such as ?page=2 from the request and calculate the offset from it.

4. Generate one page per row (dynamic pages)

This is HubDB's most powerful feature, and it requires CMS Hub Professional or Enterprise. In the table settings, enable dynamic pages and choose a template. Each row's page path is the last segment of its URL, for example /team/anna-reyes, and the page title comes from the row's title column, all served by a single template.

Inside the template, the current row is available as dynamic_page_hubdb_row, and it is empty on the listing page. The table's ID is available as dynamic_page_hubdb_table_id:

{% if dynamic_page_hubdb_row %}
  <h1>{{ dynamic_page_hubdb_row.name }}</h1>
  <p>{{ dynamic_page_hubdb_row.bio }}</p>
{% else %}
  {# Listing view: no row selected #}
  {% for row in hubdb_table_rows(dynamic_page_hubdb_table_id) %}
    <a href="{{ request.path }}/{{ row.hs_path }}">{{ row.name }}</a>
  {% endfor %}
{% endif %}

The same template therefore serves both the index page and every detail page. Build the link from the row's hs_path rather than hardcoding URLs.

5. Practical tips

  • Publish after every edit. The most common "my change isn't showing" cause is an unpublished table.
  • Filter in the query, not in a loop. Letting HubDB filter and sort is faster than looping over every row and using {% if %}.
  • Keep columns focused. Use clear, snake_case column names, since they become HubL property names.
  • Mind page performance and limits. A page or email can make at most 10 hubdb_table_rows calls, so query once and reuse the variable. Use limit to avoid loading rows you do not show.
  • Know the table limits. A table can hold up to 10,000 rows and 250 columns, and an account up to 1,000 tables and 1 million rows in total.
  • Plan for editors. Use select and checkbox columns where you can, so editors pick values instead of typing free text.

Wrapping up

HubDB separates content from presentation: developers write the template once, and editors manage the rows. Start with a simple listing, add filters, then move to dynamic pages when you need a detail page per row. Test in a sandbox with a copy of your table before changing a production site.

Sources