{% set g = module.style %} {% set a = module.advanced %} {# Prepare data source - manual cards or HubDB #} {% set card_items = [] %} {% set hubdb_error = none %} {% if module.data_source == 'hubdb' %} {# HubDB data source #} {% if module.hubdb_config.table_id %} {% set table = hubdb_table(module.hubdb_config.table_id) %} {% if table %} {% set query_params = {} %} {% if module.hubdb_config.filter_column and module.hubdb_config.filter_value %} {% do query_params.update({module.hubdb_config.filter_column: module.hubdb_config.filter_value}) %} {% endif %} {# limit must be a query param — hubdb_table_rows() has no 3rd positional arg. #} {% do query_params.update({"limit": module.hubdb_config.limit|default(9)}) %} {% set card_items = hubdb_table_rows(table.id, query_params) %} {% else %} {% set hubdb_error = "HubDB table not found. Please check the table ID in module settings." %} {% endif %} {% else %} {% set hubdb_error = "HubDB table not selected. Please select a table in module settings." %} {% endif %} {% else %} {# Manual cards data source #} {% set card_items = module.cards %} {% endif %} {# Mobile-first: Determine visible cards on mobile viewport #} {% set mobile_visible_count = g.cols_mobile == '1' ? 2 : 4 %} {% set tablet_visible_count = g.cols_tablet|int * 2 %} {# No preload here: it fetched the FULL-SIZE original while the first renders a resized srcset candidate (double download), and fired even for the 'no-image' variant. The first card image is already loading="eager" + fetchpriority="high". #} {# Main grid section #}
{# Optional section header #} {% if a.section_title or a.section_subtitle %}
{% if a.section_title %}

{{ a.section_title|escape }}

{% endif %} {% if a.section_subtitle %}

{{ a.section_subtitle|escape }}

{% endif %}
{% endif %} {# HubDB error message #} {% if hubdb_error %} {% endif %} {# Cards grid container #}
{# Responsive column widths — constant per module instance, computed once #} {% set mobile_width = g.cols_mobile == '1' ? 350 : 263 %} {% set tablet_width = (768 / g.cols_tablet|int)|round %} {% set desktop_width = (1200 / g.cols_desktop|int)|round %} {% for item in card_items %} {% set card_index = loop.index %} {# Mobile-first priority: Only first visible cards on mobile get high priority #} {% set is_mobile_priority = (g.cols_mobile == '1' and card_index == 1) or (g.cols_mobile == '2' and card_index <= 2) %} {% set is_priority = is_mobile_priority %}
{# Corner-flag label if present #} {% if item.badge %} {{ item.badge|escape }} {% endif %} {# Image - not shown for no-image variant #} {# Handle both manual and HubDB image sources #} {% if module.data_source == 'hubdb' %} {# HubDB: image may be URL string or object with .url property #} {% set image_url = item.image.url if item.image.url else (item.image if item.image is string else '') %} {% set img_width = item.image.width if item.image.width else null %} {% set img_height = item.image.height if item.image.height else null %} {% set img_size_known = img_width and img_height %} {% else %} {# Manual: standard module image field - check for valid src string #} {% set image_url = item.image.src if item.image.src else (item.image.url if item.image.url else (item.image if item.image is string else '')) %} {% set img_width = item.image.width if item.image.width else null %} {% set img_height = item.image.height if item.image.height else null %} {% set img_size_known = img_width and img_height %} {% endif %} {# Only render image if we have a valid URL string #} {% if g.card_variant != 'no-image' and image_url and image_url is string and image_url != '' %}
{% set loading = is_priority ? 'eager' : (a.lazy_load ? 'lazy' : (item.image.loading|default('lazy'))) %} {% set fetchpriority = is_priority ? 'high' : 'auto' %} {# Check if image is HubSpot-hosted (resize_image_url only works with HubSpot URLs) #} {% set is_hubspot_image = image_url is string_containing 'hubspot' or image_url is string_containing 'hubspotusercontent' or image_url is string_containing 'f.hubspotusercontent' %} {# Generate properly sized URLs — only when native dimensions are known to avoid upscaling #} {% if is_hubspot_image and img_size_known %} {% set img_mobile = resize_image_url(image_url, mobile_width) %} {% set img_tablet = resize_image_url(image_url, tablet_width) %} {% set img_desktop = resize_image_url(image_url, desktop_width) %} {% set img_desktop_2x = resize_image_url(image_url, desktop_width * 2) %} {% endif %} {{ item.image_alt|default('', true)|escape }}
{% endif %} {# Card body content #}
{% if item.eyebrow %}

{{ item.eyebrow|escape }}

{% endif %}

{{ item.title|escape }}

{% if item.content and g.card_variant != 'compact' %}
{{ item.content|sanitize_html }}
{% endif %} {# CTA button or link. Manual cards: cta_url is a url-field dict (use .href). HubDB rows: cta_url is a plain string. Support both. #} {% if (item.cta_url.href or item.cta_url is string) and item.cta_text %} {% set cta_href = item.cta_url.href|default(item.cta_url, true) %} {% set target_attrs = item.new_tab ? 'target="_blank" rel="noopener noreferrer"' : '' %} {% set aria_label = item.cta_aria_label ? 'aria-label="' ~ item.cta_aria_label|escape ~ '"' : '' %} {% if item.cta_as_button %} {{ item.cta_text|escape }} {% else %} {{ item.cta_text|escape }} {% endif %} {% endif %}
{% endfor %}
{# Mobile performance monitoring and adaptive loading #} {% if a.mobile_optimization == 'adaptive' %} {% endif %} {# Mobile-first critical CSS - only load what mobile needs #} {% require_css %} {% end_require_css %}