Selling personalized products#
Cart line properties carry per-line shopper input — engraving text, a gift message, a chosen date — from the buy form through the cart to the order. They are DECLARED data: every property is defined first (type, rules, visibility), then collected. There are no free-form keys; an undeclared key is rejected with a typed user error.
1 — The merchant defines the field#
Settings → Metafield definitions → Cart lines. A definition has a namespace and key (together the wire key, e.g. "custom.engraving"), a value type (text, integer, decimal, boolean, date, URL, or a fixed choice), optional rules (max length, pattern, choices), and buyer access: read_write (the shopper types and sees it) or hidden (machine/merchant data — never rendered to the shopper). Apps declare their own definitions in the developer portal under their app namespace; the merchant consents at install.
2 — The theme collects it#
{% form 'cart_add' %}
<input type="hidden" name="product_id" value="{{ product.id }}">
<input type="hidden" name="variant_id" value="{{ product.selected_variant.id }}">
<input name="properties[custom.engraving]" maxlength="30"
placeholder="{{ 'products.engraving' | t }}">
<button type="submit">{{ 'products.add_to_cart' | t }}</button>
{% endform %}
One input per property, named properties[
3 — Lines split by personalization#
Line identity is (product, variant, properties): the same mug with two different engravings is TWO cart lines, each with a stable line_id. Adding the same product with the same properties merges quantities on that one line.
4 — The cart renders and targets lines#
{% for line in cart.items %}
<h4>{{ line.product.title }}</h4>
{% for key, value in line.properties %}
<p class="line-note">{{ value }}</p>
{% endfor %}
{% form 'cart_update' %}
<input type="hidden" name="product_id" value="{{ line.product_id }}">
<input type="hidden" name="line_id" value="{{ line.line_id }}">
<input type="number" name="quantity" value="{{ line.quantity }}">
{% endform %}
{% endfor %}
line.properties contains only buyer-visible properties — hidden definitions (app references, internal flags) never reach the theme or any shopper surface. At checkout, all properties snapshot onto the order line, where fulfillment sees them.