{"id":264,"date":"2026-03-20T18:23:18","date_gmt":"2026-03-20T18:23:18","guid":{"rendered":"https:\/\/magendoo.ro\/insights\/?p=264"},"modified":"2026-03-20T18:23:18","modified_gmt":"2026-03-20T18:23:18","slug":"customizing-the-aem-venia-storefront-adding-a-loyalty-banner-to-order-history","status":"publish","type":"post","link":"https:\/\/magendoo.ro\/insights\/customizing-the-aem-venia-storefront-adding-a-loyalty-banner-to-order-history\/","title":{"rendered":"Customizing the AEM Venia Storefront: Adding a Loyalty Banner to Order History"},"content":{"rendered":"\n<p><\/p>\n\n\n\n<p>How to extend the AEM CIF Venia reference storefront with a custom React component, build it with Webpack, and deploy it to a local AEM instance.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"background\">Background<\/h2>\n\n\n\n<p>The&nbsp;<a href=\"https:\/\/github.com\/adobe\/aem-cif-guides-venia\">AEM CIF Guides Venia<\/a>&nbsp;project is Adobe\u2019s reference storefront for integrating AEM (Adobe Experience Manager) with Magento\/Adobe Commerce via the Commerce Integration Framework (CIF). It uses a React-based frontend built on top of Magento\u2019s Peregrine and Venia UI libraries, bundled as AEM clientlibs via Webpack.<\/p>\n\n\n\n<p>In this article we walk through adding a \u201cLoyalty Club\u201d banner to the Order History page \u2013 a common real-world scenario where marketing wants to surface a promotion inside a customer\u2019s account area. The process covers the full development loop: branching, component creation, build, and deployment.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"prerequisites\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>AEM 6.5 or AEM as a Cloud Service SDK running locally on\u00a0<code>localhost:4502<\/code><\/li>\n\n\n\n<li>The Venia reference storefront project cloned and deployed<\/li>\n\n\n\n<li>A Magento 2.4.x backend with GraphQL enabled<\/li>\n\n\n\n<li>Node.js 16.x (the project\u2019s Webpack 4 and sass-loader 7 toolchain requires it)<\/li>\n\n\n\n<li>Maven 3.6+<\/li>\n\n\n\n<li>Java 11<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"project-structure\">Project Structure<\/h2>\n\n\n\n<p>Understanding the project layout is essential before making changes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>aem-cif-guides-venia\/\n  ui.frontend\/          # React source code, Webpack config, npm build\n    src\/main\/\n      components\/       # React components (what we edit)\n      site\/             # Entry point (main.js), SCSS\n    dist\/               # Webpack output (not versioned)\n    webpack.common.js   # Webpack config\n    package.json\n  ui.apps\/              # AEM components, clientlibs, templates\n    src\/main\/content\/jcr_root\/apps\/venia\/\n      clientlibs\/       # Built JS\/CSS land here after npm run prod\n      components\/       # AEM HTL components\n  ui.content\/           # AEM page content, experience fragments\n    src\/main\/content\/jcr_root\/content\/venia\/\n      us\/en\/my-account\/order-history\/   # The page we're targeting\n  core\/                 # Java OSGi bundles (Sling Models, servlets)\n  all\/                  # Aggregate content package for deployment<\/code><\/pre>\n\n\n\n<p><strong>What gets versioned in git:<\/strong>&nbsp;Everything under&nbsp;<code>src\/<\/code>&nbsp;directories,&nbsp;<code>pom.xml<\/code>&nbsp;files, Webpack configs,&nbsp;<code>package.json<\/code>. The&nbsp;<code>.gitignore<\/code>&nbsp;already excludes&nbsp;<code>node_modules\/<\/code>,&nbsp;<code>target\/<\/code>,&nbsp;<code>dist\/<\/code>, and&nbsp;<code>build\/<\/code>.<\/p>\n\n\n\n<p><strong>Build flow:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ui.frontend\/src\/ --&#91;webpack]--&gt; ui.frontend\/dist\/ --&#91;clientlib]--&gt; ui.apps\/clientlibs\/<\/code><\/pre>\n\n\n\n<p>The&nbsp;<code>clientlib<\/code>&nbsp;step (via&nbsp;<code>aem-clientlib-generator<\/code>) copies the Webpack output into&nbsp;<code>ui.apps<\/code>&nbsp;in the format AEM expects, complete with&nbsp;<code>js.txt<\/code>&nbsp;and&nbsp;<code>css.txt<\/code>&nbsp;manifest files.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-1-create-a-feature-branch\">Step 1: Create a Feature Branch<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>cd aem-cif-guides-venia\ngit checkout -b feature\/loyalty-club-banner<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-2-create-the-loyaltybanner-component\">Step 2: Create the LoyaltyBanner Component<\/h2>\n\n\n\n<p>The Order History page lives at:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ui.frontend\/src\/main\/components\/OrderHistoryPage\/\n  orderHistoryPage.js   # Main page component\n  orderRow.js           # Individual order row\n  OrderDetails\/         # Order detail expansion\n  index.js              # Barrel export<\/code><\/pre>\n\n\n\n<p>We create a new&nbsp;<code>LoyaltyBanner<\/code>&nbsp;subdirectory following the same conventions as existing components.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"loyaltybannerloyaltybanner.css\"><code>LoyaltyBanner\/loyaltyBanner.css<\/code><\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>.root {\n    margin-top: 24px;\n    padding: 20px;\n    background-color: #f5f0ff;\n    border-radius: 8px;\n    border: 1px solid #7c3aed;\n}\n\n.heading {\n    margin: 0 0 8px;\n    font-size: 18px;\n    color: #5b21b6;\n}\n\n.description {\n    margin: 0;\n    font-size: 14px;\n    color: #374151;\n}\n\n.link {\n    color: #7c3aed;\n    font-weight: 600;\n    text-decoration: underline;\n}\n\n.link:hover {\n    color: #5b21b6;\n}<\/code><\/pre>\n\n\n\n<p>The CSS file uses CSS Modules \u2013 Webpack is configured to scope class names with the pattern&nbsp;<code>cmp-Venia[folder]__[name]__[local]<\/code>, so&nbsp;<code>.root<\/code>&nbsp;becomes something like&nbsp;<code>cmp-VeniaLoyaltyBanner__loyaltyBanner__root<\/code>&nbsp;at runtime. No global style collisions.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"loyaltybannerloyaltybanner.js\"><code>LoyaltyBanner\/loyaltyBanner.js<\/code><\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>import React from 'react';\nimport { FormattedMessage } from 'react-intl';\nimport { useStyle } from '@magento\/venia-ui\/lib\/classify';\nimport defaultClasses from '.\/loyaltyBanner.css';\n\nconst LoyaltyBanner = props =&gt; {\n    const classes = useStyle(defaultClasses, props.classes);\n\n    return (\n        &lt;div className={classes.root}&gt;\n            &lt;h3 className={classes.heading}&gt;\n                &lt;FormattedMessage\n                    id=\"loyaltyBanner.heading\"\n                    defaultMessage=\"Loyalty Club\"\n                \/&gt;\n            &lt;\/h3&gt;\n            &lt;p className={classes.description}&gt;\n                &lt;FormattedMessage\n                    id=\"loyaltyBanner.description\"\n                    defaultMessage=\"Join our loyalty program and earn points on every order! \"\n                \/&gt;\n                &lt;a className={classes.link} href=\"\/content\/venia\/us\/en\/loyalty\"&gt;\n                    &lt;FormattedMessage\n                        id=\"loyaltyBanner.learnMore\"\n                        defaultMessage=\"Learn more\"\n                    \/&gt;\n                &lt;\/a&gt;\n            &lt;\/p&gt;\n        &lt;\/div&gt;\n    );\n};\n\nexport default LoyaltyBanner;<\/code><\/pre>\n\n\n\n<p>Key patterns used here:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>useStyle<\/code><\/strong>\u00a0from Venia UI merges default CSS classes with any overrides passed via props, enabling style customization from parent components<\/li>\n\n\n\n<li><strong><code>FormattedMessage<\/code><\/strong>\u00a0from\u00a0<code>react-intl<\/code>\u00a0makes the text translatable. The\u00a0<code>id<\/code>\u00a0values are automatically extracted during the build (<code>npm run i18n:extract-compile<\/code>) into\u00a0<code>i18n\/en.json<\/code><\/li>\n\n\n\n<li><strong>Props-based\u00a0<code>classes<\/code>\u00a0override<\/strong>\u00a0is standard Venia convention, allowing consumers to restyle without forking<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"loyaltybannerindex.js\"><code>LoyaltyBanner\/index.js<\/code><\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>export { default } from '.\/loyaltyBanner';<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-3-wire-it-into-the-order-history-page\">Step 3: Wire It Into the Order History Page<\/h2>\n\n\n\n<p>Edit&nbsp;<code>orderHistoryPage.js<\/code>&nbsp;to import and render the banner:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code> import OrderRow from '.\/orderRow';\n import ResetButton from '@magento\/venia-ui\/lib\/components\/OrderHistoryPage\/resetButton';\n+import LoyaltyBanner from '.\/LoyaltyBanner';<\/code><\/pre>\n\n\n\n<p>Then in the JSX return, add&nbsp;<code>&lt;LoyaltyBanner \/&gt;<\/code>&nbsp;between the order list and the \u201cLoad More\u201d button:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>                 {pageContents}\n+                &lt;LoyaltyBanner \/&gt;\n                 {loadMoreButton}<\/code><\/pre>\n\n\n\n<p>That\u2019s the entire code change. Three new files and two lines modified.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-4-build\">Step 4: Build<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"node.js-version\">Node.js Version<\/h3>\n\n\n\n<p>This project requires&nbsp;<strong>Node.js 16.x<\/strong>. The Webpack 4 toolchain uses&nbsp;<code>node-sass<\/code>&nbsp;(native bindings) and older OpenSSL APIs that are not compatible with Node 18+.<\/p>\n\n\n\n<p>If you\u2019re on a newer Node version, use a version manager:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Using Homebrew (macOS)\nbrew install node@16\nPATH=\"\/opt\/homebrew\/opt\/node@16\/bin:$PATH\" npm run prod\n\n# Using nvm\nnvm use 16\nnpm run prod\n\n# Using volta\nvolta pin node@16\nnpm run prod<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"replacing-node-sass-with-dart-sass-recommended\">Replacing node-sass with Dart Sass (Recommended)<\/h3>\n\n\n\n<p>If&nbsp;<code>node-sass<\/code>&nbsp;fails to build on your platform (common on Apple Silicon), replace it with&nbsp;<code>sass<\/code>&nbsp;(Dart Sass), which is a pure JavaScript implementation:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm uninstall node-sass\nnpm install sass@1.32 --save-dev --legacy-peer-deps<\/code><\/pre>\n\n\n\n<p>Then tell&nbsp;<code>sass-loader<\/code>&nbsp;to use it in&nbsp;<code>webpack.common.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code> {\n     loader: 'sass-loader',\n     options: {\n+        implementation: require('sass'),\n         url: false\n     }\n },<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"run-the-build\">Run the Build<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>cd ui.frontend\nNODE_OPTIONS=--openssl-legacy-provider npm run prod<\/code><\/pre>\n\n\n\n<p>The&nbsp;<code>NODE_OPTIONS=--openssl-legacy-provider<\/code>&nbsp;flag is needed if your Node.js version uses OpenSSL 3.x (Node 16.17+).<\/p>\n\n\n\n<p>The build does three things:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Extract and compile i18n strings<\/strong>\u00a0\u2013 scans all\u00a0<code>FormattedMessage<\/code>\u00a0and\u00a0<code>formatMessage<\/code>\u00a0calls, generates\u00a0<code>i18n\/en.json<\/code><\/li>\n\n\n\n<li><strong>Webpack production build<\/strong>\u00a0\u2013 bundles JS, processes CSS Modules, splits chunks<\/li>\n\n\n\n<li><strong>Clientlib generation<\/strong>\u00a0\u2013 copies output from\u00a0<code>dist\/<\/code>\u00a0into\u00a0<code>ui.apps\/src\/main\/content\/jcr_root\/apps\/venia\/clientlibs\/clientlib-site\/<\/code><\/li>\n<\/ol>\n\n\n\n<p>A successful build ends with lines like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>copy: clientlib-site\/site.js \u2192 ...\/clientlibs\/clientlib-site\/js\/site.js\ncopy: clientlib-site\/styles.css \u2192 ...\/clientlibs\/clientlib-site\/css\/styles.css<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-5-deploy-to-aem\">Step 5: Deploy to AEM<\/h2>\n\n\n\n<p>The Maven build packages&nbsp;<code>ui.apps<\/code>&nbsp;(which now contains the updated clientlib) into an AEM content package and installs it via the CRX Package Manager API:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd aem-cif-guides-venia\nmvn clean install -pl ui.apps -am -PautoInstallPackage -DskipTests<\/code><\/pre>\n\n\n\n<p>Flags explained:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Flag<\/th><th>Purpose<\/th><\/tr><\/thead><tbody><tr><td><code>-pl ui.apps<\/code><\/td><td>Build only the&nbsp;<code>ui.apps<\/code>&nbsp;module<\/td><\/tr><tr><td><code>-am<\/code><\/td><td>Also make (build) dependencies (<code>core<\/code>,&nbsp;<code>ui.frontend<\/code>)<\/td><\/tr><tr><td><code>-PautoInstallPackage<\/code><\/td><td>Activate the profile that POSTs the package to&nbsp;<code>localhost:4502<\/code><\/td><\/tr><tr><td><code>-DskipTests<\/code><\/td><td>Skip unit tests for faster iteration<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>The&nbsp;<code>autoInstallPackage<\/code>&nbsp;profile is defined in the root&nbsp;<code>pom.xml<\/code>&nbsp;and defaults to&nbsp;<code>http:\/\/localhost:4502<\/code>&nbsp;with credentials&nbsp;<code>admin:admin<\/code>.<\/p>\n\n\n\n<p>After deployment, hard-refresh the page (<strong>Cmd+Shift+R<\/strong>):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>http:&#47;&#47;localhost:4502\/content\/venia\/us\/en\/my-account\/order-history.html<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-6-commit\">Step 6: Commit<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>git add ui.frontend\/src\/main\/components\/OrderHistoryPage\/LoyaltyBanner\/\ngit add ui.frontend\/src\/main\/components\/OrderHistoryPage\/orderHistoryPage.js\ngit add ui.frontend\/webpack.common.js\ngit add ui.frontend\/package.json ui.frontend\/package-lock.json\ngit add ui.frontend\/i18n\/en.json\n\ngit commit -m \"Add loyalty club banner to order history page\n\nIntroduce LoyaltyBanner React component rendered below the order list\non the my-account order history page. Uses CSS Modules and react-intl\nfor styling and i18n. Also migrates from node-sass to dart sass for\nApple Silicon compatibility.\"<\/code><\/pre>\n\n\n\n<p>Note that we&nbsp;<strong>do not<\/strong>&nbsp;commit&nbsp;<code>ui.apps\/clientlibs\/<\/code>&nbsp;build output,&nbsp;<code>node_modules\/<\/code>,&nbsp;<code>dist\/<\/code>, or&nbsp;<code>target\/<\/code>&nbsp;\u2013 these are all regenerated during CI\/CD builds.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"alternative-approaches\">Alternative Approaches<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"content-driven-experience-fragment\">Content-Driven (Experience Fragment)<\/h3>\n\n\n\n<p>Instead of hardcoding the banner in React, you can add it as AEM-authored content in the page\u2019s&nbsp;<code>.content.xml<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;loyalty_banner\n    jcr:primaryType=\"nt:unstructured\"\n    sling:resourceType=\"core\/wcm\/components\/text\/v2\/text\"\n    text=\"&lt;h3&gt;Loyalty Club&lt;\/h3&gt;&lt;p&gt;Join our loyalty program!&lt;\/p&gt;\"\n    textIsRich=\"true\"\/&gt;<\/code><\/pre>\n\n\n\n<p>Note: In the actual&nbsp;<code>.content.xml<\/code>&nbsp;file, the angle brackets inside the&nbsp;<code>text<\/code>&nbsp;attribute value must be XML-escaped (<code>&amp;lt;<\/code>&nbsp;and&nbsp;<code>&amp;gt;<\/code>). The unescaped version is shown here for readability.<\/p>\n\n\n\n<p>Or reference an Experience Fragment. This lets content authors update the message without a code deployment. Best for production; the React approach is faster for a POC.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hybrid-react-component-that-fetches-a-content-fragment\">Hybrid: React Component That Fetches a Content Fragment<\/h3>\n\n\n\n<p>For maximum flexibility, create a React component that fetches an AEM Content Fragment via the AEM Content Fragment API and renders it. This gives you React\u2019s component model with AEM\u2019s authoring flexibility.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"summary\">Summary<\/h2>\n\n\n\n<p>The development loop for customizing the AEM Venia storefront is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Branch \u2192 Edit React in ui.frontend\/src \u2192 npm run prod \u2192 mvn install \u2192 Verify \u2192 Commit<\/code><\/pre>\n\n\n\n<p>The key insight is that&nbsp;<code>ui.frontend<\/code>&nbsp;is a standard React\/Webpack project that happens to output its build artifacts as AEM clientlibs. If you know React, you already know how to build CIF components \u2013 the AEM-specific parts are just packaging and deployment.<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>How to extend the AEM CIF Venia reference storefront with a custom React component, build it with Webpack, and deploy it to a local AEM instance. Background The&nbsp;AEM CIF Guides Venia&nbsp;project is Adobe\u2019s reference storefront for integrating AEM (Adobe Experience Manager) with Magento\/Adobe Commerce via the Commerce Integration Framework (CIF). It uses a React-based frontend [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":265,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"site-container-style":"default","site-container-layout":"default","site-sidebar-layout":"default","disable-article-header":"default","disable-site-header":"default","disable-site-footer":"default","disable-content-area-spacing":"default","footnotes":""},"categories":[32,9],"tags":[],"class_list":["post-264","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-aem","category-magento-2"],"_links":{"self":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts\/264","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/comments?post=264"}],"version-history":[{"count":1,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts\/264\/revisions"}],"predecessor-version":[{"id":266,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/posts\/264\/revisions\/266"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/media\/265"}],"wp:attachment":[{"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/media?parent=264"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/categories?post=264"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/magendoo.ro\/insights\/wp-json\/wp\/v2\/tags?post=264"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}