This is the How-tos bundle for Adobe Commerce Storefront on Edge Delivery Services # How-tos > Step-by-step storefront tasks for authors and developers > Generated: 2026-10-21T15:11:09.006Z > Source: https://experienceleague.adobe.com/en/tools/commerce-storefront --- # Render an additional wishlist The Wishlist drop-in renders the shopper's active wishlist out of the box. Sometimes you want a **second, purpose-built list** somewhere else on the storefront: a "Save for Later" list in the cart, a "Buy again" list on an account page, or a curated list on a landing page. This how-to shows the general pattern for rendering an additional wishlist beside the main one, and uses **Save for Later in the cart** as the worked example. The two mechanics that make this possible are: - **`wishlistId`** identifies *which* list a container renders and mutates, instead of the active one. - **`scope`** gives that container its own event channel, so its data never overwrites the active wishlist's. > The active wishlist lives on a single shared channel. Without a `scope`, a second container would emit and listen on that same channel and the two lists would overwrite each other. Always pair a specific `wishlistId` with a `scope`. ## Before you begin You should be familiar with: - [Wishlist functions](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/wishlist/functions/) — `getWishlists`, `createWishlist`, `getWishlistById`, `addProductsToWishlist`, `mergeWishlists` - [Wishlist events](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/wishlist/events/#wishlistdata-emits-and-listens) — the scoped `wishlist/data` event - [Wishlist container](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/wishlist/containers/wishlist/) — the `wishlistId` and `scope` props This how-to shows the general approach. The snippets are a reference, not a drop-in-complete integration. ## The pattern ### 1. Choose a stable scope and resolve the list ID Pick a constant `scope` string for your list. It is the event channel every call for this list shares, so keep it stable and unique on the page. Resolving the `wishlistId` differs by authentication state, so wrap it in a helper. Authenticated shoppers have server lists you look up (or create) by name. Guests have no server lists, so you use a reserved local key that the drop-in persists in browser storage. ```js const LIST_NAME = 'Save for Later'; // used to find/create the authenticated list const GUEST_KEY = 'save-for-later'; // reserved local key for guests const SCOPE = 'save-for-later'; // this list's event channel async function resolveListId() { const lists = await WishlistApi.getWishlists(); // getWishlists() returns an array for authenticated shoppers and a single // object for guests. if (!Array.isArray(lists)) return GUEST_KEY; let list = lists.find((w) => w.name === LIST_NAME); if (!list) { list = await WishlistApi.createWishlist(LIST_NAME); } return list?.id ?? null; } ``` > Using the same value as both the guest key and part of your scope keeps the rest of the code auth-agnostic: you pass one `listId` through every call regardless of who is signed in. ### 2. Render the container with `wishlistId` and `scope` Render the `Wishlist` container into your own element, passing the resolved ID and the scope. Everything else (move-to-cart, routing, product data) is the same as the main wishlist. ```js function renderList(listId, $container) { wishlistRender.render(Wishlist, { wishlistId: listId, scope: SCOPE, moveProdToCart: Cart.addProductsToCart, routeProdDetailPage: (product) => getProductLink(product.urlKey, product.sku), getProductData: pdpApi.getProductData, getRefinedProduct: pdpApi.getRefinedProduct, })($container); } ``` Because the container is given a `wishlistId`, it loads and mutates only that list and never touches the active-list state. ### 3. Drive UI off the scoped `wishlist/data` event Subscribe to `wishlist/data` on your scope to keep a count or heading in sync. With `{ eager: true }`, the handler also runs with the current value as soon as it subscribes. Because it is scoped, it only ever receives this list's data. ```js events.on( 'wishlist/data', (list) => setCount(list?.items_count ?? 0), { eager: true, scope: SCOPE }, ); ``` ### 4. Add items to the specific list Pass the resolved `wishlistId` to `addProductsToWishlist` to target this list instead of the active one. In the Save for Later example, you add the cart line to the list and then remove it from the cart. ```js await WishlistApi.addProductsToWishlist( [{ sku: item.sku, quantity: item.quantity || 1 }], listId, ); await Cart.updateProductsFromCart([{ uid: item.uid, quantity: 0 }]); ``` > A targeted add to an authenticated list does not emit on the active channel, so no `wishlist/alert` toast fires automatically. Re-render the section (or show your own confirmation) after the add. For a guest list, pass `{ scope }` as the third argument if you want the updated list broadcast on your channel. ### 5. Merge the guest list on login Guests build their list in browser storage. When a guest signs in, fold that local list into the server list so nothing is lost. Resolve the server list, fetch it on your scope, then hand it to `mergeWishlists` with the guest key. The drop-in owns reading the list, deduplicating items by SKU and selected options, and clearing it afterward. ```js async function mergeGuestListIntoServer(serverListId) { if (!serverListId || serverListId === GUEST_KEY) return; const serverList = await WishlistApi.getWishlistById(serverListId, 200, 1, { scope: SCOPE, }); if (serverList) { await WishlistApi.mergeWishlists(serverList, GUEST_KEY); } } events.on('authenticated', async (authenticated) => { if (!authenticated) return; const serverListId = await resolveListId(); await mergeGuestListIntoServer(serverListId); renderList(serverListId, $container); }); ``` ### 6. Set a guest expiry (optional) The default guest wishlist never expires, but a named guest list can. Configure a time to live, in days, in your wishlist initializer, keyed by the same list key you used above. ```js title="scripts/initializers/wishlist.js" await initializers.mountImmediately(initialize, { isGuestWishlistEnabled: true, guestWishlistTtl: { 'save-for-later': 14, // the guest list expires 14 days after its last save }, }); ``` See [Guest list expiry](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/wishlist/initialization/#guest-list-expiry) for details. ## Recap To render an additional wishlist beside the active one: - Resolve a `wishlistId` (server list for authenticated shoppers, reserved local key for guests) and pick a stable `scope`. - Render the `Wishlist` container with both, and drive your UI from the scoped `wishlist/data` event. - Target the list explicitly in `addProductsToWishlist` / `removeProductsFromWishlist`, and merge the guest list into the server list on login with `mergeWishlists`. The same pattern works for any secondary list, not just Save for Later. Swap the list name, key, and scope for your use case. --- # Show targeted content to selected shoppers Add a targeted block to show a promotion, message, or content fragment to selected shoppers. Choose customer groups, segments, or cart rules to control who sees the content. ## Before you start This guide assumes you are comfortable with how commerce block tables are laid out in a document. If you are new to block name rows, key-value settings, and merged full-width content rows, read [Block table structure](https://experienceleague.adobe.com/en/tools/commerce-storefront/merchants/blocks/block-tables/) first. Ask your administrator to confirm the available customer groups, segments, or cart rules, and obtain the identifiers you need. Your storefront must already support the targeted block. Use the configuration example below when following [the authoring steps](#how-to-enable-and-configure-personalization). ## Configuration Add this table to your document to configure a targeted block. A targeted block is a block configuration that conditionally shows content for selected customer groups, segments, or cart price rule contexts: | targeted-block | | | --- | --- | | Customer Segments | 1, 2 (example) | | Customer Groups | 1, 3 (example) | | Cart Rules | 5 (example) | | fragment | /path/to/fragment (example) | | type | promotion-banner (example) | In the `targeted-block` table, most settings use a key-value row: a two-column row where the first cell is the setting name and the second cell is the value (for example, `true`, a number, or a label). If you do not set a fragment path, you must add a final merged full-width row—one cell that spans the full table width so you can place rich page content in one place (for example, headings, text, and images). The `targeted-block` table uses that row for inline content when you do not load a separate fragment document. - `Customer Segments` — A segment is a rule-based way to target experiences to qualifying shopper cohorts. This row holds a comma-separated list of customer segment IDs; the block only appears for shoppers who belong to those segments. Optional. - `Customer Groups` — A customer group in Commerce is used to segment shoppers for pricing, permissions, or targeted content. This row holds a comma-separated list of customer group IDs; the block only appears for shoppers in those groups. Optional. - `Cart Rules` — A cart price rule is a Commerce rule you can use as a condition for targeting content or promotions. This row holds a comma-separated list of cart rule IDs; the block only appears when those cart price rules are satisfied. Optional. - `fragment` — An optional path to a separate content document. When you set a fragment, the `TargetedBlock` loads content from that document. When you omit it, add your content in the last merged full-width row in this `targeted-block` table, as described above. Optional. - `type` — A value that groups `TargetedBlock` entries: when several blocks use the same type, only the first one whose conditions match a shopper is shown, which allows fallback-chain behavior. Set it to the type name to use in that chain. Only the first block that satisfies the conditions of each type is shown. Optional. ## How to enable and configure Personalization Personalization blocks can be configured on each page through Document Authoring. Follow the step-by-step guide below to enable and configure Personalization: ### 1. Create customer segments, groups, or cart rules You must first create the targeting criteria in Adobe Commerce Admin that will determine which customers see your personalized content. 1. Log in to the Adobe Commerce Admin. 1. Create customer segments, customer groups, or cart price rules as needed: - **Customer Segments** - Go to **Customers** > **Segments** and follow the https://experienceleague.adobe.com/en/docs/commerce-admin/customers/segments/customer-segments to create segments. - **Customer Groups** - Go to **Customers** > **Customer Groups** and follow the https://experienceleague.adobe.com/en/docs/commerce-admin/customers/customer-groups to create groups. - **Cart Price Rules** - Go to **Marketing** > **Promotions** > **Cart Price Rules** and follow the https://experienceleague.adobe.com/en/docs/commerce-admin/marketing/promotions/cart-rules/price-rules-cart to create rules. ### 2. How to add and configure the Personalization block You can configure each page to display personalized content by setting targeting criteria and optionally specifying a fragment path or block type. 1. Open the page where you want to display personalized content. 1. Create a new table in your document with the block name `targeted-block`. 1. Add configuration properties based on your targeting needs: **Example: Target specific customer segments:** | targeted-block | | | --- | --- | | Customer Segments | 1, 2 | **Example: Load content from a fragment:** | targeted-block | | | --- | --- | | fragment | /path/to/fragment | **Example: Use fallback with block type:** Create two blocks with the same `type` value. The first block (with conditions) displays when conditions are met, and the second block (fallback) displays otherwise: | targeted-block | | | --- | --- | | Cart Rules | 5 | | type | promotion-banner | Then add a fallback block with the same type but no conditions: | targeted-block | | | --- | --- | | type | promotion-banner | > **Specify a fragment** If you don't specify a `fragment`, the content must be placed in the last merged full-width row, as described in the Configuration section. ## Check the result Preview the page in the shopper context that should match your criteria, then check a context that should not match. Confirm that the targeted content appears only where intended before publishing. Ask your developer for help when you cannot reproduce the required customer or cart context. ## Section metadata Control the section styling, spacing, and layout that wraps your commerce block. All properties are optional: | section-metadata | | | --- | --- | | Style | light (optional) | | Padding | medium (optional) | | Margin | small (optional) | | Column Width | 30% (optional) | | Gap | small (optional) | > **Learn more** See the [Section Metadata guide](https://experienceleague.adobe.com/en/tools/commerce-storefront/merchants/blocks/section-metadata/) for all available values and the [Page Metadata guide](https://experienceleague.adobe.com/en/tools/commerce-storefront/merchants/blocks/page-metadata/) for SEO and caching options. --- # Add product recommendations to a page Add a recommendation unit to a storefront page using a `product-recommendations` block. The unit you choose determines which products are recommended and whether the block needs product context. ## Before you begin Ask your administrator to confirm that Product Recommendations is available for your Commerce backend and that you can create or use a recommendation unit. Your storefront must already support the recommendations block. Follow [the authoring steps](#how-to-enable-and-configure-product-recommendations), using the configuration examples below. ## Configuration Add this table to your document to configure the block: | product-recommendations | | | --- | --- | | recId | 2ed7ea09-d9bd-4691-91c3-2c0a6d4fd869 (example) | | currentsku | crz-eco-tr-std-2017 (example) | | currentprice | 49.99 (example) | - **recId**: Recommendation unit ID from the Admin. Required for all pages. - **currentsku**: Seed product SKU. Required on non-PDP pages for **anchor-based** recommendation unit types (for example, more-like-this, visual similarity, viewed-viewed, viewed-bought, bought-bought). On PDP pages, the boilerplate reads the seed SKU automatically from the Adobe Client Data Layer (ACDL). - **currentprice**: Anchor product price (numeric). Required on non-PDP pages when the unit uses **dynamic or relative** price filter operators (Adobe Commerce Optimizer and Adobe Commerce as a Cloud Service only). Not required for static price filters—a fixed price range in the filter rule does not remove the need for a seed SKU on anchor-based types. On PDP pages, the boilerplate reads anchor price automatically from ACDL when needed. ### When product context is required Whether a seed SKU or anchor price is required depends on the **recommendation unit type** and the **price filter operator**, not on each other: | Unit type | Seed SKU | Anchor price | | --- | --- | --- | | **Anchor-based** (more-like-this, visual similarity, viewed-viewed, viewed-bought, bought-bought) | Required | Only for dynamic or relative price filter operators | | **Context-free** (most-viewed, most-purchased, and similar popularity or behavioral types) | Not required | Not applicable | A **static** price filter defines a fixed price range in the filter rule. It does not mean the unit can run without an anchor product. For example, a more-like-this unit with a static price filter returns no products when the seed SKU is missing, but returns results with the seed SKU alone—`currentprice` is not required. ### Storefront block configuration How you configure the block depends on the page type and unit type: - **PDP pages**: Set only `recId`. The boilerplate supplies the seed SKU from ACDL product context. On Adobe Commerce Optimizer and Adobe Commerce as a Cloud Service it also sends the ACDL anchor price automatically; the backend applies it only when the unit's filter uses a dynamic or relative operator. - **Non-PDP pages (PLP, cart, home, and so on)**: There is no product context on these pages. For **anchor-based** unit types, you must set `currentsku` in block config. Add `currentprice` only when the unit uses dynamic or relative price filter operators (Adobe Commerce Optimizer and Adobe Commerce as a Cloud Service only). - **Context-free unit types on any page**: Set only `recId`. These popularity and behavioral units do not use a seed product. > **Important** - Do not assume `recId` alone is enough on non-PDP pages. That applies to **context-free** unit types only. **Anchor-based** units on non-PDP pages require `currentsku` or they return no products. - Do not omit `currentprice` on non-PDP pages just because the unit uses a static price filter. Static filters still need a seed SKU for anchor-based types; they do not need anchor price. - Add `currentprice` on non-PDP pages only when the unit's price filter uses dynamic or relative operators. ## How to enable and configure Product Recommendations The Product Recommendations block can be configured on each page through Document Authoring. Follow the step-by-step guide below to enable and configure Product Recommendations: ### 1. Create a recommendation unit You must first create recommendation units in the Adobe Commerce Admin (PaaS) or Adobe Commerce Optimizer Admin (SaaS). #### Adobe Commerce Optimizer Admin (SaaS) The following steps apply to merchants using Adobe Commerce Optimizer or Adobe Commerce as a Cloud Service. 1. Log in to the Adobe Commerce Optimizer Admin. 1. On the Admin sidebar, go to **Recommendations**. 1. Follow the steps in the https://experienceleague.adobe.com/en/docs/commerce/optimizer/merchandising/recommendations/create to create a recommendation unit. > For Adobe Commerce Admin (PaaS), follow the steps in the https://experienceleague.adobe.com/en/docs/commerce/product-recommendations/admin/create to create a recommendation unit. ### 2. Get the recommendations unit ID You can specify which recommendation unit to display on each page by using the recommendation unit ID. 1. Navigate to the Product Recommendations page in the Adobe Commerce Admin or Adobe Commerce Optimizer Admin. 1. Click the eye icon next to the recommendation unit that you'd like to display on your store. 1. Copy the **Recommendation ID** from the modal. You will use this ID in the next step to configure the recommendations block. ### 3. Add and configure the recommendations block You can configure each page to display a specific recommendation unit by setting its recommendation ID. 1. Open the page where you want to display the recommendation unit. 1. Create a new table in your document with the block name `product-recommendations`. For product detail pages, set the `recId` to match the recommendation unit ID from the previous step. For example: | product-recommendations | | | --- | --- | | recId | 2ed7ea09-d9bd-4691-91c3-2c0a6d4fd869 | > **PDP pages** — set only `recId` for all unit types. The boilerplate reads seed SKU (and anchor price when needed) from ACDL. **Non-PDP pages — anchor-based unit types** — set `recId` and `currentsku`. For example, a more-like-this unit on a category page: | product-recommendations | | | --- | --- | | recId | 2ed7ea09-d9bd-4691-91c3-2c0a6d4fd869 | | currentsku | crz-eco-tr-std-2017 | This applies even when the unit uses a **static** price filter. The seed SKU is required; anchor price is not. **Non-PDP pages — dynamic or relative price filters** — also set `currentprice` on Adobe Commerce Optimizer or Adobe Commerce as a Cloud Service: | product-recommendations | | | --- | --- | | recId | 2ed7ea09-d9bd-4691-91c3-2c0a6d4fd869 | | currentsku | crz-eco-tr-std-2017 | | currentprice | 49.99 | **Non-PDP pages — context-free unit types** (most-viewed, most-purchased, and similar) — set only `recId`, same as on PDP. ## Check the result Preview the page and confirm that the selected recommendation unit appears. For a unit that needs a seed product, check the product context required by the examples above. Publish when the unit displays as expected; ask your administrator or developer to investigate an empty unit rather than changing its identifier at random. ## Section metadata Control the section styling, spacing, and layout that wraps your commerce block. All properties are optional: | section-metadata | | | --- | --- | | Style | light (optional) | | Padding | medium (optional) | | Margin | small (optional) | | Column Width | 30% (optional) | | Gap | small (optional) | > **Learn more** See the [Section Metadata guide](https://experienceleague.adobe.com/en/tools/commerce-storefront/merchants/blocks/section-metadata/) for all available values and the [Page Metadata guide](https://experienceleague.adobe.com/en/tools/commerce-storefront/merchants/blocks/page-metadata/) for SEO and caching options. --- # Add terms and conditions to checkout Publish agreement documents and link them from the checkbox labels shoppers see at checkout. Work with your administrator and developer to confirm how your storefront defines its agreements before changing their configuration. ## Before you begin - Obtain approved agreement text and the languages or store views that need it. - Check that you can author and publish documents and update the storefront's placeholders sheet. - Ask your developer whether the storefront uses its default agreement, custom agreements, or Admin-managed Commerce agreements, and which Admin settings apply. > The default storefront defines a manually accepted agreement with the label key `Checkout.TermsAndConditions.label`. Adding agreement records in Admin does not automatically replace that code-defined agreement. Additional agreements or a different agreement source require developer configuration; see the [TermsAndConditions reference](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/checkout/containers/terms-and-conditions/). > **TermsAndConditions configurable on a per store-view basis** For a storefront configured to use store-view-specific agreements, prepare the corresponding documents and labels for each language. Ask your developer to confirm that checkout uses the intended agreement source for each store view. Visit the [Multi-store setup](https://experienceleague.adobe.com/en/tools/commerce-storefront/setup/configuration/multistore-setup/) documentation for more information on how to build blocks and configure the storefront structure based on **Edge Delivery Services (EDS)**. ## How to enable and configure the Terms and Conditions Confirm the applicable Admin configuration with your developer, publish the agreement documents, and update the labels for the agreement source your storefront uses. ### 1. Enable the component If your developer confirms that your checkout requires the Commerce Admin setting, ask an administrator to enable it as follows: 1. Log in to the Admin Panel. 1. On the Admin sidebar, go to **Stores** > _Settings_ > **Configuration**. 1. In the left panel, expand **Sales** and choose **Checkout**. 1. Expand the **Checkout Options** section. 1. Set **Enable Terms and Conditions** to "Yes". 1. Click **Save Config**. See https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/point-of-purchase/checkout/terms-and-conditions documentation for more information on how to enable the terms and conditions using the Admin Panel. ### 2. Create the agreement document For agreement labels that link to storefront documents, create the content in Document Authoring: 1. Create an agreement document at the intended path, such as `/terms-and-conditions` or a locale-specific path. 1. Add the approved agreement text and any required links. 1. Preview and publish the document. 1. Open its published URL and confirm that shoppers can read it without an authoring login. Keep this URL for the checkbox label. ### 3. Configure the agreement checkbox label Use the **EDS placeholders** or the **Admin Panel** instructions in the following tabs to configure the agreement checkbox label. ### Using EDS placeholders For the default storefront agreement, update the **`placeholders`** sheet using the existing label key and the following information. For additional agreements, ask your developer which label keys their configuration uses. - An **_identifier_**: It's the reference that identifies the label translated - A **_label_**: The text displayed next to the checkbox comes from the storefront placeholders for the shopper's language. It accepts HTML links to your published agreement documents. Similarly, the merchant can configure the translation for the validation error that appears when the user clicks the `Place Order` button and an agreement is not checked, using the **"Checkout.TermsAndConditions.error"** identifier. Sample of **placeholders** sheet: ```txt "Checkout.TermsAndConditions.label", "I have read, understand, and accept the [Terms of Use, Terms of Sales, Privacy Policy, and Return Policy](/en/privacy-policy)." "Checkout.TermsAndConditions.error", "Please accept the Terms and Conditions to continue." ``` Replace the example wording with approved text and the example link with the published URL of your agreement document. Follow [Change labels and placeholders](https://experienceleague.adobe.com/en/tools/commerce-storefront/how-tos/change-labels-and-placeholders/) to preview and publish the updated sheet. These labels are used to render the `TermsAndConditions` container in the **commerce-checkout.js** block file. Refer to the Adobe Commerce documentation for examples on how to use these labels. ### Using the Admin Panel Use this option only when your developer has configured checkout to use Admin-managed Commerce agreements. It does not automatically change the default code-defined agreement. The merchant has to configure the checkbox label for each agreement using the Admin Panel. As mentioned before, the Admin Panel allows merchants to create configurations per store-view level as follows: 1. Log in to the Admin Panel. 1. On the Admin sidebar, go to **Stores** > _Settings_ > **Terms and Conditions**. 1. In the upper-right corner, click the **Add New Condition** button. 1. Enter the following **Terms and Conditions** information: - **`Condition Name`** The agreement identifier - **`Status`** Attribute to indicate if it's available or not: - 'Enabled'; the agreement will be retrieved by the GraphQL API query - 'Disabled'; the agreement won't be retrieved by the GraphQL API query - **`Applied`** Specifies how the checkbox should appear: - 'Manually': the user is required to manually check and accept the conditions to place an order - 'Automatically': the checkbox will appear checked by default, conditions are automatically accepted upon checkout - **`Store View`** Defines the scope at store view level for this agreement where you want these Terms and Conditions to be used - **`Checkbox Text`** It contains directly the text to show; it accepts HTML with links to a specific page in EDS > **Some attributes are not used** Although the following attributes are provided by the Admin Panel, they are not going to be utilized in the current solution because they will be handled in EDS. Merchants are able to work on their own solution if they want to use them: - **`Content`** As described above, the content for the agreements pages will be created **ONLY** using EDS - **`Show Content as`** (Text, HTML) Not really needed because EDS pages accept HTML code - **`Content Height (css)`** 1. Click **Save Condition**. These entries are used to render the `TermsAndConditions` container in the **commerce-checkout.js** block file. Refer to the Adobe Commerce documentation for examples of this feature. See the https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/point-of-purchase/checkout/terms-and-conditions documentation to learn how to configure the terms and conditions using the Admin Panel. > For storefront-authored agreement documents, the checkbox label must link to the relevant published document for the shopper's language or store view. ## Check the result Preview checkout for each affected store view. Confirm that the intended agreement appears, its links open the published documents, and any required manual acceptance is enforced before an order can be placed. Use your team's test-order process rather than placing an unintended live order. --- # Copy content between Document Authoring projects To migrate content from one Document Authoring project to another, you use the Traverse, Import, and Bulk Operations apps. This page shows you how. ![Migrate content between Document Authoring projects.](https://experienceleague.adobe.com/en/tools/commerce-storefront/images/merchants/MigrationProcess.svg) *Migrate content between Document Authoring projects.* ## Before you start You'll need: - Write access to both projects on DA.live - The organization and site path for each project (for example, `/acme/storefront-staging` and `/acme/storefront`) - A target project that already exists with the right GitHub repository and Commerce backend - Permission to preview and publish on the target, or a developer who can run Bulk Operations for you ## Step-by-step ### 1. Traverse the source project 1. Open https://da.live, select **Apps**, and open **Traverse**. 1. Enter the source path `/organization/site` (for example, `/acme/storefront-staging`). Start with `/`. 1. Select **Crawl**, then **Copy list**. ![Traverse your source project.](https://experienceleague.adobe.com/en/tools/commerce-storefront/images/merchants/TraverseSourceProject.png) *Traverse your source project.* ### 2. Import into the target project > Import overwrites any file in the target project that has the same name as a file in your list. Review your list before you run it and move any files you want to keep to a different folder in the target project so you can copy them back later as needed. 1. Open **Import** from **Apps** (https://da.live/apps/import). 1. Paste the file paths from the Traverse into **By Url**. 1. In the **Into** field, enter your organization and target site name (for example, `acme` and `storefront`). 1. Select the **Import** button. ![Import your content into the target project.](https://experienceleague.adobe.com/en/tools/commerce-storefront/images/merchants/ImportContent.png) *Import into the target project.* ### 3. Traverse the target project 1. Open the **Traverse** app again. 1. Enter the target path (for example, `/acme/storefront`). 1. Select the **Crawl** button, then the **Copy list** button. Use this target list in Bulk Operations, not the source list. ![Traverse your target project.](https://experienceleague.adobe.com/en/tools/commerce-storefront/images/merchants/TraverseTargetProject.png) *Traverse the target project.* ### 4. Preview and publish 1. Open the **Bulk Operations** app from **Apps**. 1. Paste the target Traverse list into the **URLs** field. 1. Choose **Preview**, then select **Submit**. Spot-check home, product list, cart, and checkout on the preview site. 1. Choose **Publish**, then select **Submit**. Confirm the live site. ![Preview and publish on the target site.](https://experienceleague.adobe.com/en/tools/commerce-storefront/images/merchants/BulkOperations.png) *Preview and publish on the target site.* ## Verify - Open migrated content in https://da.live and on the preview site. - Spot check the home (index) page, product list page, product detail page, cart page, and checkout page. ## Reference ### What Import copies The **Traverse** app lists page paths in a project. The **Import** app copies only the paths you paste from that list. A typical crawl lists storefront documents such as `cart.html` and `checkout.html`, not every image or label sheet file. Import does not copy storefront code, Commerce catalog data, or shopper cart session data in Commerce. After Import, you still run the **Bulk Operations** app so the preview and live sites show the update. ### Images and media When you import a path, you copy the page content and the image references inside that page. Traverse does not list a top-level `media` folder or hidden asset folders. Add `media/...`, sheet, or JSON paths to your list when you need them. If you see a broken image on the target preview site, the page references a media path (for example, `/media/hero.png`) that was not imported. Add that path to the Import list and run Import again, or upload the file in the target project on DA.live. See https://docs.da.live/authors/guides/adding-media on docs.da.live. ### If button labels or URLs look wrong Review [placeholder sheets](https://experienceleague.adobe.com/en/tools/commerce-storefront/merchants/blocks/placeholder-sheets/) (label text for cart, checkout, and other blocks) and `redirects.json` for URL-forwarding rules. ### Troubleshooting | Symptom | What to try | | --- | --- | | Crawl returns nothing | Start the path with `/`. Confirm you have access to the project folder. | | Import fails | Confirm access to both projects. | | Broken images on preview | Add missing `media/...` paths to the Import list and import again. | | Live site unchanged | Run **Publish** after **Preview**. | ## Related topics - https://docs.da.live/authors/guides/apps/content-tree (docs.da.live) --- # Translate storefront content Use the **Document Authoring Tool** (da.live) Translation app to translate your storefront content into multiple languages. After translation, ask your development team to complete the Commerce-specific tasks for placeholder files and store view verification (see [Commerce localization tasks](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/all/commerce-tasks/)). > **Before translating** Complete the [multistore setup](https://experienceleague.adobe.com/en/tools/commerce-storefront/setup/configuration/multistore-setup/) to create the folder structure, configure store views, and set up the currency and headers. This guide covers translating the content in those folders. ## Prerequisites - You have a storefront project in the Document Authoring app (https://da.live). - You know the basics of using the Document Authoring tool (https://da.live/docs). ## Overview The following diagram provides the basic workflow for translating the Edge Delivery content of your storefront into multiple languages using the Document Authoring apps from https://da.live. ![Workflow for localizing your storefront](https://experienceleague.adobe.com/en/tools/commerce-storefront/merchants/images/localization-steps.png) *Workflow for localizing your storefront* ## Step-by-step These steps provide the details for the translation workflow shown in the diagram. The steps describe how to translate the default English store to another language. In this example, the default English store is translated to French (`fr`). ### 1. Navigate to the apps page for your storefront 1. Open your storefront project in the Document Authoring tool (https://da.live). 1. Select Apps from the left sidebar. ### 2. Use the Traverse app to get a list of site URLs 1. Open the **Traverse** app by selecting its *Go* button. 1. On the Crawl Tree page, enter the organization name and site name for your storefront. Example: `/acme/storefront`. Ensure that you enter a forward slash (`/`) at the beginning of the string or it will not work. 1. Select the **Crawl** button to start the crawl. 1. Select the **Copy list** button after the URLs for the site have been traversed. This puts all the URLs in your clipboard. 1. Return to the Apps page. ### 3. Use the Translation app to translate your storefront 1. Open the **Translate** app by selecting its *Go* button. 1. On the Localization page, enter the title for the translation project: `acme-storefront-fr`. 1. Paste the list of URLs from the **Traverse** app into the **URLs** field. > Exclude configuration files such as `metadata.json`, `redirects.json`, and `sitemap.json` unless your developer has asked you to include them. These are not ordinary page content. 1. Select the **Next** button. 1. View the validated URLs to ensure that they are all correct. 1. Select the **Next** button. 1. Select one or more languages you want to translate to. In this example, the translation is to French (`fr`). 1. For the French language, select **Translate** from the selector, then select the **Create project** button. 1. From the project page, select the **Send all for translation** button. > **Translation services** By default, the Translation app uses the Google Translate service to translate the content of the storefront into the selected language. Your organization can also connect other translation services for a more robust translation experience. For more information on translation services and configuration, see https://www.aem.live/docs/translation-and-localization. ### 4. View the translated site 1. Navigate back to the main (root) folder for your English storefront. 1. Scroll the site files and folders to find the `fr` folder and open it. 1. Select a document to view the translated content. For example, select the `index` document to view the translated home page. ## Check the result Open representative documents in the translated folder and review their text and links. Ask your development team to complete [Commerce localization tasks](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/all/commerce-tasks/) and confirm that the translated pages use the intended store view, currency, and storefront labels before publishing them. ## Additional resources ### Edge Delivery Services documentation - https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/edge-delivery/overview - Adobe Experience League - https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/edge-delivery/document-authoring/authoring - Adobe Experience League - https://www.aem.live/docs/ - aem.live ### Translation and localization - https://www.aem.live/docs/translation-and-localization - aem.live - https://da.live/docs/administration/translation - da.live ## Next steps After translating your content with the da.live Translation app: 1. **Hand off Commerce tasks**: Ask your development team to follow [Commerce localization tasks](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/all/commerce-tasks/) to: - Translate drop-in placeholder files (`cart.json`, `checkout.json`, `pdp.json`) - Verify store view configuration aligns with your multistore setup - Test Commerce functionality (product data, pricing, currency, store switcher) 2. **Review best practices**: See Commerce-specific best practices in [Commerce localization tasks](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/all/commerce-tasks/). 3. **Troubleshooting**: For Commerce-specific issues, refer to the troubleshooting section in [Commerce localization tasks](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/all/commerce-tasks/). ## Troubleshooting For da.live Translation app issues, see: - https://www.aem.live/docs/translation-and-localization - aem.live - https://da.live/docs/administration/translation - da.live - https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/edge-delivery/document-authoring/authoring - Adobe Experience League For Adobe Commerce store view configuration issues, see: - https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/site-store/store-views - Adobe Experience League - https://experienceleague.adobe.com/en/docs/commerce-admin/config/scope-change - Adobe Experience League --- # Add a block to a page Add a block table to a document to place a shopping feature or content element on your page. Your storefront must already support the block you choose; adding a new block implementation requires a developer. ## Before you begin - Open the page in [Document Authoring](https://experienceleague.adobe.com/en/tools/commerce-storefront/merchants/quick-start/document-authoring/). - Find the block name and required settings in the [Commerce block reference](https://experienceleague.adobe.com/en/tools/commerce-storefront/merchants/blocks/), or use a content block supported by your storefront. - Make sure you have permission to preview and publish the page. ## Add the block 1. Insert a table where you want the block to appear in the document. 1. Enter the block name in the first row, for example, `commerce-cart`. If the table has two columns, merge the cells in this row. 1. Add the settings required by the block's reference page. Put the setting name in the first column and its value in the second column. Some blocks need only the name row. 1. Preview the page and confirm that the block appears where you placed it. 1. Publish the page when the result looks right. For examples of settings and merged content rows, see [Block table structure](https://experienceleague.adobe.com/en/tools/commerce-storefront/merchants/blocks/block-tables/). ## Check the result Open the published page and confirm that the block appears with the settings you chose. If it does not appear, check the block name and required rows against its reference page. For an example that also includes page metadata and section styling, follow [Create your first commerce page](https://experienceleague.adobe.com/en/tools/commerce-storefront/how-tos/create-commerce-page/). --- # Enable Payment Services payment buttons in checkout The Commerce boilerplate's checkout page already includes the Payment Services credit card form, so shoppers can pay by card out-of-the-box. To enable payment with PayPal, Apple Pay, or Google Pay, additional frontend code is required to render those buttons on the checkout page. This how-to shows you how. ## What you'll build By the end of this how-to, you'll have updated the `commerce-checkout` block so that selecting PayPal, Apple Pay, or Google Pay as a payment method: - Shows a short instructional notice, for example, "Complete your purchase using the PayPal button below." - Replaces the "Place order" button at the bottom of the page with the matching branded button. Here's the result for each payment method: ![Credit card form on the checkout page](https://experienceleague.adobe.com/en/tools/commerce-storefront/how-tos/images/aps-checkout-credit-card.png) ![PayPal button on the checkout page](https://experienceleague.adobe.com/en/tools/commerce-storefront/how-tos/images/aps-checkout-paypal-buttons.png) ![Apple Pay button on the checkout page](https://experienceleague.adobe.com/en/tools/commerce-storefront/how-tos/images/aps-checkout-apple-pay.png) ![Google Pay button on the checkout page](https://experienceleague.adobe.com/en/tools/commerce-storefront/how-tos/images/aps-checkout-google-pay.png) ## Prerequisites Before you begin, make sure you have: - A working [Commerce storefront](https://experienceleague.adobe.com/en/tools/commerce-storefront/get-started/create-storefront/) on Edge Delivery Services - Familiarity with [customizing blocks](https://experienceleague.adobe.com/en/tools/commerce-storefront/blocks/customize-blocks/) in your code repository - Completed the onboarding steps described in [Payment Services installation](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/payment-services/installation/) - [@dropins/storefront-payment-services](https://www.npmjs.com/package/@dropins/storefront-payment-services) at version [4.2.1](https://www.npmjs.com/package/@dropins/storefront-payment-services/v/4.2.1) or higher in `package.json` ## Step-by-step The following steps describe how to add PayPal, Apple Pay, and Google Pay buttons to the `commerce-checkout` block. > **Express payment methods** The code snippets in this how-to call PayPal, Apple Pay, and Google Pay _express payment methods_. This code uses the term for a payment method that skips the credit card form and renders its own branded button in place of "Place order,". ### 1. Prepare "place order" action slot In the boilerplate's default checkout, the "Place order" button sits at the bottom of the page, the last action after shipping, payment, billing, terms and conditions, and promo codes on mobile. When a shopper selects PayPal, Apple Pay, or Google Pay, that same spot needs to hold the branded button instead, so both buttons need to share one dynamic slot. > **Why not render it inline?** An alternative would be to render the selected method's branded button below the payment options (in the same spot where the credit card form appears), hiding the default "Place order" button while that payment method stays selected. On smaller mobile screens, everything that follows (the billing address form, terms and conditions, and promo code inputs) can push the branded payment button out of view, leaving shoppers with no visible way to continue. Rendering the button in the same slot as "Place order" keeps the final action at the bottom, visible no matter what renders above it. To add that shared slot, adapt your `blocks/commerce-checkout` block as follows: 1. Navigate to the `blocks/commerce-checkout/containers.js` file and find the `renderPlaceOrder` function. ```js /** * Renders place order button with handler functions - follows multi-step pattern * @param {HTMLElement} container - DOM element to render the place order button in * @param {Object} options - Configuration object with handler functions * @param {Function} options.handleValidation - Validation handler function * @param {Function} options.handlePlaceOrder - Place order handler function * @returns {Promise} - The rendered place order component */ export const renderPlaceOrder = async (container, options = {}) => renderContainer( CONTAINERS.PLACE_ORDER_BUTTON, async () => CheckoutProvider.render(PlaceOrder, { handleValidation: options.handleValidation, handlePlaceOrder: options.handlePlaceOrder, })(container), ); ``` 1. Replace it with the following code. ```js // Serializes every mount/unmount of the Place Order slot's occupant, so a new occupant is // never rendered while the previous one is still being removed let placeOrderSlotQueue = Promise.resolve(); let placeOrderSlotMount = null; // Promise | null /** * Mounts a new occupant into the Place Order slot, unmounting the previous one, if there is any. * @param {(container: HTMLElement) => Promise} mount - Starts the dropin render. * @param {HTMLElement} container - The Place Order slot container element. * @returns {Promise} */ const mountPlaceOrderSlot = (mount, container) => { placeOrderSlotQueue = unmountPlaceOrderSlot().then(async () => { try { placeOrderSlotMount = mount(container); await placeOrderSlotMount; } catch (error) { console.error('Failed to mount Place Order slot occupant:', error); placeOrderSlotMount = null; } }); return placeOrderSlotQueue; }; /** * Unmounts whatever currently occupies the Place Order slot, if anything. * @returns {Promise} */ const unmountPlaceOrderSlot = () => { placeOrderSlotQueue = placeOrderSlotQueue.then(async () => { if (placeOrderSlotMount) { try { const api = await placeOrderSlotMount; api.remove(); } catch (error) { console.error('Failed to unmount Place Order slot occupant:', error); } finally { placeOrderSlotMount = null; } } }); return placeOrderSlotQueue; }; /** * Renders place order button with handler functions - follows multi-step pattern * @param {HTMLElement} container - DOM element to render the place order button in * @param {Object} options - Configuration object with handler functions * @param {Function} options.handleValidation - Validation handler function * @param {Function} options.handlePlaceOrder - Place order handler function * @returns {Promise} */ export const renderPlaceOrder = async (container, options = {}) => mountPlaceOrderSlot( (el) => CheckoutProvider.render(PlaceOrder, { handleValidation: options.handleValidation, handlePlaceOrder: options.handlePlaceOrder, })(el), container, ); /** * Checks whether the standard Place Order button currently occupies the Place Order slot, * as opposed to an express payment method's own button (or nothing, if unmounted). * @param {HTMLElement} container - The Place Order slot element. * @returns {boolean} */ export const isPlaceOrderRendered = (container) => ( !!container.querySelector('.checkout-place-order') ); ``` 1. In `blocks/commerce-checkout/commerce-checkout.css`, find the `.checkout__place-order` rule inside the mobile media query block: ```css @media only screen and (min-width: 320px) and (max-width: 768px) { /* ...your other existing rules */ .checkout__place-order { order: 4; } } ``` 1. Replace it with the following, keeping it inside that same media query: ```css @media only screen and (min-width: 320px) and (max-width: 768px) { /* ...your other existing rules */ /* Style place order slot so it applies to both the place order button and the payment buttons. */ .checkout__place-order { order: 4; background-color: var(--color-neutral-200); padding: 0 var(--spacing-medium) var(--spacing-big) var(--spacing-medium); } /* Neutralize the checkout dropin's own background/padding so it doesn't stack with the rule above */ .checkout__place-order .checkout-place-order { background-color: transparent; padding: 0; } } ``` ### 2. Render payment buttons in the "place order" slot This step adds PayPal, Apple Pay, and Google Pay support to `renderPaymentMethods`, each rendering its branded button into the "place order" slot added in the previous step. 1. Navigate to the `blocks/commerce-checkout/containers.js` file and make sure you have the following imports. ```js // Payment Services Dropin import { PaymentMethodCode, PaymentLocation } from '@dropins/storefront-payment-services/api.js'; import ApplePay from '@dropins/storefront-payment-services/containers/ApplePay.js'; import CreditCard from '@dropins/storefront-payment-services/containers/CreditCard.js'; import GooglePay from '@dropins/storefront-payment-services/containers/GooglePay.js'; import PayPalButtons from '@dropins/storefront-payment-services/containers/PayPalButtons.js'; import { render as PaymentServices } from '@dropins/storefront-payment-services/render.js'; // Order Dropin import * as orderApi from '@dropins/storefront-order/api.js'; ``` 1. In the same file, find the `renderPaymentMethods` function. ```js /** * Renders payment methods with credit card integration - original regular checkout functionality * @param {HTMLElement} container - DOM element to render payment methods in * @returns {Promise} - The rendered payment methods component */ export const renderPaymentMethods = async (container) => renderContainer( CONTAINERS.PAYMENT_METHODS, async () => CheckoutProvider.render(PaymentMethods, { slots: { Methods: { [PaymentMethodCode.CREDIT_CARD]: { render: (ctx) => { const $creditCard = document.createElement('div'); PaymentServices.render(CreditCard)($creditCard); ctx.replaceHTML($creditCard); }, }, [PaymentMethodCode.SMART_BUTTONS]: { enabled: false, }, [PaymentMethodCode.APPLE_PAY]: { enabled: false, }, [PaymentMethodCode.APM]: { enabled: false, }, [PaymentMethodCode.GOOGLE_PAY]: { enabled: false, }, [PaymentMethodCode.VAULT]: { enabled: false, }, [PaymentMethodCode.FASTLANE]: { enabled: false, }, }, }, })(container), ); ``` 1. Replace it with the code below, which adds PayPal, Apple Pay, and Google Pay. Don't need all three? Leave any you don't need as `enabled: false` and skip its render handler. ```js /** * Resolves with the payment method codes Payment Services has confirmed are available * for checkout, waiting for its initialization event if it hasn't fired yet. * @returns {Promise} */ const getAvailablePaymentServicesMethods = () => new Promise((resolve) => { const subscription = events.on('payment-services/initialized/checkout', ({ availablePaymentMethods }) => { subscription?.off(); resolve(availablePaymentMethods); }, { eager: true }); }); /** * Creates a short notice explaining that the given express payment method's button renders in * the Place Order slot instead of inline in the Payment section. * @param {string} label - Display label of the express payment method (e.g. "Apple Pay") * @returns {HTMLElement} - The notice element */ const createExpressPaymentNotice = (label) => { const $notice = document.createElement('p'); $notice.textContent = `Complete your purchase using the ${label} button below.`; return $notice; }; /** * Renders payment method options, showing a credit card form inline, and PayPal, Apple Pay, * and Google Pay as branded checkout buttons in the Place Order slot. * @param {HTMLElement} paymentMethodsContainer - DOM element to render payment methods in * @param {HTMLElement} placeOrderButtonContainer - DOM element express payment buttons mount into, * in place of the standard Place Order button * @param {Function} validateCheckoutForms - Function that returns true if all Checkout forms are valid * @returns {Promise} - The rendered payment methods component */ export const renderPaymentMethods = async ( paymentMethodsContainer, placeOrderButtonContainer, validateCheckoutForms, ) => renderContainer( CONTAINERS.PAYMENT_METHODS, async () => { const availablePaymentServicesMethods = await getAvailablePaymentServicesMethods(); return CheckoutProvider.render(PaymentMethods, { slots: { Methods: { [PaymentMethodCode.CREDIT_CARD]: { render: (ctx) => { const $creditCard = document.createElement('div'); PaymentServices.render(CreditCard)($creditCard); ctx.replaceHTML($creditCard); }, enabled: availablePaymentServicesMethods.includes(PaymentMethodCode.CREDIT_CARD), }, [PaymentMethodCode.PAYPAL_BUTTONS]: { render: (ctx) => { mountPlaceOrderSlot((el) => PaymentServices.render(PayPalButtons, { onButtonClick: (showPaymentSheet) => { if (validateCheckoutForms()) { showPaymentSheet(); } }, onSuccess: ({ cartId }) => { orderApi.placeOrder(cartId); }, onError: (localizedError) => { events.emit('checkout/error', { message: localizedError.message, }); }, })(el), placeOrderButtonContainer); ctx.replaceHTML(createExpressPaymentNotice('PayPal')); }, enabled: availablePaymentServicesMethods.includes(PaymentMethodCode.PAYPAL_BUTTONS), }, [PaymentMethodCode.APPLE_PAY]: { render: (ctx) => { const checkoutData = events.lastPayload('checkout/updated') || events.lastPayload('checkout/initialized') || null; if (checkoutData === null) { console.error('Cannot render apple pay button without checkout data.'); unmountPlaceOrderSlot(); ctx.replaceHTML(document.createElement('div')); return; } mountPlaceOrderSlot((el) => PaymentServices.render(ApplePay, { location: PaymentLocation.CHECKOUT, getCartId: () => ctx.cartId, isVirtualCart: checkoutData.isVirtual, onButtonClick: (showPaymentSheet) => { if (validateCheckoutForms()) { showPaymentSheet(); } }, onSuccess: ({ cartId }) => orderApi.placeOrder(cartId), onError: (localizedError) => { events.emit('checkout/error', { message: localizedError.message, }); }, })(el), placeOrderButtonContainer); ctx.replaceHTML(createExpressPaymentNotice('Apple Pay')); }, enabled: availablePaymentServicesMethods.includes(PaymentMethodCode.APPLE_PAY), }, [PaymentMethodCode.APM]: { enabled: false, }, [PaymentMethodCode.GOOGLE_PAY]: { render: (ctx) => { mountPlaceOrderSlot((el) => PaymentServices.render(GooglePay, { onButtonClick: (showPaymentSheet) => { if (validateCheckoutForms()) { showPaymentSheet(); } }, onSuccess: ({ cartId }) => orderApi.placeOrder(cartId), onError: (localizedError) => { events.emit('checkout/error', { message: localizedError.message, }); }, })(el), placeOrderButtonContainer); ctx.replaceHTML(createExpressPaymentNotice('Google Pay')); }, enabled: availablePaymentServicesMethods.includes(PaymentMethodCode.GOOGLE_PAY), }, [PaymentMethodCode.VAULT]: { enabled: false, }, [PaymentMethodCode.FASTLANE]: { enabled: false, }, }, }, })(paymentMethodsContainer); }, ); ``` 1. In `blocks/commerce-checkout/commerce-checkout.js`, find the call to `renderPaymentMethods` and add the `$placeOrder` and `handleValidation` arguments. > **Method not showing up?** Each method's `enabled` flag depends on `availablePaymentServicesMethods`, which mirrors its checkout visibility toggle in Commerce Admin: - Credit Card: "Show on checkout page" - Apple Pay: "Show Apple Pay on checkout page" - Google Pay: "Show Google Pay on checkout page" - PayPal buttons: "Show buttons on checkout page" If a method doesn't appear after adding this code, check its toggle first. (See https://experienceleague.adobe.com/en/docs/commerce/payment-services/configure/configure-admin for details). Even with the toggle on, its own browser, device, country, or account requirements can still keep it from appearing, as indicated in https://experienceleague.adobe.com/en/docs/commerce/payment-services/payments-checkout/payments-options. ```js renderPaymentMethods($paymentMethods, $placeOrder, handleValidation), ``` 1. In the same file, find the `trySubmitPaymentServicesCreditCard` function. ```js const trySubmitPaymentServicesCreditCard = async () => { try { await paymentsApi.submitCreditCard(); return true; } catch (error) { switch (error.code) { case 'payment-services/credit-card-form-not-rendered': console.error('Credit card form not rendered.'); return false; case 'payment-services/credit-card-form-invalid': // Credit card form invalid; abort order placement return false; default: throw error; } } }; ``` 1. Replace it with the code below so credit card errors go through the same error-messaging path as the new payment buttons. ```js const trySubmitPaymentServicesCreditCard = async () => { try { await paymentsApi.submitCreditCard(); return true; } catch (error) { if (error.localized) { events.emit('checkout/error', { message: error.message, }); return false; } switch (error.code) { case 'payment-services/credit-card-form-not-rendered': console.error('Credit card form not rendered.'); return false; case 'payment-services/credit-card-form-invalid': // Credit card form invalid; abort order placement return false; default: throw error; } } }; ``` ### 3. Restore place order button on method switch This step restores the "Place order" button whenever a shopper switches from an express payment method back to one without a branded button. The check lives in `initializeCheckout` which, despite the name, runs on every checkout update, not just once at load. 1. Navigate to the `blocks/commerce-checkout/commerce-checkout.js` file and import the new `isPlaceOrderRendered` function added to `containers.js` in the first step. ```js import { isPlaceOrderRendered, // new // ...your other existing container imports } from './containers.js'; ``` 1. In the same file, find the `initializeCheckout` function. ```js async function initializeCheckout(data) { await initReCaptcha(0); if (data.isGuest) await displayGuestAddressForms(data); else { removeOverlaySpinner(loaderRef, $loader, $loaderStatus); await displayCustomerAddressForms(data); } } ``` 1. Replace it with the following code. ```js const EXPRESS_PAYMENT_METHODS = [ paymentsApi.PaymentMethodCode.PAYPAL_BUTTONS, paymentsApi.PaymentMethodCode.APPLE_PAY, paymentsApi.PaymentMethodCode.GOOGLE_PAY, ]; const isExpressPaymentMethod = (method) => ( method && EXPRESS_PAYMENT_METHODS.includes(method.code) ); async function initializeCheckout(data) { await initReCaptcha(0); if (data.isGuest) await displayGuestAddressForms(data); else { removeOverlaySpinner(loaderRef, $loader, $loaderStatus); await displayCustomerAddressForms(data); } // Express payment methods replace the place order button; restore it for non-express methods if (data.selectedPaymentMethod?.code && !isExpressPaymentMethod(data.selectedPaymentMethod) && !isPlaceOrderRendered($placeOrder)) { await renderPlaceOrder($placeOrder, { handleValidation, handlePlaceOrder }); } } ``` ## Example See https://github.com/hlxsites/aem-boilerplate-commerce/tree/payment-services/blocks/commerce-checkout in the `payment-services` branch of the boilerplate repository for a reference implementation of the PayPal, Apple Pay, and Google Pay integration. This branch may also include other Payment Services features, such as stored cards, covered in a separate how-to. --- # Add messages to mini cart The mini cart block already includes inline feedback messages by default: a brief message flashes at the top of the mini cart when a shopper adds or updates a product, then disappears automatically. This how-to explains how those default messages work, then shows you how to build a custom overlay-style alternative that displays the same feedback centered over the mini cart with a semi-transparent background. ## What you'll edit or create - Edit the mini cart block — `blocks/commerce-mini-cart/commerce-mini-cart.js` (to add the overlay variant) - Edit the mini cart styles — `blocks/commerce-mini-cart/commerce-mini-cart.css` (to add overlay styles) - Create an overlay version of the `showMessage()` function - Reuse the existing message placeholders — `Global.MiniCartAddedMessage` / `Global.MiniCartUpdatedMessage` ## What you'll build By the end of this how-to, you'll have: - An understanding of the inline messages that already ship by default in `commerce-mini-cart.js`, flashing at the top of the mini cart for a few seconds after a cart update. - A custom overlay variant of the same messages, styled through `commerce-mini-cart.css`, that appears centered over the mini cart instead of inline. - Message text and display duration driven by the content placeholders sheet and the `cart/product/added` and `cart/product/updated` events, for either variant. Inline messages already appear at the top of the mini cart by default for a brief period (three seconds) and then automatically disappear, providing immediate feedback to users about their cart actions. ![Minicart inline message](https://experienceleague.adobe.com/en/tools/commerce-storefront/images/dropins/cart/minicart-inline.png) *Minicart inline message* If you'd rather show that same feedback as an overlay, build a custom variant that displays a message at the top center of the mini cart with a semi-transparent background instead: ![Minicart overlay message](https://experienceleague.adobe.com/en/tools/commerce-storefront/images/dropins/cart/minicart-overlay.png) *Minicart overlay message* You can customize the appearance and behavior of either variant by modifying the following: - **Message text**: Update the translations in the content placeholders sheet under the `Global.MiniCartAddedMessage` and `Global.MiniCartUpdatedMessage` keys. - **Message styling**: Modify the CSS classes in `commerce-mini-cart.css`. The styles use design tokens (prefixed with `--`) to maintain consistency with the design system. The overlay variant can be customized as follows: - Background opacity using the alpha value in the overlay's `background-color` (the sample below uses 50%) - Message position using the `top`, `left`, and `transform` properties - Colors, spacing, shadows, and other visual properties using design tokens - **Message position**: For inline messages, change where the message appears in the mini cart by modifying the insertion point in the DOM. - **Display duration**: Change the timeout value in the `showMessage` function (default is 3000ms). ## Prerequisites Before implementing inline messages, ensure you have: - Access to the content folder to manage message localization through placeholders. - Understanding of the design system tokens used in the Commerce boilerplate template. - The `commerce-mini-cart.css` file in your `blocks/commerce-mini-cart/` directory. ## Events The inline and overlay messages respond to two cart events: - `cart/product/added`: Triggered when products are added to the cart - `cart/product/updated`: Triggered when products in the cart are updated ## Implementation To add inline or overlay messages to your mini cart, follow these steps: > The **Inline messages** tab below walks through the mini cart's existing default implementation, included here for reference and comparison — if you're using the boilerplate as-is, this code is already in `commerce-mini-cart.js`. The **Overlay messages** tab is the new code you add as a custom alternative. ### 1. Retrieve translations for message texts using placeholders Get translations for custom messages from the content folder. ```javascript const placeholders = await fetchPlaceholders(); // Access the message texts from the Global placeholders sheet const MESSAGES = { ADDED: placeholders?.Global?.MiniCartAddedMessage, UPDATED: placeholders?.Global?.MiniCartUpdatedMessage, }; ``` ### 2. Create the appropriate message containers Inline messages require a container for the update message and a shadow wrapper to display the message. Overlay messages require an overlay container and a message container. ### Inline messages ```javascript // Create a container for the update message const updateMessage = document.createElement('div'); updateMessage.className = 'commerce-mini-cart__update-message'; // Create a shadow wrapper const shadowWrapper = document.createElement('div'); shadowWrapper.className = 'commerce-mini-cart__message-wrapper'; shadowWrapper.appendChild(updateMessage); ``` ### Overlay messages ```javascript // Create an overlay container const overlay = document.createElement('div'); overlay.className = 'commerce-mini-cart__overlay'; // Create a message container const messageContainer = document.createElement('div'); messageContainer.className = 'commerce-mini-cart__message'; overlay.appendChild(messageContainer); ``` ### 3. Create a function to show and hide messages Create a function that displays the message in the container and then hides it after a specified duration, such as three seconds. ### Inline messages ```javascript const showMessage = (message) => { updateMessage.textContent = message; updateMessage.classList.add('commerce-mini-cart__update-message--visible'); shadowWrapper.classList.add('commerce-mini-cart__message-wrapper--visible'); setTimeout(() => { updateMessage.classList.remove('commerce-mini-cart__update-message--visible'); shadowWrapper.classList.remove('commerce-mini-cart__message-wrapper--visible'); }, 3000); }; ``` ### Overlay messages ```javascript const showMessage = (message) => { messageContainer.textContent = message; overlay.classList.add('commerce-mini-cart__overlay--visible'); setTimeout(() => { overlay.classList.remove('commerce-mini-cart__overlay--visible'); }, 3000); }; ``` ### 4. Add event listeners for cart updates Listen for the `cart/product/added` and `cart/product/updated` events and display the appropriate message. ```javascript events.on('cart/product/added', () => showMessage(MESSAGES.ADDED), { eager: true, }); events.on('cart/product/updated', () => showMessage(MESSAGES.UPDATED), { eager: true, }); ``` ### 5. Insert the message container into the mini cart block Add the message container to the mini cart block to display the messages. ### Inline messages ```javascript // Find the products container and add the message div at the top const productsContainer = block.querySelector('.cart-mini-cart__products'); if (productsContainer) { productsContainer.insertBefore(shadowWrapper, productsContainer.firstChild); } else { console.info('Products container not found, appending message to block'); block.appendChild(shadowWrapper); } ``` ### Overlay messages ```javascript block.appendChild(overlay); ``` ### 6. Update the CSS styles Add styles to your `commerce-mini-cart.css` file. ### Inline messages ```css .commerce-mini-cart__update-message { display: none; font: var(--type-body-2-default-font); letter-spacing: var(--type-body-2-default-letter-spacing); } .commerce-mini-cart__message-wrapper { background-color: var(--color-positive-200); border-radius: var(--shape-border-radius-1); padding: var(--spacing-xsmall); display: none; margin-bottom: var(--spacing-small); } .commerce-mini-cart__message-wrapper--visible, .commerce-mini-cart__update-message--visible { display: block; } ``` ### Overlay messages ```css .commerce-mini-cart__overlay { background-color: rgb(0 0 0 / 50%); display: none; position: absolute; inset: 0; z-index: 1000; border-radius: var(--shape-border-radius-1); } .commerce-mini-cart__message { background-color: var(--color-positive-200); border-radius: var(--shape-border-radius-1); padding: var(--spacing-small); position: absolute; top: var(--spacing-medium); left: 50%; transform: translateX(-50%); font: var(--type-body-2-default-font); letter-spacing: var(--type-body-2-default-letter-spacing); box-shadow: var(--shape-shadow-3); width: 90%; max-width: 400px; text-align: center; } .commerce-mini-cart__overlay--visible { display: block; } ``` ## Complete example Here's a complete example of implementing inline and overlay messages in your `commerce-mini-cart.js` block file: ### Inline messages ```javascript // Initializers export default async function decorate(block) { const { 'start-shopping-url': startShoppingURL = '', 'cart-url': cartURL = '', 'checkout-url': checkoutURL = '', } = readBlockConfig(block); // Get translations for custom messages const placeholders = await fetchPlaceholders(); const MESSAGES = { ADDED: placeholders?.Global?.MiniCartAddedMessage, UPDATED: placeholders?.Global?.MiniCartUpdatedMessage, }; // Create a container for the update message const updateMessage = document.createElement('div'); updateMessage.className = 'commerce-mini-cart__update-message'; // Create shadow wrapper const shadowWrapper = document.createElement('div'); shadowWrapper.className = 'commerce-mini-cart__message-wrapper'; shadowWrapper.appendChild(updateMessage); const showMessage = (message) => { updateMessage.textContent = message; updateMessage.classList.add('commerce-mini-cart__update-message--visible'); shadowWrapper.classList.add('commerce-mini-cart__message-wrapper--visible'); setTimeout(() => { updateMessage.classList.remove('commerce-mini-cart__update-message--visible'); shadowWrapper.classList.remove('commerce-mini-cart__message-wrapper--visible'); }, 3000); }; // Add event listeners for cart updates events.on('cart/product/added', () => showMessage(MESSAGES.ADDED), { eager: true, }); events.on('cart/product/updated', () => showMessage(MESSAGES.UPDATED), { eager: true, }); block.innerHTML = ''; // Render MiniCart first await provider.render(MiniCart, { routeEmptyCartCTA: startShoppingURL ? () => rootLink(startShoppingURL) : undefined, routeCart: cartURL ? () => rootLink(cartURL) : undefined, routeCheckout: checkoutURL ? () => rootLink(checkoutURL) : undefined, routeProduct: (product) => rootLink(`/products/${product.url.urlKey}/${product.topLevelSku}`), })(block); // Find the products container and add the message div at the top const productsContainer = block.querySelector('.cart-mini-cart__products'); if (productsContainer) { productsContainer.insertBefore(shadowWrapper, productsContainer.firstChild); } else { console.info('Products container not found, appending message to block'); block.appendChild(shadowWrapper); } return block; } ``` ### Overlay messages ```javascript // Initializers export default async function decorate(block) { const { 'start-shopping-url': startShoppingURL = '', 'cart-url': cartURL = '', 'checkout-url': checkoutURL = '', } = readBlockConfig(block); // Get translations for custom messages const placeholders = await fetchPlaceholders(); const MESSAGES = { ADDED: placeholders?.Global?.MiniCartAddedMessage, UPDATED: placeholders?.Global?.MiniCartUpdatedMessage, }; block.innerHTML = ''; // Render MiniCart first await provider.render(MiniCart, { routeEmptyCartCTA: startShoppingURL ? () => rootLink(startShoppingURL) : undefined, routeCart: cartURL ? () => rootLink(cartURL) : undefined, routeCheckout: checkoutURL ? () => rootLink(checkoutURL) : undefined, routeProduct: (product) => rootLink(`/products/${product.url.urlKey}/${product.topLevelSku}`), })(block); // Create overlay container const overlay = document.createElement('div'); overlay.className = 'commerce-mini-cart__overlay'; // Create message container const messageContainer = document.createElement('div'); messageContainer.className = 'commerce-mini-cart__message'; overlay.appendChild(messageContainer); block.appendChild(overlay); const showMessage = (message) => { messageContainer.textContent = message; overlay.classList.add('commerce-mini-cart__overlay--visible'); setTimeout(() => { overlay.classList.remove('commerce-mini-cart__overlay--visible'); }, 3000); }; // Add event listeners for cart updates events.on('cart/product/added', () => showMessage(MESSAGES.ADDED), { eager: true, }); events.on('cart/product/updated', () => showMessage(MESSAGES.UPDATED), { eager: true, }); return block; } ``` And here's the accompanying CSS file (`commerce-mini-cart.css`): ### Inline messages ```css .commerce-mini-cart__update-message { display: none; font: var(--type-body-2-default-font); letter-spacing: var(--type-body-2-default-letter-spacing); } .commerce-mini-cart__message-wrapper { background-color: var(--color-positive-200); border-radius: var(--shape-border-radius-1); padding: var(--spacing-xsmall); display: none; margin-bottom: var(--spacing-small); } .commerce-mini-cart__message-wrapper--visible, .commerce-mini-cart__update-message--visible { display: block; } ``` ### Overlay messages ```css .commerce-mini-cart__overlay { background-color: rgb(0 0 0 / 50%); display: none; position: absolute; inset: 0; z-index: 1000; border-radius: var(--shape-border-radius-1); } .commerce-mini-cart__message { background-color: var(--color-positive-200); border-radius: var(--shape-border-radius-1); padding: var(--spacing-small); position: absolute; top: var(--spacing-medium); left: 50%; transform: translateX(-50%); font: var(--type-body-2-default-font); letter-spacing: var(--type-body-2-default-letter-spacing); box-shadow: var(--shape-shadow-3); width: 90%; max-width: 400px; text-align: center; } .commerce-mini-cart__overlay--visible { display: block; } ``` ## Related - [MiniCart container](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/cart/containers/mini-cart/) — reference for the mini cart you extended. - [Slots](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/cart/slots/) — the slot used to inject messages. --- # Add page metadata Set a page's title, description, or Commerce properties by adding a metadata table to its document. These settings apply to the page, rather than to one block or section. ## Before you begin Open your page in [Document Authoring](https://experienceleague.adobe.com/en/tools/commerce-storefront/merchants/quick-start/document-authoring/). Choose the properties you need from the [Page metadata reference](https://experienceleague.adobe.com/en/tools/commerce-storefront/merchants/blocks/page-metadata/), and check that you can preview and publish the page. ## Add the metadata table 1. Find the existing metadata table in your document. If there is none, insert a table with two columns above your Commerce blocks. 1. Merge the cells in the first row and enter `metadata`. 1. Add a row for each property. For example, enter `Title` in the first cell and your page title in the second cell. 1. Add a `Description` row with a short description of the page. Keep any existing properties the page needs, such as `sku` or `template`. 1. Save your changes, then preview the page and check its browser-tab title. 1. Publish the page when the title and page content look right. ## Check the result Open the published page and confirm that its browser-tab title matches the value you entered. Use the [metadata examples](https://experienceleague.adobe.com/en/tools/commerce-storefront/merchants/blocks/page-metadata/#examples) when you need social-sharing settings, Commerce properties, or a custom template. --- # Add a custom payment method ## What you'll edit or create - Edit the checkout block — `blocks/commerce-checkout/commerce-checkout.js` - Edit the `Methods` slot on the `PaymentMethods` container - Edit the `handlePlaceOrder` callback on the `PlaceOrder` container ## What you'll build By the end of this how-to, you'll have: - A Braintree handler added to the `Methods` slot of the [`PaymentMethods`](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/checkout/containers/payment-methods/) container in `commerce-checkout.js`, with `autoSync` set to `false`. - The Braintree drop-in UI rendered inline when a shopper selects Braintree during checkout. - Braintree payment processing wired into the `handlePlaceOrder` callback of the [`PlaceOrder`](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/checkout/containers/place-order/) container, so orders complete using the Braintree payment nonce. The Checkout drop-in component provides extensibility features for integrating third-party payment providers. Use slots to customize the list of payment methods shown during the checkout process. > **Supported payment providers** The Checkout drop-in supports Adyen payment methods (including Bancontact) and payment extensions in addition to the Braintree example below. See the [release notes](https://experienceleague.adobe.com/en/tools/commerce-storefront/releases/) for the latest supported providers. ## Step-by-step This how-to walks you through integrating Braintree as a payment provider with the Commerce boilerplate template. While we use Braintree as an example, you can adapt these same steps for other payment providers. ### 1. Prerequisites For this how-to, you must configure the Braintree extension on your Adobe Commerce backend before integrating it with the Commerce boilerplate template. The Braintree extension is bundled with Adobe Commerce and can be https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/payments/braintree in the Admin. If you choose to integrate with a different payment provider, consider the following: - The provider must be supported by Adobe Commerce. - The provider likely offers an extension that you must install and configure on your Adobe Commerce backend. ### 2. Add the Braintree client SDK To integrate the Braintree payment provider with the Commerce boilerplate template, you must add the Braintree client SDK to your project. ### HTML element Use the following `script` tag to add the Braintree client SDK to an HTML file. ```html ``` ### Import declaration Use the following `import` declaration to add the Braintree client SDK directly to the `commerce-checkout.js` block file. ```js import 'https://js.braintreegateway.com/web/dropin/1.43.0/js/dropin.min.js'; ``` ### 3. Define a custom handler 1. Create a `braintreeInstance` variable to manage the Braintree drop-in instance. ```js let braintreeInstance; ``` 1. Update the [`PaymentMethods`](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/checkout/containers/payment-methods/) container to include a custom handler for the Braintree payment method. Set `autoSync` to `false` to prevent automatic calls to the [`setPaymentMethod`](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/checkout/functions/#setpaymentmethod) function when the payment method changes. ```js CheckoutProvider.render(PaymentMethods, { slots: { Methods: { braintree: { autoSync: false, render: async (ctx) => { const container = document.createElement('div'); window.braintree.dropin.create({ authorization: 'sandbox_cstz6tw9_sbj9bzvx2ngq77n4', container, }, (err, dropinInstance) => { if (err) { console.error(err); } braintreeInstance = dropinInstance; }); ctx.replaceHTML(container); }, }, }, }, })($paymentMethods), ``` ### 4. Handle the payment method Implement the Braintree payment logic within the `handlePlaceOrder` handler of the [`PlaceOrder`](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/checkout/containers/place-order/) container. This involves processing the payment using the Braintree https://developer.paypal.com/braintree/docs/guides/payment-method-nonces. ```js CheckoutProvider.render(PlaceOrder, { handlePlaceOrder: async ({ cartId, code }) => { await displayOverlaySpinner(); try { switch (code) { case 'braintree': { braintreeInstance.requestPaymentMethod(async (err, payload) => { if (err) { removeOverlaySpinner(); console.error(err); return; } await checkoutApi.setPaymentMethod({ code: 'braintree', braintree: { is_active_payment_token_enabler: false, payment_method_nonce: payload.nonce, }, }); await orderApi.placeOrder(cartId); }); break; } default: { // Place order await orderApi.placeOrder(cartId); } } } catch (error) { console.error(error); throw error; } finally { await removeOverlaySpinner(); } }, })($placeOrder), ``` ## Related - [Checkout overview](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/checkout/) — where payment methods fit in the flow. - [Add a stored payment method](https://experienceleague.adobe.com/en/tools/commerce-storefront/how-tos/payment-services-my-account/) — a related payment task in the account area. --- # Add custom product lines to cart summary ## What you'll edit or create - Edit the cart block — `blocks/commerce-cart/commerce-cart.js` - Edit the `ProductAttributes` slot on the `CartSummaryList` container - Edit the `Footer` slot on the `CartSummaryList` container ## What you'll build By the end of this how-to, you'll have: - A `ProductAttributes` slot added to the `CartSummaryList` container in `blocks/commerce-cart/commerce-cart.js` that shows a custom product attribute, like shipping notes, on cart items that have it. - A `Footer` slot in the same container that displays promotional text from a cart price rule beneath qualifying items. ## Prerequisites This how-to requires that you create the following entities in the Adobe Commerce Admin: - A custom product attribute. Here, the product attribute is assigned the label `Shipping Notes`, and the **Catalog Input Type for Store Owner* is set to **Text Field**. You can optionally set the **Used for Sorting in Product Listing** option to **Yes** to increase the visibility of products using the product attribute in the Products grid. https://experienceleague.adobe.com/en/docs/commerce-admin/catalog/product-attributes/product-attributes describes how to create a custom product attribute. In addition, you must assign the product attribute to one or more products. In this how-to, the text fields will contain the strings "These item(s) are available to ship on Nov 1, 2024" and "FINAL SALE: This item ships separately and is ineligible for return.". - A custom cart price rule. In this how-to, a cart price rule named `25% Off $75+ with Code BOO24` has been created. Its definition defines the coupon code, the discount amount, and the conditions that must be met to apply the discount. https://experienceleague.adobe.com/en/docs/commerce-admin/marketing/promotions/cart-rules/price-rules-cart describes how to create a cart price rule. ## Step-by-step The following steps describe how to modify the https://github.com/hlxsites/aem-boilerplate-commerce/blob/main/blocks/commerce-cart/commerce-cart.js block file in the boilerplate template to add custom content to the `CartSummaryList` container. ### 1. Add text from a custom product attribute In this task, we'll add text that provides shipping information when certain conditions apply. For example, an item might be out of stock, and therefore cannot be shipped immediately. Or maybe the product is on clearance and cannot be returned. The `CartSummaryList` component is extended to display text defined by a merchant in the Admin using a custom product attribute. If the custom product attribute is not assigned to a product, then no additional information is displayed. The following images show how these custom lines can be rendered: ![Cart item with custom shipping notification](https://experienceleague.adobe.com/en/tools/commerce-storefront/images/dropins/cart/cart-product-line-shipping.png) ** ![Cart item with custom final sale notification](https://experienceleague.adobe.com/en/tools/commerce-storefront/images/dropins/cart/cart-product-line-final.png) ** 1. Open the `blocks/commerce-cart/commerce-cart.js` boilerplate file. This file imports the `CartSummaryList` container, and we want to use a slot to display the custom product attribute. Find the `provider.render(CartSummaryList, {` line in the file and insert a `ProductAttributes` slot with the following code: ```javascript slots: { ProductAttributes: (ctx) => { // Prepend Product Attributes const ProductAttributes = ctx.item?.productAttributes; ProductAttributes?.forEach((attr) => { if(attr.code === "shipping_notes") { if(attr.selected_options) { const selectedOptions = attr.selected_options .filter((option) => option.label.trim() !== '') .map((option) => option.label) .join(', '); if(selectedOptions) { const productAttribute = document.createElement('div'); productAttribute.innerText = `${attr.code}: ${selectedOptions}`; ctx.appendChild(productAttribute); } } else if (attr.value) { const productAttribute = document.createElement('div'); productAttribute.innerText = `${attr.code}: ${attr.value}`; ctx.appendChild(productAttribute); } } }) }, ``` This code creates a slot named `ProductAttributes` that displays the custom product attribute `Shipping Notes`, if it is assigned to a product. If the corresponding attribute is found, the slot creates a new `div` element and appends the attribute code and value to the element. The element is then appended to the `ctx` element, which is the product line in the cart summary. 1. Save the file and generate the page to see the changes. ### 2. Display promotional information in the footer of a cart item Now we'll add information defined in a custom cart price rule to the footer of the `CartSummaryList` container. If the conditions set in the cart price rules are not met, then no additional information is displayed. For example, if a specific coupon has not been applied or if the subtotal threshold has not been met, then this information is not displayed. ![Display coupon information](https://experienceleague.adobe.com/en/tools/commerce-storefront/images/dropins/cart/cart-product-line-footer.png) *Display coupon information* 1. The `Footer` slot only receives the current item in its context — cart-level information like the applied cart price rule's label lives on the cart itself, not on the item. Track the cart's `appliedDiscounts` in a variable your block updates whenever the cart changes: ```javascript import { events } from '@dropins/tools/event-bus.js'; let appliedDiscounts = []; events.on('cart/data', (data) => { appliedDiscounts = data?.appliedDiscounts ?? []; }, { eager: true }); ``` 1. Add a `Footer` slot beneath the `ProductAttributes` slot. ```javascript slots: { ProductAttributes: (ctx) => { ... } Footer: (ctx) => { // Runs on mount const wrapper = document.createElement('div'); ctx.appendChild(wrapper); // Append the cart's applied discount labels on every update, for items that qualify ctx.onChange((next) => { wrapper.innerHTML = ''; const itemQualifies = next.item?.discountPercentage || next.item?.savingsAmount; if (!itemQualifies) return; appliedDiscounts.forEach(({ label }) => { const discount = document.createElement('div'); discount.style.color = '#3d3d3d'; discount.innerText = label; wrapper.appendChild(discount); }); }); }, ``` This code creates a slot named `Footer`, which displays the promotional information defined in the custom cart price rule beneath items that received a discount. The `next.item?.discountPercentage` and `next.item?.savingsAmount` checks identify items the discount applied to; the label text itself comes from the cart's `appliedDiscounts`, not from the item. 1. Save the file. Add products that total at least $75 and apply the BOO24 coupon code to the cart. The page displays the rule name beneath each item in the cart. ## Related - [Slots](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/cart/slots/) — the slot mechanism this how-to builds on. - [Customize order summary lines](https://experienceleague.adobe.com/en/tools/commerce-storefront/how-tos/order-summary-lines/) — apply the same approach to the order summary. --- # Add supporting content to product pages Add a campaign banner, carousel, headings, or other supporting content above or below the shopping controls on your product pages. ## Before you begin Open `/products/default` in Document Authoring and choose a content block supported by your storefront. Use [Preview a product](https://experienceleague.adobe.com/en/tools/commerce-storefront/how-tos/preview-product/) to select a real product for the inline preview. > This document is shared by product pages. Content you add here appears on all pages that use this template. To show content only for selected products or categories, use [Show enrichment content for selected products or categories](https://experienceleague.adobe.com/en/tools/commerce-storefront/how-tos/show-enrichment-content/) instead. ## Add the supporting content 1. Find the `product-details` table in `/products/default`. 1. Place your content above or below that table, in the order shoppers should see it. You can insert ordinary text or images, or [add a supported block table](https://experienceleague.adobe.com/en/tools/commerce-storefront/how-tos/add-block-to-page/). For example, insert a carousel table above the product details: | carousel | | | --- | --- | | Your first campaign image | First slide caption | | Your second campaign image | Second slide caption | Merge the first-row cells in the document. Replace the image labels with actual images, and replace the captions with your text. Each row below the header is one slide. 1. Open the inline live-preview pane and confirm that the content appears in the intended position relative to the product details. 1. Preview another product to check that the content is appropriate for the shared template. 1. Publish the updated document when the layout looks right. ## Check the result Open a real product URL on the published storefront. Confirm that the supporting content appears in the expected order and that the product's shopping controls remain available. For recommendation units, follow [Add product recommendations to a page](https://experienceleague.adobe.com/en/tools/commerce-storefront/how-tos/add-product-recommendations/). --- # Integrate address verification ## What you'll edit or create - Create the autocomplete script from the sample — `blocks/commerce-checkout/address-autocomplete.js` - Edit the checkout block — `blocks/commerce-checkout/commerce-checkout.js` - Edit the `initializeCheckout()` function ## What you'll build By the end of this how-to, you'll have: - A copy of the `address-autocomplete.js` sample script (from `/public/samples/address-autocomplete.js`) added to your project and configured with your Google API key. - Google Places autocomplete wired into the shipping form in `commerce-checkout.js`, triggering suggestions as the shopper types a street address. - Selected suggestions that automatically fill in the street, city, country, and postal code fields. You might want to enhance the shopper experience by streamlining the process of populating and verifying the shipping address, thereby reducing the risk of user error. You can achieve this by implementing a third-party address lookup and autocomplete APIs, such as those provided by https://mapsplatform.google.com/maps-products/#places. This how-to describes how to override any field in a checkout address form and extend it to integrate with this service. The implementation supports backend-configurable validation and full form submission integration. Upon successful completion of this how-to, a form similar to the following will be displayed: ![Autocomplete shipping address](https://experienceleague.adobe.com/en/tools/commerce-storefront/images/dropins/checkout/address-lookup.png) *Autocomplete shipping address* ## Step-by-step The following steps describe how to integrate the Google Address Validation API with the Commerce boilerplate template using the provided address autocomplete implementation. ### 1. Prerequisites For this how-to, you must have a valid Google API key. https://developers.google.com/maps/documentation/javascript/get-api-key describes the process to obtain and set up this key. ### 2. Download and configure the address autocomplete implementation 1. **Download the implementation:** Copy the `address-autocomplete.js` file from `/public/samples/address-autocomplete.js` in this documentation repository to your project directory. 2. **Replace the API key placeholder:** Open the `address-autocomplete.js` file and replace `ADD-YOUR-GOOGLE-API-KEY-HERE` with your actual Google API key: ```javascript const CONFIG = { googleApiKey: 'YOUR_ACTUAL_GOOGLE_API_KEY', // ... rest of configuration }; ``` ### 3. Import and initialize the autocomplete service In your `commerce-checkout.js` file, make the following changes to enable address autocomplete: 1. **Import the autocomplete service:** ```javascript import { initializeAutocompleteWhenReady } from './address-autocomplete.js'; ``` 2. **Initialize the autocomplete in the `initializeCheckout` function:** ```javascript const initializeCheckout = async () => { // ... existing checkout initialization code ... // Initialize address autocomplete for shipping form const shippingContainer = document.querySelector('[data-commerce-checkout-shipping]'); if (shippingContainer) { initializeAutocompleteWhenReady(shippingContainer, 'input[name="street"]'); } // ... rest of initialization code ... }; ``` The `initializeAutocompleteWhenReady` function automatically: - Waits for the address form to be rendered - Attaches autocomplete functionality to the street input field - Handles form field population when an address is selected - Manages Google Maps API loading and initialization ## Example The complete address autocomplete implementation is available in `/public/samples/address-autocomplete.js`. This implementation includes: - **AddressAutocompleteService class**: Handles Google Places API integration - **initializeAutocompleteWhenReady function**: Utility function for easy integration - **Automatic form field population**: Populates street, city, country, and postal code fields - **Keyboard navigation**: Arrow keys, Enter, and Escape support - **Error handling**: Graceful fallback when Google Maps API is unavailable For additional customization options and advanced usage, see the implementation comments in the sample file. ## Related - [AddressValidation container](https://experienceleague.adobe.com/en/tools/commerce-storefront/dropins/checkout/containers/address-validation/) — reference for the validation UI. - [Validate a shipping address](https://experienceleague.adobe.com/en/tools/commerce-storefront/how-tos/validate-shipping-address/) — a related address task. --- # Apply section styling Apply a supported style to the section around your content or Commerce blocks. Section metadata changes the section's presentation; it does not change an individual block's settings. ## Before you begin Open the page in [Document Authoring](https://experienceleague.adobe.com/en/tools/commerce-storefront/merchants/quick-start/document-authoring/) and choose a style supported by your storefront. See [Section metadata](https://experienceleague.adobe.com/en/tools/commerce-storefront/merchants/blocks/section-metadata/) for the table format and supported property types. > Your developer defines the available styles. Adding a style name to a document does not create its styling. Ask your developer before using a new style. ## Add section metadata 1. Find the section containing the blocks you want to style. 1. Insert a two-column table below those blocks, within the same section. If a section-metadata table already exists, update it instead. 1. Merge the cells in the first row and enter `section-metadata`. 1. Add a row with `Style` in the first cell and the supported style name in the second cell. For example, use `highlight` only if your storefront supports that style. 1. Save your changes and preview the page. Confirm that the intended section has changed, not an adjacent section. 1. Publish the page when the result looks right. ## Check the result Open the published page and confirm that the style applies to the intended section. If nothing changes, check the style name with your developer and confirm that the metadata table is in the same section as the content. --- # Generate category pages programmatically ## What you'll edit or create - Create a Node.js generation script - Create the `product-list-page` block - Create the `metadata` block When you need more category pages than are practical to create with the [manual how-to](https://experienceleague.adobe.com/en/tools/commerce-storefront/how-tos/manual-category-page/), but the automated [AEM Commerce Prerender](https://experienceleague.adobe.com/en/tools/commerce-storefront/setup/configuration/aem-prerender/) solution doesn't fit your catalog's scale or requirements, you can generate the pages with a script. You produce the same category document — a `product-list-page` block plus a `metadata` block — but instead of authoring it in [Document Authoring](https://docs.da.live/), you build it from a template, push it to Document Authoring through its Source API, and publish the path through the Edge Delivery Services pipeline. This is a developer workflow: you write and run the script, and you own when it runs again. > **How this differs from prerendering** The [prerender solution](https://experienceleague.adobe.com/en/tools/commerce-storefront/setup/configuration/aem-prerender/) publishes category HTML as a [Bring Your Own Markup](https://www.aem.live/developer/byom) overlay and detects catalog changes for you. This approach instead creates a real, editable Document Authoring document, and there is no change detection — you re-run the script (or schedule it) to refresh a page. That trade is what lets you sidestep the prerender limits: you control the catalog query, the pagination, and the content that goes on each page. ## What you'll build By the end of this how-to, you'll have: - A Node.js script that fetches category data from Adobe Commerce and builds a Document Authoring source document containing a `product-list-page` block and a `metadata` block. - A call to the Document Authoring Source API that creates the document at your chosen path. - A call to the AEM Admin API that previews and publishes that path, making the category page live. ## Prerequisites Before starting this how-to, make sure you have: - A working [Commerce storefront](https://experienceleague.adobe.com/en/tools/commerce-storefront/get-started/create-storefront/) on Edge Delivery Services - Comfort writing and running a JavaScript (Node.js) script - Access to your Commerce catalog's GraphQL endpoint — Catalog Service (Adobe Commerce as a Cloud Service or PaaS) or the Merchandising Services API (Adobe Commerce Optimizer) - An Adobe IMS bearer token for the Document Authoring Source API and one for the AEM Admin API (see Step 4 for how to generate these) ## How it works Your script fetches category data from Adobe Commerce, builds a Document Authoring document from a template containing the `product-list-page` and `metadata` blocks, then publishes it through the Document Authoring and Edge Delivery Services APIs — the same preview/publish pipeline the rest of your site uses. The tasks below walk through each part in detail. Re-run the script whenever the category or its metadata changes. The output is a normal Document Authoring document, so an author can still open and edit it afterward. ### 1. Step 1: Fetch category data from Commerce Query your Commerce backend for the category and the products you want to list. Use your own query — `YOUR_CATALOG_QUERY` below stands in for it. ```js // Adobe Commerce as a Cloud Service / PaaS + Catalog Service: post to your Catalog Service GraphQL endpoint. // Adobe Commerce Optimizer: post to your Merchandising GraphQL endpoint with your Optimizer headers. const response = await fetch(CATALOG_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', // ...your store/context headers... }, body: JSON.stringify({ query: YOUR_CATALOG_QUERY, variables: { categoryPath }, }), }); const { data } = await response.json(); ``` For ready-made category queries — fetching categories, the category tree, and product listing data by category path — see the queries in the https://github.com/adobe-rnd/aem-commerce-prerender/blob/main/actions/queries.js (`actions/queries.js` on `main`), such as `CategoriesQuery`, `CategoryTreeQuery`, `CategoryTreeBySlugsQuery`, and `PlpProductSearchQuery`. For the schema each backend exposes, see the https://developer.adobe.com/commerce/webapi/graphql/schema/catalog-service/ (Adobe Commerce as a Cloud Service and PaaS) or the https://developer.adobe.com/commerce/services/optimizer/merchandising-services/ (Adobe Commerce Optimizer). ### 2. Step 2: Build the document from a template Render the category data into a Document Authoring source document. A DA source document is HTML with a specific shape: a `` holding `
`, `
`, and `