diff --git a/docs/content/guides/4.customization/1.index.md b/docs/content/guides/4.customization/1.index.md new file mode 100644 index 0000000000..cc86d1b049 --- /dev/null +++ b/docs/content/guides/4.customization/1.index.md @@ -0,0 +1,22 @@ +--- +title: Alokai Customization Guide +layout: default +--- + +# Alokai Customization Guide + +On this page, you'll find a set of guides that will help you understand some advanced concepts of Alokai and how to customize the application to your requirements. + +::card{title="Alokai with Next.js pages router" icon="tabler:file" to="/guides/customization/pages-router" } + +#description +Learn how to customize Alokai application with Next.js pages router. +:: + +
+ +::card{title="Alokai with Next.js app router" icon="tabler:cube" to="/guides/customization/app-router" } + +#description +Learn how to customize Alokai application with Next.js app router +:: \ No newline at end of file diff --git a/docs/content/guides/4.customization/1.pages-router/1.index.md b/docs/content/guides/4.customization/1.pages-router/1.index.md new file mode 100644 index 0000000000..eb6a24652b --- /dev/null +++ b/docs/content/guides/4.customization/1.pages-router/1.index.md @@ -0,0 +1,51 @@ +--- +title: Alokai Customization Guide +layout: default +--- + +# Alokai Customization Guide + +Alokai is not a cookie-cutter solution, it is meant to flexible enough to handle even the complex use cases. This guide will take you through the most common customization scenarios. We aim to cover end-to-end implementation of realistic business requirements. + +Some of the customizations you'll do throughout this guide are: + +::list{type="success"} +- Customizing the logo image +- Adjusting the theme colors +- Inserting a pre-header +- Implementing i18n (internationalization) +- Modifying the look and feel of various components +- Implementing a filter search feature +- Creating a new page with a list of brands +- Adding an "available for pickup" feature - add custom fields to our unified data model +- Customizing the product slug to change the PDP URL +- Fetching product reviews from an external service +- Building a completely new feature from scratch. You will mock a "social product images" feature +:: + +::info +Please bear in mind that this guide is not exhaustive - Alokai offers some more features that you can find in our +documentation: . +:: + + +## Prerequisites + +This guide assumes that: + +- you've gone through the [Alokai Next.js guide](/guides/alokai-essentials/alokai-next-js). +- you have access to [Alokai Enterprise](https://docs.alokai.com/enterprise) +- you have an Alokai starter project provided by the Alokai team +- you have access to SAP Commerce Cloud OCC API instance (you can use our demo instance) + + +::card{title="Next: UI Customization" icon="tabler:number-1-small" } + +#description +Learn how to customize Alokai User Interface. + +#cta +:::docs-button{to="/guides/customization/pages-router/ui-customizations"} +Start Customizing +::: +:: \ No newline at end of file diff --git a/docs/content/guides/4.customization-next-js/2.ui-customizations.md b/docs/content/guides/4.customization/1.pages-router/2.ui-customizations.md similarity index 99% rename from docs/content/guides/4.customization-next-js/2.ui-customizations.md rename to docs/content/guides/4.customization/1.pages-router/2.ui-customizations.md index e977896205..356149f0ea 100644 --- a/docs/content/guides/4.customization-next-js/2.ui-customizations.md +++ b/docs/content/guides/4.customization/1.pages-router/2.ui-customizations.md @@ -398,7 +398,7 @@ If you want to get access to it, contact our [sales team](https://docs.alokai.co Learn how to create a custom Alokai page. #cta -:::docs-button{to="/guides/customization-next-js/brands-page"} +:::docs-button{to="/guides/customization/pages-router/brands-page"} Create a new page ::: :: \ No newline at end of file diff --git a/docs/content/guides/4.customization-next-js/3.brands-page.md b/docs/content/guides/4.customization/1.pages-router/3.brands-page.md similarity index 99% rename from docs/content/guides/4.customization-next-js/3.brands-page.md rename to docs/content/guides/4.customization/1.pages-router/3.brands-page.md index 0430b6cbe6..22497ac222 100644 --- a/docs/content/guides/4.customization-next-js/3.brands-page.md +++ b/docs/content/guides/4.customization/1.pages-router/3.brands-page.md @@ -239,7 +239,7 @@ If you want to get access to it, contact our [sales team](https://docs.alokai.co Learn how to add custom fields to the Unified Data Model #cta -:::docs-button{to="/guides/customization-next-js/add-custom-field"} +:::docs-button{to="/guides/customization/pages-router/add-custom-field"} Customize UDL ::: :: \ No newline at end of file diff --git a/docs/content/guides/4.customization-next-js/4.add-custom-field.md b/docs/content/guides/4.customization/1.pages-router/4.add-custom-field.md similarity index 97% rename from docs/content/guides/4.customization-next-js/4.add-custom-field.md rename to docs/content/guides/4.customization/1.pages-router/4.add-custom-field.md index b66e955195..b5867f6da8 100644 --- a/docs/content/guides/4.customization-next-js/4.add-custom-field.md +++ b/docs/content/guides/4.customization/1.pages-router/4.add-custom-field.md @@ -70,7 +70,7 @@ If you want to get access to it, contact our [sales team](https://docs.alokai.co Learn how to override normalizers. #cta -:::docs-button{to="/guides/customization-next-js/changing-product-slug"} +:::docs-button{to="/guides/customization/pages-router/changing-product-slug"} Next ::: :: diff --git a/docs/content/guides/4.customization-next-js/5.changing-product-slug.md b/docs/content/guides/4.customization/1.pages-router/5.changing-product-slug.md similarity index 98% rename from docs/content/guides/4.customization-next-js/5.changing-product-slug.md rename to docs/content/guides/4.customization/1.pages-router/5.changing-product-slug.md index 3625770146..057bf66934 100644 --- a/docs/content/guides/4.customization-next-js/5.changing-product-slug.md +++ b/docs/content/guides/4.customization/1.pages-router/5.changing-product-slug.md @@ -120,7 +120,7 @@ Read more about normalizers here: https://docs.alokai.com/storefront/unified-dat Learn how to call a 3rd party back-end service and replace OOTB API method. #cta -:::docs-button{to="/guides/customization-next-js/method-overriding"} +:::docs-button{to="/guides/customization/pages-router/method-overriding"} Next ::: :: \ No newline at end of file diff --git a/docs/content/guides/4.customization-next-js/6.method-overriding.md b/docs/content/guides/4.customization/1.pages-router/6.method-overriding.md similarity index 97% rename from docs/content/guides/4.customization-next-js/6.method-overriding.md rename to docs/content/guides/4.customization/1.pages-router/6.method-overriding.md index 4b3d18cd1f..afa9574c29 100644 --- a/docs/content/guides/4.customization-next-js/6.method-overriding.md +++ b/docs/content/guides/4.customization/1.pages-router/6.method-overriding.md @@ -92,7 +92,7 @@ If you want to get access to it, contact our [sales team](https://docs.alokai.co Learn how to implement a custom feature by creating an extension. #cta -:::docs-button{to="/guides/customization-next-js/adding-extension"} +:::docs-button{to="/guides/customization/pages-router/adding-extension"} Next ::: :: \ No newline at end of file diff --git a/docs/content/guides/4.customization-next-js/7.adding-extension.md b/docs/content/guides/4.customization/1.pages-router/7.adding-extension.md similarity index 100% rename from docs/content/guides/4.customization-next-js/7.adding-extension.md rename to docs/content/guides/4.customization/1.pages-router/7.adding-extension.md diff --git a/docs/content/guides/4.customization/1.pages-router/_dir.yml b/docs/content/guides/4.customization/1.pages-router/_dir.yml new file mode 100644 index 0000000000..973500ce8f --- /dev/null +++ b/docs/content/guides/4.customization/1.pages-router/_dir.yml @@ -0,0 +1,5 @@ +title: Next.js pages router +sidebarRoot: true +navigation: + icon: tabler:file + diff --git a/docs/content/guides/4.customization-next-js/images/available-for-pickup.webp b/docs/content/guides/4.customization/1.pages-router/images/available-for-pickup.webp similarity index 100% rename from docs/content/guides/4.customization-next-js/images/available-for-pickup.webp rename to docs/content/guides/4.customization/1.pages-router/images/available-for-pickup.webp diff --git a/docs/content/guides/4.customization-next-js/images/brands-missing-trans.webp b/docs/content/guides/4.customization/1.pages-router/images/brands-missing-trans.webp similarity index 100% rename from docs/content/guides/4.customization-next-js/images/brands-missing-trans.webp rename to docs/content/guides/4.customization/1.pages-router/images/brands-missing-trans.webp diff --git a/docs/content/guides/4.customization-next-js/images/brands-page.webp b/docs/content/guides/4.customization/1.pages-router/images/brands-page.webp similarity index 100% rename from docs/content/guides/4.customization-next-js/images/brands-page.webp rename to docs/content/guides/4.customization/1.pages-router/images/brands-page.webp diff --git a/docs/content/guides/4.customization-next-js/images/custom-slug.webp b/docs/content/guides/4.customization/1.pages-router/images/custom-slug.webp similarity index 100% rename from docs/content/guides/4.customization-next-js/images/custom-slug.webp rename to docs/content/guides/4.customization/1.pages-router/images/custom-slug.webp diff --git a/docs/content/guides/4.customization-next-js/images/customizations.webp b/docs/content/guides/4.customization/1.pages-router/images/customizations.webp similarity index 100% rename from docs/content/guides/4.customization-next-js/images/customizations.webp rename to docs/content/guides/4.customization/1.pages-router/images/customizations.webp diff --git a/docs/content/guides/4.customization-next-js/images/logo-on-green.webp b/docs/content/guides/4.customization/1.pages-router/images/logo-on-green.webp similarity index 100% rename from docs/content/guides/4.customization-next-js/images/logo-on-green.webp rename to docs/content/guides/4.customization/1.pages-router/images/logo-on-green.webp diff --git a/docs/content/guides/4.customization-next-js/images/plp-customization.webp b/docs/content/guides/4.customization/1.pages-router/images/plp-customization.webp similarity index 100% rename from docs/content/guides/4.customization-next-js/images/plp-customization.webp rename to docs/content/guides/4.customization/1.pages-router/images/plp-customization.webp diff --git a/docs/content/guides/4.customization-next-js/images/react-dev-tools.webp b/docs/content/guides/4.customization/1.pages-router/images/react-dev-tools.webp similarity index 100% rename from docs/content/guides/4.customization-next-js/images/react-dev-tools.webp rename to docs/content/guides/4.customization/1.pages-router/images/react-dev-tools.webp diff --git a/docs/content/guides/4.customization-next-js/images/reviews.webp b/docs/content/guides/4.customization/1.pages-router/images/reviews.webp similarity index 100% rename from docs/content/guides/4.customization-next-js/images/reviews.webp rename to docs/content/guides/4.customization/1.pages-router/images/reviews.webp diff --git a/docs/content/guides/4.customization-next-js/images/social-images.webp b/docs/content/guides/4.customization/1.pages-router/images/social-images.webp similarity index 100% rename from docs/content/guides/4.customization-next-js/images/social-images.webp rename to docs/content/guides/4.customization/1.pages-router/images/social-images.webp diff --git a/docs/content/guides/4.customization-next-js/1.index.md b/docs/content/guides/4.customization/2.app-router/1.index.md similarity index 95% rename from docs/content/guides/4.customization-next-js/1.index.md rename to docs/content/guides/4.customization/2.app-router/1.index.md index fdfba568f9..07790b4d05 100644 --- a/docs/content/guides/4.customization-next-js/1.index.md +++ b/docs/content/guides/4.customization/2.app-router/1.index.md @@ -45,7 +45,7 @@ This guide assumes that: Learn how to customize Alokai User Interface. #cta -:::docs-button{to="/guides/customization-next-js/ui-customizations"} +:::docs-button{to="/guides/customization/app-router/ui-customizations"} Start Customizing ::: :: \ No newline at end of file diff --git a/docs/content/guides/4.customization/2.app-router/2.ui-customizations.md b/docs/content/guides/4.customization/2.app-router/2.ui-customizations.md new file mode 100644 index 0000000000..c7f980d955 --- /dev/null +++ b/docs/content/guides/4.customization/2.app-router/2.ui-customizations.md @@ -0,0 +1,380 @@ +--- +title: Basic UI customizations +layout: default +navigation: + icon: tabler:number-1-small +--- + +# UI customizations + +Out of the box, an Alokai application ships with a default design system built on top of [Storefront UI](https://docs.storefrontui.io). While this helps ensure that your application looks good instantly, it's likely one of the first things you'll want to customize. + +Since you have control over the storefront's code in either the Next.js or Nuxt application, you have access to all the code that can modify the look and feel of the application. This guide will walk you through some of the most common customization scenarios and will hopefully help you get familiar with the structure of the codebase. + +In this chapter, you will: + +::list{type="success"} +- change the default logo to your custom one +- adjust the theme colors to suit the new logo +- add a pre-header with i18n +- customize the look of product cards on the product listing page +- add facet/filter search feature +:: + +In the end, your application will look something like this: + +![Customizations](./images/customizations.webp) + +## Changing the logo + +The first step to making any changes is to identify what component is actually responsible for certain elements. + +In this example, we need to find and modify the component that contains the logo. You can either: + +1. Drill through the Next.js application starting from `apps/storefront-unified-nextjs/app/[locale]/(cms)/[[...slug]]/page.tsx`, which represents +the homepage (actually all CMS pages). The layout for this page is located in `apps/storefront-unified-nextjs/app/[locale]/(cms)/layout.tsx`, which re-exports +`BaseDefaultLayout`. In that layout you can find that `Navbar` component is responsible for rendering the navbar. Within that component we have `NavbarTop` component +which renders the logo. +2. Or you can use [React Developer Tools](https://react.dev/learn/react-developer-tools) to localize the component visually: +![React Dev Tools](./images/react-dev-tools.webp) + +Doing one of these, you'll find that the logo is located in `apps/storefront-unified-nextjs/components/navigations/navbar-top.tsx` component. + +Now, we have to change that component and replace `SfIconAlokaiFull` with your custom image. For this example, let's use [LogoIpsum](https://logoipsum.com/) to generate a sample logo. Download a sample logo and place it in `apps/storefront-unified-nextjs/public/images` folder. + +```html + + + +logo +``` + +The result should look like this: + +![Logo](./images/logo-on-green.webp) + +However, this logo doesn't look good on the storefront's default green background. We can change that by removing the `filled` property from `NavbarTop` component in `Navbar` component. This will make the navbar transparent. + +```diff[apps/storefront-unified-nextjs/app/[locale]/(default)/components/navbar.tsx] +- ++ +``` + +The logo should look better now. However, buttons in the navbar have become invisible. That's because they are white. + +::tip +#title +An Extra Challenge +#default +Within the `NavbarTop` component, find which tailwind class is responsible for making the buttons white and remove it. +:: + +## Adjusting theme colors + +The primary green color does not play well with the colors in the logo. To fix this we will adjust our theme colors. + +The Storefront uses custom Tailwind CSS colors throughout the application, so you can make large UI changes by adjusting the colors in the `tailwind.config.ts` file. If you don't have a color palette already, you can use [Tailwind Colors](https://tailwindcss.com/docs/customizing-colors) to generate one. + +Then, edit `apps/storefront-unified-nextjs/tailwind.config.ts` and paste your colors under `theme.extend`, rename the color to "primary" + + +```ts + // ... + theme: { + extend: { + colors: { + primary: { + 50: '#fff0f9', + 100: '#ffe3f5', + 200: '#ffc6eb', + 300: '#ff98d9', + 400: '#ff58bd', + 500: '#ff27a1', + 600: '#ff0c81', + 700: '#df005f', + 800: '#b8004f', + 900: '#980345', + 950: '#5f0025', + }, + }, + // ... +``` + +You can read more about theming in [the Storefront UI docs](https://docs.storefrontui.io/v2/customization/theming). + +## Adding a pre-header + +A common use case is to add a pre-header to the top of each page with something like promotional codes or a call to action. If you feel confident that you can do this, try it out before reading on. + +### Solution + +1. Create a new `pre-header.tsx` file in `apps/storefront-unified-nextjs/components/navigations` folder with the following content: + +```tsx +import { SfIconInfo } from '@storefront-ui/react'; + +export function PreHeader() { + return ( +
+ + Limited offer. Use code: ALOKAI2024 +
+ ); +} +``` + +2. Add `PreHeader` component to `NavbarTop`: + +```tsx +export default function NavbarTop({ children, className, filled }: NavbarTopProps) { + const t = useTranslations('NavbarTop'); + const messages = useMessages(); + + return ( + <> /* [!code ++] */ + /* [!code ++] */ +
+
+ + logo + + {children} + + + +
+
+ /* [!code ++] */ + ); +} +``` + +## Pre-header internationalization (i18n) + +Let's make this example more interesting by making the pre-header localized. +[next-intl](https://next-intl-docs.vercel.app/) package comes installed in your storefront and is our recommended solution for internationalization. + +1. First, we need to add translations. Translation files are located under `apps/storefront-unified-nextjs/lang` folder. +There's a separate subfolder for each language (e.g. `en`, `de`). +Open both `en/base.json` and `de/base.json` files and add a new translation there: + +```diff [en/base.json] +}, ++ "PreHeader": { ++ "promoText": "Limited offer. Use code: ALOKAI2024" ++ }, + "Navbar": { +``` + +```diff [de/base.json] + }, ++ "PreHeader": { ++ "promoText": "Begrenztes Angebot. Verwenden Sie den Code: ALOKAI2024" + }, + "Navbar": { +``` + +2. Now, we can use the translations in our `PreHeader` component. We'll utilize the `useTranslations` from `next-intl` package. +Your `pre-header.tsx` should look like this now: + +```tsx [apps/storefront-unified-nextjs/components/navigations/pre-header.tsx] +import { SfIconInfo } from '@storefront-ui/react'; +import { useTranslations } from 'next-intl'; + +export function PreHeader() { + const t = useTranslations('PreHeader'); + return ( +
+ + {t('promoText')} +
+ ); +} + +``` + +## Modifying the product card on PLP + +As a challenge, try to implement these design changes on your own by making the changes to the `ProductCardVertical` component. + +![PLP customizations](./images/plp-customization.webp) + +To check your solution, you can look at [our implementation](https://github.com/vsf-customer/extensibility-demo-v2/blob/main/apps/storefront-unified-nextjs/components/product-card-vertical.tsx). + +## Facet search on PLP + +Now let's try something more ambitious - we'll extend facet's (aka filters) behavior. We want to be able to filter/search through the facets. + +Before jumping to the solution, think about how would you do this yourself. Investigate the application and which parts you need to modify. + +### Solution + +1. Under `apps/storefront-unified-nextjs/components/products-listing` create a new `FilterSearch` component that would be our searchbox. As a starting point, you can use [Storefront UI's Search Block](https://docs.storefrontui.io/v2/react/blocks/search). + + ```tsx [apps/storefront-unified-nextjs/components/products-listing/filter-search.tsx] + import { SfIconCancel, SfIconSearch, SfInput } from '@storefront-ui/react'; + import { useRef, useState, type ChangeEvent, type FormEvent, type KeyboardEvent } from 'react'; + + export type FilterSearchProps = { + onSearch: (value: string) => void; + }; + + export default function FilterSearch({ onSearch }: FilterSearchProps) { + const inputRef = useRef(null); + const [searchValue, setSearchValue] = useState(''); + + const isResetButton = Boolean(searchValue); + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + }; + + const handleFocusInput = () => { + inputRef.current?.focus(); + }; + + const handleReset = () => { + setSearchValue(''); + onSearch(''); + handleFocusInput(); + }; + + const handleChange = (event: ChangeEvent) => { + const ph = event.target.value; + setSearchValue(ph); + onSearch(ph); + }; + + const handleInputKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') handleReset(); + }; + + return ( +
+ } + slotSuffix={ + isResetButton && ( + + ) + } + /> + + ); + } + ``` + +2. Modify `Facet` component in `apps/storefront-unified-nextjs/components/products-listing/facets.tsx` by adding `FilterSearch` component and implementing filtering logic: + + ```tsx [apps/storefront-unified-nextjs/components/products-listing/facets.tsx] + function Facet({ + containerClassName, + expandableListProps, + facet, + itemRenderer: FacetItem, + multiSelect = false, + }: FacetProps) { + const [searchPhrase, setSearchPhrase] = useState(''); /* [!code ++] */ + const [values, setValues] = useState(facet.values); /* [!code ++] */ + + /* [!code ++:9] */ + useEffect(() => { + if (searchPhrase === '') { + setValues(facet.values); + } else { + setValues( + facet.values.filter((item) => item.label.toLocaleLowerCase().includes(searchPhrase.toLocaleLowerCase())), + ); + } + }, [facet, searchPhrase]); + + const [selected, setSelected] = useQueryState( + `${FACET_QUERY_PREFIX}${facet.name}`, + parseAsArrayOf(parseAsString).withDefault([]).withOptions({ shallow: false }), + ); + function toggleFacet(value: string) { + if (selected.includes(value)) { + const updated = selected.filter((v) => v !== value); + return setSelected(updated.length ? updated : null); + } + + return setSelected(multiSelect ? [...selected, value] : [value]); + } + + return ( + + {facet.label} + + } + summaryClassName="pt-4" + > + /* [!code ++] */ +
+ + {facet.values.map((item) => (/* [!code --] */ + {values.map((item) => ( /* [!code ++] */ + toggleFacet(item.value)} + selected={selected.includes(item.value)} + /> + ))} + +
+
+ ); + } + ``` + +::info +You can find a complete project example in this repository: . +If you want to get access to it, contact our [sales team](https://docs.alokai.com/enterprise). +:: + +
+ + +::card{title="Next: Adding new page" icon="tabler:number-2-small" } + +#description +Learn how to create a custom Alokai page. + +#cta +:::docs-button{to="/guides/customization/app-router/brands-page"} +Create a new page +::: +:: \ No newline at end of file diff --git a/docs/content/guides/4.customization/2.app-router/3.brands-page.md b/docs/content/guides/4.customization/2.app-router/3.brands-page.md new file mode 100644 index 0000000000..1c6f300eac --- /dev/null +++ b/docs/content/guides/4.customization/2.app-router/3.brands-page.md @@ -0,0 +1,143 @@ +--- +layout: default +navigation: + icon: tabler:number-2-small +--- + +# Adding a New Page + +In this chapter, we will create a new custom page. It will be a page listing all the brands that will look like this: + +![Brands page](./images/brands-page.webp) + +The process of creating a new page with Alokai is no different than creating one with [Next.js](https://nextjs.org/docs/app/building-your-application/routing/pages). However, when building your page, Alokai offers a few useful features that you'll want to take advantage of. + +1. Create `apps/storefront-unified-nextjs/app/[locale]/(default)/brands` folder and `page.tsx` file with the folowing content: + +```tsx [apps/storefront-unified-nextjs/app/[locale]/(default)/brands/page.tsx] +import { Link } from '@/config/navigation'; +import { getSdk } from '@/sdk'; +import { Maybe } from '@/types'; +import { SfCategory } from 'storefront-middleware/types'; + +type BrandsAlphabetically = Record; + +// utility function to transform data into a structure that is friendlier for rendering +function categoriesToBrands(categories: Maybe | undefined): BrandsAlphabetically { + const result: BrandsAlphabetically = {}; + + categories?.forEach((category) => { + const firstLetter = category.name.at(0)?.toLocaleUpperCase() ?? ''; + if (result[firstLetter]) { + result[firstLetter]?.push(category); + } else { + result[firstLetter] = [category]; + } + }); + + return result; +} + +export default async function Brands() { + const sdk = getSdk(); + const categories = await sdk.unified.getCategories({ ids: ['brands'] }); + const brands = categoriesToBrands(categories?.[0]?.subcategories); + const letters = Object.keys(brands); + + return ( +
+

All Brands

+
+ {letters.map((letter) => ( + + {letter} + + ))} +
+
+ {letters.map((letter) => ( +
+

{letter}

+
+ {brands[letter].map((brand) => ( +
+ + {brand.name} + +
+ ))} +
+
+ ))} +
+
+ ); +} +``` + +This code uses `sdk.unified.getCategories` method to fetch the 'brands' category. The subcategories represent different brands. + +Your backend might not have such a special category so you need to figure out yourself where to fetch the list of brands from. + +`categoriesToBrands` transforms the category data into a structure that is easier to render in the way we want. + +2. Open `http://localhost:3000/brands` and check if it works. + +3. Lastly, we would like to add some custom translations to our page. We will translate the "All Brands" heading. +We've learned how to add translations in the previous chapter.However, this time we're using translations in an `async` +component and it requires slightly different syntax + +Open both `en/base.json` and `de/base.json` files and add a new translation there: + +```diff [en/base.json] +}, ++ "Brands": { ++ "allBrands": "All Brands" ++ }, +``` + +```diff [de/base.json] + }, ++ "Brands": { ++ "allBrands": "Alle Marken" + }, +``` + +Modify `apps/storefront-unified-nextjs/app/[locale]/(default)/brands/page.tsx`: + +```tsx [apps/storefront-unified-nextjs/app/[locale]/(default)/brands/page.tsx] +//... + const categories = await sdk.unified.getCategories({ ids: ['brands'] }); + const t = await getTranslations('Brands'); // [!code ++] + const brands = categoriesToBrands(categories?.[0]?.subcategories); +// ... +

All Brands

// [!code --] +

{t('allBrands')}

// [!code ++] +// ... +``` + + + + +::info +You can find a complete project example in this repository: +If you want to get access to it, contact our [sales team](https://docs.alokai.com/enterprise). +:: + + +
+ + +::card{title="Next: Customizing Unified Data Model" icon="tabler:number-3-small" } + +#description +Learn how to add custom fields to the Unified Data Model + +#cta +:::docs-button{to="/guides/customization/app-router/add-custom-field"} +Customize UDL +::: +:: \ No newline at end of file diff --git a/docs/content/guides/4.customization/2.app-router/4.add-custom-field.md b/docs/content/guides/4.customization/2.app-router/4.add-custom-field.md new file mode 100644 index 0000000000..2ac4355aca --- /dev/null +++ b/docs/content/guides/4.customization/2.app-router/4.add-custom-field.md @@ -0,0 +1,76 @@ +--- +title: Adding custom fields +layout: default +navigation: + icon: tabler:number-3-small +--- + +# Adding custom fields to the Unified Data Model / Implementing "Available for pickup" feature + +It's a common case to enrich the default data models with custom fields. + +In this chapter, you will learn: + +::list{type="success"} +- how to add a custom field to the unified product data model in the middleware +- how to utilize that field in the storefront +:: + +By adding a "pickup availability" feature. + +![Available for pickup](./images/available-for-pickup.webp) + +1. Open `apps/storefront-middleware/integrations/sapcc/extensions/unified.ts` file and modify the code accordingly: + +```diff [apps/storefront-middleware/integrations/sapcc/extensions/unified.ts] +export const unifiedApiExtension = createUnifiedExtension({ + normalizers: { + addCustomFields: [ ++ { ++ normalizeProduct(context, input) { ++ return { ++ availableForPickup: input.availableForPickup, ++ }; ++ }, ++ }, + ], + }, +``` + +Within `addCustomFields`, we extend the normalizer functions. We take the raw input (coming from eCommerce) and have to +return a set of custom fields. + +::info +Read more about normalizers and custom fields here: https://docs.alokai.com/storefront/unified-data-layer/normalizers +:: + +2. Now, `availableForPickup` field should be available in the front end, so let's use it. Replace the hardcoded placeholder +in the `PurchaseCard` component: + + +```diff [apps/storefront-unified-nextjs/components/purchase-card.tsx] +- {t.rich('additionalInfo.pickup', { +- link: (chunks) => ( +- +- {chunks} +- +- ), +- })} ++

Pickup {product.$custom?.availableForPickup ? '' : 'not'} available

+``` + +And that's it. You can find a complete project example in this repository: +If you want to get access to it, contact our [sales team](https://docs.alokai.com/enterprise). + + +::card{title="Next: Change product slug" icon="tabler:number-4-small" } + +#description +Learn how to override normalizers. + +#cta +:::docs-button{to="/guides/customization/app-router/changing-product-slug"} +Next +::: +:: + diff --git a/docs/content/guides/4.customization/2.app-router/5.changing-product-slug.md b/docs/content/guides/4.customization/2.app-router/5.changing-product-slug.md new file mode 100644 index 0000000000..5151799870 --- /dev/null +++ b/docs/content/guides/4.customization/2.app-router/5.changing-product-slug.md @@ -0,0 +1,123 @@ +--- +title: Changing product slug +layout: default +navigation: + icon: tabler:number-4-small +--- + +# Overriding Normalizer / Changing Product Slug + +Sometimes, instead of adding new attributes to the data model we need to modify the existing ones. We can do this by overriding normalizers (functions that transform raw data into the unified data model). In this guide, we will modify the product slug, to control the PDP URL (e.g. for the sake of SEO requirements). + +Here's how the result will look like: +![Custom product slug](./images/custom-slug.webp) + +## Overriding the normalizer + +Open `apps/storefront-middleware/integrations/sapcc/extensions/unified.ts` file and modify the code accordingly: + +```diff [apps/storefront-middleware/integrations/sapcc/extensions/unified.ts] +- import { createUnifiedExtension } from "@vsf-enterprise/unified-api-sapcc"; ++ import { createUnifiedExtension, normalizers as defaultNormalizers } from "@vsf-enterprise/unified-api-sapcc"; + +// ... + + export const unifiedApiExtension = createUnifiedExtension({ + normalizers: { + addCustomFields: [ + { + normalizeProduct(context, input) { + return { + availableForPickup: input.availableForPickup, + }; + }, + }, + ], ++ override: { ++ normalizeProductCatalogItem(context, input) { ++ const product = defaultNormalizers.normalizeProductCatalogItem(context, input); ++ const newSlug = "p-" + product.slug; ++ return { ++ ...product, ++ slug: newSlug, ++ }; ++ }, ++ }, +``` + +Here's what happens in that code: + +* We override the product catalog item normalizer - a function that normalizes each item of the product list. +* We use the default normalizer to normalize the product data because we want only to modify one field - not the whole object. +* We add a prefix to the current product slug but you can implement any kind of logic you want here. +* In the end, we return the normalized product and overwrite the slug field. + + +## Scaling it up + +As you might have noticed, adding normalization logic directly to a single file is not a good idea because it would +quickly become unmanageably large. Fortunately, there's a utility that helps split that code into multiple files. + +1. Create `/storefront-middleware/integrations/sapcc/extensions/normalizers/` folder and `productCatalogItemNormalizer.ts` +file in it. Copy and paste this code into it: + +```ts [apps/storefront-middleware/integrations/sapcc/extensions/normalizers/productCatalogItemNormalizer.ts] +import { + normalizers as defaultNormalizers, + defineNormalizer, +} from "@vsf-enterprise/unified-api-sapcc"; + +export const productCatalogItemNormalizer = defineNormalizer.normalizeProductCatalogItem( + (context, input) => { + const product = defaultNormalizers.normalizeProductCatalogItem(context, input); + const newSlug = "p-" + product.slug; + return { + ...product, + slug: newSlug, + }; + }, +); +``` + +Create a barrel import file `/storefront-middleware/integrations/sapcc/extensions/normalizers/index.ts` + +```ts [/storefront-middleware/integrations/sapcc/extensions/normalizers/index.ts] +export * from "./productCatalogItemNormalizer"; +``` + +2. Modify `/storefront-middleware/integrations/sapcc/extensions/unified.ts` accordingly: + +```diff [/storefront-middleware/integrations/sapcc/extensions/unified.ts] + override: { +- normalizeProductCatalogItem(context, input) { +- const product = defaultNormalizers.normalizeProductCatalogItem(context, input); +- const newSlug = "p-" + product.slug; +- return { +- ...product, +- slug: newSlug, +- }; +- }, ++ normalizeProductCatalogItem: productCatalogItemNormalizer, + }, +``` + +You can find a complete project example in this repository: +If you want to get access to it, contact our [sales team](https://docs.alokai.com/enterprise). + +::info +Read more about normalizers here: https://docs.alokai.com/storefront/unified-data-layer/normalizers +:: + +
+ + +::card{title="Next: Calling custom endpoint" icon="tabler:number-5-small" } + +#description +Learn how to call a 3rd party back-end service and replace OOTB API method. + +#cta +:::docs-button{to="/guides/customization/app-router/method-overriding"} +Next +::: +:: \ No newline at end of file diff --git a/docs/content/guides/4.customization/2.app-router/6.method-overriding.md b/docs/content/guides/4.customization/2.app-router/6.method-overriding.md new file mode 100644 index 0000000000..9b7ef2bc50 --- /dev/null +++ b/docs/content/guides/4.customization/2.app-router/6.method-overriding.md @@ -0,0 +1,102 @@ +--- +title: Calling custom endpoints +layout: default +navigation: + icon: tabler:number-5-small +--- + +# Overriding API methods / Getting product reviews from an external source + +Alokai Middleware is called an integration layer - it's responsible for combining data from different sources and +presenting it to the front end in a simple form. In this chapter, we will tackle a common business case - fetching reviews +from an external system. It is a common scenario that product reviews are managed by a dedicated service rather than the +eCommerce platform. + +Thanks to the Unified Data Model we won't need to touch the front end at all to implement this. + +1. Open `apps/storefront-middleware/integrations/sapcc/extensions/unified.ts` and add this code: + +```diff [apps/storefront-middleware/integrations/sapcc/extensions/unified.ts] +- import { createUnifiedExtension } from "@vsf-enterprise/unified-api-sapcc"; ++ import { createUnifiedExtension, SfProductReview } from "@vsf-enterprise/unified-api-sapcc"; ++ import crypto from "crypto" +// ... + ++ interface DummyReview { ++ rating: number; ++ comment: string; ++ date: string; ++ reviewerName: string; ++ reviewerEmail: string; ++ } ++ interface DummyProduct { ++ reviews: DummyReview[]; ++ } + +// ... + +export const unifiedApiExtension = createUnifiedExtension({ + + // ... + ++ methods: { ++ override: { ++ getProductReviews: async (context, args) => { ++ const product: DummyProduct = await ( ++ await fetch(`https://dummyjson.com/products/${args.productId.slice(0, 2)}`) ++ ).json(); ++ const reviews: SfProductReview[] = product.reviews.map((review) => ({ ++ id: crypto.randomUUID().toString(), ++ createdAt: review.date, ++ rating: review.rating, ++ reviewer: review.reviewerName, ++ text: review.comment, ++ title: review.comment.slice(0, 10), ++ })); ++ return { ++ pagination: { ++ currentPage: 1, ++ pageSize: reviews.length, ++ totalPages: 1, ++ totalResults: reviews.length, ++ }, ++ reviews: reviews, ++ }; ++ }, ++ }, ++ }, +``` + +What we do here is overwrite the OOTB `getProductReviews` that by default fetches reviews from the eCommerce platform. +We used [DummyJSON API](https://dummyjson.com) to fetch some random product data and extract reviews from it. Imagine that it could be any review +service. Then we transform the proprietary data model into the Unified Data Model. + +2. Open a product page and expand "Customer Reviews". You should see some random reviews now. + +![Product reviews](./images/reviews.webp) + +3. To make our code more scalable we can extract the new method from the `unified.ts` file with the help of the `defineApi` +function. + +```ts +const getProductReviews = defineApi.getProductReviews(async (context, args) => { + // function body +}); +``` + +You can find a complete project example in this repository: +If you want to get access to it, contact our [sales team](https://docs.alokai.com/enterprise). + +[Learn more about overriding API methods](https://docs.alokai.com/storefront/integration-and-setup/overriding-api-methods) + + +::card{title="Next: Adding custom feature" icon="tabler:number-6-small" } + +#description +Learn how to implement a custom feature by creating an extension. + +#cta +:::docs-button{to="/guides/customization/app-router/adding-extension"} +Next +::: +:: \ No newline at end of file diff --git a/docs/content/guides/4.customization/2.app-router/7.custom-methods.md b/docs/content/guides/4.customization/2.app-router/7.custom-methods.md new file mode 100644 index 0000000000..840fe598df --- /dev/null +++ b/docs/content/guides/4.customization/2.app-router/7.custom-methods.md @@ -0,0 +1,122 @@ +--- +title: Custom methods +layout: default +navigation: + icon: tabler:number-6-small +--- + +# Adding New API Methods + +The Unified Data Model contains the most commonly used data for eCommerce backends, but there will be times when you need additional information, combine data from multiple sources, or create a new data structure. + +In this guide, we will implement a mock of "social product images" feature - a feature where we display product images posted on social media. For the sake of simplicity we won't reach a real social network, we will use [Lorem Picsum](https://picsum.photos/) API to mock it. The result will look like this: + +![Social Images](./images/social-images.webp) + +This guide is based on this documentation -> [Creating New API Methods](/unified-data-layer/integration-and-setup/creating-new-api-methods) + +1. First, we need to create a custom method in the middleware. + +Define input and output types of your method in `apps/storefront-middleware/api/custom-methods/types.ts`: + + +```ts [apps/storefront-middleware/api/custom-methods/types.ts] +export interface SocialImagesArgs { + seed: string; +} + +export interface SocialImagesResponse { + id: string; + author: string; + width: number; + height: number; + url: string; + download_url: string; +} +``` + +Then, define a method by creating `apps/storefront-middleware/api/custom-methods/getSocialImages` file with the following content: + +```ts +import { type IntegrationContext } from "../../types"; +import { SocialImagesArgs, SocialImagesResponse } from "./types"; + +export async function getSocialImages( + context: IntegrationContext, + args: SocialImagesArgs, +): Promise { + const image: SocialImagesResponse = await ( + await fetch(`https://picsum.photos/seed/${args.seed}/info`) + ).json(); + return image; +} +``` + +And export the method in the `/api/socialImagesExtension/index.ts` file. + +```ts [/api/myExtension/index.ts] +export * from "./getSocialImages"; +``` + +Now, thanks to the SDK synchronization, the `getSocialImages` method will be available and typed under `custom` +namespace when you use the SDK in your Storefront. + +```ts +// Storefront project +const { data } = sdk.custom.getSocialImages({/* args */}); +``` + +2. Now let's implement the UI for this feature. + +Create `SocialImages` component under `apps/storefront-unified-nextjs/components/social-images.tsx` + +```ts [apps/storefront-unified-nextjs/components/social-images.tsx] +import Image from 'next/image'; + +import { getSdk } from '@/sdk'; + +export async function SocialImages({ productId }: { productId: string }) { + const sdk = getSdk(); + const socialImage = await sdk.custom.getSocialImages({ seed: productId }); + + return ( +
+ social image +

By {socialImage.author}

+
+ ); +} +``` + +Add this component to Product Details Page. Open `apps/storefront-unified-nextjs/app/[locale]/(default)/product/[slug]/[id]/page.tsx` and use +`AccordionItem` component to add `SocialImages` component. + +```tsx [apps/storefront-unified-nextjs/app/[locale]/(default)/product/[slug]/[id]/page.tsx] + + /* [!code ++:9] */ + + Social Images + } + > + + + +``` + +Finally, we have implemented a completely custom feature. You can find a complete project example in this repository: + +If you want to get access to it, contact our [sales team](https://docs.alokai.com/enterprise). + + +::info +Read more about adding new API methods here: https://docs.alokai.com/storefront/integration-and-setup/creating-new-api-methods +:: diff --git a/docs/content/guides/4.customization/2.app-router/_dir.yml b/docs/content/guides/4.customization/2.app-router/_dir.yml new file mode 100644 index 0000000000..9d0a76f86a --- /dev/null +++ b/docs/content/guides/4.customization/2.app-router/_dir.yml @@ -0,0 +1,5 @@ +title: Next.js app router +sidebarRoot: true +navigation: + icon: tabler:cube + diff --git a/docs/content/guides/4.customization/2.app-router/images/available-for-pickup.webp b/docs/content/guides/4.customization/2.app-router/images/available-for-pickup.webp new file mode 100644 index 0000000000..19f9dc8e69 Binary files /dev/null and b/docs/content/guides/4.customization/2.app-router/images/available-for-pickup.webp differ diff --git a/docs/content/guides/4.customization/2.app-router/images/brands-missing-trans.webp b/docs/content/guides/4.customization/2.app-router/images/brands-missing-trans.webp new file mode 100644 index 0000000000..1f761c2c13 Binary files /dev/null and b/docs/content/guides/4.customization/2.app-router/images/brands-missing-trans.webp differ diff --git a/docs/content/guides/4.customization/2.app-router/images/brands-page.webp b/docs/content/guides/4.customization/2.app-router/images/brands-page.webp new file mode 100644 index 0000000000..4424a35211 Binary files /dev/null and b/docs/content/guides/4.customization/2.app-router/images/brands-page.webp differ diff --git a/docs/content/guides/4.customization/2.app-router/images/custom-slug.webp b/docs/content/guides/4.customization/2.app-router/images/custom-slug.webp new file mode 100644 index 0000000000..7b0e927de3 Binary files /dev/null and b/docs/content/guides/4.customization/2.app-router/images/custom-slug.webp differ diff --git a/docs/content/guides/4.customization/2.app-router/images/customizations.webp b/docs/content/guides/4.customization/2.app-router/images/customizations.webp new file mode 100644 index 0000000000..771f77040d Binary files /dev/null and b/docs/content/guides/4.customization/2.app-router/images/customizations.webp differ diff --git a/docs/content/guides/4.customization/2.app-router/images/logo-on-green.webp b/docs/content/guides/4.customization/2.app-router/images/logo-on-green.webp new file mode 100644 index 0000000000..b46dfaa6bf Binary files /dev/null and b/docs/content/guides/4.customization/2.app-router/images/logo-on-green.webp differ diff --git a/docs/content/guides/4.customization/2.app-router/images/plp-customization.webp b/docs/content/guides/4.customization/2.app-router/images/plp-customization.webp new file mode 100644 index 0000000000..33cd64d0c2 Binary files /dev/null and b/docs/content/guides/4.customization/2.app-router/images/plp-customization.webp differ diff --git a/docs/content/guides/4.customization/2.app-router/images/react-dev-tools.webp b/docs/content/guides/4.customization/2.app-router/images/react-dev-tools.webp new file mode 100644 index 0000000000..df75e9c4da Binary files /dev/null and b/docs/content/guides/4.customization/2.app-router/images/react-dev-tools.webp differ diff --git a/docs/content/guides/4.customization/2.app-router/images/reviews.webp b/docs/content/guides/4.customization/2.app-router/images/reviews.webp new file mode 100644 index 0000000000..e863a86c2b Binary files /dev/null and b/docs/content/guides/4.customization/2.app-router/images/reviews.webp differ diff --git a/docs/content/guides/4.customization/2.app-router/images/social-images.webp b/docs/content/guides/4.customization/2.app-router/images/social-images.webp new file mode 100644 index 0000000000..acf2d6b961 Binary files /dev/null and b/docs/content/guides/4.customization/2.app-router/images/social-images.webp differ diff --git a/docs/content/guides/4.customization-next-js/_dir.yml b/docs/content/guides/4.customization/_dir.yml similarity index 100% rename from docs/content/guides/4.customization-next-js/_dir.yml rename to docs/content/guides/4.customization/_dir.yml