This is page 59 of 61. Use http://codebase.md/taurgis/sfcc-dev-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .DS_Store ├── .github │ ├── dependabot.yml │ ├── instructions │ │ ├── mcp-node-tests.instructions.md │ │ └── mcp-yml-tests.instructions.md │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── documentation.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bug_fix.md │ │ ├── documentation.md │ │ └── new_tool.md │ ├── pull_request_template.md │ └── workflows │ ├── ci.yml │ ├── deploy-pages.yml │ ├── publish.yml │ └── update-docs.yml ├── .gitignore ├── .husky │ └── pre-commit ├── aegis.config.docs-only.json ├── aegis.config.json ├── aegis.config.with-dw.json ├── AGENTS.md ├── ai-instructions │ ├── claude-desktop │ │ └── claude_custom_instructions.md │ ├── cursor │ │ └── .cursor │ │ └── rules │ │ ├── debugging-workflows.mdc │ │ ├── hooks-development.mdc │ │ ├── isml-templates.mdc │ │ ├── job-framework.mdc │ │ ├── performance-optimization.mdc │ │ ├── scapi-endpoints.mdc │ │ ├── security-patterns.mdc │ │ ├── sfcc-development.mdc │ │ ├── sfra-controllers.mdc │ │ ├── sfra-models.mdc │ │ ├── system-objects.mdc │ │ └── testing-patterns.mdc │ └── github-copilot │ └── copilot-instructions.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── docs │ ├── best-practices │ │ ├── cartridge_creation.md │ │ ├── isml_templates.md │ │ ├── job_framework.md │ │ ├── localserviceregistry.md │ │ ├── ocapi_hooks.md │ │ ├── performance.md │ │ ├── scapi_custom_endpoint.md │ │ ├── scapi_hooks.md │ │ ├── security.md │ │ ├── sfra_client_side_js.md │ │ ├── sfra_controllers.md │ │ ├── sfra_models.md │ │ └── sfra_scss.md │ ├── dw_campaign │ │ ├── ABTest.md │ │ ├── ABTestMgr.md │ │ ├── ABTestSegment.md │ │ ├── AmountDiscount.md │ │ ├── ApproachingDiscount.md │ │ ├── BonusChoiceDiscount.md │ │ ├── BonusDiscount.md │ │ ├── Campaign.md │ │ ├── CampaignMgr.md │ │ ├── CampaignStatusCodes.md │ │ ├── Coupon.md │ │ ├── CouponMgr.md │ │ ├── CouponRedemption.md │ │ ├── CouponStatusCodes.md │ │ ├── Discount.md │ │ ├── DiscountPlan.md │ │ ├── FixedPriceDiscount.md │ │ ├── FixedPriceShippingDiscount.md │ │ ├── FreeDiscount.md │ │ ├── FreeShippingDiscount.md │ │ ├── PercentageDiscount.md │ │ ├── PercentageOptionDiscount.md │ │ ├── PriceBookPriceDiscount.md │ │ ├── Promotion.md │ │ ├── PromotionMgr.md │ │ ├── PromotionPlan.md │ │ ├── SlotContent.md │ │ ├── SourceCodeGroup.md │ │ ├── SourceCodeInfo.md │ │ ├── SourceCodeStatusCodes.md │ │ └── TotalFixedPriceDiscount.md │ ├── dw_catalog │ │ ├── Catalog.md │ │ ├── CatalogMgr.md │ │ ├── Category.md │ │ ├── CategoryAssignment.md │ │ ├── CategoryLink.md │ │ ├── PriceBook.md │ │ ├── PriceBookMgr.md │ │ ├── Product.md │ │ ├── ProductActiveData.md │ │ ├── ProductAttributeModel.md │ │ ├── ProductAvailabilityLevels.md │ │ ├── ProductAvailabilityModel.md │ │ ├── ProductInventoryList.md │ │ ├── ProductInventoryMgr.md │ │ ├── ProductInventoryRecord.md │ │ ├── ProductLink.md │ │ ├── ProductMgr.md │ │ ├── ProductOption.md │ │ ├── ProductOptionModel.md │ │ ├── ProductOptionValue.md │ │ ├── ProductPriceInfo.md │ │ ├── ProductPriceModel.md │ │ ├── ProductPriceTable.md │ │ ├── ProductSearchHit.md │ │ ├── ProductSearchModel.md │ │ ├── ProductSearchRefinementDefinition.md │ │ ├── ProductSearchRefinements.md │ │ ├── ProductSearchRefinementValue.md │ │ ├── ProductVariationAttribute.md │ │ ├── ProductVariationAttributeValue.md │ │ ├── ProductVariationModel.md │ │ ├── Recommendation.md │ │ ├── SearchModel.md │ │ ├── SearchRefinementDefinition.md │ │ ├── SearchRefinements.md │ │ ├── SearchRefinementValue.md │ │ ├── SortingOption.md │ │ ├── SortingRule.md │ │ ├── Store.md │ │ ├── StoreGroup.md │ │ ├── StoreInventoryFilter.md │ │ ├── StoreInventoryFilterValue.md │ │ ├── StoreMgr.md │ │ ├── Variant.md │ │ └── VariationGroup.md │ ├── dw_content │ │ ├── Content.md │ │ ├── ContentMgr.md │ │ ├── ContentSearchModel.md │ │ ├── ContentSearchRefinementDefinition.md │ │ ├── ContentSearchRefinements.md │ │ ├── ContentSearchRefinementValue.md │ │ ├── Folder.md │ │ ├── Library.md │ │ ├── MarkupText.md │ │ └── MediaFile.md │ ├── dw_crypto │ │ ├── CertificateRef.md │ │ ├── CertificateUtils.md │ │ ├── Cipher.md │ │ ├── Encoding.md │ │ ├── JWE.md │ │ ├── JWEHeader.md │ │ ├── JWS.md │ │ ├── JWSHeader.md │ │ ├── KeyRef.md │ │ ├── Mac.md │ │ ├── MessageDigest.md │ │ ├── SecureRandom.md │ │ ├── Signature.md │ │ ├── WeakCipher.md │ │ ├── WeakMac.md │ │ ├── WeakMessageDigest.md │ │ ├── WeakSignature.md │ │ └── X509Certificate.md │ ├── dw_customer │ │ ├── AddressBook.md │ │ ├── AgentUserMgr.md │ │ ├── AgentUserStatusCodes.md │ │ ├── AuthenticationStatus.md │ │ ├── Credentials.md │ │ ├── Customer.md │ │ ├── CustomerActiveData.md │ │ ├── CustomerAddress.md │ │ ├── CustomerCDPData.md │ │ ├── CustomerContextMgr.md │ │ ├── CustomerGroup.md │ │ ├── CustomerList.md │ │ ├── CustomerMgr.md │ │ ├── CustomerPasswordConstraints.md │ │ ├── CustomerPaymentInstrument.md │ │ ├── CustomerStatusCodes.md │ │ ├── EncryptedObject.md │ │ ├── ExternalProfile.md │ │ ├── OrderHistory.md │ │ ├── ProductList.md │ │ ├── ProductListItem.md │ │ ├── ProductListItemPurchase.md │ │ ├── ProductListMgr.md │ │ ├── ProductListRegistrant.md │ │ ├── Profile.md │ │ └── Wallet.md │ ├── dw_extensions.applepay │ │ ├── ApplePayHookResult.md │ │ └── ApplePayHooks.md │ ├── dw_extensions.facebook │ │ ├── FacebookFeedHooks.md │ │ └── FacebookProduct.md │ ├── dw_extensions.paymentrequest │ │ ├── PaymentRequestHookResult.md │ │ └── PaymentRequestHooks.md │ ├── dw_extensions.payments │ │ ├── SalesforceBancontactPaymentDetails.md │ │ ├── SalesforceCardPaymentDetails.md │ │ ├── SalesforceEpsPaymentDetails.md │ │ ├── SalesforceIdealPaymentDetails.md │ │ ├── SalesforceKlarnaPaymentDetails.md │ │ ├── SalesforcePaymentDetails.md │ │ ├── SalesforcePaymentIntent.md │ │ ├── SalesforcePaymentMethod.md │ │ ├── SalesforcePaymentRequest.md │ │ ├── SalesforcePaymentsHooks.md │ │ ├── SalesforcePaymentsMgr.md │ │ ├── SalesforcePaymentsSiteConfiguration.md │ │ ├── SalesforcePayPalOrder.md │ │ ├── SalesforcePayPalOrderAddress.md │ │ ├── SalesforcePayPalOrderPayer.md │ │ ├── SalesforcePayPalPaymentDetails.md │ │ ├── SalesforceSepaDebitPaymentDetails.md │ │ └── SalesforceVenmoPaymentDetails.md │ ├── dw_extensions.pinterest │ │ ├── PinterestAvailability.md │ │ ├── PinterestFeedHooks.md │ │ ├── PinterestOrder.md │ │ ├── PinterestOrderHooks.md │ │ └── PinterestProduct.md │ ├── dw_io │ │ ├── CSVStreamReader.md │ │ ├── CSVStreamWriter.md │ │ ├── File.md │ │ ├── FileReader.md │ │ ├── FileWriter.md │ │ ├── InputStream.md │ │ ├── OutputStream.md │ │ ├── PrintWriter.md │ │ ├── RandomAccessFileReader.md │ │ ├── Reader.md │ │ ├── StringWriter.md │ │ ├── Writer.md │ │ ├── XMLIndentingStreamWriter.md │ │ ├── XMLStreamConstants.md │ │ ├── XMLStreamReader.md │ │ └── XMLStreamWriter.md │ ├── dw_job │ │ ├── JobExecution.md │ │ └── JobStepExecution.md │ ├── dw_net │ │ ├── FTPClient.md │ │ ├── FTPFileInfo.md │ │ ├── HTTPClient.md │ │ ├── HTTPRequestPart.md │ │ ├── Mail.md │ │ ├── SFTPClient.md │ │ ├── SFTPFileInfo.md │ │ ├── WebDAVClient.md │ │ └── WebDAVFileInfo.md │ ├── dw_object │ │ ├── ActiveData.md │ │ ├── CustomAttributes.md │ │ ├── CustomObject.md │ │ ├── CustomObjectMgr.md │ │ ├── Extensible.md │ │ ├── ExtensibleObject.md │ │ ├── Note.md │ │ ├── ObjectAttributeDefinition.md │ │ ├── ObjectAttributeGroup.md │ │ ├── ObjectAttributeValueDefinition.md │ │ ├── ObjectTypeDefinition.md │ │ ├── PersistentObject.md │ │ ├── SimpleExtensible.md │ │ └── SystemObjectMgr.md │ ├── dw_order │ │ ├── AbstractItem.md │ │ ├── AbstractItemCtnr.md │ │ ├── Appeasement.md │ │ ├── AppeasementItem.md │ │ ├── Basket.md │ │ ├── BasketMgr.md │ │ ├── BonusDiscountLineItem.md │ │ ├── CouponLineItem.md │ │ ├── CreateAgentBasketLimitExceededException.md │ │ ├── CreateBasketFromOrderException.md │ │ ├── CreateCouponLineItemException.md │ │ ├── CreateOrderException.md │ │ ├── CreateTemporaryBasketLimitExceededException.md │ │ ├── GiftCertificate.md │ │ ├── GiftCertificateLineItem.md │ │ ├── GiftCertificateMgr.md │ │ ├── GiftCertificateStatusCodes.md │ │ ├── Invoice.md │ │ ├── InvoiceItem.md │ │ ├── LineItem.md │ │ ├── LineItemCtnr.md │ │ ├── Order.md │ │ ├── OrderAddress.md │ │ ├── OrderItem.md │ │ ├── OrderMgr.md │ │ ├── OrderPaymentInstrument.md │ │ ├── OrderProcessStatusCodes.md │ │ ├── PaymentCard.md │ │ ├── PaymentInstrument.md │ │ ├── PaymentMethod.md │ │ ├── PaymentMgr.md │ │ ├── PaymentProcessor.md │ │ ├── PaymentStatusCodes.md │ │ ├── PaymentTransaction.md │ │ ├── PriceAdjustment.md │ │ ├── PriceAdjustmentLimitTypes.md │ │ ├── ProductLineItem.md │ │ ├── ProductShippingCost.md │ │ ├── ProductShippingLineItem.md │ │ ├── ProductShippingModel.md │ │ ├── Return.md │ │ ├── ReturnCase.md │ │ ├── ReturnCaseItem.md │ │ ├── ReturnItem.md │ │ ├── Shipment.md │ │ ├── ShipmentShippingCost.md │ │ ├── ShipmentShippingModel.md │ │ ├── ShippingLineItem.md │ │ ├── ShippingLocation.md │ │ ├── ShippingMethod.md │ │ ├── ShippingMgr.md │ │ ├── ShippingOrder.md │ │ ├── ShippingOrderItem.md │ │ ├── SumItem.md │ │ ├── TaxGroup.md │ │ ├── TaxItem.md │ │ ├── TaxMgr.md │ │ ├── TrackingInfo.md │ │ └── TrackingRef.md │ ├── dw_order.hooks │ │ ├── CalculateHooks.md │ │ ├── OrderHooks.md │ │ ├── PaymentHooks.md │ │ ├── ReturnHooks.md │ │ └── ShippingOrderHooks.md │ ├── dw_rpc │ │ ├── SOAPUtil.md │ │ ├── Stub.md │ │ └── WebReference.md │ ├── dw_suggest │ │ ├── BrandSuggestions.md │ │ ├── CategorySuggestions.md │ │ ├── ContentSuggestions.md │ │ ├── CustomSuggestions.md │ │ ├── ProductSuggestions.md │ │ ├── SearchPhraseSuggestions.md │ │ ├── SuggestedCategory.md │ │ ├── SuggestedContent.md │ │ ├── SuggestedPhrase.md │ │ ├── SuggestedProduct.md │ │ ├── SuggestedTerm.md │ │ ├── SuggestedTerms.md │ │ ├── Suggestions.md │ │ └── SuggestModel.md │ ├── dw_svc │ │ ├── FTPService.md │ │ ├── FTPServiceDefinition.md │ │ ├── HTTPFormService.md │ │ ├── HTTPFormServiceDefinition.md │ │ ├── HTTPService.md │ │ ├── HTTPServiceDefinition.md │ │ ├── LocalServiceRegistry.md │ │ ├── Result.md │ │ ├── Service.md │ │ ├── ServiceCallback.md │ │ ├── ServiceConfig.md │ │ ├── ServiceCredential.md │ │ ├── ServiceDefinition.md │ │ ├── ServiceProfile.md │ │ ├── ServiceRegistry.md │ │ ├── SOAPService.md │ │ └── SOAPServiceDefinition.md │ ├── dw_system │ │ ├── AgentUserStatusCodes.md │ │ ├── Cache.md │ │ ├── CacheMgr.md │ │ ├── HookMgr.md │ │ ├── InternalObject.md │ │ ├── JobProcessMonitor.md │ │ ├── Log.md │ │ ├── Logger.md │ │ ├── LogNDC.md │ │ ├── OrganizationPreferences.md │ │ ├── Pipeline.md │ │ ├── PipelineDictionary.md │ │ ├── RemoteInclude.md │ │ ├── Request.md │ │ ├── RequestHooks.md │ │ ├── Response.md │ │ ├── RESTErrorResponse.md │ │ ├── RESTResponseMgr.md │ │ ├── RESTSuccessResponse.md │ │ ├── SearchStatus.md │ │ ├── Session.md │ │ ├── Site.md │ │ ├── SitePreferences.md │ │ ├── Status.md │ │ ├── StatusItem.md │ │ ├── System.md │ │ └── Transaction.md │ ├── dw_util │ │ ├── ArrayList.md │ │ ├── Assert.md │ │ ├── BigInteger.md │ │ ├── Bytes.md │ │ ├── Calendar.md │ │ ├── Collection.md │ │ ├── Currency.md │ │ ├── DateUtils.md │ │ ├── Decimal.md │ │ ├── FilteringCollection.md │ │ ├── Geolocation.md │ │ ├── HashMap.md │ │ ├── HashSet.md │ │ ├── Iterator.md │ │ ├── LinkedHashMap.md │ │ ├── LinkedHashSet.md │ │ ├── List.md │ │ ├── Locale.md │ │ ├── Map.md │ │ ├── MapEntry.md │ │ ├── MappingKey.md │ │ ├── MappingMgr.md │ │ ├── PropertyComparator.md │ │ ├── SecureEncoder.md │ │ ├── SecureFilter.md │ │ ├── SeekableIterator.md │ │ ├── Set.md │ │ ├── SortedMap.md │ │ ├── SortedSet.md │ │ ├── StringUtils.md │ │ ├── Template.md │ │ └── UUIDUtils.md │ ├── dw_value │ │ ├── EnumValue.md │ │ ├── MimeEncodedText.md │ │ ├── Money.md │ │ └── Quantity.md │ ├── dw_web │ │ ├── ClickStream.md │ │ ├── ClickStreamEntry.md │ │ ├── Cookie.md │ │ ├── Cookies.md │ │ ├── CSRFProtection.md │ │ ├── Form.md │ │ ├── FormAction.md │ │ ├── FormElement.md │ │ ├── FormElementValidationResult.md │ │ ├── FormField.md │ │ ├── FormFieldOption.md │ │ ├── FormFieldOptions.md │ │ ├── FormGroup.md │ │ ├── FormList.md │ │ ├── FormListItem.md │ │ ├── Forms.md │ │ ├── HttpParameter.md │ │ ├── HttpParameterMap.md │ │ ├── LoopIterator.md │ │ ├── PageMetaData.md │ │ ├── PageMetaTag.md │ │ ├── PagingModel.md │ │ ├── Resource.md │ │ ├── URL.md │ │ ├── URLAction.md │ │ ├── URLParameter.md │ │ ├── URLRedirect.md │ │ ├── URLRedirectMgr.md │ │ └── URLUtils.md │ ├── sfra │ │ ├── account.md │ │ ├── address.md │ │ ├── billing.md │ │ ├── cart.md │ │ ├── categories.md │ │ ├── content.md │ │ ├── locale.md │ │ ├── order.md │ │ ├── payment.md │ │ ├── price-default.md │ │ ├── price-range.md │ │ ├── price-tiered.md │ │ ├── product-bundle.md │ │ ├── product-full.md │ │ ├── product-line-items.md │ │ ├── product-search.md │ │ ├── product-tile.md │ │ ├── querystring.md │ │ ├── render.md │ │ ├── request.md │ │ ├── response.md │ │ ├── server.md │ │ ├── shipping.md │ │ ├── store.md │ │ ├── stores.md │ │ └── totals.md │ └── TopLevel │ ├── APIException.md │ ├── arguments.md │ ├── Array.md │ ├── ArrayBuffer.md │ ├── BigInt.md │ ├── Boolean.md │ ├── ConversionError.md │ ├── DataView.md │ ├── Date.md │ ├── Error.md │ ├── ES6Iterator.md │ ├── EvalError.md │ ├── Fault.md │ ├── Float32Array.md │ ├── Float64Array.md │ ├── Function.md │ ├── Generator.md │ ├── global.md │ ├── Int16Array.md │ ├── Int32Array.md │ ├── Int8Array.md │ ├── InternalError.md │ ├── IOError.md │ ├── Iterable.md │ ├── Iterator.md │ ├── JSON.md │ ├── Map.md │ ├── Math.md │ ├── Module.md │ ├── Namespace.md │ ├── Number.md │ ├── Object.md │ ├── QName.md │ ├── RangeError.md │ ├── ReferenceError.md │ ├── RegExp.md │ ├── Set.md │ ├── StopIteration.md │ ├── String.md │ ├── Symbol.md │ ├── SyntaxError.md │ ├── SystemError.md │ ├── TypeError.md │ ├── Uint16Array.md │ ├── Uint32Array.md │ ├── Uint8Array.md │ ├── Uint8ClampedArray.md │ ├── URIError.md │ ├── WeakMap.md │ ├── WeakSet.md │ ├── XML.md │ ├── XMLList.md │ └── XMLStreamError.md ├── docs-site │ ├── .gitignore │ ├── App.tsx │ ├── components │ │ ├── Badge.tsx │ │ ├── BreadcrumbSchema.tsx │ │ ├── CodeBlock.tsx │ │ ├── Collapsible.tsx │ │ ├── ConfigBuilder.tsx │ │ ├── ConfigHero.tsx │ │ ├── ConfigModeTabs.tsx │ │ ├── icons.tsx │ │ ├── Layout.tsx │ │ ├── LightCodeContainer.tsx │ │ ├── NewcomerCTA.tsx │ │ ├── NextStepsStrip.tsx │ │ ├── OnThisPage.tsx │ │ ├── Search.tsx │ │ ├── SEO.tsx │ │ ├── Sidebar.tsx │ │ ├── StructuredData.tsx │ │ ├── ToolCard.tsx │ │ ├── ToolFilters.tsx │ │ ├── Typography.tsx │ │ └── VersionBadge.tsx │ ├── constants.tsx │ ├── index.html │ ├── main.tsx │ ├── metadata.json │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── AIInterfacesPage.tsx │ │ ├── ConfigurationPage.tsx │ │ ├── DevelopmentPage.tsx │ │ ├── ExamplesPage.tsx │ │ ├── FeaturesPage.tsx │ │ ├── HomePage.tsx │ │ ├── SecurityPage.tsx │ │ ├── ToolsPage.tsx │ │ └── TroubleshootingPage.tsx │ ├── postcss.config.js │ ├── public │ │ ├── .well-known │ │ │ └── security.txt │ │ ├── 404.html │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── explain-product-pricing-methods-no-mcp.png │ │ ├── explain-product-pricing-methods.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── llms.txt │ │ ├── robots.txt │ │ ├── site.webmanifest │ │ └── sitemap.xml │ ├── README.md │ ├── scripts │ │ ├── generate-search-index.js │ │ ├── generate-sitemap.js │ │ └── search-dev.js │ ├── src │ │ └── styles │ │ ├── input.css │ │ └── prism-theme.css │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── types.ts │ ├── utils │ │ ├── search.ts │ │ └── toolsData.ts │ └── vite.config.ts ├── eslint.config.js ├── jest.config.js ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── scripts │ └── convert-docs.js ├── SECURITY.md ├── server.json ├── src │ ├── clients │ │ ├── base │ │ │ ├── http-client.ts │ │ │ ├── oauth-token.ts │ │ │ └── ocapi-auth-client.ts │ │ ├── best-practices-client.ts │ │ ├── cartridge-generation-client.ts │ │ ├── docs │ │ │ ├── class-content-parser.ts │ │ │ ├── class-name-resolver.ts │ │ │ ├── documentation-scanner.ts │ │ │ ├── index.ts │ │ │ └── referenced-types-extractor.ts │ │ ├── docs-client.ts │ │ ├── log-client.ts │ │ ├── logs │ │ │ ├── index.ts │ │ │ ├── log-analyzer.ts │ │ │ ├── log-client.ts │ │ │ ├── log-constants.ts │ │ │ ├── log-file-discovery.ts │ │ │ ├── log-file-reader.ts │ │ │ ├── log-formatter.ts │ │ │ ├── log-processor.ts │ │ │ ├── log-types.ts │ │ │ └── webdav-client-manager.ts │ │ ├── ocapi │ │ │ ├── code-versions-client.ts │ │ │ ├── site-preferences-client.ts │ │ │ └── system-objects-client.ts │ │ ├── ocapi-client.ts │ │ └── sfra-client.ts │ ├── config │ │ ├── configuration-factory.ts │ │ └── dw-json-loader.ts │ ├── core │ │ ├── handlers │ │ │ ├── abstract-log-tool-handler.ts │ │ │ ├── base-handler.ts │ │ │ ├── best-practices-handler.ts │ │ │ ├── cartridge-handler.ts │ │ │ ├── client-factory.ts │ │ │ ├── code-version-handler.ts │ │ │ ├── docs-handler.ts │ │ │ ├── job-log-handler.ts │ │ │ ├── job-log-tool-config.ts │ │ │ ├── log-handler.ts │ │ │ ├── log-tool-config.ts │ │ │ ├── sfra-handler.ts │ │ │ ├── system-object-handler.ts │ │ │ └── validation-helpers.ts │ │ ├── server.ts │ │ └── tool-definitions.ts │ ├── index.ts │ ├── main.ts │ ├── services │ │ ├── file-system-service.ts │ │ ├── index.ts │ │ └── path-service.ts │ ├── tool-configs │ │ ├── best-practices-tool-config.ts │ │ ├── cartridge-tool-config.ts │ │ ├── code-version-tool-config.ts │ │ ├── docs-tool-config.ts │ │ ├── job-log-tool-config.ts │ │ ├── log-tool-config.ts │ │ ├── sfra-tool-config.ts │ │ └── system-object-tool-config.ts │ ├── types │ │ └── types.ts │ └── utils │ ├── cache.ts │ ├── job-log-tool-config.ts │ ├── job-log-utils.ts │ ├── log-cache.ts │ ├── log-tool-config.ts │ ├── log-tool-constants.ts │ ├── log-tool-utils.ts │ ├── logger.ts │ ├── ocapi-url-builder.ts │ ├── path-resolver.ts │ ├── query-builder.ts │ ├── utils.ts │ └── validator.ts ├── tests │ ├── __mocks__ │ │ ├── docs-client.ts │ │ ├── src │ │ │ └── clients │ │ │ └── base │ │ │ └── http-client.js │ │ └── webdav.js │ ├── base-handler.test.ts │ ├── base-http-client.test.ts │ ├── best-practices-handler.test.ts │ ├── cache.test.ts │ ├── cartridge-handler.test.ts │ ├── class-content-parser.test.ts │ ├── class-name-resolver.test.ts │ ├── client-factory.test.ts │ ├── code-version-handler.test.ts │ ├── code-versions-client.test.ts │ ├── config.test.ts │ ├── configuration-factory.test.ts │ ├── docs-handler.test.ts │ ├── documentation-scanner.test.ts │ ├── file-system-service.test.ts │ ├── job-log-handler.test.ts │ ├── job-log-utils.test.ts │ ├── log-client.test.ts │ ├── log-handler.test.ts │ ├── log-processor.test.ts │ ├── logger.test.ts │ ├── mcp │ │ ├── AGENTS.md │ │ ├── node │ │ │ ├── activate-code-version-advanced.full-mode.programmatic.test.js │ │ │ ├── code-versions.full-mode.programmatic.test.js │ │ │ ├── generate-cartridge-structure.docs-only.programmatic.test.js │ │ │ ├── get-available-best-practice-guides.docs-only.programmatic.test.js │ │ │ ├── get-available-sfra-documents.programmatic.test.js │ │ │ ├── get-best-practice-guide.docs-only.programmatic.test.js │ │ │ ├── get-hook-reference.docs-only.programmatic.test.js │ │ │ ├── get-job-execution-summary.full-mode.programmatic.test.js │ │ │ ├── get-job-log-entries.full-mode.programmatic.test.js │ │ │ ├── get-latest-debug.full-mode.programmatic.test.js │ │ │ ├── get-latest-error.full-mode.programmatic.test.js │ │ │ ├── get-latest-info.full-mode.programmatic.test.js │ │ │ ├── get-latest-job-log-files.full-mode.programmatic.test.js │ │ │ ├── get-latest-warn.full-mode.programmatic.test.js │ │ │ ├── get-log-file-contents.full-mode.programmatic.test.js │ │ │ ├── get-sfcc-class-documentation.docs-only.programmatic.test.js │ │ │ ├── get-sfcc-class-info.docs-only.programmatic.test.js │ │ │ ├── get-sfra-categories.docs-only.programmatic.test.js │ │ │ ├── get-sfra-document.programmatic.test.js │ │ │ ├── get-sfra-documents-by-category.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definition.full-mode.programmatic.test.js │ │ │ ├── get-system-object-definitions.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definitions.full-mode.programmatic.test.js │ │ │ ├── list-log-files.full-mode.programmatic.test.js │ │ │ ├── list-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-best-practices.docs-only.programmatic.test.js │ │ │ ├── search-custom-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-job-logs-by-name.full-mode.programmatic.test.js │ │ │ ├── search-job-logs.full-mode.programmatic.test.js │ │ │ ├── search-logs.full-mode.programmatic.test.js │ │ │ ├── search-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-sfcc-methods.docs-only.programmatic.test.js │ │ │ ├── search-sfra-documentation.docs-only.programmatic.test.js │ │ │ ├── search-site-preferences.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-groups.full-mode.programmatic.test.js │ │ │ ├── summarize-logs.full-mode.programmatic.test.js │ │ │ ├── tools.docs-only.programmatic.test.js │ │ │ └── tools.full-mode.programmatic.test.js │ │ ├── README.md │ │ ├── test-fixtures │ │ │ └── dw.json │ │ └── yaml │ │ ├── activate-code-version.docs-only.test.mcp.yml │ │ ├── activate-code-version.full-mode.test.mcp.yml │ │ ├── get_latest_error.test.mcp.yml │ │ ├── get-available-best-practice-guides.docs-only.test.mcp.yml │ │ ├── get-available-best-practice-guides.full-mode.test.mcp.yml │ │ ├── get-available-sfra-documents.docs-only.test.mcp.yml │ │ ├── get-available-sfra-documents.full-mode.test.mcp.yml │ │ ├── get-best-practice-guide.docs-only.test.mcp.yml │ │ ├── get-best-practice-guide.full-mode.test.mcp.yml │ │ ├── get-code-versions.docs-only.test.mcp.yml │ │ ├── get-code-versions.full-mode.test.mcp.yml │ │ ├── get-hook-reference.docs-only.test.mcp.yml │ │ ├── get-hook-reference.full-mode.test.mcp.yml │ │ ├── get-job-execution-summary.full-mode.test.mcp.yml │ │ ├── get-job-log-entries.full-mode.test.mcp.yml │ │ ├── get-latest-debug.full-mode.test.mcp.yml │ │ ├── get-latest-error.full-mode.test.mcp.yml │ │ ├── get-latest-info.full-mode.test.mcp.yml │ │ ├── get-latest-job-log-files.full-mode.test.mcp.yml │ │ ├── get-latest-warn.full-mode.test.mcp.yml │ │ ├── get-log-file-contents.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-documentation.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-documentation.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-info.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-info.full-mode.test.mcp.yml │ │ ├── get-sfra-categories.docs-only.test.mcp.yml │ │ ├── get-sfra-categories.full-mode.test.mcp.yml │ │ ├── get-sfra-document.docs-only.test.mcp.yml │ │ ├── get-sfra-document.full-mode.test.mcp.yml │ │ ├── get-sfra-documents-by-category.docs-only.test.mcp.yml │ │ ├── get-sfra-documents-by-category.full-mode.test.mcp.yml │ │ ├── get-system-object-definition.docs-only.test.mcp.yml │ │ ├── get-system-object-definition.full-mode.test.mcp.yml │ │ ├── get-system-object-definitions.docs-only.test.mcp.yml │ │ ├── get-system-object-definitions.full-mode.test.mcp.yml │ │ ├── list-log-files.full-mode.test.mcp.yml │ │ ├── list-sfcc-classes.docs-only.test.mcp.yml │ │ ├── list-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-best-practices.docs-only.test.mcp.yml │ │ ├── search-best-practices.full-mode.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.test.mcp.yml │ │ ├── search-job-logs-by-name.full-mode.test.mcp.yml │ │ ├── search-job-logs.full-mode.test.mcp.yml │ │ ├── search-logs.full-mode.test.mcp.yml │ │ ├── search-sfcc-classes.docs-only.test.mcp.yml │ │ ├── search-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-sfcc-methods.docs-only.test.mcp.yml │ │ ├── search-sfcc-methods.full-mode.test.mcp.yml │ │ ├── search-sfra-documentation.docs-only.test.mcp.yml │ │ ├── search-sfra-documentation.full-mode.test.mcp.yml │ │ ├── search-site-preferences.docs-only.test.mcp.yml │ │ ├── search-site-preferences.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-groups.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-groups.full-mode.test.mcp.yml │ │ ├── summarize-logs.full-mode.test.mcp.yml │ │ ├── tools.docs-only.test.mcp.yml │ │ └── tools.full-mode.test.mcp.yml │ ├── oauth-token.test.ts │ ├── ocapi-auth-client.test.ts │ ├── ocapi-client.test.ts │ ├── path-service.test.ts │ ├── query-builder.test.ts │ ├── referenced-types-extractor.test.ts │ ├── servers │ │ ├── sfcc-mock-server │ │ │ ├── mock-data │ │ │ │ └── ocapi │ │ │ │ ├── code-versions.json │ │ │ │ ├── custom-object-attributes-customapi.json │ │ │ │ ├── custom-object-attributes-globalsettings.json │ │ │ │ ├── custom-object-attributes-versionhistory.json │ │ │ │ ├── site-preferences-ccv.json │ │ │ │ ├── site-preferences-fastforward.json │ │ │ │ ├── site-preferences-sfra.json │ │ │ │ ├── site-preferences-storefront.json │ │ │ │ ├── site-preferences-system.json │ │ │ │ ├── system-object-attribute-groups-campaign.json │ │ │ │ ├── system-object-attribute-groups-category.json │ │ │ │ ├── system-object-attribute-groups-order.json │ │ │ │ ├── system-object-attribute-groups-product.json │ │ │ │ ├── system-object-attribute-groups-sitepreferences.json │ │ │ │ ├── system-object-attributes-customeraddress.json │ │ │ │ ├── system-object-attributes-product-expanded.json │ │ │ │ ├── system-object-attributes-product.json │ │ │ │ ├── system-object-definition-category.json │ │ │ │ ├── system-object-definition-customer.json │ │ │ │ ├── system-object-definition-customeraddress.json │ │ │ │ ├── system-object-definition-order.json │ │ │ │ ├── system-object-definition-product.json │ │ │ │ ├── system-object-definitions-old.json │ │ │ │ └── system-object-definitions.json │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── README.md │ │ │ ├── scripts │ │ │ │ └── setup-logs.js │ │ │ ├── server.js │ │ │ └── src │ │ │ ├── app.js │ │ │ ├── config │ │ │ │ └── server-config.js │ │ │ ├── middleware │ │ │ │ ├── auth.js │ │ │ │ ├── cors.js │ │ │ │ └── logging.js │ │ │ ├── routes │ │ │ │ ├── ocapi │ │ │ │ │ ├── code-versions-handler.js │ │ │ │ │ ├── oauth-handler.js │ │ │ │ │ ├── ocapi-error-utils.js │ │ │ │ │ ├── ocapi-utils.js │ │ │ │ │ ├── site-preferences-handler.js │ │ │ │ │ └── system-objects-handler.js │ │ │ │ ├── ocapi.js │ │ │ │ └── webdav.js │ │ │ └── utils │ │ │ ├── mock-data-loader.js │ │ │ └── webdav-xml.js │ │ └── sfcc-mock-server-manager.ts │ ├── sfcc-mock-server.test.ts │ ├── site-preferences-client.test.ts │ ├── system-objects-client.test.ts │ ├── utils.test.ts │ ├── validation-helpers.test.ts │ └── validator.test.ts ├── tsconfig.json └── tsconfig.test.json ``` # Files -------------------------------------------------------------------------------- /docs/best-practices/isml_templates.md: -------------------------------------------------------------------------------- ```markdown 1 | # Salesforce B2C Commerce ISML Templates: Best Practices & Development Guide 2 | 3 | This guide provides comprehensive best practices for developing ISML (Internet Store Markup Language) templates within the Salesforce B2C Commerce Storefront Reference Architecture (SFRA). Master these principles to build secure, maintainable, and high-performing storefront experiences. 4 | 5 | **IMPORTANT**: Before developing ISML templates, consult the **Performance and Stability Best Practices** and **SFRA Controllers** guides from this MCP server. Understanding the MVC separation and controller patterns is essential for proper ISML development. 6 | 7 | ## Core Principles 8 | 9 | ### The Golden Rule: Logic-Free Templates 10 | 11 | **NEVER use `<isscript>` for business logic in ISML templates.** This is the most critical rule in SFRA development. 12 | 13 | ISML templates are presentation-only layers that should receive all data from controllers via the `pdict` object. Any business logic, data manipulation, or API calls belong in controllers or models. 14 | 15 | #### ❌ Anti-Pattern: Logic in Templates 16 | ```html 17 | <isscript> 18 | var ProductMgr = require('dw/catalog/ProductMgr'); 19 | var product = ProductMgr.getProduct(pdict.pid); 20 | var price = product.getPriceModel().getPrice(); 21 | </isscript> 22 | <div class="price">${price}</div> 23 | ``` 24 | 25 | #### ✅ Correct Pattern: Data from Controller 26 | ```javascript 27 | // Controller 28 | server.get('Show', function (req, res, next) { 29 | var product = ProductFactory.get({ pid: req.querystring.pid }); 30 | res.render('product/productDetails', { product: product }); 31 | next(); 32 | }); 33 | ``` 34 | 35 | ```html 36 | <!-- ISML Template --> 37 | <div class="price">${pdict.product.price.sales.formatted}</div> 38 | ``` 39 | 40 | ### Exception: Asset Management Only 41 | 42 | The **only** acceptable use of `<isscript>` is for managing static assets: 43 | 44 | ```html 45 | <isscript> 46 | var assets = require('*/cartridge/scripts/assets.js'); 47 | assets.addCss('/css/product.css'); 48 | assets.addJs('/js/product.js'); 49 | </isscript> 50 | ``` 51 | 52 | ## Security Best Practices 53 | 54 | ### XSS Prevention with Proper Encoding 55 | 56 | **Always rely on default encoding.** The `<isprint>` tag automatically HTML-encodes output to prevent XSS attacks. 57 | 58 | #### ✅ Secure (Default Behavior) 59 | ```html 60 | <div class="search-term"> 61 | You searched for: <isprint value="${pdict.searchPhrase}" /> 62 | </div> 63 | ``` 64 | 65 | #### ❌ Vulnerable 66 | ```html 67 | <div class="search-term"> 68 | You searched for: <isprint value="${pdict.searchPhrase}" encoding="off" /> 69 | </div> 70 | ``` 71 | 72 | ### Context-Specific Encoding 73 | 74 | For non-HTML contexts, use `dw.util.SecureEncoder`: 75 | 76 | ```html 77 | <isscript> 78 | var SecureEncoder = require('dw/util/SecureEncoder'); 79 | </isscript> 80 | 81 | <!-- For JavaScript context --> 82 | <script> 83 | var searchTerm = "${SecureEncoder.forJavaScript(pdict.searchPhrase)}"; 84 | </script> 85 | 86 | <!-- For HTML attributes --> 87 | <input type="hidden" value="${SecureEncoder.forHTMLAttribute(pdict.token)}" /> 88 | ``` 89 | 90 | ## Template Architecture Patterns 91 | 92 | ### 1. Layout Decorators 93 | 94 | Use decorators for consistent page structure: 95 | 96 | #### SFRA Default Decorator Templates 97 | 98 | **IMPORTANT**: SFRA provides only **two default decorator templates** out of the box: 99 | 100 | 1. **`common/layout/page`** - Standard storefront pages (homepage, PDP, search, account, etc.) 101 | 2. **`common/layout/checkout`** - Secure checkout process pages 102 | 103 | If you need additional layout patterns (modal dialogs, email templates, mobile-specific layouts, etc.), you must **create custom decorator templates** in your cartridge's `templates/default/common/layout/` directory. 104 | 105 | #### Common Custom Decorators You May Need to Create: 106 | 107 | ```html 108 | <!-- Custom Modal Layout --> 109 | common/layout/modal.isml 110 | 111 | <!-- Email Template Layout --> 112 | common/layout/email.isml 113 | 114 | <!-- Popup/Lightbox Layout --> 115 | common/layout/popup.isml 116 | 117 | <!-- Print-Friendly Layout --> 118 | common/layout/print.isml 119 | 120 | <!-- Mobile App Layout --> 121 | common/layout/mobile.isml 122 | ``` 123 | 124 | #### Default Template Usage Examples: 125 | 126 | **Using the Standard Page Layout**: 127 | ```html 128 | <!-- ✅ Standard storefront pages --> 129 | <isdecorate template="common/layout/page"> 130 | <div class="product-details"> 131 | <h1><isprint value="${pdict.product.productName}" /></h1> 132 | <!-- Content goes here --> 133 | </div> 134 | </isdecorate> 135 | ``` 136 | 137 | **Using the Checkout Layout**: 138 | ```html 139 | <!-- ✅ Secure checkout pages --> 140 | <isdecorate template="common/layout/checkout"> 141 | <div class="checkout-content"> 142 | <h1>Checkout</h1> 143 | <!-- Checkout form content --> 144 | </div> 145 | </isdecorate> 146 | ``` 147 | 148 | #### Creating Custom Decorator Templates: 149 | 150 | If you need a custom layout, create it in your cartridge at `templates/default/common/layout/[name].isml`: 151 | 152 | **Example Custom Modal Layout** (`common/layout/modal.isml`): 153 | ```html 154 | <!doctype html> 155 | <html lang="${require('dw/util/Locale').getLocale(request.locale).getLanguage()}"> 156 | <head> 157 | <isinclude template="common/htmlHead" /> 158 | </head> 159 | <body class="modal-body"> 160 | <div class="modal-container"> 161 | <header class="modal-header"> 162 | <button type="button" class="close" data-dismiss="modal"> 163 | <span>×</span> 164 | </button> 165 | </header> 166 | <main class="modal-content"> 167 | <isreplace/> 168 | </main> 169 | </div> 170 | </body> 171 | </html> 172 | ``` 173 | 174 | **Using Your Custom Layout**: 175 | ```html 176 | <!-- ✅ Using custom decorator template --> 177 | <isdecorate template="common/layout/modal"> 178 | <div class="modal-body-content"> 179 | <h2>Modal Title</h2> 180 | <p>Modal content goes here</p> 181 | </div> 182 | </isdecorate> 183 | ``` 184 | 185 | ### 2. Component Modularity 186 | 187 | Break complex templates into reusable components: 188 | 189 | **Product Tile Component** (`product/components/productTile.isml`): 190 | ```html 191 | <div class="product-tile" data-pid="${pdict.product.id}"> 192 | <div class="tile-image"> 193 | <isinclude template="product/components/productTileImage" /> 194 | </div> 195 | 196 | <div class="tile-body"> 197 | <div class="pdp-link"> 198 | <a class="link" href="${pdict.product.url}"> 199 | <isprint value="${pdict.product.productName}" /> 200 | </a> 201 | </div> 202 | 203 | <isinclude template="product/components/pricing/main" /> 204 | 205 | <isinclude template="product/components/productTileActions" /> 206 | </div> 207 | </div> 208 | ``` 209 | 210 | **Usage in Grid**: 211 | ```html 212 | <div class="row product-grid"> 213 | <isloop items="${pdict.productSearch.productHits}" var="productHit"> 214 | <div class="col-6 col-sm-4"> 215 | <isinclude template="product/components/productTile" /> 216 | <isset name="product" value="${productHit.product}" scope="page" /> 217 | </div> 218 | </isloop> 219 | </div> 220 | ``` 221 | 222 | ### 3. Remote Includes vs Local Includes (Advanced Fragment Architecture) 223 | 224 | Remote includes are NOT just an alternative syntax – they change request boundaries, data scoping, caching strategy, security posture, and performance characteristics. Choose them only when a fragment truly needs an independent cache policy or isolation. 225 | 226 | | Attribute | Local Include `<isinclude template="...">` | Remote Include `<isinclude url="...">` | 227 | |-----------|---------------------------------------------|-------------------------------------------| 228 | | Processing | Single request on Application Server | Parent request + secondary request orchestrated by Web Adapter | 229 | | Data Scope | Full access to parent `pdict` & variables | Isolated – only URL query params available | 230 | | Cache Policy | Inherits parent (lowest TTL wins) | Independent TTL per fragment | 231 | | Typical Use | Reusable markup, product tile partials | Mini-cart, personalized promo slot, dynamic inventory widget | 232 | | Overhead | Minimal | Extra HTTP round trip per include | 233 | | Security | Inherits parent auth state | New unauthenticated request – must add explicit protection | 234 | | Failure Mode | Template error breaks page render | Fragment timeout can delay full page assembly | 235 | 236 | #### 3.1 When to Use Which 237 | Use a local include unless ALL are true: 238 | 1. Fragment volatility differs meaningfully from page shell 239 | 2. Output can be parameterized via simple query params 240 | 3. Performance gain from separate caching outweighs added request 241 | 4. Security implications are understood and mitigated 242 | 243 | #### 3.2 Implementing a Remote Include 244 | Template example (mini-cart header icon kept uncached while page shell cached hours): 245 | ```html 246 | <div class="header-cart" data-component="mini-cart"> 247 | <isinclude url="${URLUtils.url('Cart-MiniCart')}" /> 248 | <!-- Controller route name MUST match exactly --> 249 | </div> 250 | ``` 251 | 252 | Controller (excerpt): 253 | ```javascript 254 | var server = require('server'); 255 | var cache = require('*/cartridge/scripts/middleware/cache'); 256 | 257 | server.get('MiniCart', 258 | server.middleware.include, // Gatekeeper: only valid inside remote include flow 259 | cache.applyShortPromotionSensitiveCache, // Or cache.disableCaching() equivalent for uncached 260 | function (req, res, next) { 261 | // Build isolated model – no parent pdict access 262 | res.render('components/header/miniCart'); 263 | next(); 264 | } 265 | ); 266 | 267 | module.exports = server.exports(); 268 | ``` 269 | 270 | #### 3.3 Passing Data 271 | All context must be URL params: 272 | ```html 273 | <isinclude url="${URLUtils.url('Page-Include', 'cid', 'footer-content-asset')}" /> 274 | ``` 275 | Avoid over-parameterization – each unique full URL becomes a distinct cache entry (cache fragmentation risk). 276 | 277 | #### 3.4 Caching Strategy Patterns 278 | | Scenario | Page Shell TTL | Remote Include TTL | Rationale | 279 | |----------|----------------|--------------------|-----------| 280 | | Marketing landing + live inventory badge | 12h | 5m | Keep inventory fresh without invalidating hero layout | 281 | | Category grid + personalized banner | 4h | 15m | Personalized offers rotate more often than navigation | 282 | | PDP + mini-cart | 2h | 0 (uncached) | Basket state must reflect current session | 283 | 284 | Keep total remote includes per page conservative (<20 recommended) to avoid waterfall latency. 285 | 286 | #### 3.5 Performance Anti-Patterns 287 | | Anti-Pattern | Why It’s Harmful | Better Alternative | 288 | |--------------|------------------|--------------------| 289 | | One remote include per product tile in a grid | N extra HTTP requests, destroys cache efficiency | Single parent render with local includes | 290 | | Adding position/index params that change per render | Creates unique cache keys, low hit ratio | Omit non-functional varying params | 291 | | Deep nesting (include inside include chain) | Hard to debug, compounding latency | Flatten architecture – coalesce related fragments | 292 | 293 | #### 3.6 Security Criticals 294 | Remote include endpoints are effectively public unless you add authentication middleware (e.g., `userLoggedIn.validateLoggedIn`). NEVER expose PII or account state in an unprotected include. 295 | 296 | Always start chain with: 297 | ```javascript 298 | server.get('SensitiveFragment', 299 | server.middleware.include, 300 | userLoggedIn.validateLoggedIn, // if user-specific 301 | function (req, res, next) { ... } 302 | ); 303 | ``` 304 | 305 | #### 3.7 Observability & Debugging 306 | Use extended request IDs in logs: `reqId-depth-index` (e.g., `AbC123-1-02`). Search logs to isolate slow fragment origins. 307 | 308 | #### 3.8 Checklist 309 | ```text 310 | [ ] Justified different cache TTL 311 | [ ] URL params minimal & non-sensitive 312 | [ ] server.middleware.include first 313 | [ ] Additional auth middleware if user data 314 | [ ] Explicit cache middleware (or disabled) 315 | [ ] Fragment count on page < 20 316 | [ ] No nested chains beyond depth 2 317 | ``` 318 | 319 | If you cannot satisfy most checklist items, prefer a local include. 320 | 321 | #### 3.9 When NOT to Use Remote Includes 322 | - Pure presentational partials (icons, button groups) 323 | - Iterative children of a paginated list 324 | - Form bodies that rely on parent controller’s validation context 325 | - Anything requiring access to parent `pdict` objects without trivial serialization 326 | 327 | --- 328 | 329 | ## Utility Helpers Available in Templates 330 | 331 | SFRA templates run inside the B2C Commerce template processor, which autowires a set of utility objects and classes so you can call them without additional imports. Knowing what is available keeps templates lean and avoids unnecessary `<isscript>` blocks. 332 | 333 | ### Top-Level Variables 334 | 335 | The template scope automatically exposes: 336 | 337 | - `pdict` 338 | - `out` 339 | - `request` 340 | - `session` 341 | 342 | Each object gives you direct access to storefront context data and helper APIs. 343 | 344 | ### `dw.system.Request` 345 | 346 | Because `request` is a top-level variable, you can call methods directly without `require` statements. 347 | 348 | | Method | Description | 349 | | --- | --- | 350 | | `getHttpCookies()` → `Cookies` | Returns the `Cookies` object so you can inspect or manipulate client-side cookies. | 351 | | `getHttpHeaders()` → `Map` | Returns a map of HTTP header values on the current request. | 352 | | `isHttpSecure()` → `Boolean` | Indicates whether the request is secure (`HTTPS`). | 353 | | `isSCAPI()` → `Boolean` | Distinguishes between OCAPI and SCAPI requests in extension points (hooks). | 354 | 355 | **Example** 356 | 357 | ```html 358 | <td class="price merchandizetotal"> 359 | <isprint value="${request.custom.Container.adjustedMerchandiseTotalNetPrice}" /> 360 | </td> 361 | ``` 362 | 363 | ### `dw.system.Session` 364 | 365 | The `session` top-level variable exposes the current storefront or Business Manager session (Business Manager sessions return `null` for customer lookups). 366 | 367 | | Method | Description | 368 | | --- | --- | 369 | | `getCustomer()` → `Customer` | Returns the current customer associated with the storefront session; `null` in Business Manager. | 370 | | `isCustomerAuthenticated()` → `Boolean` | Indicates whether the customer for this session is authenticated (equivalent to `customer.isAuthenticated()`). | 371 | 372 | **Example** 373 | 374 | ```html 375 | <isprint value="${session.getCustomer().firstname}" /> 376 | ``` 377 | 378 | ### `dw.util.StringUtils` 379 | 380 | `StringUtils` is pre-imported, letting you call static helpers by their simple name to format and sanitize output. 381 | 382 | | Method | Description | 383 | | --- | --- | 384 | | `formatDate(date)` → `String` | Formats a `Date` with the current site’s default date format. | 385 | | `formatInteger(number)` → `String` | Formats a number using the site’s default integer format; floats are coerced to integers. | 386 | | `formatNumber(number)` → `String` | Formats a number using the site’s default number format. | 387 | | `garble(str, replaceChar, suffixLength)` → `String` | Masks a string, leaving the last `suffixLength` characters intact. | 388 | | `pad(str, width)` → `String` | Pads a string to a target width (useful for alignment in tables). | 389 | | `stringToHtml(str)` → `String` | Converts a string to an HTML-safe representation. | 390 | | `stringToWml(str)` → `String` | Converts a string to a WML-safe representation. | 391 | | `stringToXml(str)` → `String` | Converts a string to an XML-safe representation. | 392 | | `trim(str)` → `String` | Removes leading and trailing whitespace. | 393 | | `truncate(str, maxLength, mode, suffix)` → `String` | Truncates text using the provided mode and optional suffix. | 394 | 395 | **Example** 396 | 397 | ```html 398 | <isprint value="${StringUtils.pad('abc', 5)}" /> 399 | ``` 400 | 401 | ### `dw.web.URLUtils` 402 | 403 | `URLUtils` is also pre-imported. Use it for URL generation instead of manual string concatenation. 404 | 405 | | Method | Description | 406 | | --- | --- | 407 | | `abs(action, ...namesAndParams)` → `URL` | Generates an absolute URL using protocol and host from the calling request. | 408 | | `http(action, ...namesAndParams)` → `URL` | Generates an absolute HTTP URL using site preferences when available. | 409 | | `https(action, ...namesAndParams)` → `URL` | Generates an absolute HTTPS URL; respects secure host preferences. | 410 | | `httpsWebRoot()` → `URL` | Returns an absolute HTTPS web root URL for static asset references. | 411 | | `httpWebRoot()` → `URL` | Returns an absolute HTTP web root URL for static asset references. | 412 | | `url(action, ...namesAndParams)` → `URL` | Generates a relative URL. | 413 | | `webRoot()` → `URL` | Returns a relative web root URL for referencing static media. | 414 | 415 | **Example** 416 | 417 | ```html 418 | <form 419 | action="${URLUtils.httpsContinue()}" 420 | method="post" 421 | id="${pdict.CurrentForms.login.login.htmlName}" 422 | > 423 | </form> 424 | ``` 425 | 426 | --- 427 | 428 | ## ISML Expressions 429 | 430 | ISML expressions let templates embed storefront logic and data access inline, using syntax that mirrors JavaScript expressions. They are the primary way to render data from the Pipeline Dictionary (`pdict`) without resorting to `<isscript>` blocks. 431 | 432 | ### Expression Basics 433 | 434 | - **Syntax**: `${ ... }` where `${` begins the expression and `}` ends it. 435 | - **Scope**: Expressions evaluate in the context of template variables (`pdict`, `request`, `session`, etc.). 436 | - **Usage**: Place expressions directly in markup or inside tag attributes. Always ensure the data you reference has been added to `pdict` (by the controller, decorator, or include). 437 | 438 | #### Common Pattern 439 | 440 | ```html 441 | ${pdict.<KeyAttributeName>} 442 | ``` 443 | 444 | ### Expression Examples 445 | 446 | ```html 447 | <!-- Attribute value --> 448 | <isprint value="${pdict.Product.name}" /> 449 | 450 | <!-- Method call on a pdict object --> 451 | "${pdict.Product.getLongDescription() != null}" 452 | 453 | <!-- Using URL helper within an attribute --> 454 | <form 455 | action="${URLUtils.continueURL()}" 456 | method="post" 457 | id="${pdict.CurrentForms.cart.htmlName}" 458 | > 459 | </form> 460 | ``` 461 | 462 | ### Additional Quick References 463 | 464 | | Expression | Description | 465 | | --- | --- | 466 | | `${pdict.Product}` | References the current product object. | 467 | | `${pdict.Content.template}` | Accesses the content asset template attribute. | 468 | | `${pdict.ProductPrices}` | Outputs the product prices data structure placed in `pdict`. | 469 | | `${pdict.Order.orderNo}` | Outputs the current order number. | 470 | 471 | ### Protected `</body>` Tag 472 | 473 | The literal string `</body>` is reserved by the ISML parser. Do **not** include it in comments or inline scripts. If you need to emit the closing body tag from JavaScript, obfuscate it: 474 | 475 | ```javascript 476 | var endBodyIndex = markup.indexOf('</bo' + 'dy>'); 477 | ``` 478 | 479 | --- 480 | 481 | ### 4. Essential ISML Tags & Usage 482 | Renumbered after expanded section. 483 | 484 | ### Conditional Logic 485 | 486 | ```html 487 | <isif condition="${pdict.product.available}"> 488 | <span class="in-stock">In Stock</span> 489 | <iselseif condition="${pdict.product.preorderAvailable}"> 490 | <span class="preorder">Available for Pre-order</span> 491 | <iselse> 492 | <span class="out-of-stock">Out of Stock</span> 493 | </isif> 494 | ``` 495 | 496 | ### Loops with Status 497 | 498 | ```html 499 | <isloop items="${pdict.productSearch.productHits}" var="productHit" status="loopstate"> 500 | <div class="product-item ${loopstate.first ? 'first' : ''} ${loopstate.last ? 'last' : ''}"> 501 | <!-- Product content --> 502 | </div> 503 | 504 | <isif condition="${loopstate.count % 3 === 0 && !loopstate.last}"> 505 | </div><div class="row"> <!-- Start new row every 3 items --> 506 | </isif> 507 | </isloop> 508 | ``` 509 | 510 | ### Variable Setting 511 | 512 | Use `<isset>` sparingly and only for simple template variables: 513 | 514 | ```html 515 | <isset name="showRatings" value="${pdict.product.ratingsEnabled && pdict.product.reviews.count > 0}" scope="page" /> 516 | 517 | <isif condition="${showRatings}"> 518 | <isinclude template="product/components/reviews/reviewsSection" /> 519 | </isif> 520 | ``` 521 | 522 | ## Performance Optimization 523 | 524 | ### Caching Strategy 525 | 526 | **Controller-based caching** (preferred): 527 | ```javascript 528 | // In controller 529 | server.get('Show', cache.applyDefaultCache, function (req, res, next) { 530 | // Controller logic 531 | }); 532 | ``` 533 | 534 | **Template-based caching** (for personalized content, also preferred in the controller): 535 | ```html 536 | <iscache type="relative" hour="1" varyby="price_promotion" /> 537 | ``` 538 | 539 | ### Efficient Asset Loading 540 | 541 | ```html 542 | <isscript> 543 | var assets = require('*/cartridge/scripts/assets.js'); 544 | assets.addCss('/css/product.css'); 545 | assets.addJs('/js/product.js'); 546 | </isscript> 547 | ``` 548 | 549 | ## Localization Best Practices 550 | 551 | ### Simple Text Resources 552 | 553 | ```html 554 | <!-- Properties file: checkout.properties --> 555 | <!-- label.billing.address=Billing Address --> 556 | 557 | <label class="form-control-label"> 558 | ${Resource.msg('label.billing.address', 'checkout', 'Billing Address')} 559 | </label> 560 | ``` 561 | 562 | ### Parameterized Messages 563 | 564 | ```html 565 | <!-- Properties file: cart.properties --> 566 | <!-- items.in.cart=You have {0} items totaling {1} --> 567 | 568 | <div class="cart-summary"> 569 | ${Resource.msgf('items.in.cart', 'cart', null, pdict.basket.numItems, pdict.basket.subTotal)} 570 | </div> 571 | ``` 572 | 573 | ## Common Patterns & Examples 574 | 575 | ### 1. Form Rendering with Validation 576 | 577 | ```html 578 | <form action="${URLUtils.url('Account-SubmitRegistration')}" method="post"> 579 | <input type="hidden" name="${pdict.csrf.tokenName}" value="${pdict.csrf.token}"/> 580 | 581 | <div class="form-group ${pdict.forms.profile.customer.firstname.invalid ? 'is-invalid' : ''}"> 582 | <label for="registration-form-fname"> 583 | ${Resource.msg('field.customer.firstname', 'registration', null)} 584 | </label> 585 | <input type="text" 586 | class="form-control" 587 | id="registration-form-fname" 588 | name="${pdict.forms.profile.customer.firstname.htmlName}" 589 | value="${pdict.forms.profile.customer.firstname.htmlValue}" 590 | <isprint value="${pdict.forms.profile.customer.firstname.attributes}" encoding="off" />> 591 | 592 | <isif condition="${pdict.forms.profile.customer.firstname.invalid}"> 593 | <div class="invalid-feedback"> 594 | <isprint value="${pdict.forms.profile.customer.firstname.error}" /> 595 | </div> 596 | </isif> 597 | </div> 598 | </form> 599 | ``` 600 | 601 | ### 2. Product Grid with Pagination 602 | 603 | ```html 604 | <div class="search-results"> 605 | <div class="row product-grid"> 606 | <isloop items="${pdict.productSearch.productHits}" var="productHit"> 607 | <div class="col-6 col-sm-4 col-lg-3"> 608 | <isset name="product" value="${productHit.product}" scope="page" /> 609 | <isinclude template="product/components/productTile" /> 610 | </div> 611 | </isloop> 612 | </div> 613 | 614 | <isif condition="${pdict.productSearch.count > pdict.productSearch.pageSize}"> 615 | <isinclude template="search/components/pagination" /> 616 | </isif> 617 | </div> 618 | ``` 619 | 620 | ### 3. Responsive Image Handling 621 | 622 | ```html 623 | <picture class="product-image"> 624 | <source media="(max-width: 544px)" 625 | srcset="${pdict.product.images.small[0].url}"> 626 | <source media="(max-width: 768px)" 627 | srcset="${pdict.product.images.medium[0].url}"> 628 | <img src="${pdict.product.images.large[0].url}" 629 | alt="${pdict.product.images.large[0].alt}" 630 | title="${pdict.product.images.large[0].title}" 631 | class="img-fluid"> 632 | </picture> 633 | ``` 634 | 635 | ## Error Handling in Templates 636 | 637 | ### Defensive Programming 638 | 639 | ```html 640 | <isif condition="${pdict.product && pdict.product.available}"> 641 | <div class="product-availability"> 642 | <isif condition="${pdict.product.availabilityModel.availability > 0}"> 643 | <span class="in-stock"> 644 | ${Resource.msgf('label.quantity.in.stock', 'product', null, pdict.product.availabilityModel.availability)} 645 | </span> 646 | <iselse> 647 | <span class="limited-stock"> 648 | ${Resource.msg('label.limited.availability', 'product', null)} 649 | </span> 650 | </isif> 651 | </div> 652 | </isif> 653 | ``` 654 | 655 | ### Graceful Degradation 656 | 657 | ```html 658 | <div class="product-reviews"> 659 | <isif condition="${pdict.product.reviews && pdict.product.reviews.count > 0}"> 660 | <div class="reviews-summary"> 661 | <span class="review-count">${pdict.product.reviews.count}</span> 662 | <isinclude template="product/components/reviewStars" /> 663 | </div> 664 | <iselse> 665 | <div class="no-reviews"> 666 | ${Resource.msg('label.no.reviews', 'product', 'No reviews yet')} 667 | </div> 668 | </isif> 669 | </div> 670 | ``` 671 | 672 | ## Testing & Debugging 673 | 674 | ### Template Comments for Development 675 | 676 | ```html 677 | <iscomment> 678 | TODO: Implement wishlist functionality 679 | Controller needs to pass wishlist status in product model 680 | </iscomment> 681 | 682 | <iscomment> 683 | Debug: Product ID = ${pdict.product.id} 684 | Available = ${pdict.product.available} 685 | Price = ${pdict.product.price.sales.value} 686 | </iscomment> 687 | ``` 688 | 689 | ### Conditional Debug Output 690 | 691 | ```html 692 | <isif condition="${dw.system.System.getInstanceType() === dw.system.System.DEVELOPMENT_SYSTEM}"> 693 | <div class="debug-info" style="background: yellow; padding: 10px;"> 694 | <strong>Debug Info:</strong><br> 695 | Product ID: ${pdict.product.id}<br> 696 | Template: product/productDetails.isml<br> 697 | Timestamp: ${new Date()} 698 | </div> 699 | </isif> 700 | ``` 701 | 702 | ## Template Decoration and Layout 703 | 704 | ### The `<isreplace>` Element 705 | 706 | The `<isreplace>` element is a crucial component of SFCC's template decoration system. It identifies where the decorated content should be included within a decorator template. 707 | 708 | #### Syntax 709 | ```html 710 | <isreplace/> 711 | ``` 712 | 713 | #### Purpose and Behavior 714 | 715 | The decorator template uses `<isreplace/>` to specify the insertion point for decorated content. Understanding its behavior is essential for proper template architecture: 716 | 717 | - **Single `<isreplace/>` (typical)**: The decorated content is inserted at the location of the tag 718 | - **Multiple `<isreplace/>` tags**: The decorated content is duplicated at each tag location 719 | - **Zero `<isreplace/>` tags**: The decorated content is effectively omitted from the output 720 | 721 | #### Example 1: Basic Template Decoration 722 | 723 | **Rendering Template (uses decorator):** 724 | ```html 725 | <isset name="DecoratorTemplate" value="common/layout/page" scope="page"/> 726 | <isdecorate template="${DecoratorTemplate}"> 727 | <div class="product-details"> 728 | <h1>${pdict.product.displayName}</h1> 729 | <div class="price">${pdict.product.price.sales.formatted}</div> 730 | </div> 731 | </isdecorate> 732 | ``` 733 | 734 | **Decorator Template (`common/layout/page.isml`):** 735 | ```html 736 | <!DOCTYPE html> 737 | <html> 738 | <head> 739 | <title>${pdict.pageTitle}</title> 740 | <meta charset="UTF-8"> 741 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 742 | </head> 743 | <body> 744 | <div id="page-container"> 745 | <isinclude template="components/header" /> 746 | 747 | <main id="main-content"> 748 | <div class="content-wrapper"> 749 | <isreplace/> 750 | </div> 751 | </main> 752 | 753 | <isinclude template="components/footer" /> 754 | </div> 755 | </body> 756 | </html> 757 | ``` 758 | 759 | #### Example 2: Multiple `<isreplace/>` Tags 760 | 761 | **Decorator Template with Sidebar:** 762 | ```html 763 | <div class="layout-two-column"> 764 | <aside class="sidebar"> 765 | <div class="sidebar-content"> 766 | <isreplace/> 767 | </div> 768 | </aside> 769 | 770 | <main class="main-content"> 771 | <div class="content-area"> 772 | <isreplace/> 773 | </div> 774 | </main> 775 | </div> 776 | ``` 777 | 778 | *In this example, the decorated content appears in both the sidebar and main content areas.* 779 | 780 | #### Example 3: Conditional Content Placement 781 | 782 | **Advanced Decorator Pattern:** 783 | ```html 784 | <div class="page-layout"> 785 | <isinclude template="components/breadcrumb" /> 786 | 787 | <isif condition="${pdict.showInSidebar}"> 788 | <div class="with-sidebar"> 789 | <main class="content"> 790 | <isreplace/> 791 | </main> 792 | <aside class="sidebar"> 793 | <isinclude template="components/relatedProducts" /> 794 | </aside> 795 | </div> 796 | <iselse> 797 | <main class="full-width"> 798 | <isreplace/> 799 | </main> 800 | </isif> 801 | </div> 802 | ``` 803 | 804 | #### Best Practices for `<isreplace>` 805 | 806 | 1. **Single Replacement Point**: Use one `<isreplace/>` per decorator for clarity and maintainability 807 | 2. **Semantic Placement**: Position `<isreplace/>` within semantic HTML elements (`<main>`, `<section>`, etc.) 808 | 3. **CSS Class Structure**: Wrap `<isreplace/>` in containers with appropriate CSS classes for styling 809 | 4. **Documentation**: Comment complex decorator patterns to explain the layout structure 810 | 811 | #### Common Patterns 812 | 813 | **Standard Page Layout:** 814 | ```html 815 | <isdecorate template="common/layout/page"> 816 | <!-- Page-specific content goes here --> 817 | </isdecorate> 818 | ``` 819 | 820 | **Modal/Dialog Layout:** 821 | ```html 822 | <isdecorate template="common/layout/modal"> 823 | <!-- Modal content goes here --> 824 | </isdecorate> 825 | ``` 826 | 827 | **Email Template Layout:** 828 | ```html 829 | <isdecorate template="common/layout/email"> 830 | <!-- Email content goes here --> 831 | </isdecorate> 832 | ``` 833 | 834 | ## SFRA Base Templates Architecture 835 | 836 | Understanding SFRA's base template structure is crucial for effective storefront development. SFRA provides a well-organized hierarchy of layout templates, page-specific templates, and reusable components that work together to create consistent user experiences. 837 | 838 | **IMPORTANT**: SFRA uses **Bootstrap 4** as its foundational CSS framework, providing responsive grid systems, utility classes, and component styling throughout all templates. Understanding Bootstrap classes and responsive breakpoints is essential for effective SFRA development. 839 | 840 | ### Core Layout Templates 841 | 842 | SFRA uses a **three-tier layout system** with base layouts that define the overall page structure: 843 | 844 | #### 1. Main Page Layout (`common/layout/page.isml`) 845 | 846 | The primary layout for most storefront pages including homepage, product details, search results, and category pages: 847 | 848 | ```html 849 | <iscontent type="text/html" charset="UTF-8" compact="true"/> 850 | <isinclude template="/components/modules" sf-toolkit="off" /> 851 | 852 | <!DOCTYPE html> 853 | <html lang="${require('dw/util/Locale').getLocale(request.getLocale()).getLanguage()}"> 854 | <head> 855 | <!--[if gt IE 9]><!--> 856 | <isinclude sf-toolkit="off" template="/common/scripts" /> 857 | <!--<![endif]--> 858 | <isinclude template="/common/htmlHead" /> 859 | <isif condition="${pdict.canonicalUrl}" > 860 | <link rel="canonical" href="${pdict.canonicalUrl}"/> 861 | </isif> 862 | <isactivedatahead/> 863 | <isinclude template="/components/schema" /> 864 | </head> 865 | <body> 866 | ${dw.system.HookMgr.callHook('app.template.beforeHeader', 'beforeHeader', pdict) || ''} 867 | 868 | <div class="page" data-action="${pdict.action}" data-querystring="${pdict.queryString}" > 869 | <isinclude template="/components/header/pageHeader" /> 870 | <div role="main" id="maincontent"> 871 | <isreplace/> 872 | </div> 873 | <isinclude template="/components/footer/pageFooter" /> 874 | </div> 875 | <div class="error-messaging"></div> 876 | <div class="modal-background"></div> 877 | <iscontentasset aid="cookie_hint" /> 878 | <!--[if lt IE 10]> 879 | <isinclude sf-toolkit="off" template="/common/scripts" /> 880 | <![endif]--> 881 | <iscomment> 882 | hook for Marketing Cloud connector & other integration which need to inject 883 | logic at the page end 884 | IMPORTANT: Note that this hook will be called to cached as well as uncached pages 885 | which means you need to put privacy information into another remote include 886 | </iscomment> 887 | ${dw.system.HookMgr.callHook('app.template.afterFooter', 'afterFooter', pdict) || ''} 888 | <isinclude url="${URLUtils.url('ConsentTracking-Check')}"/> 889 | </body> 890 | </html> 891 | ``` 892 | 893 | **Key Features:** 894 | - **Full navigation header**: Includes main menu, search, and account links via `pageHeader` 895 | - **Hook integration**: `beforeHeader` and `afterFooter` hooks for customization 896 | - **SEO elements**: Canonical URLs, structured data, meta tags 897 | - **Accessibility**: Proper ARIA roles and semantic HTML 898 | - **Analytics support**: ActiveData integration 899 | - **Error handling**: Error messaging and modal background containers 900 | 901 | #### 2. Checkout Layout (`common/layout/checkout.isml`) 902 | 903 | Complete layout for checkout process with minimal navigation header: 904 | 905 | ```html 906 | <iscontent type="text/html" charset="UTF-8" compact="true"/> 907 | <isinclude template="/components/modules" sf-toolkit="off" /> 908 | 909 | <!DOCTYPE html> 910 | <html lang="${require('dw/util/Locale').getLocale(request.getLocale()).getLanguage()}"> 911 | <head> 912 | <!--[if gt IE 9]><!--> 913 | <isinclude sf-toolkit="off" template="/common/scripts" /> 914 | <!--<![endif]--> 915 | <isinclude template="/common/htmlHead" /> 916 | <isactivedatahead/> 917 | </head> 918 | <body> 919 | ${dw.system.HookMgr.callHook('app.template.beforeHeader', 'beforeHeader', pdict) || ''} 920 | 921 | <div class="page"> 922 | <isinclude template="/components/header/pageHeaderNomenu" /> 923 | <div role="main" id="maincontent"> 924 | <isreplace/> 925 | </div> 926 | <isinclude template="/components/footer/pageFooter" /> 927 | </div> 928 | <!--[if lt IE 10]> 929 | <isinclude sf-toolkit="off" template="/common/scripts" /> 930 | <![endif]--> 931 | 932 | <iscomment> 933 | hook for Marketing Cloud connector & other integration which need to inject 934 | logic at the page end 935 | IMPORTANT: Note that this hook will be called to cached as well as uncached pages 936 | which means you need to put privacy information into another remote include 937 | </iscomment> 938 | ${dw.system.HookMgr.callHook('app.template.afterFooter', 'afterFooter', pdict) || ''} 939 | 940 | <isinclude url="${URLUtils.url('ConsentTracking-Check')}"/> 941 | </body> 942 | </html> 943 | ``` 944 | 945 | **Key Features:** 946 | - **Simplified header**: `pageHeaderNomenu` without main navigation menu 947 | - **Secure context**: HTTPS enforcement and minimal external dependencies 948 | - **Same structure as main layout**: Maintains consistency while removing distractions 949 | - **Hook support**: Full hook integration for customization 950 | - **Accessibility**: Maintains ARIA roles and semantic structure 951 | 952 | #### 3. Page Designer Store Layout (`common/layout/pdStorePage.isml`) 953 | 954 | Specialized layout for Page Designer store pages with campaign banner support: 955 | 956 | ```html 957 | <iscontent type="text/html" charset="UTF-8" compact="true"/> 958 | <isinclude template="/components/modules" sf-toolkit="off" /> 959 | 960 | <!-- Include Page Designer Campaign Banner JavaScript and Styles only once here rather than at component level. --> 961 | <!-- There should only be one Campagin Banner added on a PD page. Multiple Banners is unsupported at the moment. --> 962 | 963 | <isif condition="${pdict.resetEditPDMode}"> 964 | <script> var resetCampaignBannerSessionToken = true; </script> 965 | </isif> 966 | 967 | <isscript> 968 | var assets = require('*/cartridge/scripts/assets.js'); 969 | assets.addCss('/css/experience/components/commerceAssets/campaignBanner.css'); 970 | assets.addJs('/js/campaignBanner.js'); 971 | </isscript> 972 | 973 | <!DOCTYPE html> 974 | <html lang="${require('dw/util/Locale').getLocale(request.getLocale()).getLanguage()}"> 975 | <head> 976 | <!--[if gt IE 9]><!--> 977 | <isinclude sf-toolkit="off" template="/common/scripts" /> 978 | <!--<![endif]--> 979 | <isinclude template="/common/htmlHead" /> 980 | <isif condition="${pdict.canonicalUrl}" > 981 | <link rel="canonical" href="${pdict.canonicalUrl}"/> 982 | </isif> 983 | <isactivedatahead/> 984 | <isinclude template="/components/schema" /> 985 | </head> 986 | <body> 987 | ${dw.system.HookMgr.callHook('app.template.beforeHeader', 'beforeHeader', pdict) || ''} 988 | 989 | <div class="page" data-action="${pdict.action}" data-querystring="${pdict.queryString}" > 990 | <isinclude template="/components/header/pdStorePageHeader" /> 991 | <div role="main" id="maincontent"> 992 | <isreplace/> 993 | </div> 994 | <isinclude template="/components/footer/pageFooter" /> 995 | </div> 996 | <div class="error-messaging"></div> 997 | <div class="modal-background"></div> 998 | <iscontentasset aid="cookie_hint" /> 999 | <!--[if lt IE 10]> 1000 | <isinclude sf-toolkit="off" template="/common/scripts" /> 1001 | <![endif]--> 1002 | <iscomment> 1003 | hook for Marketing Cloud connector & other integration which need to inject 1004 | logic at the page end 1005 | IMPORTANT: Note that this hook will be called to cached as well as uncached pages 1006 | which means you need to put privacy information into another remote include 1007 | </iscomment> 1008 | ${dw.system.HookMgr.callHook('app.template.afterFooter', 'afterFooter', pdict) || ''} 1009 | <isinclude url="${URLUtils.url('ConsentTracking-Check')}"/> 1010 | </body> 1011 | </html> 1012 | ``` 1013 | 1014 | **Key Features:** 1015 | - **Page Designer header**: Uses `pdStorePageHeader` for Page Designer-specific navigation 1016 | - **Campaign banner support**: Includes campaign banner CSS and JavaScript assets 1017 | - **Edit mode support**: Handles Page Designer edit mode with session token reset 1018 | - **Same core structure**: Maintains consistency with main layout 1019 | - **SEO and analytics**: Full SEO and analytics support 1020 | 1021 | ### SFRA Page Template Patterns 1022 | 1023 | #### Homepage Template Structure 1024 | 1025 | ```html 1026 | <isdecorate template="common/layout/page"> 1027 | <isscript> 1028 | var assets = require('*/cartridge/scripts/assets.js'); 1029 | assets.addJs('/js/productTile.js'); 1030 | assets.addCss('/css/homePage.css'); 1031 | </isscript> 1032 | 1033 | <div class="home-main homepage"> 1034 | <isslot id="home-main-m" description="Main home page slot." context="global" /> 1035 | </div> 1036 | 1037 | <div class="container home-categories homepage"> 1038 | <div class="row home-main-categories no-gutters"> 1039 | <isslot id="home-categories-m" description="Categories slots on the home page." context="global" /> 1040 | </div> 1041 | </div> 1042 | 1043 | <div class="container home-product-tiles homepage"> 1044 | <div class="hp-product-grid" itemtype="http://schema.org/SomeProducts" itemid="#product"> 1045 | <isslot id="home-products-m" description="Product tiles on the home page." context="global" /> 1046 | </div> 1047 | </div> 1048 | </isdecorate> 1049 | ``` 1050 | 1051 | **Pattern Highlights:** 1052 | - **Content slots**: Uses Business Manager-configurable content slots 1053 | - **Asset management**: Page-specific CSS and JavaScript loading 1054 | - **SEO structure**: Schema.org markup for products 1055 | - **Responsive design**: Bootstrap grid system integration 1056 | 1057 | #### Product Detail Page Structure 1058 | 1059 | ```html 1060 | <isdecorate template="common/layout/page"> 1061 | <isscript> 1062 | var assets = require('*/cartridge/scripts/assets'); 1063 | assets.addJs('/js/productDetail.js'); 1064 | assets.addCss('/css/product/detail.css'); 1065 | </isscript> 1066 | 1067 | <isset name="product" value="${pdict.product}" scope="page" /> 1068 | <isset name="isQuickView" value="${false}" scope="page" /> 1069 | <isset name="isProductSet" value="${pdict.product.productType === 'set'}" scope="page" /> 1070 | 1071 | <isobject object="${product.raw}" view="detail"> 1072 | <div class="container product-detail product-wrapper" data-pid="${product.id}"> 1073 | <div class="row"> 1074 | <div class="col-12"> 1075 | <div class="product-breadcrumb d-md-none"> 1076 | <isinclude template="components/breadcrumbs/pageBreadcrumbs"/> 1077 | </div> 1078 | 1079 | <div class="row"> 1080 | <div class="d-md-none col-sm-12"> 1081 | <h1 class="product-name">${product.productName}</h1> 1082 | </div> 1083 | </div> 1084 | </div> 1085 | </div> 1086 | <!-- Product images, details, add to cart, etc. --> 1087 | </div> 1088 | </isobject> 1089 | </isdecorate> 1090 | ``` 1091 | 1092 | **Pattern Highlights:** 1093 | - **Product context**: `<isobject>` tag for product data binding 1094 | - **Variable scoping**: Page-scoped variables for template logic 1095 | - **Mobile-first design**: Responsive breakpoint handling 1096 | - **SEO optimization**: Structured product data and breadcrumbs 1097 | 1098 | #### Cart Page Structure 1099 | 1100 | ```html 1101 | <isdecorate template="common/layout/page"> 1102 | <isscript> 1103 | var assets = require('*/cartridge/scripts/assets.js'); 1104 | assets.addCss('/css/cart.css'); 1105 | </isscript> 1106 | 1107 | <isif condition="${pdict.reportingURLs && pdict.reportingURLs.length}"> 1108 | <isinclude template="reporting/reportingUrls" /> 1109 | </isif> 1110 | 1111 | <div class="cart-error-messaging cart-error"> 1112 | <isif condition="${pdict.valid.error && pdict.items.length !== 0}"> 1113 | <div class="alert alert-danger alert-dismissible valid-cart-error fade show" role="alert"> 1114 | <button type="button" class="close" data-dismiss="alert" aria-label="Close"> 1115 | <span aria-hidden="true">×</span> 1116 | </button> 1117 | ${pdict.valid.message} 1118 | </div> 1119 | </isif> 1120 | </div> 1121 | 1122 | <div class="container"> 1123 | <h1 class="page-title">${Resource.msg('title.cart','cart',null)}</h1> 1124 | <div class="row cart-header"> 1125 | <div class="col-sm-4 hidden-xs-down"> 1126 | <a class="continue-shopping-link" href="${URLUtils.url('Home-Show')}" title="${Resource.msg('link.continue.shopping','cart',null)}"> 1127 | ${Resource.msg('link.continue.shopping','cart',null)} 1128 | </a> 1129 | </div> 1130 | <div class="col-sm-3 text-center"> 1131 | <h2 class="number-of-items">${Resource.msgf('label.number.items.in.cart','cart', null, pdict.numItems)}</h2> 1132 | </div> 1133 | <div class="col-sm-5 text-right hidden-xs-down"> 1134 | <div> 1135 | <span>${Resource.msg('info.need.help','cart',null)}</span> 1136 | <span><a class="help-phone-number" href="tel:${Resource.msg('info.phone.number','common',null)}">${Resource.msg('info.phone.number','common',null)}</a></span> 1137 | </div> 1138 | </div> 1139 | </div> 1140 | <hr class="no-margin-top"> 1141 | </div> 1142 | 1143 | <isif condition="${pdict.items.length === 0}"> 1144 | <div class="container cart-empty"> 1145 | <div class="row"> 1146 | <div class="col-12 text-center"> 1147 | <h1>${Resource.msg('info.cart.empty.msg','cart',null)}</h1> 1148 | </div> 1149 | </div> 1150 | </div> 1151 | <iselse/> 1152 | <div class="container cart cart-page"> 1153 | <div class="row"> 1154 | <!---product cards---> 1155 | <div class="col-sm-7 col-md-8"> 1156 | <isloop items="${pdict.items}" var="lineItem"> 1157 | <isif condition="${lineItem.productType === 'bundle'}"> 1158 | <isinclude template="cart/productCard/cartBundleCard" /> 1159 | <iselse/> 1160 | <isif condition="${lineItem.noProduct === true}"> 1161 | <isinclude template="cart/productCard/uncategorizedCartProductCard" /> 1162 | <iselse/> 1163 | <isinclude template="cart/productCard/cartProductCard" /> 1164 | </isif> 1165 | </isif> 1166 | </isloop> 1167 | <isinclude template="cart/cartApproachingDiscount" /> 1168 | </div> 1169 | <!---totals, and checkout actions---> 1170 | <div class="col-sm-5 col-md-4 totals"> 1171 | <isinclude template="cart/cartPromoCode" /> 1172 | <div class="coupons-and-promos"> 1173 | <isinclude template="cart/cartCouponDisplay" /> 1174 | </div> 1175 | <div class="row"> 1176 | <isinclude template="cart/cartShippingMethodSelection" /> 1177 | </div> 1178 | <isinclude template="cart/cartTotals" /> 1179 | <div class="row"> 1180 | <div class="col-12 checkout-continue"> 1181 | <isinclude template="cart/checkoutButtons" /> 1182 | </div> 1183 | </div> 1184 | </div> 1185 | </div> 1186 | <isinclude template="cart/cartRemoveProductModal"/> 1187 | </div> 1188 | 1189 | <isinclude template="cart/cartRemoveCouponModal"/> 1190 | </isif> 1191 | <div class="container"> 1192 | <isslot id="cart-recommendations-m" description="Recommended products" context="global" /> 1193 | </div> 1194 | </isdecorate> 1195 | ``` 1196 | 1197 | **Pattern Highlights:** 1198 | - **Error handling**: Comprehensive validation error display with dismissible alerts 1199 | - **Analytics integration**: Reporting URL includes for tracking 1200 | - **User feedback**: Alert system for cart validation messages 1201 | - **Empty cart state**: Dedicated empty cart display with clear messaging 1202 | - **Product cards**: Dynamic product card rendering based on product type (bundle, regular, uncategorized) 1203 | - **Responsive layout**: Bootstrap grid system for desktop and mobile layouts 1204 | - **Promotional features**: Coupon display, promo codes, and approaching discount messaging 1205 | - **Checkout integration**: Seamless transition to checkout with proper button placement 1206 | - **Recommendations**: Content slot for product recommendations 1207 | - **Modal support**: Product removal and coupon removal modals 1208 | 1209 | #### Account Page Structure 1210 | 1211 | ```html 1212 | <isdecorate template="common/layout/page"> 1213 | <isscript> 1214 | var assets = require('*/cartridge/scripts/assets.js'); 1215 | assets.addJs('/js/account.js'); 1216 | assets.addCss('/css/account.css'); 1217 | </isscript> 1218 | 1219 | <div class="hero slant-down account-hero"> 1220 | <div class="container"> 1221 | <h1 class="page-title">${Resource.msg('page.heading.myaccount','account',null)}</h1> 1222 | </div> 1223 | </div> 1224 | 1225 | <div class="container"> 1226 | <div class="row justify-content-center"> 1227 | <div class="col"> 1228 | <div class="myaccount-row"> 1229 | <!-- Account Navigation --> 1230 | <div class="col-sm-3"> 1231 | <div class="account-nav"> 1232 | <isinclude template="account/accountNav" /> 1233 | </div> 1234 | </div> 1235 | 1236 | <!-- Account Content --> 1237 | <div class="col-sm-9"> 1238 | <div class="card"> 1239 | <div class="card-header"> 1240 | <h2 class="pull-left">${pdict.pageTitle}</h2> 1241 | </div> 1242 | <div class="card-body card-body-positioning"> 1243 | <!-- Page-specific content will be inserted here --> 1244 | <isif condition="${pdict.addressBook}"> 1245 | <isinclude template="account/addressBook" /> 1246 | <iselseif condition="${pdict.orderHistory}"> 1247 | <isinclude template="account/orderHistory" /> 1248 | <iselseif condition="${pdict.profile}"> 1249 | <isinclude template="account/profile" /> 1250 | <iselseif condition="${pdict.paymentInstruments}"> 1251 | <isinclude template="account/payment/paymentMethods" /> 1252 | <iselse> 1253 | <isinclude template="account/dashboard" /> 1254 | </isif> 1255 | </div> 1256 | </div> 1257 | </div> 1258 | </div> 1259 | </div> 1260 | </div> 1261 | </div> 1262 | </isdecorate> 1263 | ``` 1264 | 1265 | **Pattern Highlights:** 1266 | - **Account navigation**: Consistent sidebar navigation across all account pages 1267 | - **Card-based layout**: Professional UI with card components 1268 | - **Conditional content**: Dynamic content loading based on page type 1269 | - **Responsive design**: Mobile-friendly account management interface 1270 | 1271 | #### Search Results Page Structure 1272 | 1273 | ```html 1274 | <isdecorate template="common/layout/page"> 1275 | <isscript> 1276 | var assets = require('*/cartridge/scripts/assets.js'); 1277 | assets.addJs('/js/search.js'); 1278 | assets.addCss('/css/search.css'); 1279 | </isscript> 1280 | 1281 | <div class="search-results-header"> 1282 | <div class="container"> 1283 | <div class="row"> 1284 | <div class="col-sm-12"> 1285 | <h1 class="header page-title"> 1286 | <isif condition="${pdict.productSearch.isCategorySearch}"> 1287 | ${pdict.productSearch.category.name} 1288 | <iselse> 1289 | ${Resource.msgf('heading.search.results', 'search', null, pdict.productSearch.searchKeywords)} 1290 | </isif> 1291 | </h1> 1292 | 1293 | <!-- Search Result Count --> 1294 | <div class="result-count pull-left"> 1295 | <isinclude template="search/searchResultsCount"/> 1296 | </div> 1297 | </div> 1298 | </div> 1299 | </div> 1300 | </div> 1301 | 1302 | <div class="container search-results"> 1303 | <div class="row search-nav"> 1304 | <div class="tab-content col-12"> 1305 | <div class="tab-pane container active" id="product-search-results"> 1306 | <div class="row grid-header"> 1307 | <div class="result-count col-6 col-md-9 order-2 order-md-1"> 1308 | <isinclude template="search/searchResultsCount"/> 1309 | </div> 1310 | <div class="col-6 col-md-3 order-1 order-md-2"> 1311 | <isinclude template="search/sortOrderMenu"/> 1312 | </div> 1313 | </div> 1314 | 1315 | <!-- Refinements and Product Grid --> 1316 | <div class="row"> 1317 | <!-- Refinements Sidebar --> 1318 | <div class="refinement-bar col-md-3"> 1319 | <isinclude template="search/refinements"/> 1320 | </div> 1321 | 1322 | <!-- Product Grid --> 1323 | <div class="col-sm-12 col-md-9"> 1324 | <div class="container"> 1325 | <isif condition="${pdict.productSearch.productHits.length > 0}"> 1326 | <!-- Product Grid --> 1327 | <div class="row product-grid" itemtype="http://schema.org/SomeProducts" itemid="#product"> 1328 | <isloop items="${pdict.productSearch.productHits}" var="productHit"> 1329 | <div class="col-6 col-sm-4 product-tile-wrapper"> 1330 | <isset name="product" value="${productHit.product}" scope="page" /> 1331 | <div class="product-tile" data-pid="${product.id}" data-gtm-product='${JSON.stringify(product.gtm)}'> 1332 | <isinclude template="product/productTile"/> 1333 | </div> 1334 | </div> 1335 | </isloop> 1336 | </div> 1337 | 1338 | <!-- Pagination --> 1339 | <div class="row"> 1340 | <isinclude template="search/searchResultsPagination"/> 1341 | </div> 1342 | 1343 | <!-- Show More Products --> 1344 | <div class="row"> 1345 | <div class="col-12 text-center"> 1346 | <div class="show-more btn btn-outline-primary" data-url="${pdict.productSearch.showMoreUrl}"> 1347 | ${Resource.msg('button.search.showmore', 'search', null)} 1348 | </div> 1349 | </div> 1350 | </div> 1351 | <iselse> 1352 | <!-- No Results --> 1353 | <div class="row justify-content-center"> 1354 | <div class="col"> 1355 | <div class="no-results-message"> 1356 | <h3>${Resource.msg('heading.no.results', 'search', null)}</h3> 1357 | <p>${Resource.msg('msg.no.results', 'search', null)}</p> 1358 | 1359 | <!-- Search Suggestions --> 1360 | <isif condition="${pdict.productSearch.searchSuggestions && pdict.productSearch.searchSuggestions.length > 0}"> 1361 | <div class="search-suggestions"> 1362 | <p>${Resource.msg('label.search.suggestions', 'search', null)}</p> 1363 | <ul class="suggestions-list"> 1364 | <isloop items="${pdict.productSearch.searchSuggestions}" var="suggestion"> 1365 | <li><a href="${suggestion.url}">${suggestion.value}</a></li> 1366 | </isloop> 1367 | </ul> 1368 | </div> 1369 | </isif> 1370 | </div> 1371 | </div> 1372 | </div> 1373 | </isif> 1374 | </div> 1375 | </div> 1376 | </div> 1377 | </div> 1378 | </div> 1379 | </div> 1380 | </div> 1381 | 1382 | <!-- Recently Viewed --> 1383 | <div class="container"> 1384 | <isinclude template="search/components/recentlyViewed"/> 1385 | </div> 1386 | </isdecorate> 1387 | ``` 1388 | 1389 | **Pattern Highlights:** 1390 | - **Dual search support**: Category browsing and keyword search functionality 1391 | - **Advanced filtering**: Refinement sidebar with faceted search 1392 | - **Responsive grid**: Product tiles with mobile-optimized layout 1393 | - **Pagination system**: Multiple pagination patterns including "show more" 1394 | - **No results handling**: Graceful degradation with search suggestions 1395 | 1396 | #### Category Landing Page Structure 1397 | 1398 | ```html 1399 | <isdecorate template="common/layout/page"> 1400 | <isscript> 1401 | var assets = require('*/cartridge/scripts/assets.js'); 1402 | assets.addJs('/js/categoryLanding.js'); 1403 | assets.addCss('/css/categoryLanding.css'); 1404 | </isscript> 1405 | 1406 | <!-- Category Hero Section --> 1407 | <div class="hero category-hero" 1408 | <isif condition="${pdict.category.image}">style="background-image: url('${pdict.category.image}')"</isif>> 1409 | <div class="container"> 1410 | <div class="row"> 1411 | <div class="col-12 text-center"> 1412 | <h1 class="category-title">${pdict.category.displayName}</h1> 1413 | <isif condition="${pdict.category.description}"> 1414 | <p class="category-description">${pdict.category.description}</p> 1415 | </isif> 1416 | </div> 1417 | </div> 1418 | </div> 1419 | </div> 1420 | 1421 | <!-- Breadcrumbs --> 1422 | <div class="container"> 1423 | <div class="row"> 1424 | <div class="col-12"> 1425 | <isinclude template="components/breadcrumbs/pageBreadcrumbs"/> 1426 | </div> 1427 | </div> 1428 | </div> 1429 | 1430 | <!-- Category Content Slots --> 1431 | <isif condition="${pdict.category.template && pdict.category.template !== ''}"> 1432 | <!-- Custom Category Template --> 1433 | <isinclude template="${pdict.category.template}"/> 1434 | <iselse> 1435 | <!-- Default Category Content --> 1436 | <div class="container category-content"> 1437 | <!-- Category Slots --> 1438 | <div class="category-slots"> 1439 | <isslot id="category-banner" description="Category banner content" context="category" context-object="${pdict.category.raw}" /> 1440 | </div> 1441 | 1442 | <!-- Sub-categories --> 1443 | <isif condition="${pdict.category.subCategories && pdict.category.subCategories.length > 0}"> 1444 | <div class="sub-categories"> 1445 | <div class="row"> 1446 | <div class="col-12"> 1447 | <h2 class="sub-categories-title">${Resource.msg('heading.browse.categories', 'search', null)}</h2> 1448 | </div> 1449 | </div> 1450 | <div class="row"> 1451 | <isloop items="${pdict.category.subCategories}" var="subCategory"> 1452 | <div class="col-6 col-md-4 col-lg-3"> 1453 | <div class="category-tile"> 1454 | <a href="${subCategory.url}" class="category-link"> 1455 | <isif condition="${subCategory.image}"> 1456 | <div class="category-image"> 1457 | <img src="${subCategory.image}" 1458 | alt="${subCategory.displayName}" 1459 | class="img-fluid"> 1460 | </div> 1461 | </isif> 1462 | <div class="category-info"> 1463 | <h3 class="category-name">${subCategory.displayName}</h3> 1464 | <isif condition="${subCategory.productCount}"> 1465 | <span class="product-count"> 1466 | ${Resource.msgf('label.category.product.count', 'search', null, subCategory.productCount)} 1467 | </span> 1468 | </isif> 1469 | </div> 1470 | </a> 1471 | </div> 1472 | </div> 1473 | </isloop> 1474 | </div> 1475 | </div> 1476 | </isif> 1477 | 1478 | <!-- Featured Products --> 1479 | <isif condition="${pdict.productSearch && pdict.productSearch.productHits.length > 0}"> 1480 | <div class="featured-products"> 1481 | <div class="row"> 1482 | <div class="col-12"> 1483 | <h2 class="featured-title">${Resource.msg('heading.featured.products', 'search', null)}</h2> 1484 | </div> 1485 | </div> 1486 | <div class="row product-grid"> 1487 | <isloop items="${pdict.productSearch.productHits}" var="productHit" begin="0" end="7"> 1488 | <div class="col-6 col-md-4 col-lg-3"> 1489 | <isset name="product" value="${productHit.product}" scope="page" /> 1490 | <isinclude template="product/productTile"/> 1491 | </div> 1492 | </isloop> 1493 | </div> 1494 | <div class="row"> 1495 | <div class="col-12 text-center"> 1496 | <a href="${pdict.category.url}?showAll=true" class="btn btn-primary"> 1497 | ${Resource.msg('button.view.all.products', 'search', null)} 1498 | </a> 1499 | </div> 1500 | </div> 1501 | </div> 1502 | </isif> 1503 | 1504 | <!-- Category Content Slots --> 1505 | <div class="category-content-slots"> 1506 | <isslot id="category-content" description="Category content area" context="category" context-object="${pdict.category.raw}" /> 1507 | </div> 1508 | </div> 1509 | </isif> 1510 | </isdecorate> 1511 | ``` 1512 | 1513 | **Pattern Highlights:** 1514 | - **Hero section**: Visual category introduction with background imagery 1515 | - **Content slots**: Business Manager-configurable category content 1516 | - **Sub-category navigation**: Hierarchical category browsing 1517 | - **Featured products**: Limited product showcase with view-all option 1518 | - **Flexible templating**: Support for custom category templates 1519 | 1520 | #### Login/Registration Page Structure 1521 | 1522 | ```html 1523 | <isdecorate template="common/layout/page"> 1524 | <isscript> 1525 | var assets = require('*/cartridge/scripts/assets.js'); 1526 | assets.addJs('/js/login.js'); 1527 | assets.addCss('/css/login.css'); 1528 | </isscript> 1529 | 1530 | <div class="hero slant-down login-banner"> 1531 | <h1 class="page-title">${Resource.msg('title.login.page', 'login', null)}</h1> 1532 | </div> 1533 | 1534 | <div class="container login-page"> 1535 | <div class="row justify-content-center"> 1536 | <!-- Login Form --> 1537 | <div class="col-md-6"> 1538 | <div class="card login-form-nav"> 1539 | <div class="card-header"> 1540 | <h2>${Resource.msg('heading.returning.customers', 'login', null)}</h2> 1541 | </div> 1542 | 1543 | <div class="card-body"> 1544 | <form action="${URLUtils.url('Account-Login')}" class="login" method="post" name="login-form"> 1545 | <input type="hidden" name="${pdict.csrf.tokenName}" value="${pdict.csrf.token}"/> 1546 | 1547 | <!-- Email Field --> 1548 | <div class="form-group 1549 | <isif condition="${pdict.loginForm.username.invalid}">is-invalid</isif>"> 1550 | <label class="form-control-label" for="login-form-email"> 1551 | <isprint value="${pdict.loginForm.username.label}" encoding="htmlcontent" /> 1552 | </label> 1553 | <input type="email" 1554 | required 1555 | class="form-control" 1556 | id="login-form-email" 1557 | name="${pdict.loginForm.username.htmlName}" 1558 | value="${pdict.loginForm.username.value}" 1559 | autocomplete="email"> 1560 | <isif condition="${pdict.loginForm.username.invalid}"> 1561 | <div class="invalid-feedback"> 1562 | <isprint value="${pdict.loginForm.username.error}" /> 1563 | </div> 1564 | </isif> 1565 | </div> 1566 | 1567 | <!-- Password Field --> 1568 | <div class="form-group 1569 | <isif condition="${pdict.loginForm.password.invalid}">is-invalid</isif>"> 1570 | <label class="form-control-label" for="login-form-password"> 1571 | <isprint value="${pdict.loginForm.password.label}" encoding="htmlcontent" /> 1572 | </label> 1573 | <input type="password" 1574 | required 1575 | class="form-control" 1576 | id="login-form-password" 1577 | name="${pdict.loginForm.password.htmlName}" 1578 | autocomplete="current-password"> 1579 | <isif condition="${pdict.loginForm.password.invalid}"> 1580 | <div class="invalid-feedback"> 1581 | <isprint value="${pdict.loginForm.password.error}" /> 1582 | </div> 1583 | </isif> 1584 | </div> 1585 | 1586 | <!-- Remember Me --> 1587 | <div class="form-group custom-control custom-checkbox"> 1588 | <input type="checkbox" 1589 | class="custom-control-input" 1590 | id="rememberMe" 1591 | name="${pdict.loginForm.rememberMe.htmlName}" 1592 | value="true" 1593 | <isif condition="${pdict.loginForm.rememberMe.checked}">checked</isif>> 1594 | <label class="custom-control-label" for="rememberMe"> 1595 | ${Resource.msg('field.login.remember.me', 'login', null)} 1596 | </label> 1597 | </div> 1598 | 1599 | <!-- Login Button --> 1600 | <button type="submit" class="btn btn-primary btn-block login-btn"> 1601 | ${Resource.msg('button.text.loginform', 'login', null)} 1602 | </button> 1603 | 1604 | <!-- Forgot Password --> 1605 | <div class="forgot-password text-center"> 1606 | <a href="${URLUtils.url('Account-PasswordReset')}" title="${Resource.msg('link.login.forgotpassword', 'login', null)}"> 1607 | ${Resource.msg('link.login.forgotpassword', 'login', null)} 1608 | </a> 1609 | </div> 1610 | </form> 1611 | </div> 1612 | </div> 1613 | </div> 1614 | 1615 | <!-- Registration Form --> 1616 | <div class="col-md-6"> 1617 | <div class="card registration-form-nav"> 1618 | <div class="card-header"> 1619 | <h2>${Resource.msg('heading.new.customers', 'login', null)}</h2> 1620 | </div> 1621 | 1622 | <div class="card-body"> 1623 | <p>${Resource.msg('msg.create.account', 'login', null)}</p> 1624 | 1625 | <form action="${URLUtils.url('Account-SubmitRegistration')}" 1626 | class="registration" 1627 | method="post" 1628 | name="registration-form"> 1629 | <input type="hidden" name="${pdict.csrf.tokenName}" value="${pdict.csrf.token}"/> 1630 | 1631 | <!-- First Name --> 1632 | <div class="form-group 1633 | <isif condition="${pdict.registrationForm.customer.firstname.invalid}">is-invalid</isif>"> 1634 | <label class="form-control-label" for="registration-form-fname"> 1635 | <isprint value="${pdict.registrationForm.customer.firstname.label}" encoding="htmlcontent" /> 1636 | </label> 1637 | <input type="text" 1638 | required 1639 | class="form-control" 1640 | id="registration-form-fname" 1641 | name="${pdict.registrationForm.customer.firstname.htmlName}" 1642 | value="${pdict.registrationForm.customer.firstname.value}" 1643 | autocomplete="given-name"> 1644 | <isif condition="${pdict.registrationForm.customer.firstname.invalid}"> 1645 | <div class="invalid-feedback"> 1646 | <isprint value="${pdict.registrationForm.customer.firstname.error}" /> 1647 | </div> 1648 | </isif> 1649 | </div> 1650 | 1651 | <!-- Last Name --> 1652 | <div class="form-group 1653 | <isif condition="${pdict.registrationForm.customer.lastname.invalid}">is-invalid</isif>"> 1654 | <label class="form-control-label" for="registration-form-lname"> 1655 | <isprint value="${pdict.registrationForm.customer.lastname.label}" encoding="htmlcontent" /> 1656 | </label> 1657 | <input type="text" 1658 | required 1659 | class="form-control" 1660 | id="registration-form-lname" 1661 | name="${pdict.registrationForm.customer.lastname.htmlName}" 1662 | value="${pdict.registrationForm.customer.lastname.value}" 1663 | autocomplete="family-name"> 1664 | <isif condition="${pdict.registrationForm.customer.lastname.invalid}"> 1665 | <div class="invalid-feedback"> 1666 | <isprint value="${pdict.registrationForm.customer.lastname.error}" /> 1667 | </div> 1668 | </isif> 1669 | </div> 1670 | 1671 | <!-- Email --> 1672 | <div class="form-group 1673 | <isif condition="${pdict.registrationForm.customer.email.invalid}">is-invalid</isif>"> 1674 | <label class="form-control-label" for="registration-form-email"> 1675 | <isprint value="${pdict.registrationForm.customer.email.label}" encoding="htmlcontent" /> 1676 | </label> 1677 | <input type="email" 1678 | required 1679 | class="form-control" 1680 | id="registration-form-email" 1681 | name="${pdict.registrationForm.customer.email.htmlName}" 1682 | value="${pdict.registrationForm.customer.email.value}" 1683 | autocomplete="email"> 1684 | <isif condition="${pdict.registrationForm.customer.email.invalid}"> 1685 | <div class="invalid-feedback"> 1686 | <isprint value="${pdict.registrationForm.customer.email.error}" /> 1687 | </div> 1688 | </isif> 1689 | </div> 1690 | 1691 | <!-- Password --> 1692 | <div class="form-group 1693 | <isif condition="${pdict.registrationForm.newPasswords.newpassword.invalid}">is-invalid</isif>"> 1694 | <label class="form-control-label" for="registration-form-password"> 1695 | <isprint value="${pdict.registrationForm.newPasswords.newpassword.label}" encoding="htmlcontent" /> 1696 | </label> 1697 | <input type="password" 1698 | required 1699 | class="form-control" 1700 | id="registration-form-password" 1701 | name="${pdict.registrationForm.newPasswords.newpassword.htmlName}" 1702 | autocomplete="new-password"> 1703 | <isif condition="${pdict.registrationForm.newPasswords.newpassword.invalid}"> 1704 | <div class="invalid-feedback"> 1705 | <isprint value="${pdict.registrationForm.newPasswords.newpassword.error}" /> 1706 | </div> 1707 | </isif> 1708 | </div> 1709 | 1710 | <!-- Confirm Password --> 1711 | <div class="form-group 1712 | <isif condition="${pdict.registrationForm.newPasswords.newpasswordconfirm.invalid}">is-invalid</isif>"> 1713 | <label class="form-control-label" for="registration-form-password-confirm"> 1714 | <isprint value="${pdict.registrationForm.newPasswords.newpasswordconfirm.label}" encoding="htmlcontent" /> 1715 | </label> 1716 | <input type="password" 1717 | required 1718 | class="form-control" 1719 | id="registration-form-password-confirm" 1720 | name="${pdict.registrationForm.newPasswords.newpasswordconfirm.htmlName}" 1721 | autocomplete="new-password"> 1722 | <isif condition="${pdict.registrationForm.newPasswords.newpasswordconfirm.invalid}"> 1723 | <div class="invalid-feedback"> 1724 | <isprint value="${pdict.registrationForm.newPasswords.newpasswordconfirm.error}" /> 1725 | </div> 1726 | </isif> 1727 | </div> 1728 | 1729 | <!-- Privacy Policy --> 1730 | <div class="form-group custom-control custom-checkbox"> 1731 | <input type="checkbox" 1732 | required 1733 | class="custom-control-input" 1734 | id="add-to-email-list" 1735 | name="${pdict.registrationForm.customer.addtoemaillist.htmlName}" 1736 | value="true"> 1737 | <label class="custom-control-label" for="add-to-email-list"> 1738 | ${Resource.msg('label.registration.email.subscribe', 'login', null)} 1739 | </label> 1740 | </div> 1741 | 1742 | <!-- Create Account Button --> 1743 | <button type="submit" class="btn btn-primary btn-block create-account-btn"> 1744 | ${Resource.msg('button.createaccount.registration', 'login', null)} 1745 | </button> 1746 | </form> 1747 | </div> 1748 | </div> 1749 | </div> 1750 | </div> 1751 | </div> 1752 | </isdecorate> 1753 | ``` 1754 | 1755 | **Pattern Highlights:** 1756 | - **Dual-form layout**: Login and registration side-by-side 1757 | - **Form validation**: Client and server-side validation with error display 1758 | - **Accessibility features**: Proper form labels, autocomplete attributes 1759 | - **Security elements**: CSRF protection and password requirements 1760 | - **Mobile responsiveness**: Stacked layout on smaller screens 1761 | 1762 | ## Common Components Architecture 1763 | 1764 | SFRA organizes reusable components in the `/components/` directory: 1765 | 1766 | #### HTML Head Component (`common/htmlHead.isml`) 1767 | 1768 | ```html 1769 | <meta charset=UTF-8> 1770 | <meta http-equiv="x-ua-compatible" content="ie=edge"> 1771 | <meta name="viewport" content="width=device-width, initial-scale=1"> 1772 | 1773 | <isif condition="${dw.system.System.getInstanceType() != dw.system.System.PRODUCTION_SYSTEM}"> 1774 | <title>${pdict.CurrentPageMetaData.title} | ${Resource.msg('global.site.name', 'version', null)} | ${Resource.msg('global.version.number', 'version', null)}</title> 1775 | <iselse/> 1776 | <title><isprint value="${pdict.CurrentPageMetaData.title}" encoding="htmlcontent" /></title> 1777 | </isif> 1778 | 1779 | <meta name="description" content="${pdict.CurrentPageMetaData.description ? pdict.CurrentPageMetaData.description : Resource.msg('global.storename','common',null)}"/> 1780 | <meta name="keywords" content="${pdict.CurrentPageMetaData.keywords ? pdict.CurrentPageMetaData.keywords : Resource.msg('global.storename','common',null)}"/> 1781 | 1782 | <!-- Rule-based page meta tags --> 1783 | <isloop items="${pdict.CurrentPageMetaData.pageMetaTags}" var="pageMetaTag"> 1784 | <isif condition="${pageMetaTag.name}"> 1785 | <meta name="<isprint value="${pageMetaTag.ID}">" content="<isprint value="${pageMetaTag.content}">"> 1786 | <iselseif condition="${pageMetaTag.property}"> 1787 | <meta property="<isprint value="${pageMetaTag.ID}">" content="<isprint value="${pageMetaTag.content}">"> 1788 | </isif> 1789 | </isloop> 1790 | 1791 | <!-- Favicons --> 1792 | <link rel="icon" type="image/png" href="${URLUtils.staticURL('/images/favicons/favicon-196x196.png')}" sizes="196x196" /> 1793 | <!-- Additional favicon sizes... --> 1794 | ``` 1795 | 1796 | **Component Features:** 1797 | - **Environment awareness**: Different titles for non-production environments 1798 | - **SEO management**: Dynamic meta tags from Page Designer or manual configuration 1799 | - **Multi-device support**: Comprehensive favicon set 1800 | - **Content security**: Proper encoding for meta content 1801 | 1802 | ### SFRA Template Directory Structure 1803 | 1804 | ``` 1805 | templates/default/ 1806 | ├── common/ # Shared layout and utility templates 1807 | │ ├── layout/ 1808 | │ │ ├── page.isml # Main page layout 1809 | │ │ ├── checkout.isml # Checkout layout 1810 | │ │ └── pdStorePage.isml # Page Designer layout 1811 | │ ├── htmlHead.isml # HTML head component 1812 | │ ├── scripts.isml # JavaScript includes 1813 | │ └── consent.isml # Cookie consent 1814 | ├── components/ # Reusable UI components 1815 | │ ├── header/ # Header variations 1816 | │ ├── footer/ # Footer components 1817 | │ ├── breadcrumbs/ # Navigation breadcrumbs 1818 | │ ├── modules.isml # Module registration 1819 | │ └── schema.isml # Structured data 1820 | ├── home/ # Homepage templates 1821 | │ └── homePage.isml # Main homepage 1822 | ├── product/ # Product-related templates 1823 | │ ├── productD etails.isml # PDP template 1824 | │ ├── productTile.isml # Product tile component 1825 | │ └── quickView.isml # Quick view modal 1826 | ├── cart/ # Shopping cart templates 1827 | │ └── cart.isml # Cart page 1828 | ├── checkout/ # Checkout process templates 1829 | │ └── checkout.isml # Main checkout 1830 | ├── account/ # Customer account templates 1831 | │ ├── login.isml # Login page 1832 | │ ├── profile.isml # Account profile 1833 | │ └── orderHistory.isml # Order history 1834 | ├── search/ # Search and category templates 1835 | │ └── searchResults.isml # Search results page 1836 | └── error/ # Error page templates 1837 | ├── error.isml # Generic error page 1838 | └── notFound.isml # 404 page 1839 | ``` 1840 | 1841 | ### Best Practices for SFRA Template Development 1842 | 1843 | #### 1. Follow the Layout Hierarchy 1844 | 1845 | ```html 1846 | <!-- ✅ Use appropriate base layout --> 1847 | <isdecorate template="common/layout/page"> 1848 | <!-- Standard page content --> 1849 | </isdecorate> 1850 | 1851 | <!-- ✅ Use checkout layout for secure pages --> 1852 | <isdecorate template="common/layout/checkout"> 1853 | <!-- Checkout/payment content --> 1854 | </isdecorate> 1855 | ``` 1856 | 1857 | #### 2. Leverage Component Reusability 1858 | 1859 | ```html 1860 | <!-- ✅ Include reusable components --> 1861 | <isinclude template="components/breadcrumbs/pageBreadcrumbs" /> 1862 | <isinclude template="components/product/productTile" /> 1863 | ``` 1864 | 1865 | #### 3. Implement Proper Asset Management 1866 | 1867 | ```html 1868 | <!-- ✅ Page-specific assets --> 1869 | <isscript> 1870 | var assets = require('*/cartridge/scripts/assets.js'); 1871 | assets.addCss('/css/product/detail.css'); 1872 | assets.addJs('/js/productDetail.js'); 1873 | </isscript> 1874 | ``` 1875 | 1876 | #### 4. Use Semantic HTML Structure 1877 | 1878 | ```html 1879 | <!-- ✅ Semantic and accessible markup --> 1880 | <main id="maincontent" role="main"> 1881 | <article class="product-detail"> 1882 | <header class="product-header"> 1883 | <h1 class="product-name">${product.productName}</h1> 1884 | </header> 1885 | <section class="product-images"> 1886 | <!-- Product imagery --> 1887 | </section> 1888 | </article> 1889 | </main> 1890 | ``` 1891 | 1892 | #### 5. Implement Responsive Design Patterns 1893 | 1894 | ```html 1895 | <!-- ✅ Mobile-first responsive structure --> 1896 | <div class="row"> 1897 | <div class="col-12 col-md-6 col-lg-4"> 1898 | <!-- Mobile: full width, tablet: half, desktop: one-third --> 1899 | </div> 1900 | </div> 1901 | 1902 | <!-- ✅ Progressive disclosure --> 1903 | <div class="product-breadcrumb d-md-none"> 1904 | <!-- Only show on mobile --> 1905 | </div> 1906 | <div class="d-none d-md-block"> 1907 | <!-- Only show on tablet and up --> 1908 | </div> 1909 | ``` 1910 | 1911 | This SFRA base template architecture provides a solid foundation for building consistent, maintainable, and scalable storefronts while following Commerce Cloud best practices for performance, security, and user experience 1912 | 1913 | 1914 | ## Quick Reference Tables 1915 | 1916 | ### Essential ISML Tags 1917 | | Tag | Purpose | Example | 1918 | |-----|---------|---------| 1919 | | `<isprint>` | Output data safely | `<isprint value="${pdict.product.name}" />` | 1920 | | `<isif>` | Conditional logic | `<isif condition="${product.available}">` | 1921 | | `<isloop>` | Iterate collections | `<isloop items="${products}" var="product">` | 1922 | | `<isinclude>` | Include templates | `<isinclude template="components/header" />` | 1923 | | `<isdecorate>` | Apply layout | `<isdecorate template="common/layout/page">` | 1924 | | `<isset>` | Set variables | `<isset name="showPrice" value="${true}" scope="page" />` | 1925 | 1926 | ### URL Generation Functions 1927 | | Function | Purpose | Example | 1928 | |----------|---------|---------| 1929 | | `URLUtils.url()` | Relative URLs | `URLUtils.url('Product-Show', 'pid', product.id)` | 1930 | | `URLUtils.https()` | Secure URLs | `URLUtils.https('Account-Login')` | 1931 | | `URLUtils.staticURL()` | Static assets | `URLUtils.staticURL('/images/logo.png')` | 1932 | 1933 | Remember: **Keep business logic in controllers, use ISML only for presentation, and always encode output for security.** 1934 | ```