Loops & Listings
Use Composer's loop system to build dynamic post grids, product listings, term archives, user directories, and external API feeds.
Composer's loop system lets you iterate over WordPress content directly inside a Composer widget's HTML using a {% for %} loop. One widget becomes a dynamic listing that renders every item in the result set.
The free version supports get_posts (posts and custom post types) only. get_products, get_terms, get_users, and get_api require UiChemy Pro, as do the pagination renderers (loop_pagination() and loop_load_more()). current_page() itself is Free, it only reads a URL parameter, no rendering involved.
The Loop Syntax
{% for post in get_posts({ post_type: 'post', posts_per_page: 6 }) %}
<article class="card">
<img src="{{ post.thumbnail.src('medium') }}" alt="{{ post.thumbnail.alt }}">
<h3>{{ post.title }}</h3>
<p>{{ post.excerpt|truncate(100) }}</p>
<a href="{{ post.link }}">Read More</a>
</article>
{% endfor %}
Inside the {% for %} block, the loop variable (post in the example) is a full data provider, every field, filter, and chained expression available on post works inside the loop.
Loop Source 1: get_posts
Query any WordPress post type. All WP_Query arguments are supported.
{% for item in get_posts({
post_type: 'portfolio',
posts_per_page: 9,
orderby: 'date',
order: 'DESC'
}) %}
<div class="item">{{ item.title }}</div>
{% endfor %}
Key parameters:
| Parameter | Type | Notes |
|---|---|---|
post_type | string or array | Default: 'post'. Any registered post type. |
posts_per_page | number | Default: 10. Max: 100. |
orderby | string | date, title, menu_order, rand, modified, comment_count, ID |
order | string | ASC or DESC |
post__in | array | Include only specific post IDs: [12, 45, 67] |
post__not_in | array | Exclude specific post IDs |
author | number | Filter by author ID |
s | string | Search query |
tax_query | array | Filter by taxonomy/term (standard WP_Query format) |
meta_key | string | Order by or filter on a meta key |
meta_value | string | Meta value to match |
paged | number | Page number for pagination. Use current_page() for dynamic paging. |
avoid_duplicates | boolean | Skip posts already rendered by an earlier loop on this page |
Taxonomy filter example:
{% for post in get_posts({
post_type: 'post',
posts_per_page: 6,
tax_query: [{
taxonomy: 'category',
field: 'slug',
terms: ['news', 'updates']
}]
}) %}
Loop Source 2: get_products (Pro)
Query WooCommerce products. Same parameters as get_posts with post_type forced to product.
{% for product in get_products({
posts_per_page: 8,
orderby: 'date',
meta_key: '_featured',
meta_value: 'yes'
}) %}
<div class="product-card">
<img src="{{ product.thumbnail.src('woocommerce_thumbnail') }}" alt="{{ product.thumbnail.alt }}">
<h3>{{ product.title }}</h3>
<p class="price">{{ product.price|raw }}</p>
{% if product.is_on_sale %}
<span class="badge">Sale</span>
{% endif %}
<a href="{{ product.link }}">View Product</a>
</div>
{% endfor %}
Loop Source 3: get_terms (Pro)
Query taxonomy terms, categories, tags, or any custom taxonomy.
{% for term in get_terms({
taxonomy: 'category',
hide_empty: true,
number: 12
}) %}
<a href="{{ term.link }}" class="tag-chip">
{{ term.name }} ({{ term.count }})
</a>
{% endfor %}
Key parameters:
| Parameter | Type | Notes |
|---|---|---|
taxonomy | string | Default: 'category'. Any registered taxonomy. |
hide_empty | boolean | Default: true. Exclude terms with no posts. |
number | number | Max items. Default: 100. |
orderby | string | name, count, slug, term_id |
order | string | ASC or DESC |
include | array | Include only specific term IDs |
exclude | array | Exclude specific term IDs |
parent | number | Only direct children of this term ID |
Loop Source 4: get_users (Pro)
Query WordPress site users.
{% for member in get_users({ role: 'author', number: 12 }) %}
<div class="team-card">
<img src="{{ member.avatar('150') }}" alt="{{ member.name }}">
<h3>{{ member.name }}</h3>
<p>{{ member.bio }}</p>
<a href="{{ member.link }}">View Posts</a>
</div>
{% endfor %}
Key parameters:
| Parameter | Type | Notes |
|---|---|---|
role | string | WordPress role: 'author', 'editor', 'subscriber', etc. |
number | number | Max items. Default: 100. |
orderby | string | display_name, registered, ID |
order | string | ASC or DESC |
include | array | Include only specific user IDs |
exclude | array | Exclude specific user IDs |
Loop Source 5: get_api, External JSON (Pro)
Fetch any external JSON endpoint and loop its items. Results are cached for 5 minutes by default.
{% for repo in get_api({
url: 'https://api.github.com/users/yourname/repos',
limit: 6,
cache: 600
}) %}
<div class="repo-card">
<h3>{{ repo.name }}</h3>
<p>{{ repo.description }}</p>
<a href="{{ repo.html_url }}">View on GitHub</a>
</div>
{% endfor %}
Key parameters:
| Parameter | Type | Notes |
|---|---|---|
url | string | The JSON endpoint URL (required) |
method | string | GET (default) or POST |
path | string | Dot-notation path into the response. Use when the array is nested: 'data.results' |
headers | object | Request headers, for Authorization, Accept, etc. |
body | any | Request body for POST requests |
limit | number | Max items to return (0 = all, max 100) |
cache | number | Cache duration in seconds. Default: 300. Set to 0 to disable caching. |
Nested response example:
{# API returns { "data": { "items": [...] } } #}
{% for item in get_api({ url: 'https://api.example.com/feed', path: 'data.items', limit: 5 }) %}
<p>{{ item.title }}</p>
{% endfor %}
Access nested fields with dot notation: {{ item.author.name }}, {{ item.metadata.tags }}.
API keys passed via headers are visible to anyone who reads the Composer widget's code. For sensitive APIs, route the request through a server-side WordPress function instead.
Avoid Duplicates
When you have two loops on the same page (a Featured Posts section and a Latest Posts section) and don't want the same post appearing twice, add avoid_duplicates: true to the second loop:
{# First loop, featured posts #}
{% for post in get_posts({ post_type: 'post', meta_key: '_featured', meta_value: 'yes', posts_per_page: 3 }) %}
<!-- featured card -->
{% endfor %}
{# Second loop, latest posts, skips anything shown above #}
{% for post in get_posts({ post_type: 'post', posts_per_page: 6, avoid_duplicates: true }) %}
<!-- latest card -->
{% endfor %}
avoid_duplicates works across the entire page request, any post ID rendered by any earlier get_posts or get_products loop is excluded.
Pagination
current_page() is Free, loop_pagination() requires Pro. To paginate a loop, add paged: current_page() to the query and render pagination links with loop_pagination():
{% for post in get_posts({
post_type: 'post',
posts_per_page: 9,
paged: current_page()
}) %}
<article><!-- card content --></article>
{% endfor %}
{{ loop_pagination()|raw }}
current_page() reads the ?loop_page=N URL parameter (falls back to WordPress's standard paged query var). loop_pagination() renders standard WordPress-style numbered pagination links that update ?loop_page=N.
Pagination works across loops on the same page. If you have two paginated loops on one page, they share the same page number.
Numeric Range Loops
For simple repeating patterns that aren't backed by data:
{% for i in range(1, 5) %}
<div class="star">★</div>
{% endfor %}
range(start, end, step), max span of 1000.
Safety Limits
Every loop source is capped at 100 items, regardless of what you request. range() has a max span of 1000. The ?loop_more= offset parameter (used with loop_load_more(), Pro) caps out at 10,000.
Breadcrumbs (Pro)
breadcrumbs() and breadcrumbs_html() build a context-aware breadcrumb trail, home, hierarchical pages, taxonomy archives, post type archives, author, search, 404, and WooCommerce shop, with optional schema.org BreadcrumbList structured data. There's also a matching Breadcrumbs dynamic tag for the no-code panel. See Dynamic Tags.
Use Cases
- A blog post grid, showing your latest posts, filtered by category, tag, or custom field.
- A product listing, built from
get_productsinstead of relying on WooCommerce's own archive template. - A directory or team page, looping over users or terms to render a card per person or category.
- Pulling in content from outside WordPress,
get_apirenders a loop from any external JSON endpoint.