Main Collection Grid: Merchandisable Promo Tiles, Load-More and a Filter Blocklist
Dawn's collection grid extended in four directions: promo tiles a merchandiser places by grid position, a textarea that suppresses unwanted filter values, load-more over the Section Rendering API, and an infinite-scroll fallback.
Shopify generates every filter value your product data implies, gives merchandisers no way to place editorial inside a product grid, and paginates with numbered links on a mobile-heavy audience. We extended Dawn's collection grid with position-placed promo blocks, a merchandiser-editable filter blocklist, section-rendered load-more and an infinite-scroll option — reusing the theme's own facet transport rather than adding a second one.
Fact Strip
- Client: Nudestix
- Surface: Product listing page
- Templates served: 2
- Complexity: High
- Attribution: Deploi-authored additions on top of Dawn 6.0.2's collection grid section. The facet form, the section-rendering transport and the paginated loop are Dawn's; the promo blocks, the filter blocklist, load-more, infinite scroll and the tag suppression are ours.
- Status: Live, verified 2026-09-06
- Evidence: Four code artifacts plus three client working records
- Platform primitives: 5
The Problem
Three gaps met on one page. Shopify's storefront filtering generates a filter value for everything your product data implies, and there's no admin control for hiding one — so operational tags meant for internal use surface to shoppers as filterable attributes. The default grid gives merchandisers no way to put editorial or campaign content inside the product flow; anything promotional sits above or below it, where it is not part of browsing. And Dawn paginates with numbered links, a desktop convention on a store whose traffic is mostly phones. Products tagged for other systems compounded it: they had to leave the grid without leaving the collection.
The Constraint
This is Dawn 6.0.2, so the surrounding machinery had to survive intact. FacetFiltersForm, the ?section_id= re-render path and {% paginate %} are how filtering already worked, and anything layered on top had to keep working through them rather than beside them. Shopify's storefront filters cannot be selectively hidden from the admin — the suppression has to happen at render time in the theme. And there is no native primitive for "insert a tile at grid position N": Liquid has no way to interleave section blocks into a paginated product loop, so grid position has to be computed against forloop.index as the loop runs.
What We Built
The section keeps Dawn's core — {% render 'facets' %} wrapping a {% paginate collection.products %} loop — and adds four things to it.
Promo tiles. A promo_grid block type carrying a collection handle, a numeric grid position, an image, a width of single, double or full, copy, button text and link, and background, text and hover colors. The block is injected into the <li> stream when forloop.index matches its position and its handle matches the current collection, de-duplicated across the desktop and mobile variants through a shownPromos accumulator. Button colors are read from data-* attributes into CSS custom properties on DOMContentLoaded, so a merchandiser's color choice survives a re-render that rebuilds the markup.
A filter blocklist. A hidden_filter textarea setting is passed into the facets snippet, which splits it on a comma-space separator and suppresses matching values from both the desktop and mobile facet lists. It is a plain textarea on purpose: the list changes with the catalog, and changing it is a merchandiser task, not a deploy.
Load more. A button carrying data-current-page, data-total-pages and data-section-id, handled by FacetFiltersForm.initializeLoadMore(). It fetches the current URL with page and section_id appended and appends only the grid items, skipping promo tiles whose copy already exists. Because the request is the current URL, active facets and sort carry forward with no extra state to manage.
Infinite scroll. An enable_infinite_scroll checkbox arms a fallback that watches for the Load More button entering the viewport and activates it, throttled so a fast scroll cannot queue requests. A .fallback-pagination block reveals Dawn's numbered pagination if the fetch path fails, so the page degrades to what Dawn already shipped.
Tag suppression is a single {% unless product.tags contains 'hidden' %} around the card render.
Why This Way
Dawn was already re-fetching the section on every filter change. Load-more just asks it for the next page. One code path instead of two, cached filter responses reused for pagination, and no second copy of the card markup to keep in step with the Liquid original. A dedicated pagination endpoint would have been tidier on a whiteboard and worse in the theme.
Promo tiles became theme-editor blocks rather than hard-coded markup because placement changes every campaign, and scoping each block to a collection handle lets one section instance carry tiles for several collections without a merchandiser opening a Liquid file.
The costs come with the layering, and they don't go away. Because the grid is rebuilt by a section fetch, anything bound to a card has to be re-bound afterwards — review widgets and inline scripts both need re-initialization on every append, and every widget you install on this page from now on inherits that obligation. And because promo tiles consume grid columns, the per-page count and the tile position have to agree.
Why Not an App
In-grid promotional tiles and load-more are two of the most common reasons a merchant buys a collection-page app, and both are small once you are already inside the theme: the load-more handler is a few dozen lines of fetch around a transport Dawn already provides. Buying it adds a script tag to every product listing page, a second rendering path for cards that already have one, and a recurring fee for behavior the theme was three settings away from. The filter blocklist is clearer still — the value is generated at render time from product data, so suppressing it is theme code regardless of what else is bought.
Implementation Notes
- Per-page count is reduced by two when a promo tile is configured for the current collection, so a double-width tile does not push the grid onto a ragged row.
- Load-more re-fetches the current URL with
pageandsection_idappended, which means active facets and the active sort carry into the next page without being tracked separately. - The append step compares promo-tile heading and body copy between the fetched markup and what is already in the grid, so a tile is never appended to the page twice.
FacetFiltersForm.renderPagereads a gift-collection URL attribute as its base when present, so the same facet form can drive a section that swaps in a different collection entirely.- The grid container re-injects an inline script element after each re-render, because a show-and-hide helper lives inline in the section body and does not survive replacement.
- The review widget's loader is called after every append, so star ratings re-bind on newly added cards rather than appearing only on the products that were there at first paint.
- A hidden-product count feeds the load-more visibility logic, so tag-suppressed products cannot make the last page look short enough for the control to hide itself a page early.
Edge Cases
- An empty collection renders the localized empty state and hides the facets aside entirely, rather than offering filters over nothing.
- Products carrying the suppression tag are skipped in the grid but still counted by the collection's product count. The load-more visibility math exists to absorb that gap.
- Load more is not rendered at all unless there is more than one page and a next page actually exists.
- On a fetch error the button is re-enabled and the numbered-pagination fallback is revealed; on an empty result the button hides itself permanently.
- A promo tile whose button link a merchandiser left blank falls back to an inert href rather than navigating to the current page.
- The mobile facet drawer restores the body's overflow and position on close, so a shopper who dismisses it is never left on a frozen page.
Platform Primitives Used
- Section Rendering API — the transport for facet changes and, by reuse, for load-more; one request shape serves both.
- Liquid section schema and settings — carries the promo blocks, the filter blocklist textarea and the infinite-scroll switch, so all three are merchandiser-editable.
- Storefront filtering — supplies every facet value on the page; the blocklist subtracts from that set at render time rather than replacing it.
- Metafields — back the comparison hooks the grid cards expose, keeping that data in Shopify rather than in the section.
- Metaobjects — referenced by the filter values that render as swatches, so a swatch's label and color stay merchandiser-managed.
Integrations in Play
- PowerReviews — coexists with the grid; its loader is invoked after each append so review stars re-bind on cards that did not exist when the page first painted.
Where It Runs
Two templates on one surface: the standard collection template and a variant that leads with a video banner. Both mount the same section, so promo blocks are scoped by collection handle rather than by template: one configuration serves both, and a campaign tile never leaks onto the other.
What This Demonstrates
Primary: Filters, faceted navigation and progressive grid loading — the governance half of it, where a merchandiser controls which filter values a shopper sees. It is also a collection-merchandising build: the in-grid promo block is how editorial reaches the product flow at all.
How We Know
Four code artifacts read directly from the theme — the collection grid section, the shared facets snippet, the facets module and an inline helper script — running to roughly 900 lines. Three client working records cover the same scope: two specifications asking for infinite scroll with a load-more control as the failsafe, and naming filter values to keep out of the facet lists. One section implements both requirements, so they are documented here as one build.
Related Builds
- Shade-level collection grid hydrated from an external faceting service — the same storefront's other collection template, and the comparison for what happens when the grid leaves Liquid.
- Vertical filter rail and an in-place mobile filter accordion — a different storefront rebuilding the facet interface rather than the grid.
- Collection page: three filter modes, in-grid promo blocks and infinite scroll — the same feature set reached through a premium theme's settings.
The Buy-vs-Build Question
Everything on this page is a line item in some collection-page app's feature list, which makes it a useful test of where the buy line sits. Our answer: facets stay Shopify's, and the interface around them stays yours, because the interface is where the merchandising judgment lives. See the filters and faceted navigation decision.
Ready to Give Your Merchandisers Control of the Grid?
Promo placement, filter suppression and pagination shouldn't need a developer every campaign. Contact us today and we'll show you what your collection page could hand back.