From be4a97865b0432e2fc93c379c0b37e611fb94e2a Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Fri, 31 Jul 2026 13:57:18 +0700 Subject: [PATCH 1/4] add personalized product guide --- .../docs/storefront/themes/guides/meta.json | 2 +- .../themes/guides/personalized-products.mdx | 134 ++++++++++++++++++ .../storefront/themes/templates/objects.mdx | 27 ++++ next-env.d.ts | 2 +- 4 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 content/docs/storefront/themes/guides/personalized-products.mdx diff --git a/content/docs/storefront/themes/guides/meta.json b/content/docs/storefront/themes/guides/meta.json index 6b56a043..c53f1de0 100644 --- a/content/docs/storefront/themes/guides/meta.json +++ b/content/docs/storefront/themes/guides/meta.json @@ -1,4 +1,4 @@ { "title": "Guides", - "pages": ["custom-page-templates", "custom-product-templates", "product-variants"] + "pages": ["custom-page-templates", "custom-product-templates", "product-variants", "personalized-products"] } diff --git a/content/docs/storefront/themes/guides/personalized-products.mdx b/content/docs/storefront/themes/guides/personalized-products.mdx new file mode 100644 index 00000000..d8f0ac65 --- /dev/null +++ b/content/docs/storefront/themes/guides/personalized-products.mdx @@ -0,0 +1,134 @@ +--- +title: Personalized Products Guide +sidebar_label: Personalized Products +tags: + - Guide +--- +import { Callout } from 'fumadocs-ui/components/callout'; +import { Step, Steps } from 'fumadocs-ui/components/steps'; +import { Cards, Card } from 'fumadocs-ui/components/card'; + +Some products need customer input at the time of purchase — an engraving on a mug, a monogram on a bag, a gift message on a card. This information is captured as **line item properties**: name and value pairs attached to a cart line rather than to the product itself. No variant, SKU, or inventory is required for each possible value. + + + + + +### Add Property Inputs to the Product Template + +Property inputs are named `properties[]`, where `` is the label stored with the line. Add them inside the [add-to-cart form](/docs/storefront/themes/templates/tags#cart_form) alongside the fields generated by the `cart_form` tag. + +```django title="templates/catalogue/product.html" +{% purchase_info_for_product request product as session %} +{% if session.availability.is_available_to_buy %} + {% cart_form request product 'single' as cart_form %} +
+ {% csrf_token %} + {% for field in cart_form %} + {% if field.is_hidden %} + {{ field }} + {% else %} + + {{ field }} + {% endif %} + {% endfor %} + + + +
+{% endif %} +``` + +Add one input per property. A mug with an engraving and a font choice uses `properties[Engraving]` and `properties[Font]`. + +
+ + + +### Display Properties in the Cart + +The cart template receives a `formset` of cart line forms. Each `form.instance` is a [line](/docs/storefront/themes/templates/objects#line) with a `properties` list of `key` and `value` pairs. + +```django title="partials/cart_line_properties.html" +{% for property in properties %} + {% if property.value %} +
{{ property.key }}: {{ property.value }}
+ {% endif %} +{% endfor %} +``` + +```django title="templates/cart.html" +{% for form in formset %} + {% with line=form.instance %} + {% include "partials/cart_line_properties.html" with properties=line.properties %} + {% endwith %} +{% endfor %} +``` + + +Themes that ship their own side cart JavaScript need to render properties there too. Request `properties { key value }` on the cart lines in your side cart query and output them alongside the product title. + + +
+ +
+ +### Behavior to Expect + +| Behavior | Detail | +| ----- | ------ | +| Unique lines | The same product added with different property values creates a separate cart line for each combination. Adding it again with identical values increases the quantity of the existing line. | +| Empty values | A property submitted with an empty value does not create a separate line. It is still recorded on the line, so guard your display with `{% if property.value %}`. | +| Hidden properties | Property names starting with an underscore, such as `properties[_source]`, are stored but excluded from `line.properties` and from the Storefront GraphQL API. Use them for data that should not be shown to customers. | +| Value length | Values longer than 500 characters are truncated to 500 characters. | +| Checkboxes | When a checkbox named `properties[...]` changes, the value submitted is `yes` or `no`. | +| File uploads | File inputs are not supported. Inputs with `type="file"` are ignored. | +| Set once | Properties are captured when the line is added to the cart. There is no update path for changing them afterwards. | + +### Add Properties with the Storefront GraphQL API + +Themes that add to the cart through the [Storefront GraphQL API](/docs/storefront/graphql) pass properties on each line as a JSON object of name and value pairs. The field is available on [createCart](/docs/storefront/graphql/mutations/create-cart) and [addCartLines](/docs/storefront/graphql/mutations/add-cart-lines), and is returned on cart lines as `properties { key value }`. + +```json title="addCartLines Variables" +{ + "input": { + "cartId": "", + "lines": [ + { + "productPk": 1, + "quantity": 1, + "properties": { "Engraving": "Alex" } + } + ] + } +} +``` + + +`properties` must be a JSON object. Any other value returns the error `Properties must be a JSON object.` + + +### Related + + + + + + + diff --git a/content/docs/storefront/themes/templates/objects.mdx b/content/docs/storefront/themes/templates/objects.mdx index aa3c45e7..388d8b78 100644 --- a/content/docs/storefront/themes/templates/objects.mdx +++ b/content/docs/storefront/themes/templates/objects.mdx @@ -758,6 +758,32 @@ There are three filter types: `price_range`, `boolean`, and `list`. Each type ha | `label` | String | Display label for this value. | +### line + +Cart line object available on the cart page (`templates/cart.html`) through the `formset` context. Each form in the formset exposes its cart line as `form.instance`. + +```django title="Example Cart Line Properties" +{% for form in formset %} + {% with line=form.instance %} + {% for property in line.properties %} +
{{ property.key }}: {{ property.value }}
+ {% endfor %} + {% endwith %} +{% endfor %} +``` + +| Property | Type | Description | +| ----- | ------ | ------ | +| `properties` | List | Line item properties captured on the product page, see [Personalized Products](/docs/storefront/themes/guides/personalized-products). Property names starting with an underscore are excluded. | + +**line.properties** + +| Property | Type | Description | +| ----- | ------ | ------ | +| `key` | String | The property name, taken from the `properties[]` input on the product page. | +| `value` | String | The value submitted by the customer. | + + ## Template Contexts All templates receive the [Global Objects](#global-objects) (`store`, `settings`, `currencies`, `languages_active_storefront`, `menus`, `products`, `product_categories`, `posts`, `post_categories`, `privacy_policy`, `terms_and_conditions`, `subscription_terms_and_conditions`, `request`, `storefront_geos`). The table below lists additional view-specific context variables passed to each template. @@ -765,6 +791,7 @@ All templates receive the [Global Objects](#global-objects) (`store`, `settings` | Template | View-Specific Context | | ----- | ------ | | `templates/index.html` | Global objects only. Use the [`where`](/docs/storefront/themes/templates/tags#where) tag to query products and categories. | +| `templates/cart.html` | `formset` (cart line forms, each `form.instance` is a [line](#line)) | | `templates/catalogue/product.html` | `product`, `variant_form`, `interval_count_choices` | | `templates/catalogue/index.html` | `products` (paginated), `paginator`, `page_obj` | | `templates/catalogue/category.html` | `category`, `products` (paginated), `filters`, `has_active_filter`, `paginator`, `page_obj` | diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818f..9edff1c7 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. From 8a424d052e9e5743a791812da52cdc02e67f0f5f Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Fri, 31 Jul 2026 14:16:53 +0700 Subject: [PATCH 2/4] improve examples --- .../themes/guides/personalized-products.mdx | 43 ++++++++------ .../docs/storefront/themes/templates/tags.mdx | 57 +++++++++++++++---- 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/content/docs/storefront/themes/guides/personalized-products.mdx b/content/docs/storefront/themes/guides/personalized-products.mdx index d8f0ac65..1d280f8b 100644 --- a/content/docs/storefront/themes/guides/personalized-products.mdx +++ b/content/docs/storefront/themes/guides/personalized-products.mdx @@ -18,25 +18,32 @@ Some products need customer input at the time of purchase — an engraving on a Property inputs are named `properties[]`, where `` is the label stored with the line. Add them inside the [add-to-cart form](/docs/storefront/themes/templates/tags#cart_form) alongside the fields generated by the `cart_form` tag. +The example below is the Intro Bootstrap add-to-cart form. The `col-12` block holding the engraving label and input is the only addition — everything else is the theme as shipped. + ```django title="templates/catalogue/product.html" -{% purchase_info_for_product request product as session %} -{% if session.availability.is_available_to_buy %} - {% cart_form request product 'single' as cart_form %} -
- {% csrf_token %} - {% for field in cart_form %} - {% if field.is_hidden %} - {{ field }} - {% else %} - - {{ field }} - {% endif %} - {% endfor %} - - - -
-{% endif %} +{% block product_cart_form %} +
+ {% purchase_info_for_product request product as session %} + {% if session.availability.is_available_to_buy %} + {% cart_form request product 'single' as cart_form %} +
+ {% csrf_token %} + {% include "partials/form_fields.html" with form=cart_form breakpoint_col='col-3 col-md-2' size='lg' nolabel=True %} +
+ + +
+
+
+ +
+
+
+ {% else %} +
{% t "store.catalogue.out_of_stock" %}
+ {% endif %} +
+{% endblock %} ``` Add one input per property. A mug with an engraving and a font choice uses `properties[Engraving]` and `properties[Font]`. diff --git a/content/docs/storefront/themes/templates/tags.mdx b/content/docs/storefront/themes/templates/tags.mdx index 1657263f..728fe116 100644 --- a/content/docs/storefront/themes/templates/tags.mdx +++ b/content/docs/storefront/themes/templates/tags.mdx @@ -100,24 +100,61 @@ The `cart_form` tag generates an add-to-cart form for a product. Required on eve {% cart_form request product 'single' as cart_form %} ``` -```django title="Example Product Add to Cart Form" -{% cart_form request product 'single' as cart_form %} -
- {% csrf_token %} - {{ cart_form }} - -
+The tag returns a form object, not rendered HTML. Loop over it to render each field in your own markup, as the Intro Bootstrap theme does through its `partials/form_fields.html` partial. + +```django title="templates/catalogue/product.html" +{% block product_cart_form %} +
+ {% purchase_info_for_product request product as session %} + {% if session.availability.is_available_to_buy %} + {% cart_form request product 'single' as cart_form %} +
+ {% csrf_token %} + {% include "partials/form_fields.html" with form=cart_form breakpoint_col='col-3 col-md-2' size='lg' nolabel=True %} +
+
+ +
+
+
+ {% else %} +
{% t "store.catalogue.out_of_stock" %}
+ {% endif %} +
+{% endblock %} +``` + +```django title="partials/form_fields.html" +{% if form.is_bound and not form.is_valid %} + +{% endif %} + +{% if form.non_field_errors %} + {% for error in form.non_field_errors %} + + {% endfor %} +{% endif %} + +{% for field in form %} + {% include 'partials/form_field.html' with field=field style=style %} +{% endfor %} ``` | Argument | Description | | --- | --- | | request | The current `request` context object. | | product | The `product` context object. | -| form_type | The form type, typically `'single'`. | +| quantity_type | Accepts `'single'` or `'multiple'`. | | variable | Assigned template variable name for the form. | +Add [line item property](/docs/storefront/themes/guides/personalized-products) inputs to this form to capture customer personalization such as an engraving or gift message. + ### comment Ignores everything between `{% comment %}` and `{% endcomment %}`. An optional note may be inserted in the first tag. For example, this is useful when commenting out code for documenting why the code was disabled. From dda098b900b8c62af30894c8520207cb6f5ba1f5 Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Fri, 31 Jul 2026 14:23:16 +0700 Subject: [PATCH 3/4] improve examples --- .../themes/guides/personalized-products.mdx | 38 +++++++------------ .../docs/storefront/themes/templates/tags.mdx | 2 +- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/content/docs/storefront/themes/guides/personalized-products.mdx b/content/docs/storefront/themes/guides/personalized-products.mdx index 1d280f8b..e6ec9898 100644 --- a/content/docs/storefront/themes/guides/personalized-products.mdx +++ b/content/docs/storefront/themes/guides/personalized-products.mdx @@ -18,32 +18,22 @@ Some products need customer input at the time of purchase — an engraving on a Property inputs are named `properties[]`, where `` is the label stored with the line. Add them inside the [add-to-cart form](/docs/storefront/themes/templates/tags#cart_form) alongside the fields generated by the `cart_form` tag. -The example below is the Intro Bootstrap add-to-cart form. The `col-12` block holding the engraving label and input is the only addition — everything else is the theme as shipped. +The engraving label and input are the only addition to the add-to-cart form. ```django title="templates/catalogue/product.html" -{% block product_cart_form %} -
- {% purchase_info_for_product request product as session %} - {% if session.availability.is_available_to_buy %} - {% cart_form request product 'single' as cart_form %} -
- {% csrf_token %} - {% include "partials/form_fields.html" with form=cart_form breakpoint_col='col-3 col-md-2' size='lg' nolabel=True %} -
- - -
-
-
- -
-
-
- {% else %} -
{% t "store.catalogue.out_of_stock" %}
- {% endif %} -
-{% endblock %} +{% purchase_info_for_product request product as session %} +{% if session.availability.is_available_to_buy %} + {% cart_form request product 'single' as cart_form %} +
+ {% csrf_token %} + {% include "partials/form_fields.html" with form=cart_form %} + + + +
+{% else %} + {% t "store.catalogue.out_of_stock" %} +{% endif %} ``` Add one input per property. A mug with an engraving and a font choice uses `properties[Engraving]` and `properties[Font]`. diff --git a/content/docs/storefront/themes/templates/tags.mdx b/content/docs/storefront/themes/templates/tags.mdx index 728fe116..982575e7 100644 --- a/content/docs/storefront/themes/templates/tags.mdx +++ b/content/docs/storefront/themes/templates/tags.mdx @@ -113,7 +113,7 @@ The tag returns a form object, not rendered HTML. Loop over it to render each fi {% include "partials/form_fields.html" with form=cart_form breakpoint_col='col-3 col-md-2' size='lg' nolabel=True %}
- +
From 220696aca4dd0ab47ad4244c9c61571e0c28ce29 Mon Sep 17 00:00:00 2001 From: Alex Phelps Date: Fri, 31 Jul 2026 15:54:38 +0700 Subject: [PATCH 4/4] fix comment --- next-env.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c7..c4b7818f 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.