# Intro

### Hey, welcome to Woohoo.dance documentation 👋

Everything you need to build an E-Commerce Store with WooCommerce and Breakdance—even as a complete beginner.

### Jump right in


# Requirements

Minimum requirements:

* Woocommerce 9.5+
* Breakdance 2.2.0+
* PHP 7.4+


# Installation

1. Download the latest plugin file
2. Install and Activate it
3. Enter valid license key to receive updates


# Premade templates

Premade templates are available for copy here: <https://www.woohoo.dance/demos/>

You need to be logged in to see the Copy button.


# Checkout

Checkout is divided into smaller parts allowing you to build any layout you want.


# Quick Setup

Start with adding element called "Checkout Wrapper" which will be a parent container for everything you have in your checkout.

<figure><img src="https://1072142691-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F00iTFjPG4OvCmS1VumHy%2Fuploads%2FGmsglMWOFMon17UN2U6T%2Fimage.png?alt=media&amp;token=7e75406c-601b-4fc6-a33e-a8d22071059b" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
If you put other elements outside of Checkout Wrapper - they will stop working so make sure that you have everything inside it.
{% endhint %}

### Default template

By default Checkout Wrapper comes along with other elements so you'll get a default template for a quick headstart.


# Address Autocomplete

To enable Address Autocomplete follow these steps:

1. Create Google API key: <https://developers.google.com/maps/documentation/javascript/get-api-key>
2. Enable Places API for this key
3. Make sure to restrict this key to your site url
4. Don't restrict the API restrictions:\
   &#x20;![](https://1072142691-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F00iTFjPG4OvCmS1VumHy%2Fuploads%2Fdw2MC1nncEC4XNeq0xUr%2Fimage.png?alt=media\&token=3b13d63c-3432-4b13-868c-10f1b54eb76d)
5. Copy and Paste this key in Breakdance Settings > API Keys > Google Maps API Key
6. Open Checkout page with Breakdance builder
7. On Checkout Wrapper - make sure that Address autocomplete is enabled:![](https://1072142691-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F00iTFjPG4OvCmS1VumHy%2Fuploads%2FR2MNWENskBHJ421FgJNP%2Fimage.png?alt=media\&token=2c902efd-2577-4ef7-ad2c-80387d805efa)

{% embed url="<https://drive.google.com/file/d/1TjsZjLdkrVxm1PYp5Vo6_Ls5JoteJkn0/view?usp=sharing>" %}


# Multisteps

Anywhere inside Checkout Wrapper - add **Checkout Multi Steps** element. This will act like a wrapper for all of your steps.

Inside this wrapper you can add **Checkout Step** elements representing each step. ![](https://1072142691-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F00iTFjPG4OvCmS1VumHy%2Fuploads%2F3aSWZ0huUgpwakrzqOHd%2Fimage.png?alt=media\&token=2907cb0e-4a60-4914-8b5f-645206f6146a)

To navigate between them, use **Checkout Next Step** and **Checkout Previous Step** buttons.

Simple as that.


# Checkout Hooks

By default we are modifying checkout hooks so some of your third-party integrations via plugins might not work out of the box. If you want to keep the default hooks, uncheck this option "Use Checkout hook as element":

<figure><img src="https://1072142691-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F00iTFjPG4OvCmS1VumHy%2Fuploads%2Fi2lu6CtOWLrAlMBrSLRz%2Fimage.png?alt=media&amp;token=7a8e5913-27f4-4736-845c-73f16d3853f5" alt=""><figcaption></figcaption></figure>

So if you have something extra that is showing let's say after billing fields: *woocommerce\_after\_checkout\_billing\_form* then it will show there where you place the Billing fields element.

If you need more control over this. Keep the function "Use Checkout hook as element" enabled and also make sure to have "Use hook as element" enabled on Checkout wrapper:

<figure><img src="https://1072142691-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F00iTFjPG4OvCmS1VumHy%2Fuploads%2FFh25I23r5OJBCGJ41Rb1%2Fimage.png?alt=media&amp;token=fca86a17-fc8e-4da3-a5b3-cbd5e0d7c584" alt=""><figcaption></figcaption></figure>

Then you can just add element **Checkout hook** and place it **anywhere** in the Checkout wrapper.

<figure><img src="https://1072142691-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F00iTFjPG4OvCmS1VumHy%2Fuploads%2FoK9OBk1Z5Yvn2JlLKRJ0%2Fimage.png?alt=media&amp;token=32c8ca2a-6fbd-4ffa-9502-fb9ecc128b9c" alt=""><figcaption></figcaption></figure>

Via this element, select the hook we want to show, which in our case is *woocommerce\_after\_checkout\_billing\_form.*&#x20;


# Cart

Cart elements are meant to be used on Cart page only. It consits of 2 important wrapper elements:

* Cart Wrapper
* Cart Empty

### Cart Wrapper

Here, put everything you want to show in the cart and that you want to hide when the cart is empty.

### Cart Empty

Any content that will show when the cart is empty

### Quick preview

{% embed url="<https://youtu.be/yyHKTu7acYs>" %}


# Cart Items

This wrapper element acts like a loop to query cart contents. You can put anything inside to create your desired cart contents layout.

Dynamic elements that are meant to be used in this wrapper element:

| Element               | Description                         |
| --------------------- | ----------------------------------- |
| Cart Item Image       | Product image                       |
| Cart Item Name        | Product name                        |
| Cart Item Description | Product short description           |
| Cart Item Price       | Product price                       |
| Cart Item Quantity    | Quantity input with + and - buttons |
| Cart Item Remove      | Remove button                       |
| Cart Item Subtotal    | Product quantity x Product price    |


# Thank you page

Woohoo allows you to create a custom Thank you page. To do that, enable Thank you elements in Woohoo dashboard:

<figure><img src="https://1072142691-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F00iTFjPG4OvCmS1VumHy%2Fuploads%2FQy9kLGfeWyTyN6CRVC7N%2Fimage.png?alt=media&amp;token=b050bf8f-34fb-40cd-a8e4-c3c2b341241d" alt=""><figcaption></figcaption></figure>

#### Create a new page

Create a new page, e.g. Thank you and go to Woocommerce Settings > Advanced and link this new page for Thank you page option.

<figure><img src="https://1072142691-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F00iTFjPG4OvCmS1VumHy%2Fuploads%2FQeLXAM816xjZ9iea2d67%2Fimage.png?alt=media&amp;token=902e8902-a568-4bba-b505-1557c68a058b" alt=""><figcaption></figcaption></figure>

#### Build Thank you page

Open Thank you page with Breakdance editor. Now you gave 2 wrappers to use:

* Success wrapper - for succeeded orders
* Failed wrapper - for orders that failed

For now you can use these elements to show order details:

* Thank you order details
* Thank you payment information
* Customer details


# Email Builder

Email builder comes as a separate link in the wordpress navigation menu "Woohoo emails". It acts like a custom post type. So for each email template you need to create a new post.

{% hint style="success" %}
1 post = 1 email template
{% endhint %}

Make sure to select the correct email type and enable it:

<figure><img src="https://1072142691-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F00iTFjPG4OvCmS1VumHy%2Fuploads%2FbcbSC7FPzYP0clb185Sp%2Fimage.png?alt=media&amp;token=b7168ce7-b9e9-4771-be2b-70917a469e57" alt=""><figcaption></figcaption></figure>

If you don't want to start from scratch, check the "Apply default template" checkbox before opening Breakdance builder and you'll get a starter template to build from.

### Important

* Start with **Email container** element and put everything inside it.
* For layout - use **Email Row** or **Email Column** elements.
* Style each element individually (div, text, headings) and make sure all the text have color and font size.
* Don't use SVG for imgages, only PNGs and JPGs.


# Account builder

Account builder let you build a custom woocommerce account dashboard.

1. Add element "**Account Layout**" which is required and will hold everything inside.
2. Use "**Account navigation items**" element to build your navigation. You need to set an url slug for each navigation item. E.g. Account orders -> orders
3. Then add element "**Account Route content**" which is a container to hold the content for each account endpoint. To connect it with your navigation you need to set a slug for each Route as well. So if you set slug "orders" for one Route element, that element will be connect with navigation item with slug "orders".
4. To make it easier, we have created a simple demo which is being imported when you add Account Layout element so you can start building from that.


# Display recently viewed products

You can use Post loop builder to show recently viewed products. Follow these steps:

1. Add Post loop builder
2. Select Query > Array \
   ![](https://1072142691-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F00iTFjPG4OvCmS1VumHy%2Fuploads%2FXpLdY7RB9VfFG7c2KOqq%2Fimage.png?alt=media\&token=19dcd9da-912f-4fb4-a6a7-a4fa6b28c1f0)
3. Enter this array:

```php
return [
'post_type' => 'product',
'posts_per_page' => 4,
'meta_key' => '_last_viewed',
  'orderby' => 'meta_value',
'order' => 'DESC',
];
```

Important part is meta\_key "*last\_viewed".*&#x20;


# Woohoo filters

Woohoo Filters are AJAX-powered filters built specifically for the Breakdance Builder, designed with a strong focus on lightweight, fast performance.

Minimum requirements:

* Breakdance 2.2.0+
* PHP 7.4+


# Installation

* Download the latest plugin file
* Install and Activate it
* Enter valid license key to receive updates


# Quick start

### Basic Setup (5 Steps)

#### Step 1: Create a Filter Page

1. Create a new page in Breakdance (or edit existing)
2. This page will contain your filters and results

#### Step 2: Add Main Query Element

1. Add the **Ajax Filter Main Query** element
2. Configure settings:
   * **Query Name**: Enter a unique identifier (e.g., `products`, `blog-posts`)
   * **Post Type**: Select the post type to filter
   * **Posts Per Page**: Number of results per page
3. Add your post cards/layout inside the Main Query

#### Step 3: Add Filter Facets

1. Add **Ajax Filter Facet** elements for each filter
2. Configure each facet:
   * **Query Name**: Must match your Main Query's Query Name
   * **Facet Name**: Display label (e.g., "Category", "Price")
   * **Source**: Select data source (taxonomy, meta field, etc.)
   * **Filter Type**: Choose display type (checkbox, radio, dropdown, range, etc.)

#### Step 4: Save and Index

1. **Save** your Breakdance page
2. Go to **Woohoo Filters → Indexer** in WordPress admin
3. Click **Reindex All** to build the filter index
4. Wait for indexing to complete (progress bar shows status)

#### Step 5: Test Your Filters

1. View your page on the frontend
2. Select filter options
3. Results update automatically via AJAX

{% hint style="info" %}
Indexer feature is available since version 1.3, if you are upgrading from lower versions - watch this video:
{% endhint %}

{% embed url="<https://drive.google.com/file/d/1qfBagYpfO3Ip4ZmJnA6wsw0BDZgkgfP4/view?usp=sharing>" %}


# Core Concepts

Query Name - The Connection Key

**All elements must share the same Query Name to work together.**

The Query Name is how elements communicate with each other. Think of it as a channel - all elements tuned to the same channel will work together.

```
Example:
- Main Query: query_name = "my-products"
- Facet 1: query_name = "my-products"
- Facet 2: query_name = "my-products"
- Pagination: query_name = "my-products"

✅ All elements share "my-products" = They work together
❌ Different query names = Elements won't connect
```

#### Apply On: Input vs Submit

Most filter elements have an "Apply on" setting:

* **Input (Immediate)**: Filter triggers instantly when user makes a selection
* **Submit (Button)**: Filter waits until user clicks the Submit/Apply button

Use "Submit" mode when you have multiple filters and want users to select all options before filtering.

### The Index System

Woohoo Filters uses a pre-computed index for fast filtering instead of querying WordPress directly each time.

#### Why an Index?

| Approach         | Speed | Scalability           |
| ---------------- | ----- | --------------------- |
| Direct WP\_Query | Slow  | Poor with many posts  |
| **Index-based**  | Fast  | Scales to 100k+ posts |

#### How the Index Works

```
1. REGISTRATION (When page is saved)
   ┌─────────────────────────────────────────────────────────┐
   │ Breakdance Page Save                                    │
   │    ↓                                                    │
   │ Scan for Ajax Filter Facet elements                     │
   │    ↓                                                    │
   │ Register facets in woohoo_facets table                  │
   │    • slug (element_id)                                  │
   │    • name                                               │
   │    • source (taxonomy/category, post_meta/_price, etc.) │
   │    • page_id                                            │
   │    • settings (JSON)                                    │
   └─────────────────────────────────────────────────────────┘

2. INDEXING (Admin action)
   ┌─────────────────────────────────────────────────────────┐
   │ For each registered facet:                              │
   │    ↓                                                    │
   │ Get all posts matching post type                        │
   │    ↓                                                    │
   │ Extract facet values from each post                     │
   │    ↓                                                    │
   │ Store in woohoo_index table                             │
   │    • facet_slug                                         │
   │    • object_id (post ID)                                │
   │    • value (raw value)                                  │
   │    • display_value (formatted for display)              │
   └─────────────────────────────────────────────────────────┘

3. QUERYING (When user filters)
   ┌─────────────────────────────────────────────────────────┐
   │ User selects filter options                             │
   │    ↓                                                    │
   │ AJAX request with selections                            │
   │    ↓                                                    │
   │ Query woohoo_index for matching object_ids              │
   │    ↓                                                    │
   │ Intersect IDs across all active facets                  │
   │    ↓                                                    │
   │ Return filtered results                                 │
   └─────────────────────────────────────────────────────────┘
```

#### Index Data Flow

```
Post: "Blue T-Shirt" (ID: 123)
├── Category: "Clothing" (term_id: 5)
├── Color: "Blue" (term_id: 12)
└── Price: 29.99 (_price meta)

           ↓ Indexing ↓

woohoo_index table:
┌─────────────────┬───────────┬─────────┬───────────────┐
│ facet_slug      │ object_id │ value   │ display_value │
├─────────────────┼───────────┼─────────┼───────────────┤
│ facet_category  │ 123       │ 5       │ Clothing      │
│ facet_color     │ 123       │ 12      │ Blue          │
│ facet_price     │ 123       │ 29.99   │ $29.99        │
└─────────────────┴───────────┴─────────┴───────────────┘
```


# Elements


# Ajax Filter Main Query

**The foundation element that displays your filtered content.**

This element is required - it's where your posts/products will appear.

**Content Settings**

| Setting            | Description                                                 |
| ------------------ | ----------------------------------------------------------- |
| **Query Name**     | Unique identifier to connect with other filter elements     |
| **Post Type**      | Select which post type to query (post, page, product, etc.) |
| **Posts Per Page** | Number of items to display per page                         |
| **Posts Limit**    | Maximum total posts to query (0 = unlimited)                |
| **Offset**         | Skip first N posts                                          |
| **Order By**       | Sort by Title or Date                                       |
| **Order**          | ASC (ascending) or DESC (descending)                        |

**Advanced Settings**

| Setting                      | Description                                                                                                                           |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **Show Loading Overlay**     | Display opacity overlay during AJAX filtering                                                                                         |
| **Loader Type**              | Choose from: Default, Ellipsis, Dual Ring, Roller, Ripple, Gradient Spinner, Bouncing Bars, Clock, Pulse                              |
| **Loader Color**             | Customize loader color                                                                                                                |
| **Result Appearance Effect** | Animation when results appear: None, Fade Scale, Slide Up, Zoom In, Bounce, Slide Rotate, Blur to Focus, Fade Only, Alternating Slide |
| **Infinite Scroll**          | Auto-load more posts when scrolling                                                                                                   |
| **Scroll Offset (px)**       | Distance from bottom to trigger infinite scroll (default: 200px)                                                                      |
| **Scroll Delay (ms)**        | Wait time before loading (default: 300ms)                                                                                             |
| **Use Query Parameters**     | Sync filter values to URL for shareable/bookmarkable states                                                                           |

**Design Settings**

* Width
* Layout (List or Grid or Masonry)
* Items per row (for grid layout)
* Spacing between items
* Margin/Padding

**Usage Example**

```
1. Add Ajax Filter Main Query
2. Set Query Name: "blog-posts"
3. Post Type: "post"
4. Posts Per Page: 9
5. Layout: Grid, 3 columns
6. Add child elements for your post card design

```

**URL Query Parameters**

| Setting                  | Description                                                 | Default |
| ------------------------ | ----------------------------------------------------------- | ------- |
| **Use Query Parameters** | Sync filter values to URL for shareable/bookmarkable states | false   |

When enabled:

* Filter selections appear in URL (e.g., `?category=tech&price=100-200`)
* URLs are shareable and bookmarkable
* Browser back/forward navigation works with filters
* Page refresh maintains filter state

**Custom No Results Text**

| Setting             | Description                                                                       |
| ------------------- | --------------------------------------------------------------------------------- |
| **No Results Text** | Custom message when no posts match filters. Leave empty for translatable default. |

**Static Container Element**

A new **Static Container** Breakdance element that inserts non-repeating content (banners, CTAs, promotional blocks) into the Main Query grid.

**Settings:**

* **Position**: Render after this item number (0 = before all items)
* **Grid column span**: How many grid columns the container spans (1–12)

Place any Breakdance elements inside the Static Container — they persist through filtering and pagination. Useful for injecting a promotional banner after the 3rd product card, for example.


# API query

Connect external REST APIs as data sources for your Breakdance loops. Fetch, sync, and display data from any API endpoint — all manageable from the WordPress admin.

#### API Source Manager

Create and manage API data sources from **Woohoo Filters > API Sources** in your WordPress admin. Each source defines an API endpoint, authentication, and sync schedule.

**Supported features:**

* **Authentication methods**: Bearer token (static or dynamic), Basic Auth, Custom Headers, and OAuth2 with automatic token refresh and 401 retry
* **Response formats**: JSON (default), CSV, and XML with auto-detection
* **Custom headers and query parameters**: Add any HTTP headers or URL parameters your API requires
* **Encryption**: Sensitive credentials (API keys, tokens, passwords) are encrypted at rest using AES-256-CBC

#### Data Sync

* **Manual sync**: Trigger from the admin UI at any time
* **Sync cancellation**: Cancel a running sync via the admin UI or REST endpoint
* **Error reporting**: Detailed error messages for HTTP failures, rate limiting, and malformed responses
* **Rate limiter**: Built-in request throttling to respect API rate limits

#### Test Connection

Before committing to a full sync, use **Test Connection** to preview the API response. Shows the first few records, response headers, and confirms your authentication works.

#### Breakdance Integration

API source data is stored as a custom post type (`woohoo_api_item`) and is fully queryable in Breakdance. Use Dynamic Data fields to display API-sourced content in any Breakdance element.


# Ajax Filter Facet

**The main filtering element that lets users filter by taxonomy, custom fields, or product attributes.**

**Content > Query Settings**

| Setting                 | Description                                                                |
| ----------------------- | -------------------------------------------------------------------------- |
| **Query Name**          | Must match your Main Query's query name                                    |
| **Meta Type**           | What to filter by: Taxonomy, Custom Field, WP Post Meta, Product Attribute |
| **Select Taxonomy**     | Choose taxonomy (category, tag, custom taxonomy)                           |
| **Select Attribute**    | For WooCommerce product attributes                                         |
| **Meta Key**            | For custom field filtering                                                 |
| **Show Top-level Only** | Only show parent terms, not children                                       |
| **Use Query Parameter** | Add filter values to URL for shareable links                               |
| **Order By Options**    | Sort options by: Name, ID, Count                                           |
| **Terms Exclude**       | Exclude specific terms from appearing                                      |

**Content > General Settings**

| Setting               | Description                                                                                                        |
| --------------------- | ------------------------------------------------------------------------------------------------------------------ |
| **Layout Type**       | Dropdown, Checkbox, Radio, or Button                                                                               |
| **Logic**             | AND (must match all) or OR (match any) - for checkbox/multiselect                                                  |
| **Multiselect**       | Allow multiple button selections                                                                                   |
| **Placeholder**       | Dropdown placeholder text                                                                                          |
| **Hide Count**        | Hide the post count next to each option                                                                            |
| **Hide Empty Option** | Hide options with no matching posts                                                                                |
| **Hide 'All' Option** | Remove the "All" option (radio/button)                                                                             |
| **Label: All**        | Customize "All" option text                                                                                        |
| **Legend Label**      | Accessibility label for checkbox/radio groups                                                                      |
| **Apply On**          | Input (immediate) or Submit (wait for button)                                                                      |
| **Child of**          | Only show terms that are children of a specific parent term. Enter the parent term ID to scope the facet.          |
| **Max depth**         | Limit how many levels deep the facet tree goes. Set to `1` for top-level only, `2` for one level of children, etc. |

**Layout Types Explained**

**Dropdown**

* Clean, space-saving
* Best for: Long lists, mobile-friendly designs
* Single selection only

**Checkbox**

* Multiple selections with AND/OR logic
* Best for: Allowing multiple filter combinations
* Shows all options at once

**Radio**

* Single selection
* Best for: Mutually exclusive options
* Includes "All" option by default

**Button**

* Visual, clickable buttons
* Can be single or multiselect
* Best for: Featured filters, visual designs

**Usage Example**

```
Filter by Category (Checkbox):
1. Add Ajax Filter Facet
2. Query Name: "blog-posts"
3. Meta Type: Taxonomy
4. Select Taxonomy: "category"
5. Layout Type: Checkbox
6. Logic: OR
7. Apply On: Input
```

#### Range slider

{% embed url="<https://drive.google.com/file/d/10PfRXxbgcLrPlTs-A5QYP0eWuA8rirX7/view?usp=sharing>" %}

#### Image and color swatches

{% embed url="<https://drive.google.com/file/d/1l4GnstUII7veo5RjVU_BCH0ibAAS39VW/view?usp=sharing>" %}


# Ajax Filter Search

**Live search input that filters results by post title/content.**

**Content > Query Settings**

| Setting        | Description                |
| -------------- | -------------------------- |
| **Query Name** | Must match your Main Query |

**Content > General Settings**

| Setting             | Description                                        |
| ------------------- | -------------------------------------------------- |
| **Apply On**        | Input (live search) or Submit (button click)       |
| **Debounce (ms)**   | Delay before search triggers (default: 300ms)      |
| **Min Characters**  | Minimum characters to start searching (default: 3) |
| **Placeholder**     | Input placeholder text                             |
| **Show Clear Icon** | Show X button to clear search                      |

**Design Settings**

* Input width, padding, borders, typography
* Hover/Focus/Active states
* Clear icon customization
* Transition duration

**Important Notes**

* Only use ONE search element per query
* For "Submit" mode, pair with Ajax Filter Submit button
* Escape key clears search
* Enter key triggers immediate search (in Input mode)

**Usage Example**

```
Live Search:
1. Add Ajax Filter Search
2. Query Name: "blog-posts"
3. Apply On: Input
4. Debounce: 300ms
5. Min Characters: 3
6. Placeholder: "Search posts..."
```


# Ajax Filter Sort

**Lets users sort results by different criteria.**

**Content > Query Settings**

| Setting          | Description                              |
| ---------------- | ---------------------------------------- |
| **Query Name**   | Must match your Main Query               |
| **Sort Options** | Define available sort options (repeater) |

**Sort Option Settings**

Each sort option has:

| Setting         | Description                                                                 |
| --------------- | --------------------------------------------------------------------------- |
| **Label**       | Display text (e.g., "Newest First")                                         |
| **Source Type** | Built-in Field, Custom Field (Text), Custom Field (Numeric)                 |
| **Field**       | For built-in: Title, Date, Modified Date, Menu Order, Random, Comment Count |
| **Meta Key**    | For custom fields                                                           |
| **Order**       | ASC or DESC                                                                 |

**Content > General Settings**

| Setting               | Description                               |
| --------------------- | ----------------------------------------- |
| **Layout Type**       | Dropdown, Radio, or Button                |
| **Placeholder**       | Dropdown placeholder                      |
| **Hide 'All' Option** | Hide default/reset option                 |
| **Label: All**        | Text for default option (e.g., "Default") |
| **Legend Label**      | Accessibility label                       |
| **Apply On**          | Input or Submit                           |

**Default Sort Options**

The element comes with these presets:

* Newest First (date DESC)
* Oldest First (date ASC)
* Title A-Z (title ASC)
* Title Z-A (title DESC)

**Usage Example**

```
Product Sort:
1. Add Ajax Filter Sort
2. Query Name: "products"
3. Sort Options:
   - "Price: Low to High" | meta_value_num | _price | ASC
   - "Price: High to Low" | meta_value_num | _price | DESC
   - "Newest" | built_in | date | DESC
4. Layout: Dropdown
5. Placeholder: "Sort by..."
```


# Ajax Filter Submit

**Button to apply filters when using "Submit" mode.**

Use this when you want users to select multiple filters before applying them all at once.

**Content Settings**

| Setting        | Description                            |
| -------------- | -------------------------------------- |
| **Query Name** | Must match your Main Query             |
| **Text**       | Button text (default: "Apply filters") |

**Design Settings**

* Layout margin/padding
* Button style (primary/secondary/custom)
* Full button design customization

**Usage Example**

```
1. Set all your Facets to "Apply On: Submit"
2. Add Ajax Filter Submit
3. Query Name: "blog-posts"
4. Text: "Apply Filters"
```


# Ajax Filter Reset

**Button to clear all applied filters and reset to default state.**

**Content Settings**

| Setting        | Description                            |
| -------------- | -------------------------------------- |
| **Query Name** | Must match your Main Query             |
| **Text**       | Button text (default: "Reset filters") |

**Design Settings**

* Layout margin/padding
* Typography
* Button style customization

**Behavior**

* Clears all facet selections
* Resets search input
* Returns to page 1
* Button is disabled when no filters are active


# Ajax Pagination

**Navigate through multiple pages of results.**

Cannot be used with Infinite Scroll or Load More on the same query.

**Content Settings**

| Setting                | Description                          |
| ---------------------- | ------------------------------------ |
| **Query Name**         | Must match your Main Query           |
| **Button Type**        | Text only, Icon only, or Text & Icon |
| **Previous/Next Text** | Labels for navigation buttons        |
| **Previous/Next Icon** | Icons for navigation buttons         |
| **Icon Style**         | Size and color                       |
| **Icon Position**      | Left or Right of text                |

**Design Settings**

* Layout: Width, spacing, alignment (left/center/right), margin
* Button: Padding, background, borders, typography, shadows
* Hover/Active states with full customization
* Transition duration

**Features**

* Smart ellipsis for many pages (1 ... 5 6 7 ... 20)
* Previous/Next buttons auto-disable at boundaries
* Smooth scroll to top of results on page change
* URL updates with page number for shareable links


# Ajax Filter Load More

**Button to load additional posts without full page navigation.**

Cannot be used with Infinite Scroll or Pagination on the same query.

**Content Settings**

| Setting           | Description                             |
| ----------------- | --------------------------------------- |
| **Query Name**    | Must match your Main Query              |
| **Button Text**   | Default text (e.g., "Load More")        |
| **Loading Text**  | Text while loading (e.g., "Loading...") |
| **Maximum Loads** | Limit clicks (0 = unlimited)            |

**Design Settings**

* Layout margin/padding
* Button style customization

**Behavior**

* Appends new posts below existing ones
* Button shows loading state during fetch
* Auto-hides when all posts are loaded
* Respects max loads limit if set


# Ajax Query Count

**Displays the total number of results found.**

**Content > Query Settings**

| Setting        | Description                |
| -------------- | -------------------------- |
| **Query Name** | Must match your Main Query |

**Content > General Settings**

| Setting              | Description                     |
| -------------------- | ------------------------------- |
| **Element Tag**      | HTML tag (div, span, p, h1-h6)  |
| **Format Text**      | Text with `%total%` placeholder |
| **Text: No Results** | Message when 0 results          |
| **Text: One Result** | Message when 1 result           |

**Design Settings**

* Layout: Margin, padding, text alignment
* Typography with number styling (font weight, color)

**Usage Example**

```
1. Add Ajax Query Count
2. Query Name: "blog-posts"
3. Format Text: "Found %total% results"
4. Text: No Results: "No posts found"
5. Text: One Result: "Found 1 post"

Output: "Found 42 results"
```


# Ajax Query Results Summary

**Shows detailed pagination summary (e.g., "Showing 1-10 of 42 results").**

**Content Settings**

| Setting              | Description                                    |
| -------------------- | ---------------------------------------------- |
| **Query Name**       | Must match your Main Query                     |
| **Element Tag**      | HTML tag (div, span, p)                        |
| **Format Text**      | Use `%start%`, `%end%`, `%total%` placeholders |
| **Text: One Result** | Message for single result                      |
| **Text: No Results** | Message for no results                         |

**Design Settings**

* Layout: Margin, padding, background
* Typography: Text align, font settings, number styling

**Usage Example**

```
Format: "Results: %start% - %end% of %total% posts"

Page 1: "Results: 1 - 10 of 42 posts"
Page 2: "Results: 11 - 20 of 42 posts"
```


# Ajax Search Term

**Displays what the user is currently searching for.**

**Content > Query Settings**

| Setting        | Description                |
| -------------- | -------------------------- |
| **Query Name** | Must match your Main Query |

**Content > General Settings**

| Setting             | Description                      |
| ------------------- | -------------------------------- |
| **Format Text**     | Use `%search-term%` placeholder  |
| **Hide When Empty** | Hide element when no search term |

**Design Settings**

* Layout: Margin, padding, text alignment
* Typography with search term styling (font weight, color)

**Usage Example**

```
Format: "Searching for: %search-term%"

Output: "Searching for: javascript tutorials"
```


# Ajax Active Filters

Element to display all active filters.


# Ajax filter offcanvas

A slide-in panel container for placing filter elements, ideal for mobile-responsive filter layouts.

**Overview**

The Woohoo Offcanvas element creates a slide-in panel that can contain filter elements, search boxes, or any content. It supports multiple positions, animations, and responsive breakpoints for optimal mobile experience.

**Content Settings**

| Setting                     | Description                                                    |
| --------------------------- | -------------------------------------------------------------- |
| **Offcanvas ID**            | Unique identifier to connect with trigger elements             |
| **Display Mode**            | `Offcanvas` (always hidden) or `Responsive` (breakpoint-based) |
| **Show in Builder**         | Toggle visibility during page building                         |
| **Mobile Breakpoint (px)**  | Screen width below which offcanvas activates (responsive mode) |
| **Position**                | `Left`, `Right`, `Top`, or `Bottom`                            |
| **Show Backdrop**           | Display semi-transparent overlay behind panel                  |
| **Close on Backdrop Click** | Close panel when clicking backdrop                             |
| **Close on Escape Key**     | Close panel with Escape key                                    |
| **Animation**               | `Slide`, `Fade`, or `None`                                     |
| **Animation Duration (ms)** | Animation speed (default: 300ms)                               |

**Design Settings**

**Panel:**

* Width (for left/right positions)
* Max width
* Height (for top/bottom positions)
* Max height
* Background
* Padding
* Borders

**Backdrop:**

* Overlay color (default: rgba(0,0,0,0.5))

**Layout:**

* Margin

**Accessibility**

* `role="dialog"` for screen readers
* `aria-modal="true"` indicates modal behavior
* `aria-labelledby` for accessible labeling
* `aria-hidden` state management

**Implementation**

```
1. Add Woohoo Offcanvas element
2. Set unique Offcanvas ID (e.g., "filter-panel")
3. Choose position and display mode
4. Add filter elements inside the offcanvas
5. Use Woohoo Offcanvas Trigger to open/close
```

***

#### Woohoo Offcanvas Trigger

A button/container element that opens, closes, or toggles an Offcanvas panel.

**Overview**

The Woohoo Offcanvas Trigger connects to an Offcanvas panel by ID and controls its visibility. It's a container element, so you can place any content inside (text, icons, buttons).

**Content Settings**

| Setting                 | Description                                                         |
| ----------------------- | ------------------------------------------------------------------- |
| **Target Offcanvas ID** | ID of the Offcanvas element to control                              |
| **Action**              | `Toggle` (switch state), `Open` (show only), or `Close` (hide only) |

**Design Settings**

* Margin

**Default Children**

By default, includes a Text element with "Filters" label. Replace with your own content as needed.

**Implementation**

```
1. Add Woohoo Offcanvas Trigger element
2. Set Target Offcanvas ID to match your Offcanvas element
3. Choose action (toggle is most common)
4. Add content inside (icon, text, button, etc.)
```

***

#### Woohoo Offcanvas Close

A close button placed inside an Offcanvas panel to dismiss it.

**Overview**

The Woohoo Offcanvas Close element provides a dedicated close button within an Offcanvas panel. It automatically closes the parent Offcanvas when clicked.

**Content Settings**

None - automatically detects parent Offcanvas.

**Design Settings**

* Margin

**Default Children**

Includes an X icon by default. Customize or replace as needed.

**Implementation**

```
1. Add Woohoo Offcanvas Close inside your Offcanvas element
2. Position it where users expect a close button (typically top-right)
3. Customize the icon or content as desired
```

***

#### Woohoo Filter Portal

Teleports filter elements into an Offcanvas panel on mobile while keeping them in place on desktop.

**Overview**

The Woohoo Filter Portal enables responsive filter layouts where filters appear inline on desktop but move into an Offcanvas panel on mobile. Wrap your filter elements in a Portal, and they'll be teleported to the target Offcanvas when the breakpoint is reached.

**Content Settings**

| Setting                 | Description                                  |
| ----------------------- | -------------------------------------------- |
| **Target Offcanvas ID** | ID of the Offcanvas to teleport content into |
| **Order in Offcanvas**  | Numeric order for multiple portals (0-100)   |

**Design Settings**

None - portal is a structural element.

**How It Works**

1. On desktop (above breakpoint): Content renders in place
2. On mobile (below breakpoint): Content teleports into target Offcanvas
3. Order setting controls arrangement when multiple portals target same Offcanvas

**Implementation**

```
1. Create an Offcanvas with responsive display mode
2. Wrap filter elements in Woohoo Filter Portal
3. Set Target Offcanvas ID to match
4. Set Order if using multiple portals
```

**Example Use Case**

```
Desktop Layout:
[Sidebar with filters] [Main content with results]

Mobile Layout:
[Filter button] → Opens Offcanvas with all filters
[Main content with results]
```

{% embed url="<https://drive.google.com/file/d/1X1oF2uK86kLvX-w5ErAH4EyLxxRMZdam/view?usp=sharing>" %}


# Woohoo Map

A powerful Google Maps integration for Breakdance that supports manual locations, dynamic data, Ajax filtering, and advanced customization.


# Getting Started

#### 1. API Key Setup

**Required:** Add your Google Maps API Key in WordPress settings:

* Go to **Woohoo filters → Api keys**
* Enter your Google Maps API key
* Get API key: [Google Maps Platform](https://developers.google.com/maps/documentation/javascript/get-api-key)

#### 2. Add the Element

Drag **Woohoo Google Map** element into your Breakdance page.


# Location Modes

#### Manual Mode

Add locations directly in the builder:

1. **Content → Locations → Location Source:** Manual
2. Click **Add Location Item**
3. Enter coordinates (Latitude/Longitude) or Address
4. Customize title, subtitle, content, and image per location
5. Set custom markers per location (optional)

**Use case:** Fixed locations like store listings, office addresses

***

#### Dynamic Mode

Pull locations from WordPress posts/CPTs:

1. **Content → Locations → Location Source:** Dynamic
2. Select **Post Type** and **Posts Limit**
3. Configure field mapping:
   * **Latitude Field:** ACF field or post meta name
   * **Longitude Field:** ACF field or post meta name
   * **Address Field:** Address field name (geocoded automatically)
4. Add dynamic elements **directly inside** Woohoo Google Map element

**Use case:** Location-based posts, real estate listings, events

***

#### Dynamic-Sync Mode

Sync map with Ajax Filter Main Query:

1. **Content → Locations → Location Source:** Dynamic-sync
2. **Query Name:** Match your Ajax Filter query name
3. Add **Woohoo Map Connector** element inside your Ajax Filter card template
4. Configure connector fields (latitude, longitude, marker icons)

**Features:**

* Auto-updates when filters applied
* Bidirectional sync (marker ↔ post card)
* Shows empty state when no results

**Use case:** Filterable location directories, searchable maps


# Map Customization

#### Map Settings

**Content → Map Settings**

* **Map Type:** roadmap, satellite, hybrid, terrain
* **Initial View:** Fit markers, fit with padding, or manual center
* **Zoom Level:** 1-20 (default: 12)
* **Controls:** Toggle zoom, fullscreen, map type controls

***

#### Map Styling

**Design → Map Styling** - Choose from 3 styling methods:

**1. Preset Styles**

Built-in Google Maps themes:

* Silver, Dark, Night, Retro, Minimal, Aubergine

**2. Snazzy Maps**

Import custom JSON styles:

1. Browse [Snazzy Maps](https://snazzymaps.com/)
2. Copy JSON style code
3. Paste into **Custom JSON** field

**3. Builder Colors**

Simple color controls:

* **Land Color:** Landscape fill
* **Water Color:** Water bodies fill
* **Road Color:** Road fill
* **Label Color:** Label text color
* **Hide Labels:** Toggle to hide all labels

***

#### Markers

**Design → Markers**

**Global Markers**

Applied to all markers:

* **Custom Marker:** Upload image (32x32px recommended)
* **Active Marker:** Highlighted state image
* **Marker Size:** Width and height
* **Default Pin Color:** Google-style pin color (when no custom marker)

**Per-Location Markers (Manual mode)**

Override global markers per location in **Content → Locations → Location Items**

**Tip:** Use PNG with transparency for custom markers (or SVG)

***

#### Clustering

**Content → Clustering** - Group nearby markers at lower zoom levels

* **Enable Clustering:** On/Off
* **Cluster Radius:** Distance threshold (default: 60px)
* **Max Zoom:** Stop clustering at zoom level (default: 15)
* **Min Points:** Minimum markers to form cluster (default: 2)

**Design → Cluster Styling**

* **Cluster Style:** Classic or Ripple
* **Size Thresholds:** Small, medium, large marker counts
* **Colors:** Per-size cluster colors
* **Text:** Color and size

***

#### Infobox (Popup)

**Design → Infobox** - Customize marker popups

* **Width & Max Width:** Popup dimensions
* **Background Color:** Popup background
* **Padding:** Inner spacing
* **Borders:** Radius, width, color
* **Shadows:** Popup shadow

**Typography Controls:**

* **Title:** Heading typography
* **Subtitle:** Subheading typography
* **Content:** Body text typography

**Image:**

* **Width & Height:** Image dimensions
* **Aspect Ratio:** Image ratio
* **Border:** Image border radius and color

**Close Button:**

* **Size:** Button dimensions
* **Background:** Normal and hover colors
* **Border Radius:** Button roundness
* **Icon Color:** Normal and hover icon colors


# Dynamic-Sync Setup (Advanced)

#### Step 1: Main Query Setup

1. Add **Ajax Filter Main Query** element
2. Set **Query Name:** `my-locations`
3. Configure post query (post type, filters)

#### Step 2: Map Element

1. Add **Woohoo Google Map** element
2. **Location Source:** Dynamic-sync
3. **Query Name:** `my-locations` (same as Main Query)

#### Step 3: Card Template

Inside your Ajax Filter card template:

1. Add **Woohoo Map Connector** element
2. Map fields using dynamic data:
   * **Latitude:** Dynamic data → ACF field
   * **Longitude:** Dynamic data → ACF field
   * **Marker Icon:** (optional) Custom marker per post
3. Add infobox content (title, image, description) as children of **Woohoo Map Connector**


# Field Mapping Tips

#### ACF Field Support

* **Text Fields:** Field names (e.g., `my_latitude`)
* **Image Fields:** Returns URL automatically
* **Address Fields:** Geocoded to lat/lng

#### Dynamic Data Picker

Use Breakdance dynamic data for:

* Post title, excerpt, content
* Featured image
* ACF fields
* Custom fields

#### Geocoding

If using **Address Field** instead of lat/lng:

* Addresses geocoded automatically via Google Geocoding API
* Results cached for 24 hours
* Requires API key with Geocoding API enable


# Troubleshooting

#### Map Not Showing

* ✓ Check API key in Settings → Woohoo Filters
* ✓ Enable Maps JavaScript API in Google Cloud Console
* ✓ Check browser console for errors

#### Markers Not Appearing (Dynamic Mode)

* ✓ Verify field names match ACF/meta field names exactly
* ✓ Check lat/lng values are numeric
* ✓ If using address, enable Geocoding API

#### Dynamic-Sync Not Updating

* ✓ Query names match exactly (Main Query & Map element)
* ✓ Woohoo Map Connector present in card template
* ✓ Connector fields mapped to dynamic data

#### Custom Markers Not Showing

* ✓ Image uploaded and URL valid
* ✓ Dynamic property path set for dynamic fields
* ✓ Check browser network tab for 404 errors


# Leaflet map

A free, open-source alternative to Google Maps using Leaflet.js. No API key required.

**Overview**

The Leaflet Map element provides all the functionality of Woohoo Google Map without requiring a Google Maps API key. Uses OpenStreetMap tiles by default with multiple tile provider options.

**Content Settings**

**Locations:**

| Setting                       | Description                                                      |
| ----------------------------- | ---------------------------------------------------------------- |
| **Location Source**           | `Manual`, `Dynamic`, or `Dynamic-sync`                           |
| **Location Items**            | Repeater for manual locations (lat, lng, address, popup content) |
| **Post Type**                 | For dynamic mode                                                 |
| **Posts Limit**               | Maximum posts to display                                         |
| **Latitude/Longitude Fields** | ACF or meta field names                                          |
| **Address Field**             | For geocoding fallback                                           |
| **Custom Marker**             | Per-location marker image                                        |
| **Query Name**                | For dynamic-sync mode                                            |

**Clustering:**

| Setting               | Description                           |
| --------------------- | ------------------------------------- |
| **Enable Clustering** | Group nearby markers                  |
| **Cluster Radius**    | Pixels to group markers (default: 80) |
| **Medium Threshold**  | Point count for medium cluster        |
| **Large Threshold**   | Point count for large cluster         |

**Map Settings:**

| Setting                       | Description                                              |
| ----------------------------- | -------------------------------------------------------- |
| **Map Style**                 | Multiple tile providers (default, CartoDB, Stamen, etc.) |
| **Center Latitude/Longitude** | Initial map center                                       |
| **Zoom**                      | Initial zoom level (0-19)                                |
| **Fit All Markers**           | Auto-fit bounds to show all markers                      |
| **Bounds Padding**            | Padding around fitted bounds                             |
| **Max Zoom (fit bounds)**     | Prevent over-zooming when fitting                        |
| **Fit Bounds on Update**      | Re-fit when markers change                               |
| **Zoom Control**              | Show +/- buttons                                         |
| **Scroll Wheel Zoom**         | Enable mouse wheel zooming                               |

**Popup Settings:**

| Setting               | Description                |
| --------------------- | -------------------------- |
| **Show Close Button** | Display X on popups        |
| **Auto-pan to Popup** | Pan map to show full popup |

**Design Settings**

* Map size (width, height)
* Custom markers (image, active state, dimensions)
* Clustering colors (small, medium, large, text)
* Popup styling (width, background, borders, padding, typography)
* Spacing

**Tile Providers**

Multiple built-in map styles available:

* OpenStreetMap (default)
* CartoDB Positron (light)
* CartoDB Dark Matter
* Stamen Terrain
* Stamen Watercolor
* And more...

**Key Differences from Google Map**

| Feature          | Google Map           | Leaflet Map            |
| ---------------- | -------------------- | ---------------------- |
| API Key Required | Yes                  | No                     |
| Cost             | Paid after free tier | Free                   |
| Tile Provider    | Google               | OpenStreetMap + others |
| Geocoding        | Google               | Not built-in           |
| Street View      | Yes                  | No                     |

**Implementation**

```
1. Add Leaflet Map element to page
2. No API key configuration needed
3. Choose location source mode
4. Configure markers and styling
5. Works with Ajax Filter Main Query in dynamic-sync mode
```


# Best Practices

#### Performance

1. **Enable "Hide Empty Options"** on facets to reduce clutter
2. **Use reasonable Posts Per Page** (6-12 for grids, 10-20 for lists)
3. **Set appropriate Debounce** for live search (300-500ms)
4. **Avoid multiple identical queries** on the same page

#### User Experience

1. **Use Submit mode** for complex filter combinations
2. **Always include Reset button** for easy clearing
3. **Show result counts** to indicate filter impact
4. **Use loading indicators** for feedback during AJAX
5. **Choose appropriate layout types**:
   * Few options (2-5): Buttons or Radio
   * Many options (5+): Dropdown or Checkbox

#### SEO & Sharing

1. **Enable "Use Query Parameter"** on important facets for shareable URLs
2. **Use Ajax Search Term** to display search context
3. **Include Query Count/Summary** for result context

#### Conflicts to Avoid

* **Don't combine Infinite Scroll with Pagination or Load More**
* **Use only ONE Search element per query**
* **Ensure all elements share exact same Query Name**


# Troubleshooting

#### Elements Not Connected

**Problem**: Filters don't affect the main query

**Solution**: Verify all elements have identical Query Name (case-sensitive, no spaces)

#### Search Not Working with Submit Button

**Problem**: Apply button doesn't include search value

**Solution**: Ensure Search element's "Apply On" is set to "Submit"

#### Infinite Scroll Not Working

**Problem**: Posts don't load when scrolling

**Solutions**:

1. Remove any Pagination or Load More elements
2. Check console for conflict warnings
3. Adjust scroll offset if page is already scrolled

#### Filter Options Missing

**Problem**: Expected taxonomy terms not showing

**Solutions**:

1. Disable "Hide Empty Options" temporarily
2. Check "Show Top-level Only" setting
3. Review "Terms Exclude" list
4. Ensure posts are assigned to those terms

#### Sort Not Reflecting Visually

**Problem**: Sort is applied but UI doesn't show selection

**Solution**: This was a known bug. Ensure you have the latest plugin version where Sort facet UI initializes correctly on page load.

#### Loading State Stuck

**Problem**: Element stays in loading state

**Solutions**:

1. Check browser console for JavaScript errors
2. Verify AJAX endpoint is accessible
3. Check for PHP errors in WordPress debug log


# For developers

#### `woohoo_loop_total_posts`

Filter the total post count used by the Ajax Query Count element and pagination.

```php
apply_filters( 'woohoo_loop_total_posts', int $total_posts, WP_Query $loop, array $args );
```

**Parameters:**

| Parameter      | Type       | Description                                                             |
| -------------- | ---------- | ----------------------------------------------------------------------- |
| `$total_posts` | `int`      | The computed total post count                                           |
| `$loop`        | `WP_Query` | The query object (post\_\_in may have been modified by pre\_get\_posts) |
| `$args`        | `array`    | The original query arguments passed to WP\_Query                        |

**Example — Variations as cards:**

When your shop archive shows each WooCommerce variation as its own card, Woohoo counts parent products but your loop displays variations. This filter lets you return the correct count:

```php
add_filter( 'woohoo_loop_total_posts', function( $total_posts, $loop, $args ) {
    $post_type = $loop->get( 'post_type' );
    if ( ! is_array( $post_type ) || ! in_array( 'product_variation', $post_type, true ) ) {
        return $total_posts;
    }
    $post_in = $loop->get( 'post__in' );
    if ( empty( $post_in ) || $post_in === [ 0 ] ) {
        return $total_posts;
    }
    return count( $post_in );
}, 10, 3 );
```

#### `woohoo_facet_term_counts`

Filter facet term counts before they are displayed. Runs after counts are computed and formatted, but before exclusions and ordering.

```php
apply_filters( 'woohoo_facet_term_counts', array $result, string $target_slug, string $target_tax, array $base_ids, bool $is_product_attribute );
```

**Parameters:**

| Parameter               | Type     | Description                                                                        |
| ----------------------- | -------- | ---------------------------------------------------------------------------------- |
| `$result`               | `array`  | Array of term data, each with `slug`, `name`, `count`, etc.                        |
| `$target_slug`          | `string` | The facet slug (e.g. `"serie"`)                                                    |
| `$target_tax`           | `string` | The taxonomy name (e.g. `"product_cat"`, `"pa_colore"`)                            |
| `$base_ids`             | `array`  | Post IDs currently in the filtered result set                                      |
| `$is_product_attribute` | `bool`   | `true` for WooCommerce product attributes (`pa_*`), `false` for regular taxonomies |

**Example — Multiply taxonomy counts by variation count:**

When showing variations as cards, taxonomy facets (Category, Serie) count parent products. But your shop displays N cards per product (one per variation). This filter multiplies the count:

```php
add_filter( 'woohoo_facet_term_counts', function( $result, $slug, $tax, $base_ids, $is_attr ) {
    // Attribute facets (pa_colore) already have correct counts — skip them.
    if ( $is_attr || empty( $result ) ) {
        return $result;
    }

    foreach ( $result as $i => $term ) {
        // Your logic to multiply count by variations per product.
        // See woohoo-variations-as-cards plugin for a full implementation.
    }

    return $result;
}, 10, 5 );
```

#### `woohoo_filters/indexer/get_rows`

Filter the index rows before they are inserted into the Woohoo index table. Use this to add extra rows for data that Woohoo doesn't index by default.

```php
apply_filters( 'woohoo_filters/indexer/get_rows', array $rows, int $object_id, array $facet );
```

**Example — Index variation attributes on the parent product:**

WooCommerce stores attributes like `pa_colore` on variations, not on the parent product. Without this, the Color facet returns no results for variable products:

```php
add_filter( 'woohoo_filters/indexer/get_rows', function( $rows, $object_id, $facet ) {
    $source = $facet['source'] ?? '';
    if ( strpos( $source, 'attribute/' ) !== 0 ) {
        return $rows;
    }
    $taxonomy = 'pa_' . str_replace( 'attribute/', '', $source );
    $product  = wc_get_product( $object_id );
    if ( ! $product || ! $product->is_type( 'variable' ) ) {
        return $rows;
    }
    foreach ( $product->get_children() as $variation_id ) {
        $terms = wp_get_object_terms( $variation_id, $taxonomy );
        foreach ( $terms as $term ) {
            $rows[] = [
                'object_id'    => $object_id,
                'slug'         => $facet['slug'],
                'facet_value'  => $term->slug,
                'facet_name'   => $term->name,
                'facet_id'     => $term->term_id,
                'facet_parent' => $term->parent,
                'facet_order'  => $term->term_order ?? 0,
            ];
        }
    }
    return $rows;
}, 10, 3 );
```


