@spree/docs 0.1.279 → 0.1.281
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/developer/core-concepts/carts.md +2 -0
- package/dist/developer/customization/email-variables.md +114 -0
- package/dist/developer/customization/emails.md +132 -0
- package/dist/developer/deployment/emails.md +1 -1
- package/dist/developer/upgrades/5.6-to-6.0.md +23 -0
- package/package.json +1 -1
|
@@ -224,6 +224,8 @@ To add your own step or requirement, see
|
|
|
224
224
|
|
|
225
225
|
Each [fulfillment](fulfillments.md) offers delivery rates. Pick one per fulfillment.
|
|
226
226
|
|
|
227
|
+
A cart has no fulfillments until it has a shipping address or a pickup location, unless none of its items ships to an address (a digital-only cart has them straight away). Saving a different address replaces the fulfillments, under new ids, with the cheapest shipping rate selected, so show the customer the new options before taking payment.
|
|
228
|
+
|
|
227
229
|
```typescript Store SDK
|
|
228
230
|
const cart = await client.carts.get(cartId)
|
|
229
231
|
await client.carts.fulfillments.update(cartId, cart.fulfillments[0].id, {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Email Template Variables
|
|
3
|
+
sidebarTitle: Template Variables
|
|
4
|
+
description: Every variable the Liquid templates of customer emails receive, email by email.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
These are the emails your customers receive. Every one of their templates receives `store` and `locale`, plus the variables listed for it below. Use these names exactly: a variable not listed here does not exist, and in development and test it raises an error. See [Email Templates](emails.md) for how to override a template.
|
|
8
|
+
|
|
9
|
+
## Emails
|
|
10
|
+
|
|
11
|
+
| Template | Variables |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `spree/order_mailer/confirm_email` | `order`, `resend` |
|
|
14
|
+
| `spree/order_mailer/cancel_email` | `order`, `resend` |
|
|
15
|
+
| `spree/order_mailer/payment_link_email` | `order`, `payment_url` |
|
|
16
|
+
| `spree/order_group_mailer/confirm_email` | `order_group`, `resend` |
|
|
17
|
+
| `spree/fulfillment_mailer/fulfilled_email` | `fulfillment`, `order`, `resend` |
|
|
18
|
+
| `spree/return_mailer/refunded_email` | `return`, `order`, `resend` |
|
|
19
|
+
| `spree/digital_asset_mailer/files_ready_email` | `order`, `downloads`, `resend` |
|
|
20
|
+
| `spree/customer_mailer/password_reset_email` | `customer`, `reset_url` |
|
|
21
|
+
| `spree/customer_mailer/data_export_email` | `download_url`, `expires_at` |
|
|
22
|
+
| `spree/newsletter_mailer/email_confirmation` | `confirmation_url` |
|
|
23
|
+
| `spree/company_mailer/invitation_email` | `company`, `accept_url` |
|
|
24
|
+
|
|
25
|
+
## Values
|
|
26
|
+
|
|
27
|
+
| Variable | What it holds |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `resend` | True when staff re-sent the email. |
|
|
30
|
+
| `locale` | The language the email renders in, e.g. `en`. |
|
|
31
|
+
| `payment_url` | Where the customer pays for the order. |
|
|
32
|
+
| `reset_url` | The password reset link, carrying its token. |
|
|
33
|
+
| `accept_url` | The link that accepts the invitation, carrying its token. |
|
|
34
|
+
| `confirmation_url` | The link that confirms the subscription, carrying its token. |
|
|
35
|
+
| `download_url` | The signed link to the customer's data export. |
|
|
36
|
+
| `expires_at` | When the data export link stops working. |
|
|
37
|
+
| `downloads` | Each file bought: `filename`, `url` (carrying its token) and `expires_at`. |
|
|
38
|
+
|
|
39
|
+
## Objects
|
|
40
|
+
|
|
41
|
+
Each object's fields, and the fields of the objects nested in it.
|
|
42
|
+
|
|
43
|
+
### `store`
|
|
44
|
+
|
|
45
|
+
- `store`: `id`, `name`, `address`, `mail_from_address`, `default_currency`, `default_locale`, `url`, `support_email`, `logo_url`, `logo_width`
|
|
46
|
+
|
|
47
|
+
### `order`
|
|
48
|
+
|
|
49
|
+
- `order`: `id`, `market_id`, `withdrawal_period_ends_at`, `within_withdrawal_period`, `cart_id`, `channel_id`, `company_id`, `company_name`, `po_document_filename`, `po_document_byte_size`, `number`, `email`, `customer_note`, `po_number`, `currency`, `locale`, `total_quantity`, `coupon_code`, `fulfillment_status`, `payment_status`, `completed_at`, `item_total`, `display_item_total`, `adjustment_total`, `display_adjustment_total`, `discount_total`, `display_discount_total`, `tax_total`, `display_tax_total`, `included_tax_total`, `display_included_tax_total`, `additional_tax_total`, `display_additional_tax_total`, `total`, `display_total`, `gift_card_total`, `display_gift_card_total`, `amount_due`, `display_amount_due`, `delivery_total`, `display_delivery_total`, `fee_total`, `display_fee_total`, `store_credit_total`, `display_store_credit_total`, `covered_by_store_credit`, `customer_name`, `display_total_minus_store_credits`
|
|
50
|
+
- `order.discounts`: `id`, `promotion_id`, `name`, `description`, `code`, `amount`, `display_amount`
|
|
51
|
+
- `order.fees`: `id`, `label`, `kind`, `line_item_id`, `fulfillment_id`, `amount`, `display_amount`
|
|
52
|
+
- `order.items`: `id`, `variant_id`, `seller_id`, `preorder`, `preorder_ships_at`, `quantity`, `currency`, `name`, `slug`, `options_text`, `price`, `display_price`, `total`, `display_total`, `adjustment_total`, `display_adjustment_total`, `additional_tax_total`, `display_additional_tax_total`, `included_tax_total`, `display_included_tax_total`, `discount_total`, `display_discount_total`, `pre_tax_amount`, `display_pre_tax_amount`, `discounted_amount`, `display_discounted_amount`, `display_compare_at_amount`, `compare_at_amount`, `url`, `image_url`, `sku`, `display_amount`
|
|
53
|
+
- `order.items.option_values`: `id`, `option_type_id`, `name`, `label`, `position`, `color_code`, `option_type_name`, `option_type_label`, `image_url`
|
|
54
|
+
- `order.fulfillments`: `id`, `number`, `tracking`, `tracking_url`, `pickup_point_data`, `selected_delivery_rate_id`, `unpriced`, `cost`, `display_cost`, `total`, `display_total`, `discount_total`, `display_discount_total`, `additional_tax_total`, `display_additional_tax_total`, `included_tax_total`, `display_included_tax_total`, `tax_total`, `display_tax_total`, `status`, `fulfillment_type`, `fulfilled_at`, `delivered_at`, `items`
|
|
55
|
+
- `order.fulfillments.deliveries`: `id`, `tracking_number`, `carrier`, `carrier_name`, `service`, `status`, `tracking_url`, `estimated_delivery_at`, `delivered_at`
|
|
56
|
+
- `order.fulfillments.delivery_method`: `id`, `name`, `code`, `estimated_transit_business_days_min`, `estimated_transit_business_days_max`, `digital`, `pickup`, `pickup_point`
|
|
57
|
+
- `order.fulfillments.stock_location`: `id`, `name`, `address1`, `city`, `zipcode`, `country_code`, `country_name`, `state_code`, `state_text`, `pickup_ready_in_minutes`, `pickup_instructions`
|
|
58
|
+
- `order.fulfillments.delivery_rates`: `id`, `delivery_method_id`, `name`, `selected`, `cost`, `total`, `additional_tax_total`, `included_tax_total`, `tax_total`, `carrier`, `service_level`, `estimated_delivery_date`, `unpriced`, `display_cost`, `display_total`, `display_additional_tax_total`, `display_included_tax_total`, `display_tax_total`
|
|
59
|
+
- `order.payments`: `id`, `payment_method_id`, `response_code`, `number`, `status`, `amount`, `display_amount`, `source_type`, `source_id`, `source`
|
|
60
|
+
- `order.payments.payment_method`: `id`, `name`, `description`, `type`, `session_required`, `source_required`
|
|
61
|
+
- `order.billing_address`: `id`, `label`, `first_name`, `last_name`, `full_name`, `address1`, `address2`, `postal_code`, `city`, `phone`, `company`, `country_name`, `country_code`, `state_text`, `state_code`, `quick_checkout`, `is_default_billing`, `is_default_shipping`, `state_abbr`, `country_iso`, `state_name`
|
|
62
|
+
- `order.shipping_address`: `id`, `label`, `first_name`, `last_name`, `full_name`, `address1`, `address2`, `postal_code`, `city`, `phone`, `company`, `country_name`, `country_code`, `state_text`, `state_code`, `quick_checkout`, `is_default_billing`, `is_default_shipping`, `state_abbr`, `country_iso`, `state_name`
|
|
63
|
+
- `order.gift_card`: `id`, `code`, `status`, `currency`, `amount`, `amount_used`, `amount_authorized`, `amount_remaining`, `display_amount`, `display_amount_used`, `display_amount_remaining`, `expires_at`, `redeemed_at`, `expired`, `active`
|
|
64
|
+
- `order.market`: `id`, `name`, `currency`, `default_locale`, `tax_inclusive`, `default`, `country_codes`, `country_isos`, `supported_locales`
|
|
65
|
+
- `order.promotion_discounts`: `label`, `display_amount`, `amount`
|
|
66
|
+
- `order.manual_discounts`: `label`, `display_amount`, `amount`
|
|
67
|
+
- `order.fee_lines`: `label`, `display_amount`, `amount`
|
|
68
|
+
- `order.delivery_lines`: `label`, `display_amount`, `amount`
|
|
69
|
+
|
|
70
|
+
### `order_group`
|
|
71
|
+
|
|
72
|
+
- `order_group`: `id`, `number`, `email`, `currency`, `total`, `display_total`, `item_total`, `display_item_total`, `fulfillment_status`, `payment_status`, `completed_at`, `customer_name`, `display_total_minus_store_credits`, `po_number`, `order_count`, `display_delivery_total`, `delivery_total`, `additional_tax_total`, `display_additional_tax_total`, `gift_card_total`, `display_gift_card_total`
|
|
73
|
+
- `order_group.billing_address`: `id`, `label`, `first_name`, `last_name`, `full_name`, `address1`, `address2`, `postal_code`, `city`, `phone`, `company`, `country_name`, `country_code`, `state_text`, `state_code`, `quick_checkout`, `is_default_billing`, `is_default_shipping`, `state_abbr`, `country_iso`, `state_name`
|
|
74
|
+
- `order_group.shipping_address`: `id`, `label`, `first_name`, `last_name`, `full_name`, `address1`, `address2`, `postal_code`, `city`, `phone`, `company`, `country_name`, `country_code`, `state_text`, `state_code`, `quick_checkout`, `is_default_billing`, `is_default_shipping`, `state_abbr`, `country_iso`, `state_name`
|
|
75
|
+
- `order_group.items`: `id`, `variant_id`, `seller_id`, `preorder`, `preorder_ships_at`, `quantity`, `currency`, `name`, `slug`, `options_text`, `price`, `display_price`, `total`, `display_total`, `adjustment_total`, `display_adjustment_total`, `additional_tax_total`, `display_additional_tax_total`, `included_tax_total`, `display_included_tax_total`, `discount_total`, `display_discount_total`, `pre_tax_amount`, `display_pre_tax_amount`, `discounted_amount`, `display_discounted_amount`, `display_compare_at_amount`, `compare_at_amount`, `url`, `image_url`, `sku`, `display_amount`
|
|
76
|
+
- `order_group.items.option_values`: `id`, `option_type_id`, `name`, `label`, `position`, `color_code`, `option_type_name`, `option_type_label`, `image_url`
|
|
77
|
+
- `order_group.promotion_discounts`: `label`, `display_amount`, `amount`
|
|
78
|
+
- `order_group.manual_discounts`: `label`, `display_amount`, `amount`
|
|
79
|
+
- `order_group.fee_lines`: `label`, `display_amount`, `amount`
|
|
80
|
+
- `order_group.fulfillment_groups`: `name`, `display_cost`, `seller_names`
|
|
81
|
+
- `order_group.fulfillment_groups.items`: `id`, `url`, `image_url`, `name`, `quantity`, `sku`, `options_text`, `display_price`, `display_amount`
|
|
82
|
+
- `order_group.unfulfilled_items`: `id`, `variant_id`, `seller_id`, `preorder`, `preorder_ships_at`, `quantity`, `currency`, `name`, `slug`, `options_text`, `price`, `display_price`, `total`, `display_total`, `adjustment_total`, `display_adjustment_total`, `additional_tax_total`, `display_additional_tax_total`, `included_tax_total`, `display_included_tax_total`, `discount_total`, `display_discount_total`, `pre_tax_amount`, `display_pre_tax_amount`, `discounted_amount`, `display_discounted_amount`, `display_compare_at_amount`, `compare_at_amount`, `url`, `image_url`, `sku`, `display_amount`
|
|
83
|
+
- `order_group.unfulfilled_items.option_values`: `id`, `option_type_id`, `name`, `label`, `position`, `color_code`, `option_type_name`, `option_type_label`, `image_url`
|
|
84
|
+
- `order_group.delivery_lines`: `label`, `display_amount`, `amount`
|
|
85
|
+
|
|
86
|
+
### `fulfillment`
|
|
87
|
+
|
|
88
|
+
- `fulfillment`: `id`, `number`, `tracking`, `tracking_url`, `pickup_point_data`, `selected_delivery_rate_id`, `unpriced`, `cost`, `display_cost`, `total`, `display_total`, `discount_total`, `display_discount_total`, `additional_tax_total`, `display_additional_tax_total`, `included_tax_total`, `display_included_tax_total`, `tax_total`, `display_tax_total`, `status`, `fulfillment_type`, `fulfilled_at`, `delivered_at`, `items`, `delivery_method_name`
|
|
89
|
+
- `fulfillment.deliveries`: `id`, `tracking_number`, `carrier`, `carrier_name`, `service`, `status`, `tracking_url`, `estimated_delivery_at`, `delivered_at`
|
|
90
|
+
- `fulfillment.delivery_method`: `id`, `name`, `code`, `estimated_transit_business_days_min`, `estimated_transit_business_days_max`, `digital`, `pickup`, `pickup_point`
|
|
91
|
+
- `fulfillment.stock_location`: `id`, `name`, `address1`, `city`, `zipcode`, `country_code`, `country_name`, `state_code`, `state_text`, `pickup_ready_in_minutes`, `pickup_instructions`
|
|
92
|
+
- `fulfillment.delivery_rates`: `id`, `delivery_method_id`, `name`, `selected`, `cost`, `total`, `additional_tax_total`, `included_tax_total`, `tax_total`, `carrier`, `service_level`, `estimated_delivery_date`, `unpriced`, `display_cost`, `display_total`, `display_additional_tax_total`, `display_included_tax_total`, `display_tax_total`
|
|
93
|
+
- `fulfillment.delivery_rates.freight_summary`: `total_units`, `total_cartons`, `total_pallets`, `total_volume`, `total_weight`, `complete`
|
|
94
|
+
- `fulfillment.delivery_rates.delivery_method`: `id`, `name`, `code`, `estimated_transit_business_days_min`, `estimated_transit_business_days_max`, `digital`, `pickup`, `pickup_point`
|
|
95
|
+
- `fulfillment.manifest_items`: `id`, `url`, `image_url`, `name`, `quantity`, `sku`, `options_text`, `display_price`, `display_amount`
|
|
96
|
+
|
|
97
|
+
### `return`
|
|
98
|
+
|
|
99
|
+
- `return`: `id`, `number`, `status`, `order_id`, `reason_id`, `refund_total`, `display_refund_total`, `approved_at`, `received_at`, `refunded_at`, `canceled_at`, `display_refunded_total`
|
|
100
|
+
- `return.returned_items`: `id`, `variant_id`, `seller_id`, `preorder`, `preorder_ships_at`, `quantity`, `currency`, `name`, `slug`, `options_text`, `price`, `display_price`, `total`, `display_total`, `adjustment_total`, `display_adjustment_total`, `additional_tax_total`, `display_additional_tax_total`, `included_tax_total`, `display_included_tax_total`, `discount_total`, `display_discount_total`, `pre_tax_amount`, `display_pre_tax_amount`, `discounted_amount`, `display_discounted_amount`, `display_compare_at_amount`, `compare_at_amount`, `url`, `image_url`, `sku`, `display_amount`
|
|
101
|
+
- `return.returned_items.option_values`: `id`, `option_type_id`, `name`, `label`, `position`, `color_code`, `option_type_name`, `option_type_label`, `image_url`
|
|
102
|
+
|
|
103
|
+
### `customer`
|
|
104
|
+
|
|
105
|
+
- `customer`: `id`, `email`, `first_name`, `last_name`, `phone`, `accepts_email_marketing`, `email_marketing_consent_updated_at`, `full_name`, `available_store_credit_total`, `display_available_store_credit_total`
|
|
106
|
+
- `customer.addresses`: `id`, `label`, `first_name`, `last_name`, `full_name`, `address1`, `address2`, `postal_code`, `city`, `phone`, `company`, `country_name`, `country_code`, `state_text`, `state_code`, `quick_checkout`, `is_default_billing`, `is_default_shipping`, `state_abbr`, `country_iso`, `state_name`
|
|
107
|
+
- `customer.default_billing_address`: `id`, `label`, `first_name`, `last_name`, `full_name`, `address1`, `address2`, `postal_code`, `city`, `phone`, `company`, `country_name`, `country_code`, `state_text`, `state_code`, `quick_checkout`, `is_default_billing`, `is_default_shipping`, `state_abbr`, `country_iso`, `state_name`
|
|
108
|
+
- `customer.default_shipping_address`: `id`, `label`, `first_name`, `last_name`, `full_name`, `address1`, `address2`, `postal_code`, `city`, `phone`, `company`, `country_name`, `country_code`, `state_text`, `state_code`, `quick_checkout`, `is_default_billing`, `is_default_shipping`, `state_abbr`, `country_iso`, `state_name`
|
|
109
|
+
- `customer.newsletter_subscriber`: `id`, `email`, `created_at`, `updated_at`, `verified`, `verified_at`, `customer_id`
|
|
110
|
+
- `customer.customer_groups`: `id`, `name`
|
|
111
|
+
|
|
112
|
+
### `company`
|
|
113
|
+
|
|
114
|
+
- `company`: `id`, `name`, `kind`, `po_number_required`, `parent_id`, `ancestors`
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Email Templates
|
|
3
|
+
sidebarTitle: Emails
|
|
4
|
+
description: Change what Spree's transactional emails say and look like, with Liquid templates written in MJML.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Every email Spree sends — order confirmations, shipping notices, password resets, staff invitations — renders from a **Liquid** template written in **MJML**. Liquid fills in the data; MJML turns a dozen layout tags into the nested tables, inline styles and Outlook workarounds that email clients need, and makes the email stack on phones.
|
|
8
|
+
|
|
9
|
+
To change an email, you copy its template into your app and edit it. Nothing else changes: Spree still picks the recipient, the language and the sender, and delivers the email through your [SMTP provider](../providers/emails.md).
|
|
10
|
+
|
|
11
|
+
> **NOTE:** Templates read plain data — the same fields the [Store API](../../api-reference/store-api/introduction.md) returns, plus what only an email needs — never Ruby objects. The same template can later run outside Ruby, and merchants will be able to edit templates safely from the dashboard.
|
|
12
|
+
|
|
13
|
+
## Where templates live
|
|
14
|
+
|
|
15
|
+
A template sits at its email's path with a `.liquid` extension. To override one, create the file at the same path in your app:
|
|
16
|
+
|
|
17
|
+
| Email | Template |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| Order confirmation | `server/app/views/spree/order_mailer/confirm_email.liquid` |
|
|
20
|
+
| Shipping notification | `server/app/views/spree/fulfillment_mailer/fulfilled_email.liquid` |
|
|
21
|
+
| Customer password reset | `server/app/views/spree/customer_mailer/password_reset_email.liquid` |
|
|
22
|
+
| The layout around every email | `server/app/views/layouts/spree/base_mailer.liquid` |
|
|
23
|
+
|
|
24
|
+
The path is also the template's name, for example `spree/order_mailer/confirm_email`. [Email Template Variables](email-variables.md) lists every customer email's template and the data it receives.
|
|
25
|
+
|
|
26
|
+
Start from Spree's own template rather than a blank file: customer emails are in [`spree/emails/app/views`](https://github.com/spree/spree/tree/main/spree/emails/app/views/spree) and staff emails and the layout in [`spree/core/app/views`](https://github.com/spree/spree/tree/main/spree/core/app/views) on GitHub.
|
|
27
|
+
|
|
28
|
+
Your file wins as soon as it exists, and edits to it show on the next email without a restart. There is nothing to register. The one exception: Rails only reads `server/app/views` if the folder existed when the app started, so restart once after creating that folder. API-only apps often start without it.
|
|
29
|
+
|
|
30
|
+
## Anatomy of a template
|
|
31
|
+
|
|
32
|
+
```liquid server/app/views/spree/order_mailer/confirm_email.liquid
|
|
33
|
+
---
|
|
34
|
+
subject: "{{ store.name }} {{ 'order_mailer.confirm_email.subject' | t }} #{{ order.number }}"
|
|
35
|
+
---
|
|
36
|
+
<mj-section>
|
|
37
|
+
<mj-column css-class="hero">
|
|
38
|
+
<mj-text mj-class="heading">Thanks for your order!</mj-text>
|
|
39
|
+
<mj-text mj-class="greeting">Hi {{ order.customer_name }},</mj-text>
|
|
40
|
+
<mj-button href="{{ store.url }}/account/orders/{{ order.number }}">View your order</mj-button>
|
|
41
|
+
</mj-column>
|
|
42
|
+
</mj-section>
|
|
43
|
+
{% assign heading = 'order_mailer.confirm_email.order_summary' | t: number: order.number %}
|
|
44
|
+
{% render 'spree/shared/order_summary', heading: heading, purchase: order, items: order.items, totals: true %}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- **The subject** is the `subject` line at the top, and it is Liquid too.
|
|
48
|
+
- **The body** is a list of MJML sections. The layout wraps it with the store's logo and footer.
|
|
49
|
+
- **Copy** either comes from Spree's translations through the `t` filter, which keeps one template working in every language, or is written straight into the template for a single-language store.
|
|
50
|
+
|
|
51
|
+
The layout defines named styles you can use through `mj-class`: `heading`, `greeting`, `lead`, `note`, `section-heading` and `body`. For the MJML tags themselves, see the [MJML documentation](https://documentation.mjml.io).
|
|
52
|
+
|
|
53
|
+
## Partials
|
|
54
|
+
|
|
55
|
+
Pull a shared piece in with `{% render %}`. The name maps to a file with a leading underscore, as Rails partials do: `'spree/shared/line_item'` is `app/views/spree/shared/_line_item.liquid`. A partial only sees the values you pass it.
|
|
56
|
+
|
|
57
|
+
| Partial | Arguments | What it renders |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `spree/shared/order_summary` | `heading`, `purchase`, `items`, `totals` | A headed list of purchased items, with the order's totals when `totals` is true |
|
|
60
|
+
| `spree/shared/line_item` | `item` | One purchased item: thumbnail, name, options, quantity and amount |
|
|
61
|
+
| `spree/shared/purchase_totals` | `purchase` | Subtotal, discounts, delivery, tax, gift card, fees and total |
|
|
62
|
+
| `spree/shared/fulfillment_group` | `group`, `position`, `count` | One parcel of a multi-seller purchase |
|
|
63
|
+
| `spree/shared/summary_row` | `label`, `value`, `sub`, `total`, `translate` | One label and amount row inside an `<mj-table css-class="summary">` |
|
|
64
|
+
|
|
65
|
+
Override a partial the same way as a template, by creating the file at its path in your app. Every email that uses it changes.
|
|
66
|
+
|
|
67
|
+
## Filters
|
|
68
|
+
|
|
69
|
+
Besides [Liquid's standard filters](https://shopify.github.io/liquid/filters/abs/), templates have these. The names follow common Liquid conventions where they mean the same thing.
|
|
70
|
+
|
|
71
|
+
| Filter | Example | Result |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| `money` | `{{ '10.5' \| money }}` | `$10.50`, in the email's currency |
|
|
74
|
+
| `money_with_currency` | `{{ '10.5' \| money_with_currency }}` | `$10.50 USD` |
|
|
75
|
+
| `date` | `{{ order.completed_at \| date: 'long' }}` | A date in the store's time zone. Takes a named format (`short`, `long`, `default`) or a `strftime` pattern such as `'%Y-%m-%d'` |
|
|
76
|
+
| `t` | `{{ 'order_mailer.confirm_email.dear_customer' \| t: name: order.customer_name }}` | A Spree translation, with its placeholders filled in |
|
|
77
|
+
| `raw` | `{{ product.description_html \| raw }}` | Prints the value without escaping. See below |
|
|
78
|
+
|
|
79
|
+
Most amounts already arrive formatted, as `display_total`, `display_amount` and the like, so `money` is only needed for a raw amount.
|
|
80
|
+
|
|
81
|
+
## Escaping
|
|
82
|
+
|
|
83
|
+
Everything a template prints is HTML-escaped. A customer who types `<a href="https://evil.test">Click to verify</a>` as their name sees that text in the email, not a live link — and so does the store owner reading the new-order notification.
|
|
84
|
+
|
|
85
|
+
`raw` turns escaping off for one value. Only use it for HTML that was cleaned when it was saved, such as a product's rich-text description. Never use it on a name, an address or a note.
|
|
86
|
+
|
|
87
|
+
## The plain-text version
|
|
88
|
+
|
|
89
|
+
Spree builds each email's plain-text version from its HTML, writing links as `label (url)`, so the two can never drift apart. To write the text by hand instead, add a `.text.liquid` file next to the template:
|
|
90
|
+
|
|
91
|
+
```liquid server/app/views/spree/order_mailer/confirm_email.text.liquid
|
|
92
|
+
Thanks for your order {{ order.number }}, {{ order.customer_name }}.
|
|
93
|
+
|
|
94
|
+
Total: {{ order.display_total }}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Nothing is escaped in the text version.
|
|
98
|
+
|
|
99
|
+
## Mistakes surface in development
|
|
100
|
+
|
|
101
|
+
In development and test, a variable that does not exist raises an error instead of printing nothing, so `{{ order.nubmer }}` fails your spec rather than sending a blank. In production it prints nothing. A template that loops endlessly fails that one email instead of stalling your background jobs.
|
|
102
|
+
|
|
103
|
+
To look at every email with real data, open the mailer previews at `/rails/mailers` on your Spree server.
|
|
104
|
+
|
|
105
|
+
## Your own mailers
|
|
106
|
+
|
|
107
|
+
A mailer that inherits `Spree::BaseMailer` and renders its own ERB views with `mail` keeps working. Spree wraps its HTML in the same layout as every other email, so it carries the store's logo, header and footer. The `spree/shared/mailer_hero` and `spree/shared/mailer_button` partials are still there for those views.
|
|
108
|
+
|
|
109
|
+
To give a new email the same data contract as Spree's own, render it from a Liquid template instead:
|
|
110
|
+
|
|
111
|
+
```ruby server/app/mailers/spree/welcome_mailer.rb
|
|
112
|
+
module Spree
|
|
113
|
+
class WelcomeMailer < BaseMailer
|
|
114
|
+
def welcome_email(customer, store)
|
|
115
|
+
@current_store = store
|
|
116
|
+
|
|
117
|
+
with_store_locale(store) do
|
|
118
|
+
mail_template({ customer: email_data(customer, Spree.api.customer_serializer) }, to: customer.email)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The template goes at `server/app/views/spree/welcome_mailer/welcome_email.liquid`. `email_data` serializes a record the way templates read it; pass a URL that carries a token as its own variable rather than serializing it.
|
|
126
|
+
|
|
127
|
+
## Related
|
|
128
|
+
|
|
129
|
+
- [Email Template Variables](email-variables.md) — every customer email and the data it receives
|
|
130
|
+
- [Upgrading from 5.6 to 6.0](../upgrades/5.6-to-6.0.md#emails-render-from-liquid-templates) — moving ERB email overrides to Liquid
|
|
131
|
+
- [Sending out Emails](../deployment/emails.md) — which emails Spree sends, and handing customer emails to your storefront
|
|
132
|
+
- [Emails provider setup](../providers/emails.md) — SMTP configuration
|
|
@@ -13,7 +13,7 @@ Spree handles two categories of emails:
|
|
|
13
13
|
|
|
14
14
|
## Customer-Facing Emails
|
|
15
15
|
|
|
16
|
-
By default, **Spree sends all customer transactional emails itself**. This works for every client of the API: mobile apps, custom frontends, POS integrations — no storefront required. Delivery uses the same [SMTP configuration](#configuration) as system emails.
|
|
16
|
+
By default, **Spree sends all customer transactional emails itself**. This works for every client of the API: mobile apps, custom frontends, POS integrations — no storefront required. Delivery uses the same [SMTP configuration](#configuration) as system emails, and every email renders from a Liquid template you can override — see [Email Templates](../customization/emails.md).
|
|
17
17
|
|
|
18
18
|
Customer emails can be turned off in the admin under **Settings → Emails** — do this when your storefront takes over sending them (below), otherwise customers receive both.
|
|
19
19
|
|
|
@@ -690,6 +690,29 @@ Nothing in 5.6 could reach it: there was no API endpoint and no admin screen, so
|
|
|
690
690
|
|
|
691
691
|
If you subclassed `Spree::Report` in your own application, that class no longer has a superclass and will raise on boot. Move the question to one of the two replacements above.
|
|
692
692
|
|
|
693
|
+
## Emails render from Liquid templates
|
|
694
|
+
|
|
695
|
+
Every transactional email now renders from a Liquid template written in MJML instead of an ERB view. Templates read serializer data rather than models, keep their Rails view paths, carry their subject line, and get a plain-text version generated from the HTML. See [Email Templates](../customization/emails.md) for how they work.
|
|
696
|
+
|
|
697
|
+
**If your app overrides no emails, there is nothing to do.** The emails look the same.
|
|
698
|
+
|
|
699
|
+
**If your app overrides Spree's emails with ERB**, those files are no longer used and Spree sends its default design instead, without a warning. Look for them under `app/views/spree/*_mailer/` and `app/views/spree/shared/`, and port each to the `.liquid` file at the same path:
|
|
700
|
+
|
|
701
|
+
| ERB view (removed) | Liquid template |
|
|
702
|
+
| --- | --- |
|
|
703
|
+
| `spree/shared/_base_mailer_header`, `_base_mailer_footer`, `_base_mailer_stylesheets`, `_mailer_logo`, `_purchased_items_styles` | `layouts/spree/base_mailer.liquid` |
|
|
704
|
+
| `spree/shared/_order_summary_section`, `_purchased_items_table`, `purchased_items_table/*` | `spree/shared/_order_summary.liquid`, `_line_item.liquid`, `_purchase_totals.liquid`, `_summary_row.liquid` |
|
|
705
|
+
| `spree/shared/_fulfillment_group_section`, `_purchase_totals` | `spree/shared/_fulfillment_group.liquid`, `_purchase_totals.liquid` |
|
|
706
|
+
| `spree/<mailer>/<email>.html.erb` and `.text.erb`, for every mailer | `spree/<mailer>/<email>.liquid`, plus an optional `<email>.text.liquid` |
|
|
707
|
+
|
|
708
|
+
Other changes that come with it:
|
|
709
|
+
|
|
710
|
+
- **Subjects moved into the templates.** Each template's front matter holds its subject. `Spree::BaseMailer#order_email_subject` is removed.
|
|
711
|
+
- **The mailer view helpers are removed.** `Spree::MailHelper` (`name_for`), `Spree::BaseHelper` (`spree_storefront_resource_url`), `Spree::FulfillmentHelper` and `Spree::DigitalAssetHelper` are gone. Templates get the same values as serializer fields, such as `order.customer_name` and `item.url`.
|
|
712
|
+
- **Every email has a plain-text part**, generated from the HTML. Seven emails that had none now have one.
|
|
713
|
+
- **Alba moved into `spree_core`**, with its configuration, so core's staff emails render in installations without `spree_api`.
|
|
714
|
+
- **Your own mailers keep working.** A mailer that inherits `Spree::BaseMailer` and calls `mail` with its own ERB views is wrapped in the new email layout, and the `spree/shared/mailer_hero` and `mailer_button` partials remain for its views. See [Your own mailers](../customization/emails.md#your-own-mailers).
|
|
715
|
+
|
|
693
716
|
## Deprecated in 6.0, removed in 6.1
|
|
694
717
|
|
|
695
718
|
Every rename keeps the legacy name working for one release with a deprecation warning. The notable ones:
|