Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion content/docs/storefront/themes/guides/meta.json
Original file line numberDiff line numberDiff line change
@@ -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"]
}
131 changes: 131 additions & 0 deletions content/docs/storefront/themes/guides/personalized-products.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
---
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.

<Steps>

<Step>

### Add Property Inputs to the Product Template

Property inputs are named `properties[<name>]`, where `<name>` 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 engraving label and input are the only addition to the add-to-cart form.

```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 %}
<form id="add-to-cart" action="{% url 'cart:add' pk=product.pk %}" method="post">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Same URL-kwargs inconsistency surfaces again: this guide uses {% url 'cart:add' pk=product.pk %} while the cart_form tag reference still uses product.slug on tags.mdx:100. Make the two pages agree.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

{% csrf_token %}
{% include "partials/form_fields.html" with form=cart_form %}
<label for="engraving">Engraving</label>
<input type="text" id="engraving" name="properties[Engraving]">
<button type="submit">{% t "store.catalogue.add_to_cart" %}</button>
</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]`.

</Step>

<Step>

### 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 %}
<div>{{ property.key }}: {{ property.value }}</div>
{% 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 %}
```

<Callout type="idea">
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.
</Callout>

</Step>

</Steps>

### 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`. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The "Checkboxes" row promises that a checkbox named properties[...] submits yes or no, but no example input is shown anywhere in the guide. Add a short <input type="checkbox" name="properties[GiftWrap]"> example alongside the existing text input so theme authors do not have to guess the wire format.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

| 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": "<cart-id>",
"lines": [
{
"productPk": 1,
"quantity": 1,
"properties": { "Engraving": "Alex" }
}
]
}
}
```

<Callout type="warn">
`properties` must be a JSON object. Any other value returns the error `Properties must be a JSON object.`
</Callout>

### Related

<Cards>
<Card
title="line Object"
description="Cart line object and the properties list available in templates."
href="/docs/storefront/themes/templates/objects#line"
/>
<Card
title="cart_form Tag"
description="Generate the add-to-cart form for a product template."
href="/docs/storefront/themes/templates/tags#cart_form"
/>
<Card
title="Product Variants Guide"
description="Map variant attribute choices to product IDs when inventory is tracked per option."
href="/docs/storefront/themes/guides/product-variants"
/>
<Card
title="Storefront GraphQL API"
description="Add lines to the cart from JavaScript with the createCart and addCartLines mutations."
href="/docs/storefront/graphql"
/>
</Cards>
27 changes: 27 additions & 0 deletions content/docs/storefront/themes/templates/objects.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -758,13 +758,40 @@ 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 %}
<div>{{ property.key }}: {{ property.value }}</div>
{% 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[<name>]` 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.

| 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)) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The added templates/cart.html row says the context provides formset, but the formset type/shape is documented only via the new ### line section above. Consider giving formset its own short entry (or at minimum a sentence under line) so readers understand they get a Django formset of line forms, not a plain list.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

| `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` |
Expand Down
57 changes: 47 additions & 10 deletions content/docs/storefront/themes/templates/tags.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 %}
<form method="post" action="{% url 'cart:add' product.slug %}">
{% csrf_token %}
{{ cart_form }}
<button type="submit" class="btn btn-primary">
Add to Cart
</button>
</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 %}
<div class="catalogue_product-action">
{% purchase_info_for_product request product as session %}
{% if session.availability.is_available_to_buy %}
{% cart_form request product 'single' as cart_form %}
<form id="add-to-cart" class="row g-3" action="{% url 'cart:add' pk=product.pk %}" method="post">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: The form action now uses {% url 'cart:add' pk=product.pk %} here, but the cart_form invocation snippet immediately above on line 100 (which this PR did not touch) still uses {% url 'cart:add' product.slug %}. Two different URL kwargs on the same docs page will confuse readers.

Pick one (PK or slug) and make both examples use it, or call out explicitly when the add endpoint expects pk versus slug.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

{% csrf_token %}
{% include "partials/form_fields.html" with form=cart_form breakpoint_col='col-3 col-md-2' size='lg' nolabel=True %}
<div class="col-9 col-md-10">
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg" data-loading-text='{% t "store.catalogue.add_to_cart_loading" %}' data-disabled-text='{% t "store.catalogue.product_unavailable" %}'>{% t "store.catalogue.add_to_cart" %}</button>
</div>
</div>
</form>
{% else %}
<div class="fs-6">{% t "store.catalogue.out_of_stock" %}</div>
{% endif %}
</div>
{% endblock %}
```

```django title="partials/form_fields.html"
{% if form.is_bound and not form.is_valid %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
{% t "global.error.please_check_error" %}
</div>
{% endif %}

{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
{{ error }}
</div>
{% 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'`. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: The third positional argument to {% cart_form %} was renamed from form_type to quantity_type in this table, but I see no other change in the PR that proves the underlying tag keyword was also renamed. If the platform code still accepts form_type, this table is now incorrect and will mislead theme authors.

Verify against the source of the cart_form template tag and either keep the documented name (form_type) or rename the tag keyword in the same change so the docs stay accurate.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

| 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.
Expand Down