Shopify Horizon’s theme blocks have no built-in block repeater. You cannot connect a list, such as a product’s related blog posts, and get repeated blocks, one for each item. We added one custom block, Repeater: Blog posts, that does this. You design one card in the theme editor, and the block repeats it for every blog post chosen on the product.
On our store, each service product now shows its own four articles, picked in Shopify admin. The block is one new file, reuses Horizon’s own grid and carousel, and leaves Horizon’s files unchanged. We built and tested it on Horizon 4.2.0.
Related blog posts are one example of this approach. The same repeated-block pattern works for other modules, such as product highlights, how-it-works steps, or related products. The last section covers when it fits.

Each card is the same Post card design, repeated for the four posts saved on this product.
What changed from Dawn to Horizon
In Dawn and other themes built on section blocks, you could repeat a block from a list without code. Shopify’s metaobject guide shows the pattern with a Multirow section. You select Connect dynamic source next to the block name and choose a list of metaobjects, and the theme editor fills the section with one block per entry.
Horizon builds pages from theme blocks, which can be nested and reused across sections. In Horizon 4.2.0, native theme blocks such as Text and Group have no Connect dynamic source option at the block level, so there is no way to connect a list and have a block repeat for each entry. Merchants and developers have asked Shopify for a block repeater in Horizon. Until Shopify adds one, the repeating has to happen in the theme code.
How the repeater works
The repeater has two parts:
- A list. Here, the blog posts saved on the current product. You can also pick posts by hand in the theme editor.
- A card template. Ordinary Horizon blocks placed inside the repeater, such as a Group with an Image and a Text. Each block reads the current post: its title, cover image, and URL.
For every post in the list, the repeater renders the card once. Change the card once and every card follows. Add a post to the product and one more card appears, with no template edit.

The sidebar shows one Post card. The preview shows it repeated once for each related post.
The layout comes from Horizon itself. The repeater passes its cards to the same list component that Horizon’s Blog posts and Product list sections use, and its layout settings match those sections: grid or carousel, columns, mobile columns, carousel on mobile, gaps, and arrow style. The cards therefore behave like the rest of the theme on desktop and mobile.
Step 1: Create the Related articles metafield
Each product stores its own list of posts in a product metafield.
- In Shopify admin, go to Settings > Metafields and metaobjects > Products and select Add definition.
- Name it Related articles and check that the namespace and key are
custom.related_articles. - For the type, choose Blog post and select List of blog posts.
- Under access options, allow Storefronts to read the metafield, then save.

Step 2: Add the Repeater block
Work on an unpublished duplicate of your theme. In Shopify admin, go to Online Store > Themes, open the … menu on the duplicate, and choose Edit code. In the blocks folder, add a file named cw-repeater-posts.liquid, paste the code below, and save.
{% comment %}Modified by CheckoutWorks.dev{% endcomment %}
{%- liquid
assign cw_posts = block.settings.items
-%}
{%- capture cw_list_items -%}
{%- for cw_post in cw_posts limit: block.settings.limit -%}
<div class="resource-list__item">
{% content_for 'blocks', closest.article: cw_post %}
</div>
{%- unless forloop.last -%}<!--@list/split-->{%- endunless -%}
{%- endfor -%}
…
Collapse code
{% comment %}Modified by CheckoutWorks.dev{% endcomment %}
{%- liquid
assign cw_posts = block.settings.items
-%}
{%- capture cw_list_items -%}
{%- for cw_post in cw_posts limit: block.settings.limit -%}
<div class="resource-list__item">
{% content_for 'blocks', closest.article: cw_post %}
</div>
{%- unless forloop.last -%}<!--@list/split-->{%- endunless -%}
{%- endfor -%}
{%- endcapture -%}
{%- if cw_posts == blank and request.design_mode -%}
<div class="cw-repeater cw-repeater--empty" {{ block.shopify_attributes }}>
<p>Select blog posts or connect a product metafield to show the cards.</p>
</div>
{%- elsif cw_list_items != blank -%}
{%- assign cw_list_items_array = cw_list_items | strip | split: '<!--@list/split-->' -%}
<div class="cw-repeater" {{ block.shopify_attributes }}>
{% render 'resource-list',
list_items: cw_list_items,
list_items_array: cw_list_items_array,
settings: block.settings,
carousel_ref: 'cwRepeater',
slide_count: cw_list_items_array.size,
content_type: 'articles'
%}
</div>
{%- endif -%}
{% stylesheet %}
.cw-repeater {
width: 100%;
min-width: 0;
}
.cw-repeater--empty {
padding: var(--padding-lg, 16px);
border: 1px dashed rgb(var(--color-foreground-rgb) / var(--opacity-30));
text-align: center;
}
{% endstylesheet %}
{% schema %}
{
"name": "Repeater: Blog posts",
"tag": null,
"blocks": [
{
"type": "@theme"
},
{
"type": "@app"
}
],
"settings": [
{
"type": "paragraph",
"content": "The blocks inside are a template. They repeat once for each blog post, so one change applies to every card."
},
{
"type": "article_list",
"id": "items",
"label": "Blog posts",
"limit": 50,
"info": "Connect a product metafield or select blog posts."
},
{
"type": "range",
"id": "limit",
"label": "Maximum items",
"min": 1,
"max": 50,
"step": 1,
"default": 50
},
{
"type": "header",
"content": "t:content.cards_layout"
},
{
"type": "select",
"id": "layout_type",
"label": "t:settings.layout_type",
"options": [
{
"value": "grid",
"label": "t:options.grid"
},
{
"value": "carousel",
"label": "t:options.carousel"
}
],
"default": "grid"
},
{
"type": "checkbox",
"id": "carousel_on_mobile",
"label": "t:settings.carousel_on_mobile",
"default": true,
"visible_if": "{{ block.settings.layout_type != 'carousel' }}"
},
{
"type": "range",
"id": "columns",
"label": "t:settings.columns",
"min": 1,
"max": 8,
"step": 1,
"default": 3,
"visible_if": "{{ block.settings.layout_type == 'grid' or block.settings.layout_type == 'carousel' }}"
},
{
"type": "select",
"id": "mobile_columns",
"label": "t:settings.mobile_columns",
"options": [
{
"value": "1",
"label": "t:options.one_number"
},
{
"value": "2",
"label": "t:options.two_number"
}
],
"default": "2",
"visible_if": "{{ block.settings.layout_type == 'grid' and block.settings.carousel_on_mobile == false }}"
},
{
"type": "select",
"id": "mobile_card_size",
"label": "t:settings.mobile_card_size",
"options": [
{
"value": "60cqw",
"label": "t:options.one_number"
},
{
"value": "44cqw",
"label": "t:options.two_number"
}
],
"default": "60cqw",
"visible_if": "{{ block.settings.layout_type == 'carousel' or block.settings.carousel_on_mobile == true }}"
},
{
"type": "range",
"id": "columns_gap",
"label": "t:settings.horizontal_gap",
"min": 0,
"max": 100,
"step": 1,
"unit": "px",
"default": 16,
"visible_if": "{{ block.settings.layout_type == 'grid' or block.settings.layout_type == 'carousel' }}"
},
{
"type": "range",
"id": "rows_gap",
"label": "t:settings.vertical_gap",
"min": 0,
"max": 100,
"step": 1,
"unit": "px",
"default": 16,
"visible_if": "{{ block.settings.layout_type == 'grid' }}"
},
{
"type": "header",
"content": "t:content.carousel_navigation",
"visible_if": "{{ block.settings.layout_type == 'carousel' or block.settings.carousel_on_mobile == true }}"
},
{
"type": "select",
"id": "icons_style",
"label": "t:settings.icon",
"options": [
{
"value": "arrow",
"label": "t:options.arrows"
},
{
"value": "chevron",
"label": "t:options.chevrons"
},
{
"value": "arrows_large",
"label": "t:options.arrows_large"
},
{
"value": "chevron_large",
"label": "t:options.chevron_large"
},
{
"value": "none",
"label": "t:options.none"
}
],
"default": "arrow",
"visible_if": "{{ block.settings.layout_type == 'carousel' or block.settings.carousel_on_mobile == true }}"
},
{
"type": "select",
"id": "icons_shape",
"label": "t:settings.icon_background",
"options": [
{
"value": "none",
"label": "t:options.none"
},
{
"value": "circle",
"label": "t:options.circle"
},
{
"value": "square",
"label": "t:options.square"
}
],
"default": "none",
"visible_if": "{{ block.settings.icons_style != 'none' and block.settings.layout_type == 'carousel' or block.settings.carousel_on_mobile == true }}"
}
],
"presets": [
{
"name": "Repeater: Blog posts",
"category": "Layout",
"settings": {
"columns": 4
},
"blocks": {
"card": {
"type": "group",
"name": "Post card",
"settings": {
"link": "{{ closest.article.url }}"
},
"blocks": {
"card-image": {
"type": "image",
"name": "Image",
"settings": {
"image": "{{ closest.article.image }}"
}
},
"card-title": {
"type": "text",
"name": "Title",
"settings": {
"text": "<h3>{{ closest.article.title }}</h3>"
}
}
},
"block_order": [
"card-image",
"card-title"
]
}
},
"block_order": [
"card"
]
}
]
}
{% endschema %}
Most of the file is settings copied from Horizon’s Blog posts section, so the editor labels, defaults, and translations match the theme. The block also includes a starting card, which appears as Repeater: Blog posts in the Add block menu.
Step 3: Place it on the product template
- Open the theme editor on the duplicate theme and go to the product template you want to change.
- In a section, select Add block and choose Repeater: Blog posts from the Layout category. It arrives with a Post card that already shows each post’s cover image and title and links to the post.
- Select the repeater. Next to Blog posts, select the Connect dynamic source icon and choose the product’s Related articles.
- Set the layout. We use a carousel with four columns on desktop and the carousel on mobile, the same settings as the Blog posts section it replaced.
- Style the Post card, Image, and Title like any other Horizon block, then save.
If a heading sits above the cards, add it as a separate Text block before the repeater. Anything placed inside the repeater repeats once per post.
Step 4: Choose the posts for each product
Open a product in Shopify admin, find Related articles under Metafields, select the posts, drag them into order, and save. The storefront shows them in that order.

The same post can appear on any number of products. When you change a post’s title or cover image, every card that shows it updates on its own.
Limits to know
- Horizon-family themes only. The block is a theme block. Themes without theme block support need a different approach.
- Up to 50 posts per list. This is Shopify’s limit for this kind of list setting.
- An empty list shows no cards, but a heading above it stays. Give every product that uses the template at least one post, or place the heading where an empty list will not leave it alone on the page.
- Card links do not open in the theme editor. Horizon keeps Group links inactive in the editor so you can select the blocks inside a card. On the storefront, the whole card links to the post.
-
One list type per file. Shopify accepts one
content_for 'blocks'tag per block file and one resource type passed through it, so a repeater for products or metaobjects is a separate block file.
Horizon updates do not overwrite a new block file. The repeater does depend on Horizon’s resource-list snippet and the layout setting names of its Blog posts section. After an update, check the cards on an unpublished copy first. Our guide to updating Shopify Horizon without losing custom code covers the process.
Other modules that suit repeated blocks
This article shows one way to build a block repeater, with blog posts as the list. The approach is the same for other content: a list, a card template made of Horizon blocks, and one repeat per entry. It fits any module where these points hold:
- The layout stays the same and only the content changes. One card design, many entries.
- The number of items varies. One product has two highlights and another has six.
- Different pages need different items. Each product, collection, or service shows its own set.
- The same content appears in several places. You edit it once in Shopify admin.
Typical examples are product highlights, how-it-works steps, related products or accessories, collection shortcuts, team members, customer quotes, logo walls, and store locations. Shopify can pass products, collections, blog posts, and metaobjects to repeated blocks in this way. Metaobjects are the flexible option for content that is not a product or post.
A repeater is unnecessary when a module has a few items that rarely change and appear in one place. Horizon’s native blocks, added by hand, are simpler to maintain there.
If your store has a module like these that should repeat from its data, building it is part of our Shopify Feature Implementation service. We first agree where the content lives and who will maintain it, then build and test the repeater on a duplicate theme.