1. Conformance
The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, MAY and OPTIONAL in this document are to be interpreted as described in RFC 2119. Algorithms are written as sequences of numbered steps. Conforming implementations MAY use any equivalent formulation, as long as the observable result is the same. All arithmetic in this document is exact integer arithmetic unless a step says otherwise; “x mod y” is the remainder of floor division and is always non-negative for the non-negative operands used here; “floor(x / y)” is floor division.2. Terminology
column count (C): The number of grid columns being rendered. An integer, at least 1. authored column count (Ca): The merchant’s configured desktop column count (the first entry of the shop’scolumn_settings metafield, default
4). Block indexes are authored against a grid of Ca columns. Ca is a
required input even when rendering fewer columns.
slot: One cell of the conceptual grid. Slots are numbered 0, 1, 2,
… in reading order: left to right within a row, then the next row. The
slot with index i is in row floor(i / C), column (i mod C).
viewport class: Either "desktop" or "mobile". The web renderer
uses "mobile" when the viewport is at most 990 CSS pixels wide,
"desktop" otherwise. A phone app SHOULD use "mobile"; a tablet layout
MAY use either, but MUST use the matching column count.
product list (P): The ordered list of products to display, after
market resolution (see the integration guide). Each entry is a product.
block: A content block: placement fields index (integer, at least
0), span_columns (integer, at least 1), span_rows (integer, at least
1), aspect_ratio (decimal, width divided by height), visibility
("desktop", "mobile", or unset), plus content fields (media, text,
link). The block list (B) preserves the order of the metafield’s
reference list.
duplicate: A product duplicate entry: product (a product
reference), index (integer or null), created_at (integer, Unix
milliseconds, or null), and its own default_image and optional
hover_image. The duplicate list (D) preserves the order of the
metafield’s reference list.
item: A member of the composed sequence: a product, a duplicate, or a
block. Products and duplicates occupy exactly 1 slot. A block occupies a
rectangle of span_columns by span_rows slots.
set (of a field): A field is set when it is present with a non-empty,
non-null value. Unset fields read as empty strings (the Depict app writes
empty strings to clear previous values) or as null or absent on blocks
created before a field was added to the definition. Implementations MUST
treat empty string, null and absent identically.
merchandised view: The collection displayed in its published default
order, without user-applied sorting or filtering. An in-stock
availability filter does not disqualify a view from being merchandised.
Inputs that violate the stated ranges MUST be repaired before running the
algorithms: clamp index below 0 up to 0, and clamp span_columns or
span_rows below 1 up to 1.
3. Inputs
A renderer needs, for one collection and one market:- The product list P (market-resolved order).
- The block list B (market-resolved).
- The duplicate list D (market-resolved).
- The image setting list (market-resolved).
- The column count C for the current viewport class. C MUST come from the
shop’s
column_settingsmetafield when present ([desktop, mobile]), with defaults 4 for desktop and 2 for mobile. - The authored column count Ca (the desktop entry of the same metafield), needed by the remapping algorithm whenever C differs from Ca.
- The current viewport class.
- Whether the current view is a merchandised view.
4. Rendering pipeline
To render the grid, run these algorithms in order:- Applicability determines whether blocks and duplicates apply at all.
- Remap authored indexes re-anchors block indexes when C differs from Ca.
- Prepare the block list clamps spans and filters by visibility.
- Resolve block placement assigns every block a final, collision-free slot.
- Insert the duplicates merges duplicates into the product list, producing the item list L.
- Compose the sequence interleaves blocks with L into one ordered sequence with slot assignments.
- Lay out and size maps the sequence onto rows and determines heights.
- Present each block and product card.
5. Applicability
To determine applicability:- If the current view is a merchandised view, blocks and duplicates apply.
- Otherwise (the user changed the sort order, or applied any filter other than an in-stock availability filter), blocks and duplicates do NOT apply: the renderer MUST proceed with B and D treated as empty.
- Image settings apply in every view, including sorted and filtered views, with one exception: while the collection runs a Depict A/B test, web sessions assigned to the control variant hide blocks, duplicates AND image settings. This document assumes no active test; the integration guide describes the test metafield and the tradeoffs of ignoring tests in a native app.
6. Remapping authored indexes
Block indexes are slot numbers on the authored (desktop) grid. A wide block occupies a different number of slots on a narrower grid, which would shift every later block relative to the products around it. To keep blocks anchored to the same products on every device, the renderer replays the authored layout to find how many products precede each block, then re-places the blocks after the same number of products on the shown grid. If C equals Ca, this algorithm MUST be skipped: the block indexes are used as authored. Otherwise, run the two sub-algorithms below on the entire block list B, before the visibility filter. This is intentional and matches production: a block hidden on the current viewport class still shifts the anchors of the blocks after it.6.1 Counting products before each block
To count products before each block, given B (in list order) and Ca:- Group the blocks by max(index, 0), preserving list order within each group.
- Let covered be an empty set of slots, products be 0, and anchors be an empty map from blocks to integers.
- For slot = 0, 1, 2, … until every block has an anchor:
- If one or more blocks are grouped at this slot: let start be slot;
for each such block b in group order: let span be
min(b.span_columns, Ca); add every cell
start + r*Ca + c(for0 <= r < b.span_rows,0 <= c < span) to covered; set anchors[b] to products; set start to start + span. - Otherwise, if slot is not in covered, increment products.
- If one or more blocks are grouped at this slot: let start be slot;
for each such block b in group order: let span be
min(b.span_columns, Ca); add every cell
6.2 Re-placing the blocks
To re-place the blocks, given the anchors and C:- Stable-sort the blocks ascending by (anchors[b], b.index).
- Let covered be an empty set of slots, slot be 0, and products be 0.
- For each block b in sorted order:
- Let span be min(b.span_columns, C).
- Loop: if slot is in covered, increment slot; otherwise, if products < anchors[b], or (slot mod C) + span > C, increment products and increment slot (these slots will hold products); otherwise exit the loop.
- Add every cell
slot + r*C + c(for1 <= r < b.span_rows,0 <= c < span) to covered. (Only the continuation rows; the first row is passed over by advancing slot in the next step.) - Set b.index to slot, then set slot to slot + span.
- Return the blocks in their original list order with the updated indexes.
7. Preparing the block list
To prepare the block list, given the (possibly remapped) block list, viewport class V, and column count C:- Let prepared be an empty list.
- For each block b, in list order:
- Set b.span_columns to min(b.span_columns, C).
- If b.visibility is set and not equal to V, skip b. (A value that
is not exactly
"desktop"or"mobile"therefore hides the block on every viewport class.) - Append b to prepared.
- Return prepared.
8. Resolving block placement
Blocks request a slot throughindex, but requests can collide or fall
where the block does not fit. This algorithm assigns each block a final
slot such that no two blocks overlap and no block crosses a row boundary.
To resolve block placement, given the prepared block list and C:
- Let occupied be an empty set of slot indices.
- For each block b, in list order:
- Let s be b.index.
- Loop:
- Let columnStart be s mod C.
- If columnStart + b.span_columns is greater than C, set s to s + 1 and continue this loop. (The block would cross a row boundary.)
- If any slot of b’s candidate rectangle, that is any
s + r*C + cfor0 <= r < b.span_rowsand0 <= c < b.span_columns, is in occupied, set s to s + 1 and continue this loop. - Exit the loop.
- Add every slot of the rectangle (
s + r*C + cfor0 <= r < b.span_rows,0 <= c < b.span_columns) to occupied. - Set b’s final index to s.
- Blocks are processed in list order, so when two blocks request the same slot, the earlier block in the list wins it and the later block is pushed rightward and possibly to following rows.
- Final indices are unique, and every block’s rectangle is disjoint from every other block’s rectangle, by construction.
- A block’s final index can be far beyond the end of the product list; such blocks are handled by the trailing rule in Composing the sequence.
9. Duplicates
Duplicates are inserted into the product list before blocks are interleaved. The result is the item list L in which each duplicate is a 1-slot item, tagged as a duplicate and remembering its source entry.9.1 Ordering the duplicates
To order the duplicates, given D and the product list P:- For each duplicate d in D, compute its sort key:
- If d.index is not null, the key is d.index. (An index of 0 is a valid key.)
- Otherwise, if d.product is in P, the key is the 0-based position of d.product in P.
- Otherwise the key is positive infinity.
- Stable-sort D ascending by key. Call the result sortedD.
9.2 Inserting the duplicates
To insert the duplicates, given sortedD and P:- Let L be a copy of P.
- For each duplicate d in sortedD, in order:
- If d.index is not null, insert d into L at position min(d.index, length of L). (List splice semantics: existing entries at that position and after shift right by one.)
- Otherwise:
- Let p be the position in L of the first entry that is d’s original product (not a duplicate). If there is no such entry, skip d.
- Let related be the sublist of sortedD whose entries reference the same product as d.
- Stable-sort related ascending by created_at, treating null as positive infinity.
- Let rank be the 0-based position of d in the sorted related list.
- Insert d into L at position p + 1 + rank.
- Return L.
The web renderer keeps a slot reserved for an explicit-index duplicate
even when its original product is absent from the list, but cannot
render a card for it. Implementations of this specification MUST
instead skip any duplicate whose original product is not in P,
regardless of whether its index is set. Merchandisers do not create
such duplicates intentionally; they can only arise when a product
leaves a collection after publishing.
10. Composing the sequence
This algorithm interleaves the placed blocks with the item list L, producing the final ordered sequence and a slot assignment for every item. To compose the sequence, given C, the placed blocks (each with a final index), and L:- Let sequence be an empty list.
- Let slotOf be an empty map from items to slot indices.
- Let blockAt be a map from final index to block, for every placed block.
- Let pending be an empty map from slot index to integer. (It records where the continuation rows of multi-row blocks resume.)
- Let queue be L as a first-in-first-out queue.
- Let s be 0.
- While queue is not empty:
- If pending contains s: let k be pending[s], remove s from pending, set s to s + k, and return to step 7. (These k slots are covered by an earlier block’s continuation row.)
- Otherwise, if blockAt contains s: let b be blockAt[s], remove s from blockAt, append b to sequence, set slotOf[b] to s, then for each integer r from 1 up to and including b.span_rows - 1 set pending[s + r * C] to b.span_columns, then set s to s + b.span_columns, and return to step 7.
- Otherwise: dequeue an item i from queue, append i to sequence, set slotOf[i] to s, and set s to s + 1.
- (Trailing blocks.) For each block b remaining in blockAt, in block
list order (the order of the prepared block list; b’s final index is
disregarded from here on):
- Advance s to the smallest value s’ ≥ s such that
(s’ mod C) + b.span_columns ≤ C and no slot of the rectangle
(
s' + r*C + cfor0 <= r < b.span_rows,0 <= c < b.span_columns) is covered by an already placed block. - Append b to sequence, set slotOf[b] to s’, mark its rectangle covered, and set s to s’ + b.span_columns.
- Advance s to the smallest value s’ ≥ s such that
(s’ mod C) + b.span_columns ≤ C and no slot of the rectangle
(
- Return sequence and slotOf.
The web renderer implements step 8 through its page machinery and CSS
dense grid packing, which in degenerate configurations (for example
trailing blocks combined with multi-row blocks near the end of the
list) can order or pack trailing blocks differently. Implementations
MUST follow step 8 as written; it matches the web renderer in every
configuration merchandisers produce in practice.
11. Layout and sizing
To lay out the sequence:- The grid MUST have exactly C columns of equal width, with the
merchant’s configured column gap and row gap (see
grid_settingsin the integration guide) or the implementation’s own spacing. - Each item MUST occupy the slots assigned by Composing the sequence: products and duplicates one slot each, block b the rectangle of b.span_columns by b.span_rows slots whose top left is slotOf[b]. Blocks always occupy their configured span_rows rows.
- A row that contains products, duplicates, or parts of tile-sized blocks MUST derive its height from the product cards, and all such rows SHOULD share a uniform minimum height so that multi-row blocks align with the product rows they span. (The web renderer uses the measured height of the first product card as this minimum.)
- Native-ratio blocks. A block is rendered at its media’s native
aspect ratio if and only if it is full-width (b.span_columns ≥ C)
AND b.aspect_ratio ≥ 1 AND b.span_rows equals 1. For such a block:
- Its rendered height MUST be its rendered width divided by b.aspect_ratio.
- The row it occupies MUST size itself to that height instead of the product-derived minimum.
- Tile-sized blocks. Every other block, including full-width blocks with aspect_ratio below 1 (portrait) or span_rows above 1, MUST fill its entire spanned rectangle; its media is cropped to cover the area (center crop). aspect_ratio is not used for sizing these blocks.
- Spacer blocks. If a block’s
spacecontent is present with a non-null height H, the block’s rendered height MUST be H pixels (overriding rules 4 and 5 for height); with a null height it follows the normal sizing rules. Spacers render no media.
When the web renderer cannot measure any product card (for example a
collection with no products), it cannot size tiles, so every
full-width block falls back to its native aspect ratio rather than
becoming invisible. A native implementation whose product cards have
intrinsic sizes does not need this fallback, but MAY reproduce it.
12. Block presentation
- Media selection. If the block has a
video, render the video and ignoreimage. Otherwise, if it has animage, render the image. Otherwise, if it hasspacecontent, render an empty spacer. - Video MUST be rendered muted, looping, inline (no fullscreen
takeover, no controls), using the block’s preview image as poster, and
SHOULD play only while visible. Prefer the HLS (
m3u8) source on mobile devices and the largest-width MP4 source on desktop. - Hover image. When
hover_imageis set: on pointer devices, show it in place of the main image while hovered. On touch devices, the web renderer presents the two images as horizontally swipeable frames with the main image first; native apps SHOULD do the same or an equivalent two-frame affordance, and MAY show only the main image. - Text overlay. When
textis present, render its header and body (each only when itstextvalue is non-empty) stacked vertically over the media, in that order:- Horizontal and vertical alignment come from
horizontal_alignmentandvertical_alignment; both default tocenter. The overlay area has a 16 pixel inset from the block edges and the two text parts are separated bygap(default 0). - Each text part uses
color_hex(default white) and its bold, italic and underline flags.html_tagconveys the semantic level (h1, h2, h3, p); native implementations SHOULD map it to an equivalent type scale. - When
background_overlayis set, render it between the media and the text: as a gradient of that color fading to transparent whenoverlay_styleis exactly"gradient", and covering the whole block otherwise, including whenoverlay_styleis absent. The gradient anchors to the text’s vertical alignment: from the top edge when vertical alignment isstart, from the bottom edge whenend, and as a centered horizontal band whencenter. - When
text_shadowis true, render a soft dark drop shadow behind the text.
- Horizontal and vertical alignment come from
- Instagram attribution. When
instagram_post_metadatais present, render a small attribution element in the bottom right corner of the block showing thecreatorname, linking topost_url. - Link. At most one link field is set. If, due to stale data, more
than one is set, use the first in this order:
link_to_collection,link_to_page,link_to_product, blog article (blog_handlewithblog_article_id),external_url. The whole block is the tap target. Blog article links resolve by finding the article whose ID matchesblog_article_idwithin the blog named byblog_handle. - Alt text. Use
alt_textas the image’s accessibility label when present.
13. Product presentation
- Image settings. For every product in the grid that has an image
setting in this collection, the card’s primary image MUST be the
setting’s
default_image, and, if the card design has a hover or second image and the setting has ahover_image, that slot MUST use it. All other card content (title, price, link) is unchanged. Image settings apply only within the collection whose metafield references them. - Duplicates. A duplicate renders the same card as its original
product, except that its imagery MUST come from the duplicate’s own
default_imageand optionalhover_image. - Interaction. Image settings MUST NOT be applied to duplicates. If a product has both an image setting and a duplicate, the original card shows the image setting’s imagery and the duplicate card shows the duplicate’s imagery.
14. Pagination
Native apps and headless storefronts SHOULD render a collection as a single sequence (page size of infinity), which is what the algorithms above produce, and paginate by loading products incrementally. An implementation that reproduces the web storefront’s numbered pages MUST split the composed sequence as follows: walk the sequence in order, counting only original products (not duplicates, not blocks); a new page starts each time the count reaches a multiple of the page size; every item, duplicate and block belongs to the page that was current when it was placed; blocks placed by the trailing rule belong to the last page.15. Edge case summary
16. Test vectors
Each vector gives the exact inputs and the required outputs: the composed sequence, each item’s slot, and the visible grid as a matrix (rows of C cells;. is an empty cell; a block’s ID fills every cell it covers).
A conforming implementation MUST reproduce these outputs. All vectors
assume a merchandised view. Vectors 1 through 3 render at the authored
column count (C = Ca = 4), so the remapping algorithm is an identity for
them; vectors 4 and 5 also exercise C = 2 with Ca = 4.
Vector 1: basic interleaving
Input:- Final index of B1: 2.
- Sequence:
p1, p2, B1, p3, p4, p5, p6, p7, p8 - Slots: p1=0, p2=1, B1=2 (covers 2 and 3), p3=4, p4=5, p5=6, p6=7, p7=8, p8=9.
Vector 2: a 2 by 2 block and a pushed block
Two blocks, where the second requests a slot inside the first block’s area and is pushed, and where the first block’s second row must not swallow the slots of the items beside it. Input:- Final indices: B1 = 0 (covers 0, 1, 4, 5), B2 = 2 (slot 1 is occupied by B1, so B2 is pushed to 2).
- Sequence:
B1, B2, p1, p2, p3, p4, p5, p6 - Slots: B1=0, B2=2, p1=3, p2=6, p3=7, p4=8, p5=9, p6=10. (After p1 at slot 3, the composer reaches slot 4, finds B1’s continuation row, and skips to slot 6.)
Vector 3: duplicates
No blocks; three duplicates of two products, exercising explicit index, index-null ordering by created_at, and sequential insertion shifts. Input:- Sort keys: d1 = 1 (B’s position), d2 = 1, d3 = 0. Sorted order (stable):
d3, d1, d2. - Insertions, in order:
- d3 (index 0):
[d3, A, B, C, D] - d1 (null): B is at position 2; related duplicates of B sorted by
created_at are
[d2, d1], d1’s rank is 1; insert at 2 + 1 + 1 = 4:[d3, A, B, C, d1, D] - d2 (null): B is at position 2; d2’s rank is 0; insert at
2 + 1 + 0 = 3:
[d3, A, B, d2, C, d1, D]
- d3 (index 0):
- Sequence:
d3, A, B, d2, C, d1, Dwith slots 0 through 6.
Vector 4: visibility, clamping and trailing blocks
The same input rendered at both viewport classes. B1 is desktop-only and wider than the mobile grid; both blocks land beyond the products. Input:"desktop" (C = Ca = 4, remap is an
identity):
- Both blocks are visible. Placement in list order: B1 requests 5; columnStart 1 with span 4 does not fit, nor do 6 or 7; slot 8 has columnStart 0 and is free; B1’s final index is 8 (covers 8 through 11). B2 requests 3; columnStart 3 with span 2 does not fit; slot 4 is free; final index 4 (covers 4 and 5).
- Products fill slots 0 through 3, then the queue is empty, so both blocks are trailing. In block list order, B1 is appended first: the cursor is at slot 4, which fits a span of 4, so B1 lands at slots 4 through 7. B2 follows at slots 8 and 9. Their final indexes (8 and 4) are disregarded by the trailing rule.
- Sequence:
p1, p2, p3, p4, B1, B2. B1 is full-width with aspect_ratio 2.0 and one row, so it renders as a natural-ratio band (height = width / 2.0). B2 spans 2 of 4 columns and is tile-sized.
"mobile" (C = 2, Ca = 4):
- Remapping, counting phase on the authored grid (4 columns): slots 0 through 2 hold products (count 3); B2 is authored at slot 3 (its authored rectangle covers 3 and 4), so anchors[B2] = 3; slot 4 is covered; B1 is authored at slot 5, so anchors[B1] = 3.
- Remapping, re-placing phase on 2 columns, sorted by (anchor, index) = B2 then B1: for B2 (span 2), slots 0 through 2 are counted as products 1 through 3, slot 3 starts at column 1 where a span of 2 does not fit (counted as product 4), slot 4 fits: B2.index becomes 4. For B1 (span clamped to 2), the cursor is then at slot 6, which fits: B1.index becomes 6.
- The visibility filter then drops B1 (desktop-only). B2’s span is 2. Placement: slot 4 has columnStart 0 and is free; final index 4.
- The four products fill slots 0 through 3; B2’s slot is never reached while items remain, so B2 is a trailing block, appended at slot 4.
- Sequence:
p1, p2, p3, p4, B2. B2 is full-width on this grid (span equals C) with aspect_ratio 1.5 and one row, so it renders as a natural-ratio band.
Vector 5: remapping to a narrower grid
A full-width desktop hero above the products, with a second block anchored right after it. On mobile the hero is hidden, yet it still shapes the second block’s anchor. This vector fails if remapping is skipped or run after the visibility filter. Input:"desktop" (C = Ca = 4, remap is an
identity):
- Placement: B1’s final index is 0 (covers 0 through 3); B2’s final index is 4 (covers 4 and 5).
- Sequence:
B1, B2, p1, p2, p3, p4, p5, p6. B1 renders as a natural-ratio band (full-width, aspect_ratio 2.0, one row); B2 is tile-sized.
"mobile" (C = 2, Ca = 4):
- Remapping, counting phase (4 columns): B1 is authored at slot 0 covering 0 through 3, B2 at slot 4 covering 4 and 5. No product slots precede either block: anchors[B1] = 0 and anchors[B2] = 0.
- Remapping, re-placing phase (2 columns), sorted by (anchor, index) = B1 then B2: B1 (span clamped to 2) is placed at slot 0, the cursor moves to 2; B2 (span 2) is placed at slot 2. B1.index becomes 0, B2.index becomes 2.
- The visibility filter drops B1. B2 is placed at final index 2.
- Composition: p1 and p2 take slots 0 and 1, B2 takes slots 2 and 3, p3 through p6 take slots 4 through 7.
- Sequence:
p1, p2, B2, p3, p4, p5, p6. B2 is full-width on this grid and renders as a natural-ratio band.
17. Self-verification checklist
An implementation is conforming when:- All five test vectors reproduce exactly (sequence, slots, matrix) at every stated column count.
- Re-running vector 1 with
"index": 50for B1 appends B1 after p8 (trailing rule). - Re-running vector 2 with both blocks at
"index": 0places B1 at 0 and pushes B2 to slot 2 (list order priority). - Re-running vector 5 on mobile with B1’s visibility set to
"mobile"places B1 at slots 0 and 1 and B2 at slots 2 and 3, with all six products after them; both render as natural-ratio bands. - Re-running any vector with a sorted or filtered view renders only the products, in the sorted or filtered order, with image settings still applied.

