# Editor setup URL: https://developers.front-commerce.com/docs/3.x/get-started/editor-setup

{frontMatter.description}

## Indexing Front-Commerce documentation in your editor's AI assistant Front-Commerce documentation comes ready with [`llms.txt`](https://developers.front-commerce.com/llms.txt), [`llms-full.txt`](https://developers.front-commerce.com/llms-full.txt) and [`llms-all.txt`](https://developers.front-commerce.com/llms-all.txt) files that can be used to index the documentation in your editor's AI assistant. To use it, you need to add the following to your editor's configuration: ### Cursor 1. Press `Ctrl+Shift+P` and type `Add new custom docs` 2. Add this URL: `https://developers.front-commerce.com/llms-all.txt` 3. In the Chat window, you can now type `@Front-Commerce` to provide Front-Commerce documentation to Cursor. ### VSCode + Copilot 1. Download the Front-Commerce documentation `llms.txt` file ```bash curl -L https://developers.front-commerce.com/llms-full.txt --create-dirs -o .vscode/front-commerce.md ``` 2. In `.vscode/settings.json`, add this: ```json { "github.copilot.chat.codeGeneration.instructions": [ { "file": "./.vscode/front-commerce.md" } ] } ``` ### Other IDEs If you're using another IDE and would like to know how to use [`llms.txt`](https://developers.front-commerce.com/llms.txt) and [`llms-full.txt`](https://developers.front-commerce.com/llms-full.txt) with it, please [contact us](mailto:contact@front-commerce.com). ### Alternative: ChatGPT / Claude You can also ask Front-Commerce related question to ChatGPT or Claude. In order to do so, you will need to copy the content of `https://developers.front-commerce.com/llms-full.txt` and paste it in your prompt. Once that step is done, both AI will have access to the Front-Commerce documentation. --- # Installation URL: https://developers.front-commerce.com/docs/3.x/get-started/installation

{frontMatter.description}

import ContactLink from "@site/src/components/ContactLink"; import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; ## Preparation This guide will walk you through the steps to configure your system for installing Front-Commerce packages from our private npm package repository using a global `.npmrc` file. ### Prerequisites :::caution Front-Commerce is a **closed source product**. This documentation supposes that you have access to our private repositories. ::: Access is granted to teams that have signed the relevant legal contracts, either as a **Partner**, a **Customer** or for a **Time-Limited Trial**. Please  if you need help or further information. Before you begin, ensure that you have the following: - A [Blackswift Gitlab](https://gitlab.blackswift.cloud/) account authorized to access our [Front-Commerce private workspace](https://gitlab.blackswift.cloud/front-commerce/) - A [Blackswift Gitlab Personal Access Token](https://gitlab.blackswift.cloud/-/profile/personal_access_tokens) with the `read_api` scope - Node.js and npm installed on your machine ### Step 1: Create a Blackswift Gitlab Personal Access Token 1. Log in to your Blackswift Gitlab account. 2. Navigate to your **Profile** and click on **Access Tokens**. 3. Click on the **[Add new token](https://gitlab.blackswift.cloud/-/profile/personal_access_tokens)** button. 4. Enter a name for your token and select the `read_api` scope. 5. Click on the **Create personal access token** button. 6. Copy the token value as you will need it in the next step. ### Step 2: Create a `.npmrc` file 1. Create a `.npmrc` file on your system (see [npm documentation for more information](https://docs.npmjs.com/cli/v9/configuring-npm/npmrc#files)), with the following content: ```ini title="~/.npmrc (for example)" //gitlab.blackswift.cloud/api/v4/projects/24/packages/npm/:_authToken= @front-commerce:registry=https://gitlab.blackswift.cloud/api/v4/projects/24/packages/npm/ ``` 1. Replace `` with the token value you copied in Step 1. ### Conclusion Congratulations! You should now be able to install Front-Commerce packages. If you encounter any issues during the installation process or are looking for alternatives, please refer to the [official Gitlab documentation](https://gitlab.blackswift.cloud/help/user/packages/npm_registry/index.md#install-a-package) or contact our support team for assistance. ## Installation This is the recommended way to start a new Front-Commerce project. 1. Open your terminal and run the following command (it uses `pnpm`, but you can use `npx` if you prefer): ```shell NPM_CONFIG_LEGACY_PEER_DEPS=true pnpm dlx create-remix@latest hello-v3 --template https://new.front-commerce.app/ -y ``` 2. Wait for the installation process to complete and follow the prompts 3. Create another `.npmrc` in the project root with the following content: ```ini title=".npmrc" legacy-peer-deps=true ``` :::info About `NPM_CONFIG_LEGACY_PEER_DEPS` This option (or `legacy-peer-deps=true` in `.npmrc`) is currently required when using `@front-commerce/compat`. This is due to the Apollo version used in 2.x being incompatible with the latest version `graphql` versions used now. When not using `@front-commerce/compat`, it is not required. We still have some usages in Front-Commerce core in the current stable version. Please bare with us! ::: This is not something we expect you to do for now, as you'll have to edit your `entry.*.ts` files, `server.ts` and your root route with Front-Commerce specific code. :::tip Instead of documenting these changes that could evolve, we prefer that you contact our support team. We'll provide a friendly and up-to-date guidance in a timely fashion! Deal? ::: ## Configure your project You must now configure your project to use Front-Commerce's extensions adapted to your context. Edit the `front-commerce-config.ts` file to configure your project, and create a `.env` file with essential configurations. Here are some examples: ````mdx-code-block Use Front-Commerce without any extension: only the default theme with static data. Useful for evaluation purposes, but quickly limited. ```typescript title="front-commerce.config.js" import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; import storesConfig from "./app/config/stores"; import cacheConfig from "./app/config/caching"; export default defineConfig({ extensions: [themeChocolatine()], stores: storesConfig, cache: cacheConfig, configuration: { providers: [], }, v2_compat: { useApolloClientQueries: true, useFormsy: true, }, }); ``` Finally, create a `.env` file with the following content: ```shell title=".env" # Front-Commerce configuration FRONT_COMMERCE_ENV=development FRONT_COMMERCE_PORT=4000 FRONT_COMMERCE_URL=http://localhost:4000 FRONT_COMMERCE_COOKIE_PASS=cookie-secret FRONT_COMMERCE_CACHE_API_TOKEN=cache-api-secret ``` Use Front-Commerce with a headless Magento2 instance. ```typescript title="front-commerce.config.js" import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; import magento2 from "@front-commerce/magento2"; import storesConfig from "./app/config/stores"; import cacheConfig from "./app/config/caching"; export default defineConfig({ extensions: [magento2({ storesConfig }), themeChocolatine()], stores: storesConfig, cache: cacheConfig, configuration: { providers: [], }, v2_compat: { useApolloClientQueries: true, useFormsy: true, }, pwa: { appName: "Front-Commerce", shortName: "Front-Commerce", description: "My e-commerce application", themeColor: "#fbb03b", icon: "assets/icon.png", maskableIcon: "assets/icon.png", }, }); ``` :::info Learn more about [configuring the Magento2 extension](../03-extensions/e-commerce/magento2/_category_.yml) and the prerequisites. ::: ```shell title=".env" # Front-Commerce configuration FRONT_COMMERCE_ENV=development FRONT_COMMERCE_PORT=4000 FRONT_COMMERCE_URL=http://localhost:4000 FRONT_COMMERCE_COOKIE_PASS=cookie-secret FRONT_COMMERCE_CACHE_API_TOKEN=cache-api-secret # Magento2 configuration (see https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento2#configuration) FRONT_COMMERCE_MAGENTO2_ENDPOINT=https://magento2.example.com FRONT_COMMERCE_MAGENTO2_CONSUMER_KEY=xxxxxxxxx FRONT_COMMERCE_MAGENTO2_CONSUMER_SECRET=xxxxxxxxx FRONT_COMMERCE_MAGENTO2_ACCESS_TOKEN=xxxxxxxxx FRONT_COMMERCE_MAGENTO2_ACCESS_TOKEN_SECRET=xxxxxxxxx ``` Use Front-Commerce with a headless Magento1 instance. ```typescript title="front-commerce.config.js" import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; import magento1 from "@front-commerce/magento1"; import storesConfig from "./app/config/stores"; import cacheConfig from "./app/config/caching"; export default defineConfig({ extensions: [magento1({ storesConfig }), themeChocolatine()], stores: storesConfig, cache: cacheConfig, configuration: { providers: [], }, v2_compat: { useApolloClientQueries: true, useFormsy: true, }, }); ``` :::info Learn more about [configuring the Magento1 extension](../03-extensions/e-commerce/magento1/_category_.yaml) and the prerequisites. ::: ```shell title=".env" # Front-Commerce configuration FRONT_COMMERCE_ENV=development FRONT_COMMERCE_PORT=4000 FRONT_COMMERCE_URL=http://localhost:4000 FRONT_COMMERCE_COOKIE_PASS=cookie-secret FRONT_COMMERCE_CACHE_API_TOKEN=cache-api-secret # Magento1 configuration FRONT_COMMERCE_MAGENTO1_ENDPOINT=https://magento1.example.com FRONT_COMMERCE_MAGENTO1_CONSUMER_KEY=xxxxxxxxx FRONT_COMMERCE_MAGENTO1_CONSUMER_SECRET=xxxxxxxxx FRONT_COMMERCE_MAGENTO1_ACCESS_TOKEN=xxxxxxxxx FRONT_COMMERCE_MAGENTO1_ACCESS_TOKEN_SECRET=xxxxxxxxx ``` ```` ## Start the application You should now be able to start the application without errors by running the `dev` command using your favorite package manager (we recommend [pnpm](https://pnpm.io/)): ``` pnpm dev ``` 🎉 Congrats, you should now see this screen when opening [http://localhost:4000/](http://localhost:4000/)! ![Front-Commerce's default home page screen](assets/first-start-screen.jpg) --- # Loading data from unified GraphQL schema URL: https://developers.front-commerce.com/docs/3.x/get-started/loading-data-from-unified-graphql-schema

{frontMatter.description}

## Prerequisites Before diving into the code, ensure you have the following prerequisites in place: - A Front-Commerce project configured and ready to use - Basic knowledge of [Remix routes](./your-first-route.mdx) - [FAQ GraphQL Module](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/main/skeleton/example-extensions/faq-demo/graphql) installed and configured if you want to execute code samples without any changes ## Loading Data In your app directory, create a new route file (e.g., `app/routes/_main.faq.$slug.tsx`) and add the following code: ```tsx title="app/routes/_main.faq.$slug.tsx" import { json } from "@front-commerce/remix/node"; import { useLoaderData } from "@front-commerce/remix/react"; import type { LoaderFunctionArgs } from "@remix-run/node"; import FaqDetail from "theme/pages/FaqDetail"; import { isRouteErrorResponse, useRouteError } from "@remix-run/react"; import { FaqEntryQueryDocument } from "~/graphql/graphql"; import { FrontCommerceApp } from "@front-commerce/remix"; // The route exports a `loader` function that is responsible // for fetching data and throwing errors, ensuring that // the main component is only rendered in the "happy path". export const loader = async ({ context, params }: LoaderFunctionArgs) => { const { slug } = params; // The `loader` uses the `FrontCommerceApp` class to access the Front-Commerce // core from Remix. In this example, we use it to fetch data from the GraphQL // unified API (similar to the one you're used to). const app = new FrontCommerceApp(context.frontCommerce); const response = await app.graphql.query(FaqEntryQueryDocument, { input: { slug }, }); if (!response.faqEntry) { throw new Response("Question not found", { status: 404 }); } return json({ faqEntry: response.faqEntry, }); }; // The main component is a plain React component that receives // the data from the loader, using Remix fetching primitives (`useLoaderData`) // both on the server and on the client. export default function Component() { const { faqEntry } = useLoaderData(); return ; } // The route also exports an ErrorBoundary component that is responsible // for displaying errors. It can be used to display a custom error page. export const ErrorBoundary = () => { const error = useRouteError(); if (isRouteErrorResponse(error)) { return
FAQ : question not found
; } }; ``` This is an example of a route that fetches a question from the GraphQL API and renders it using a `` Page component (that is a plain React component). As you can see, all the logic belongs to the route! ## Reference As we can see here retrieving dynamic data from Front-Commerce unified GraphQL schema can be done in a natural way from any route. But this only scratches the surface of what you can do with Front-Commerce and Remix. - [`loader`](https://remix.run/docs/en/main/route/loader) - _Each route can define a "loader" function that provides data to the route when rendering._ - [`action`](https://remix.run/docs/en/main/route/action) - _A route `action` is a server only function to handle data mutations and other actions. If a non-GET request is made to your route (`DELETE`, `PATCH`, `POST`, or `PUT`) then the action is called before the loaders.._ - [`Component`](https://remix.run/docs/en/main/route/component) - _The default export of a route module defines the component that will render when the route matches._ - [`ErrorBoundary`](https://remix.run/docs/en/main/route/error-boundary) - _A Remix ErrorBoundary component works just like normal React [error boundaries](https://reactjs.org/docs/error-boundaries.html), but with a few extra capabilities._ - [`FrontCommerceApp`](../04-api-reference/front-commerce-remix/front-commerce-app.mdx) - _The FrontCommerceApp class is the main entry point to the Front-Commerce core. It provides access to the Front-Commerce core and to the GraphQL client._ --- # Your first route URL: https://developers.front-commerce.com/docs/3.x/get-started/your-first-route

{frontMatter.description}

# File-based routing Front-Commerce uses Remix. Remix uses [API Routes](https://remix.run/docs/en/main/guides/api-routes) to handle all the routing of you application, and display the correct page when a user navigates to a URL. All routes are located under `app/routes` folder. API Routes will use the default export of each Route file to generate the route's UI. Go ahead and create a new route `hello-world.tsx`, that will display content when users browse http://localhost:4000/hello-world: ```tsx title="app/routes/hello-world.tsx" export default function HelloWorld() { return (

Hello world!

); } ``` # Loaders In order to display dynamic content in your routes, you will need to fetch some data. This is done by using Remix's Routes API method `loader`. Here is a basic example of how a `loader` could be used: ```tsx title="app/routes/hello-world.tsx" import type { LoaderFunction } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; export const loader: LoaderFunction = () => { // This method will be called prior to rendering your client-side route, it's generally used to fetch the data for your component(s) return { name: "Jack Doe" }; }; export default function HelloWorld() { const { name } = useLoaderData(); return (

Hello {name}!

); } ``` # And many more features Routes play a fundamental part in a Remix (and Front-Commerce) application. It is the glue between the URL requested by a user and the response your application will send. We will continue to learn how to use them for the most common actions in the next sections, but here is a preview of everything a route could do: ```tsx title="app/routes/hello-world.tsx" import { json, type ActionFunction, type HeadersFunction, type LinksFunction, type LoaderFunction, type MetaFunction, } from "@remix-run/node"; import { useLoaderData, Form, useRouteError } from "@remix-run/react"; export const headers: HeadersFunction = ({ actionHeaders, loaderHeaders, parentHeaders, errorHeaders, }) => ({ "X-User-Id": loaderHeaders.get("X-User-Id"), "Cache-Control": "max-age=300, s-maxage=3600", }); export const loader: LoaderFunction = async () => { // This method will be called prior to rendering your client-side route, it's generally used to fetch the data for your component(s) const user = await fetch("https://my-site.com/api/user").then((res) => res.json() ); return json({ name: user.name }, { headers: { "X-User-Id": user.id } }); }; export const links: LinksFunction = () => { return [ { rel: "icon", href: "/favicon.png", type: "image/png", }, { rel: "stylesheet", href: "https://my-site.com/styles.css", }, { rel: "preload", href: "/images/banner.jpg", as: "image", }, ]; }; export const action: ActionFunction = async ({ params, request }) => { const data = await request.formData(); const email = data.get("email"); await fetch("https://my-site.com/api/contact-list", { method: "POST", body: JSON.stringify({ email }), }); return { success: true }; }; export const meta: MetaFunction = ({ data }) => { return [{ title: `Hello ${data.name}` }]; }; export default function HelloWorld() { const { name } = useLoaderData(); return (

Hello {name}!

Contact us:

); } export function ErrorBoundary() { const error = useRouteError(); return (
Oops, something bad happened !
{error.toString()}
); } ``` # External resources - [Contact page example](https://remix.run/docs/en/main/start/tutorial#the-contact-route-ui) - [API Routes documentation](https://remix.run/docs/en/main/guides/api-routes). - [Loading data from an external API](https://daily-dev-tips.com/posts/remix-loading-data-from-an-external-api/) - [Interactive Remix routing](https://interactive-remix-routing-v2.netlify.app/) --- # Your first test 🧪 URL: https://developers.front-commerce.com/docs/3.x/get-started/your-first-test

{frontMatter.description}

# Resources - [Vitest](https://vitest.dev/) - [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) - [@front-commerce/core/testing](../04-api-reference/front-commerce-core/testing.mdx#createfrontcommerceproviderstub) - [@remix-run/testing](https://remix.run/docs/en/main/other-api/testing) :::important Tests defined in your custom extensions will only be tested if the extension has been added to the `front-commerce.config.ts` We match tests to the following patterns: - `**/*.{test,spec}.?(c|m)[jt]s?(x)` - file based testing - `**/__tests__/*.?(c|m)[jt]s?(x)` - folder based testing ::: # Testing a Component To test a component you can implement a test file with the following structure: ```tsx title="app/theme/components/MyComponent.test.tsx" const MyComponent = () => { const intl = useIntl(); return ; }; describe("MyComponent", () => { it("should render the Hello World text", () => { render(); expect(screen.getByText("Hello World")).toBeInTheDocument(); }); }); ``` You will notice at this stage the test will fail because the `useIntl` is missing the context for the `IntlProvider`. To fix this, you can use the [`FrontCommerceProviderStub`](../04-api-reference/front-commerce-core/testing.mdx#createfrontcommerceproviderstub) to provide the context to the component. ```tsx title="app/theme/components/MyComponent.test.tsx" import { createFrontCommerceProviderStub } from "@front-commerce/core/testing"; import { render, screen } from "@testing-library/react"; const FrontCommerceProviderStub = createFrontCommerceProviderStub({ messages: { "my-button.label": "Hello World", }, }); const MyComponent = () => { const intl = useIntl(); return ; }; describe("MyComponent", () => { it("should render the Hello World text", () => { render( ); expect(screen.getByText("Hello World")).toBeInTheDocument(); }); }); ``` # Testing a Route To test a route you can implement a test file with the following structure: :::caution Do not place the test file in the `app/routes` directory. As this will be detected as a nested route by Remix. ::: ```tsx title="app/__tests__/routes/hello-world.test.tsx" import { json } from "@front-commerce/remix/node"; import { createRemixStub } from "@remix-run/testing"; import { render, screen, waitFor } from "@testing-library/react"; import { createFrontCommerceProviderStub } from "@front-commerce/core/testing"; import HelloWorldRoute from "../../routes/hello-world"; const FrontCommerceProviderStub = createFrontCommerceProviderStub(); describe("hello-world route", () => { it("should render the component with fetched data", async () => { // Define your mock data based on the expected GraphQL response structure const mockData = { shop: { url: "https://example.com", }, me: { firstname: "John", }, navigationMenu: [ { id: "1", name: "Category 1", path: "/category-1" }, { id: "2", name: "Category 2", path: "/category-2" }, { id: "3", name: "Category 3", path: "/category-3" }, ], title: "Hello World", }; const RemixStub = createRemixStub([ { path: "/", Component: () => ( ), loader: () => { return json(mockData); }, }, ]); render(); // Note that the rendering is asynchronous, so we need to wait for the component to be rendered await waitFor(() => { expect(screen.getByText(`Hi John 👋`)).toBeInTheDocument(); expect( screen.getByText(`Welcome to https://example.com`) ).toBeInTheDocument(); // Verify one of the navigation menu items is rendered expect(screen.getByText("Category 1")).toBeInTheDocument(); }); }); }); ``` --- # Add a shipping method with pickup points URL: https://developers.front-commerce.com/docs/3.x/guides/add-a-shipping-method-with-pickup-points

{frontMatter.description}

This method can also be useful if you are planning to set up a Store Locator in your shop. If you are willing to display these pickups in the checkout this guide will help you. However if you are willing to save the needed information in the user's quote, please have a look at [Use custom shipping information](./use-custom-shipping-information) instead. :::info Prerequisite To go through this guide, you'll need to have a created a [new extension with a GraphQL module](./extend-the-graphql-schema) and to know how to [fetch data in a component](./create-a-business-component#fetching-our-data). ::: ## Add pickups to your GraphQL Schema The goal here is to be able to fetch a list of pickups from GraphQL. You could create your own types and your custom implementation. However, in Front-Commerce, we've created a GraphQL interface named `FcPickup` that ensures that any pickup point can be displayed in the PostalAddressSelector component of Front-Commerce. ### Declare your custom pickup points in your extension ```ts title="my-extension/modules/shipping/index.ts" import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "CustomShipping", typeDefs: /* GraphQL */ ` type CustomPickup implements FcPickup { id: ID! name: String! address: FcPostalAddress! coordinates: GeoCoordinates! schedule: FcSchedule } extend type Query { customPickupList: [CustomPickup] } `, }); ``` Feel free to add any parameters to your query. It can for instance be useful to use an address parameter to filter based on the location of the user. You can use the `FcPostalAddressInput` for this usecase. It would look like this: ```ts title="my-extension/modules/shipping/index.ts" import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "CustomShipping", typeDefs: /* GraphQL */ ` type CustomPickup implements FcPickup { id: ID! name: String! address: FcPostalAddress! coordinates: GeoCoordinates! schedule: FcSchedule } extend type Query { // highlight-start pickupList( "The shipping address" address: FcPostalAddressInput! ): [CustomPickup] // highlight-end } `, }); ``` Moreover, the fields listed in the `CustomPickup` type are compulsory and come from `FcPickup`. But if you need more, feel free to add them to your `CustomPickup` type. ### Add a loader You will now have to create a loader that returns the list of pickups. Let's say it's named `CustomShippingLoader` and has a function named `loadPickupList`. Then this list needs to be formatted following the GraphQL types used previously. You would have something like this: ```ts title="my-extension/modules/shipping/adapters/formatPickupPoint.ts" // Please keep in mind that this formatting function is an example // and needs to be adapted to your usecase. The goal is to have the // same guys, but with the correct values. const formatPickupPoint = (pickupPoint: CustomPickupPoint) => { return { id: pickupPoint.id, name: pickupPoint.name, address: { country: pickupPoint.countryCode, locality: pickupPoint.city, postalCode: pickupPoint.zipcode, streetAddress: pickupPoint.street, // If your address doesn't need any additional information, // you can use the DefaultPostalAddress type. This is the // default implementation of the interface FcPostalAddress. // If you're unsure, use it, it's a safe bet. __typename: "DefaultPostalAddress", }, coordinates: { latitude: pickupPoint.latitude, longitude: pickupPoint.longitude, }, schedule: { monday: pickupPoint.schedule.monday, tuesday: pickupPoint.schedule.tuesday, wednesday: pickupPoint.schedule.wednesday, thursday: pickupPoint.schedule.thursday, friday: pickupPoint.schedule.friday, saturday: pickupPoint.schedule.saturday, sunday: pickupPoint.schedule.sunday, }, }; }; ``` ```ts title="my-extension/modules/shipping/loaders.ts" import { formatPickupPoint } from "./adapters/formatPickupPoint"; class CustomShippingLoader { constructor(private axiosInstance: AxiosInstance) {} async loadPickupList() { const response = await this.axiosInstance.get("/your-remote-api"); return response.data.map(formatPickupPoint); } } ``` The API can live anywhere. It can come from your backend, a shipping service, or a CMS, as long as it returns pickup points. Once you've created your loader, you can register it in your GraphQL module's runtime, along with the resolvers: ```ts title="my-extension/modules/shipping/runtime.ts" import CustomShippingLoader from "./loaders"; import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ // highlight-start resolvers: { Query: { pickupList: (parent, args, { loaders }) => loaders.CustomShipping.loadPickupList(args.address), }, }, contextEnhancer: ({ loaders }) => { const axiosInstance = ; const CustomShipping = new CustomShippingLoader(axiosInstance); return { CustomShipping, }; }, // highlight-end }); ``` ### Test your API in your playground By now, if you've registered your extension in your `front-commerce.config.ts` and restarted the server, you should be able to retreive the pickup list from GraphQL in your playground (i.e. `http://localhost:4000/playground`). ```graphql query PickupListQuery($address: FcPostalAddressInput!) { pickupList(address: $address) { id name } } ``` ## Display the list of pickup points Now that you have the list of pickup points available in GraphQL, it is now possible to display them in your application. This can be done by using the component `theme/components/organisms/PostalAddressSelector`. To do so, you need to first retreive the list of pickup from GraphQL. This can be done by using this query: ```graphql title="my-extension/theme/modules/shipping/PickupListQuery.gql" #import "theme/components/organisms/PostalAddressSelector/PostalAddressSelectorFragment.gql" query PickupListQuery { pickupList { ...PostalAddressSelectorFragment } } ``` To retrieve this data, we will be using an API route: ```tsx title="my-extension/routes/api.pickupList.tsx" import { FrontCommerceApp } from "@front-commerce/remix"; import { json } from "@front-commerce/remix/node"; import { PickupListQueryDocument } from "~/graphql/graphql"; export async function loader({ request, context }) { const app = new FrontCommerceApp(context.frontCommerce); const pickupList = await app.graphql.query(PickupListQueryDocument); // In the real world application, you would have to also handle potential errors return json(pickupList.pickupList); } ``` Once you have the data, you can use it in your component with the `useApiFetcher` hook: ```tsx title="my-extension/theme/modules/shipping/PickupList.tsx" import { useApiFetcher } from "@front-commerce/remix/react"; import PickupListQuery from "./PickupListQuery.gql"; import PostalAddressSelector from "theme/components/organisms/PostalAddressSelector"; import type { loader } from "routes/api.pickupList"; const PickupList = () => { const { data, loading, error } = useApiFetcher("/api/pickupList"); const [activeLocation, setActiveLocation] = useState(null); const [activeLocationIntention, setActiveLocationIntention] = useState(null); if (loading) { return
Loading...
; } if (error) { return
Error...
; } return ( ); }; export default PickupList; ``` Please note here that we're using the `activeLocation` and the `activeLocationIntention`. The difference between those two is that the `activeLocation` is only set if the user explicitly clicked on the button that allows them to choose a pickup point. The intention is triggered if they've clicked on the map's marker or on the address of the pickup. This allows to fine tune the UX depending on your pickup selector needs. `activeLocation` though is entirely optional and you can rely on `activeLocationIntention` instead. ## Final words Please keep in mind that this feature is built for shipping purpose in priority. This means that you may need additional work to make it work with a custom store locator page for instance. But it shouldn't be too much work and would allow you to have the same behaviors in both your store locator and your checkout. Moreover, by using this method, this also means that you are only a step away from adding a click & collect shipping method (provided you have the existing logistics in your business). If you have any questions, please . We are eager to support more use cases. --- # Add a sorting method in your Product List Page URL: https://developers.front-commerce.com/docs/3.x/guides/add-a-sorting-method-in-your-product-list-page

{frontMatter.description}

## Set an attribute as sortable In Magento1 back office, navigate to `Catalog > Attributes > Manage Attributes`. In the list shown, find the attribute you want to set as sortable (i.e. `news_from_date`), and click on it. Scroll down to section `Frontend Properties`, and set `Used for Sorting in Product Listing` to "Yes".
![Screenshot of Magento1 attribute set as sortable](./assets/magento1-sort-attribute.png)
:::info This feature is not supported for Magento2 yet. If your project requires this feature, please contact us. ::: :::info This feature is not supported for Gezy yet. If your project requires this feature, please contact us. :::
## Add sorting attribute in your theme In order to add a new sorting attribute to your PLP, you can `theme/modules/Layer/LayerSorting/useOptions.js` like so: ```jsx title="app/theme/modules/Layer/LayerSorting/useOptions.js" import useDefaultOptions from "@front-commerce/theme/modules/Layer/LayerSorting/useOptions"; const useOptions = () => { const defaultOptions = useDefaultOptions(); return { ...defaultOptions, sortOptions: defaultOptions.sortOptions.concat([ { label: "New", value: "news_from_date", }, ]), }; }; export default useOptions; ``` After these steps, you should be able to use your attribute to sort your PLP:
![Screenshot of the theme featuring a new sorting attribute](./assets/theme-sorting-attribute.png)
--- # Add a component to Storybook URL: https://developers.front-commerce.com/docs/3.x/guides/add-component-to-storybook

{frontMatter.description}

import Figure from "@site/src/components/Figure"; The concept lying behind a Design System is to document how your brand communicates with its users. This goes from global guides such as "Voice and tone", "Code Convention", "Accessibility"… to components documentation that focus on where and how to use a specific component. Such a Design System should be customized to the way your team(s) work and should evolve according to your needs. But with Front-Commerce, we provide you a base you can iterate on. Technically, we use [Storybook](https://storybook.js.org/) which is a tool that references `stories` for each component you want to document. With these stories, it will launch for you a website that allows you to browse through your components. In this guide we will go through a basic tutorial that gets you started with writing stories within Front-Commerce. But if you want to learn more about Storybook, please refer to [its documentation](https://storybook.js.org/basics/writing-stories/). {/* TODO For more detailed information about Front-Commerce specific configurations of Storybook, please refer to TODO */} ## Add your own story In order to add your own story, you need to create a new file, that will document how to use a component and will reference each edge case for this component. Each case will be what we call a `story`. As explained in [Storybook's documentation](https://storybook.js.org/basics/writing-stories/): > A story captures the rendered state of a UI component. To do this, you will create a `.stories.tsx` file next to your component. In this example, we will use the same component as in Create a UI Component: `IllustratedContent.tsx`. Hence, your stories file will be `IllustratedContent.stories.tsx` and will look like this: {/* TODO update : [Create a UI Component](./create-a-ui-component) */} ```tsx title="app/theme/components/molecules/IllustratedContent/IllustratedContent.stories.tsx" import type { Meta, StoryObj } from "@storybook/react"; import IllustratedContent from "./IllustratedContent"; import Icon from "theme/components/atoms/Icon"; import { H3 } from "theme/components/atoms/Typography/Heading"; const meta: Meta = { component: IllustratedContent, }; export default meta; type Story = StoryObj; export const Default: Story = { render: () => ( }>

Shipping within 48h

), }; ``` Thanks to this file, when you launch Storybook (`pnpm run styleguide`), it will **register** the stories for the component `IllustratedContent`. It will make it accessible in the Storybook's website under `app` > `theme` > `components` > `molecules` \> `IllustratedContent`. And if you click on the `Default` story in the navigation, it will display a default story which will show the standard usecase of the `IllustratedContent` component.
![Storybook interface with the IllustratedContent's default story selected](./assets/illustrated-content-story.png)
:::note As you might have noticed, once you have added the above `IllustratedContent` story, if you edit the story or the component itself, it will update automatically in Storybook (this feature is called hot reloading). It greatly improves the experience of authoring UI components. ::: For now, we've registered only one story for the `IllustratedContent`. But you can add more to better explain edge cases of your component. For instance you could add a story with a very long content or with an `` instead of an `` in order to better see how it will behave. ```tsx title="app/theme/components/molecules/IllustratedContent/IllustratedContent.stories.tsx" import type { Meta, StoryObj } from "@storybook/react"; import IllustratedContent from "./IllustratedContent"; import Icon from "theme/components/atoms/Icon"; import { H3 } from "theme/components/atoms/Typography/Heading"; // highlight-start import { BodyParagraph } from "theme/components/atoms/Typography/Body"; import Image from "theme/components/atoms/Image"; // highlight-end const meta: Meta = { // highlight-next-line title: "My App/Components/Molecules/IllustratedContent", component: IllustratedContent, }; export default meta; type Story = StoryObj; export const Default: Story = { render: () => ( }>

Shipping within 48h

), }; // highlight-start export const WithContent: Story = { name: "With a lot of content", render: () => ( }>

Shipping within 48h

We are using many delivery services to let you choose what is best for you!
), }; export const WithImage: Story = { name: "With an image", render: () => ( } >

Shipping within 48h

), }; // highlight-end ```
![Storybook interface with many IllustratedContent's stories](./assets/many-illustrated-content-stories.png)
This can be a nice and efficient way to show your designer how the implementation will behave and ask for feedback to improve the User Experience. :::info You can see that we added a `title` key in the `meta` object. This is a way to organize your stories in Storybook. It will create a new section in the navigation with the title you provided. In this example, the `IllustratedContent` stories will be displayed under `My App` > `Components` > `Molecules` > `IllustratedContent`. We recommend organizing your stories, especially because we will explain something later that will require having an organized structure. More information about the `title` key can be found in the [Storybook's documentation](https://storybook.js.org/docs/writing-stories/naming-components-and-hierarchy). ::: ## Add a new usecase to an existing story There are some cases where the story already exists but you want to add a new edge case. This will usually happen when you override a core component of Front-Commerce and you want to update its story to document the new use case. In this example, we will consider that you have overridden and updated the [`Money`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/cfddfe8b4e27e46c3afb6e77753f02b4ab07b37b/packages/theme-chocolatine/theme/components/atoms/Typography/Money/Money.tsx) component (as explained in [Override a component](./override-a-component.mdx)) to add a new property `hasBorder`. To add this story, you will need to override the `Money.stories.tsx` in the same way you overrode the `Money.tsx`. This means that you should copy the existing story from [`node_modules/@front-commerce/theme-chocolatine/theme/components/atoms/Typography/Money/Money.stories.tsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/cfddfe8b4e27e46c3afb6e77753f02b4ab07b37b/packages/theme-chocolatine/theme/components/atoms/Typography/Money/Money.stories.tsx) to `app/theme/components/atoms/Typography/Money/Money.stories.tsx`. Once you have copied the file, you need to change the `title` key in the `meta` object. In our example, you can either modify the base title (e.g., `title: "Components/Atoms/Typography/MoneyOverride",`) or change the organization to fit your application (e.g., `title: "My App/Components/Atoms/Typography/Money",`). ```tsx title="app/theme/components/atoms/Typography/Money/Money.stories.tsx" // ... existing code const meta: Meta = { // highlight-next-line title: "My App/Components/Atoms/Typography/Money", tags: ["autodocs"], component: Money, // ... existing code }; ``` Then, you can add override stories to document the new use case. In this example, we will add the new `hasBorder` property to the `Money` component. ```tsx title="app/theme/components/atoms/Typography/Money/Money.stories.tsx" // ... existing code argTypes: { "value.amount": { control: "number", }, "value.currency": { control: "select", options: currenciesAllowedList as any as CurrencyType[], }, // highlight-start hasBorder: { control: "boolean", }, // highlight-end }, args: { "value.amount": 19999, "value.currency": currenciesAllowedList[0], // highlight-next-line hasBorder: false, }, // ... existing code ``` ## Create stories for complex components {/* You now are able to customize your stories and document all your components. */} {/* However, you may stumble into more complex issues such as internationalisation, */} {/* routing and data fetching in some of your components. */} {/* But it is your lucky day! With Front-Commerce's components, we have had the same */} {/* issues and have created helpers that should make your life easier when */} {/* documenting those complex components. */} {/* These helpers are located in */} {/* [`node_modules/front-commerce/src/web/storybook/addons`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/tree/2.x/src/web/storybook/addons) */} {/* and you can import them by using the alias `web/storybook/addons/...`. For now, */} {/* we have four of them: */} {/* - [`web/storybook/addons/apollo/ApolloDecorator`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/784684ce56cca69ca5c2e42d5d421a8c0b4bb9c3/src/web/storybook/addons/apollo/ApolloDecorator.js): */} {/* mocks the data fetched by your components */} {/* - [`web/storybook/addons/form/formDecorator`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/784684ce56cca69ca5c2e42d5d421a8c0b4bb9c3/src/web/storybook/addons/form/formDecorator.js): */} {/* mocks a Form surrounding the input component you are documenting */} {/* - [`web/storybook/addons/router/routerDecorator`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/784684ce56cca69ca5c2e42d5d421a8c0b4bb9c3/src/web/storybook/addons/router/routerDecorator.js): */} {/* mocks the routing and the URLs of your application */} {/* TODO document usage of each of these helpers */} :::info WIP This section is currently being migrated. ::: ## Display only the relevant stories to your Design System If you run the styleguide for your own project, you may notice that Front-Commerce comes with a lot of stories. This is what serves as documentation Front-Commerce base theme's core components. However, you might not use each one of them in your final theme, and some stories might become irrelevant in your design system. To choose which ones to display, you need to update the `.storybook/main.ts` configuration file, and add the key [`stories`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/cfddfe8b4e27e46c3afb6e77753f02b4ab07b37b/skeleton/.storybook/main.ts#L24). ```diff title=".storybook/main.ts" # ...existing code export default { - stories: ["../**/*.stories.@(js|jsx|mjs|ts|tsx)", ...extensionThemeStories], + stories: ["../app/**/*.stories.@(js|jsx|mjs|ts|tsx)"], addons: [ # ...existing code ], } ``` This configuration ensures that Storybook does not load unnecessary files, improving performance and organization. Additionally, you can exclude certain folders by adjusting the glob patterns. ## Automatic documentation Storybook provides automatic documentation for your components. It can display description, prop tables, usage examples and interactive controls automatically. Here is the way to add automatic documentation to your stories based on our `IllustratedContent` example: ```tsx title="app/theme/components/molecules/IllustratedContent/IllustratedContent.stories.tsx" const meta: Meta = { title: "My App/Components/Molecules/IllustratedContent", component: IllustratedContent, // highlight-start tags: ["autodocs"], parameters: { docs: { description: { component: "A component that displays an image or an icon with text.", }, }, }, // highlight-end }; ``` You can now see the documentation of your component in Storybook:
![Storybook interface with the IllustratedContent's default story selected](./assets/illustrated-content-stories-docs.png)\*
For a more detailed documentation example, you can refer to the [`Money`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/cfddfe8b4e27e46c3afb6e77753f02b4ab07b37b/packages/theme-chocolatine/theme/components/atoms/Typography/Money/Money.stories.tsx) story example. For further details, please check the [official Storybook documentation](https://storybook.js.org/docs/writing-docs/autodocs). --- # Add your own server timings URL: https://developers.front-commerce.com/docs/3.x/guides/adding-your-own-server-timings The `Server-Timing` header communicates one or more metrics and descriptions for a given request-response cycle. In Front-Commerce, this header is handled by the `Server-Timing` service. In this guide, we will show you how to use this to surface backend server timing metrics in order to diagnose potential performance issues. :::info This feature is enabled by default when the application is not in production mode (i.e. `FRONT_COMMERCE_ENV !== "production"`). If needed, you can also enable this feature in production by setting `FRONT_COMMERCE_FORCE_ENABLE_SERVER_TIMINGS` environment variable to `true`. ::: ## Server timings in routes In this example, we'll consider a route "`_main.acme.tsx`" that uses the `Server-Timing` service to measure the time an external call takes to resolve. ```tsx title="_main.acme.tsx" import AcmeComponent from "theme/components/AcmeComponent"; import { someExternalApiCall } from "theme/components/AcmeComponent/someExternalApiCall"; import { useLoaderData } from "@front-commerce/remix/react"; import { FrontCommerceApp } from "@front-commerce/remix"; import { LoaderFunction } from "@remix-run/node"; export const loader: LoaderFunction = async ({ context }) => { const app = new FrontCommerceApp(context.frontCommerce); // highlight-next-line app.services.ServerTimings.start("someExternalApiCall"); const result = await someExternalApiCall(); // highlight-next-line app.services.ServerTimings.end("someExternalApiCall"); return { acmeResult: result }; }; export default function Acme() { const { acmeResult } = useLoaderData(); return ; } ``` By doing this, the timing metrics will automatically be added to the response's headers. Typically, those headers can be easily visually seen in the browser's developer tools, under the "network" section, for each request: ![Server timing as seen in developer console](./assets/server-timing-loader.png) ## Server timings in GraphQL modules Similarily to the route way, `ServerTimings` services can also be used from GraphQL modules. As an example, we'll add a server timing in an "Acme" GraphQL module's runtime, but note that it can be used the same way wherever the Front-Commerce services are available. In this example, we'll consider that the `AcmeLoader` has an async initialization method that will make a call to an external API, and we want to know how long this initialization process takes: ```ts title="extensions/acme/modules/acme/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; import AcmeLoader from "./loaders"; export default createGraphQLRuntime({ resolvers, contextEnhancer: ({ services }) => { const acmeLoader = new AcmeLoader(); // highlight-start services.ServerTimings.start("acmeLoader.init"); acmeLoader.init(); services.ServerTimings.end("acmeLoader.init"); // highlight-end return { AcmeLoader: acmeLoader, }; }, }); ``` Similarily to the route way, it can also be visualized in the browser's developer console: ![Server timing as seen in developer console](./assets/server-timing-graphql.png) --- # Getting Started URL: https://developers.front-commerce.com/docs/3.x/guides/analytics/getting-started import AnalyticsFigure from "./assets/analytics.svg"; import Figure from "@site/src/components/Figure"; import ContactLink from "@site/src/components/ContactLink"; import { useState } from "react"; import SinceVersion from "@site/src/components/SinceVersion";

{frontMatter.description}

Front-Commerce uses [`analytics`](https://getanalytics.io/) under the hood. If we represent how it works, it would look like this:
Across your React application, you can track events using functions such as `trackEvent`, `trackPage` or `useTrackPage`, `useTrackOnMount`. Then, the event is dispatched to all the relevant plugins registered in your application, learn more in the [Tracking API](tracking-api) docs. This means that once you have correctly configured events in your React Components, adding new tracking services is less risky: it has no impact on what is being tracked. # Analytics Overview Front-Commerce uses [`analytics`](https://getanalytics.io/) to track events across your React application. Events are dispatched to configured plugins, making it easier to add new tracking services without changing your tracking code. ## Key Features - Track events with `trackEvent` - Track page views with `trackPage` and `useTrackPage` - Track component mounts with `useTrackOnMount` - GDPR-compliant consent management - Pluggable architecture for multiple analytics services ## Configure analytics :::info If you would like to directly jump into the code, you look at the [plugins](./plugins/_category_.yml) often used within a Front-Commerce app. ::: First you will need to configure your analytics by updating the configuration file: ```ts title="app/config/analytics.ts" import { type AnalyticsConfig } from "@front-commerce/core/react"; export default { analytics: { // Make sure that your analytics is enabled enable: true, // Enables the debug mode of the `analytics` library debug: true, // Pass any default settings to your plugins defaultSettings: {}, // The list of plugins is defined here plugins: [ { // The name allows to know if the user allowed to use // this tracking service or not, it should not be confused // with the name in the plugin script. name: "google-analytics", // Usually we always need to set it to true since GDPR needConsent: true, // Some plugins may have a privacy mode allowing them // to be enabled by default. Use this configuration to always // enable it. It is irrelevant if `needConsent` is `false`. enabledByDefault: true, // Settings can be an object or a function that will be called // with the current consent authorization from the visitor, it also // receive a 2nd parameter with codes of the other consents given. // Using a function can allow to have different settings depending // on the context settings: (authorization, otherAuthorizations) => { return { measurementIds: ["G-abc123"], gtagConfig: { anonymize_ip: !authorization, }, }; }, // It should either return a promise function or a dynamic import to // the plugin script which will be added at to the `analytics` library // see listed plugins: https://getanalytics.io/plugins/#supported-analytic-tools script: () => import("@analytics/google-analytics"), }, ], } satisfies AnalyticsConfig, }; ``` ### Retrieve authorized cookie services in analytics In Front-Commerce, authorized cookie services must also be defined in `app/config/analytics.ts` in order for them to be injected in the `settings(authorization, otherAuthorizations)` callback (as its second parameter). This can be done by declaring "dummy" modules in `config/analytics.ts` such as: ```diff title="app/config/analytics.ts" // ... + { + name: "my-service", + needConsent: true, + script: () => () => { + return { + name: "my-service", + }; + }, + }, ], }, }; ``` ## The GDPR consent If your plugins need consent of the user before running, you need to setup the `cookiesServices.js` file. This file will let you define which cookies and trackings services are used within your application and will let the user chose which tracking service to allow. ### Configuration ```js title="app/config/cookiesServices.js" export default { default_en: [ { // Category of cookies to allow the user to accept all the plugins at once in a specific category title: "Analytics", description: "These cookies allows us to measure the traffic on our contents and hence to improve them.", services: [ { // The name should be the same as mentioned in the `config/analytics.js` file name: "google-analytics", title: "Google Analytics", // display all the cookies managed by Google Analytics cookies: [ "_ga", "_gat", "_gid", "__utma", "__utmb", "__utmc", "__utmt", "__utmz", ], // Display a more granular consent control per service consentOptions: [ { name: "ad_storage", title: "Ads Storage", description: "Enables storage of advertising-related data like conversion measurement and remarketing", }, { name: "ad_user_data", title: "Ads User Data", description: "Allows collection and processing of user data for advertising purposes", }, { name: "ad_personalization", title: "Ads Personalization", description: "Enables personalized advertising based on user behavior and interests", }, { name: "analytics_storage", title: "Analytics Storage", description: "Enables storage of analytics data to measure site usage and performance", }, ], description: "Google Analytics cookies, from Google, are meant to gather statistics about visits.", link: "https://support.google.com/analytics/answer/6004245", }, ], }, ], }; ``` The consent is stored in 3 separate cookies: 1. `hasConsent` - If the user provided a consent answer (`authorized` or `denied`) for all services. 2. `authorizations` - a JSON string of all consents given in the following format ```ts { [service_name]: true | false, } ``` 3. `consentAuthorizations` - a JSON string of all consents given for a specific service in the following format ```ts { [service_name]: { [consent_name]: true | false, } } ``` :::info Important The expiration for these three cookies' should be configured in [`app/config/website.js`](/docs/2.x/reference/configurations#configwebsitejs). ```js title="app/config/website.js" export default { default_image_url: "https://placehold.it/150x220", available_page_sizes: [18, 36], .... rewrittenToRoot: ["/home"], useUserTitles: false, // highlight-next-line cookieMaxAgeInMonths: 12, }; ``` ::: ### Granular Consent Updates When using custom consent options defined in your `cookiesServices.js` file, you can implement granular consent updates by adding an `updateConsent` method to your analytics plugin. This method allows you to handle consent changes for specific tracking features. The `updateConsent` method is already included in the built-in `google-analytics` and `google-tag-manager` plugins, but you'll need to implement it yourself for custom plugins. Here's how to add consent updates to your plugin: ```ts title="app/config/analytics.ts" import { type AnalyticsConfig } from "@front-commerce/core/react"; export default { analytics: { // ... plugins: [ // ... { name: "my-service", // ... script: () => () => { return { name: "my-service", initialize: () => {...}, track: () => {...}, page: () => {...}, // highlight-start methods: { updateConsent: (consent: Record) => { // handle consent update for your service // eg: {ad_storage:true, ad_user_data:false, ad_personalization:true, analytics_storage:true} }, }, // highlight-end }; } }, ], } satisfies AnalyticsConfig, }; ``` --- # Tracking API URL: https://developers.front-commerce.com/docs/3.x/guides/analytics/tracking-api The Tracking API is exposed once the analytics library is initialized with configuration. ## `trackEvent` An event is something that happens in your application. For instance, it can happens when a user clicks on a button, opens a dropdown, etc. It usually conveys meaning to your marketing team to better understand what drives your users. Most of the e-commerce related events are already implemented within Front-Commerce. But each website will have different behaviors, and it can be interesting to add your own events to see how your customers uses your application. To do so, you will need to call the method `trackEvent` from `@front-commerce/core/react`. For instance, let's say that you are building a grocery store and that you have created Recipe pages that display a list of ingredients needed for the current recipe. Below this list, you have created a button that adds all the ingredients to the cart of the user, and you want to know if this button is useful and if users click on it. To add your tracking, you would need to call the `trackEvent` method when the user clicks on the Button. Thus, your new component would look like this: ```js import React from "react"; import Button from "theme/components/atoms/Button"; // highlight-next-line import { trackEvent } from "@front-commerce/core/react"; const AddIngredientsToCart = ({ addToCart, ingredients }) => { return ( ); }; export default AddIngredientsToCart; ``` This `trackEvent` method is actually a shortcut that lets you call the `analytics.track` method from the [`analytics`](https://getanalytics.io/api/#analyticstrack) module. It uses the exact same API. :::note Please refer to the [trackPage](../../04-api-reference/front-commerce-core/react.mdx#trackevent) documentation. ::: ## `useTrackOnMount` If you don't have an actual callback to put the `trackEvent` (like `onClick`), you can use the `useTrackOnMount` hook that will let you call the `trackEvent` using [React lifecycle](http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/). For instance, in Front-Commerce's core, we are using `useTrackOnMount` to track when a user sees their cart. ```jsx import { useTrackOnMount } from "@front-commerce/remix/react"; const Component = (props) => { useTrackOnMount({ event: "Cart Viewed", hasResolved: cart, shouldUpdateEvent: shouldUpdate, createProperties: () => { const totalInclTax = cart?.totals?.totalInclTax?.value; return { cart_id: currentCartId, value: totalInclTax?.amount, currency: totalInclTax?.currency, products: cart?.items ? cart?.items.map((item, index) => ({ sku: item.sku, name: item.name, quantity: item.qty, price: item.priceInfo.price.priceInclTax.value.amount, position: index + 1, })) : [], }; }, }); return
My component
; }; ``` :::note Please refer to the [useTrackOnMount](../../04-api-reference/front-commerce-remix/react.mdx#usetrackonmount) documentation. ::: ## `useTrackPage` In tracking scripts, there is often a distinction between the `page` and the `event` even though a `page` event is only a subset of the `events`. To make this distinction clear, we provide an enhancer in the same spirit of `useTrackOnMount` hook but for page events: `useTrackPage`. Example: ```tsx import { useTrackPage } from "@front-commerce/remix/react"; function AcmePage() { useTrackPage("Acme Page"); return
Acme Page
; } ``` :::note Please refer to the [useTrackPage](../../04-api-reference/front-commerce-remix/react.mdx#usetrackpage) documentation. ::: --- # Custom Plugins URL: https://developers.front-commerce.com/docs/3.x/guides/analytics/custom-plugins import AnalyticsFigure from "./assets/analytics.svg"; import Figure from "@site/src/components/Figure"; import ContactLink from "@site/src/components/ContactLink"; import { useState } from "react"; import SinceVersion from "@site/src/components/SinceVersion";

{frontMatter.description}

Plugins are a powerful abstraction that let you: - add a new analytics provider (like Google analytics or Meta pixel) - hook into an existing analytics provider plugin - or add any kind of logic to react to visitor actions Plugins can be broken down into 2 types: - **Provider plugins** - connecting to third party analytic services - **Custom plugins** - additional features, data manipulation, & any other side effects. Both have the same signature, and are registered in the same way, here we will explore how they are implemented within a Front-Commerce app. :::info You can look at the analytics library documentation to learn more about the plugin types. - [Plugins](https://getanalytics.io/plugins) - [Writing Plugins](https://getanalytics.io/plugins/writing-plugins/) ::: ## Writing a new plugin :::tip You can also [request and contribute plugins](https://getanalytics.io/plugins/request/) within the analytics library ❤️ Open Source ::: Let's say for this example we want to create a new provider plugin for a third party analytics tool `acme`. We will name this plugin `acme`. Provider plugins typically have the following structure: 1. Load in the third party analytics script via `initialize` 2. Implement `track` or `page` events to send data into a third party analytics tool _There is also another event `identify` which is not currently implemented in Front-commerce_ 3. Have a loaded function to let analytics know when its safe to send the third party data. Here is an example of the `acme` plugin: ```ts title="analytics/plugins/acme.ts" import type { AnalyticsPlugin } from "@front-commerce/core/react"; // `settings` is the value defined in the plugin config in `config/analytics.ts` export default function acmePluginExample(settings): AnalyticsPlugin { // return object for analytics to use return { /* All plugins require a name */ name: "acme", /* Everything else below this is optional depending on your plugin requirements */ config: { whatEver: settings.whatEver, elseYouNeed: settings.elseYouNeed, }, initialize: ({ config }) => { // load provider script to page }, page: ({ payload }) => { // call provider specific page tracking }, track: ({ payload }) => { // call provider specific event tracking }, identify: ({ payload }) => { // call provider specific user identify method }, loaded: () => { // return boolean so analytics knows when it can send data to third party return !!window.myPluginLoaded; }, }; } ``` ## Tracking `CommerceEvents` :::info In `v2` this was done by extending the [`EcommercePlugin`](/docs/2.x/advanced/analytics/plugins#extending-with-ecommerceplugin) which can still be used, but has a less comprehensive API. We handle this internally for [known plugins](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/784684ce56cca69ca5c2e42d5d421a8c0b4bb9c3/src/web/core/analytics/plugins/e-commerce/e-commerce.js#L6-11) like the [`google-analytics`](https://getanalytics.io/plugins/google-analytics/) plugin. ::: If you want to track [`CommerceEvents`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/3f6ed5af24b17a9c95f1dd024238bb2892e5075b/packages/core/analytics/plugins/commerce-events.ts) in your custom plugin, you can do so by using the `commercePayload` object in the `track` method access to the Front-Commerce fully typed tagging plan for the `CommerceEvents`. ```tsx title="analytics/plugins/acme.ts" import type { AnalyticsPlugin } from "@front-commerce/core/react"; export default function acmePluginExample(settings): AnalyticsPlugin { return { name: "acme", ... track: ({ payload, commercePayload }) => { if(!commercePayload){ return // track normal events or not } // highlight-start // track commerce events const { event, ...rest } = commercePayload; switch (event) { case "Product Viewed": // track product viewed event break; case "Product Added": // track product added event break; case "Product Removed": // track product removed event break; case "Cart Viewed": // track cart viewed event break; case "Order Completed": // track order completed event break; default: // track other events break; } // highlight-end }, }; } ``` ## Using Plugins In Front-Commerce the plugins are dynamically required via the `script` property in your `app/config/analytics.ts` configuration, this allow us to only import and load scripts based on authorisation (cookies), which in turn reduces the page loading for your end users. Let's add our `acme` plugin to our `app/config/analytics.ts` file. ```ts title="app/config/analytics.ts" import { type AnalyticsConfig } from "@front-commerce/core/react"; export default { analytics: { enable: true, debug: process.env.NODE_ENV !== "production", plugins: [ // highlight-start { // remember to add the `acme` to your cookieServices config or disable the needConsent name: "acme", needConsent: true, // all these settings will be passed to your plugin script settings: { whatEver: "foo", elseYouNeed: "bar", } // add a dynamic import to load the plugin script script: () => import("./analytics/plugins/acme.ts"), }, // highlight-end ], } satisfies AnalyticsConfig, }; ``` :::info REMINDER Just like any other plugin, don't forget to setup the [`cookiesServices.js`](./01-getting-started.mdx) file accordingly, in order to load the newly created integration only when the user has given their consent. ::: --- # Addingwell URL: https://developers.front-commerce.com/docs/3.x/guides/analytics/plugins/addingwell

{frontMatter.description}

## Prerequisites Before configuring Addingwell in Front-Commerce, ensure that you have: - created an [AddingWell account](https://www.addingwell.com/) - setup your own server, as documented in the [Addingwell Get Started](https://docs.addingwell.com/b/2CA8BCF1-E67D-46DF-A2BE-F93017D8FD7A/Getting-started-with-Addingwell) official guide ## With GTM This guide will show you how to integrate Addingwell with Google Tag Manager. This way, the Analytics tag will be implemented with Google Tag Manager but the traffic will head to your custom server. ### Setup GTM You must follow the [Addingwell Send data documentation (Option 1)](https://docs.addingwell.com/b/C45DD221-2773-4DE0-B588-39B60785291A/Send-the-data-to-your-tagging-server). ### Setup GTM Analytics plugin in Front-Commerce Install the Google Tag Manager analytics plugin ```shell pnpm add @analytics/google-tag-manager ``` Then edit your analytics configuration from the `app/config/analytics.ts` to add the Google Tag Manager plugin: ```ts title="app/config/analytics.ts" import { type AnalyticsConfig } from "@front-commerce/core/react"; export default { analytics: { enable: true, debug: process.env.NODE_ENV !== "production", plugins: [ // highlight-start { name: "google-tag-manager", needConsent: false, enabledByDefault: true, settings: (authorization) => { return { containerId: "GTM-XXXXXXXX", // This should be the GTM container ID of the Container that will inject Google Tag not the server container ID }; }, script: () => import("@analytics/google-tag-manager"), }, // highlight-end ], } satisfies AnalyticsConfig, }; ``` ### Setup CSP Then you'll need to update your CSP to allow your Front-Commerce application to communicate with external services. Let's say that your analytics domain is `metrics.my-commerce.net` ```js title="app/config/cspProvider.ts" const appCSPProvider = () => { return { name: "cspConfiguration", values: { contentSecurityPolicy: { __dangerouslyDisable: false, directives: { // highlight-next-line scriptSrc: ["metrics.my-commerce.net", "www.googletagmanager.com"], // We need to add Google Tag Manager to allow GTM to inject GA4 tag into our pages frameSrc: [], styleSrc: [], imgSrc: [], fontSrc: [], // highlight-next-line connectSrc: ["metrics.my-commerce.net"], baseUri: [], }, }, }, }; }; export default appCSPProvider; ``` ## With GA4 ### Setup your server container You first need to follow the Addingwell [gtag.js configuration guide (option 3)](https://docs.addingwell.com/b/C45DD221-2773-4DE0-B588-39B60785291A/Send-the-data-to-your-tagging-server). Please note your custom tagging domain we will need this for the next step (in the guide, we will consider that the domain is `metrics.my-commerce.net`). ### Setup GA4 plugin in Front-Commerce Install the GA4 plugin with the following command: ```shell npm install @analytics/google-analytics ``` Then edit your analytics configuration from the `app/config/analytics.js` to add the Google Tag Manager plugin: ```js title="app/config/analytics.js" export default { analytics: { // highlight-next-line enable: true, debug: process.env.NODE_ENV !== "production", defaultSettings: {}, plugins: [ // highlight-start { name: "google-analytics", needConsent: false, enabledByDefault: true, settings: (authorization) => { return { customScriptSrc: "https://metrics.my-commerce.net/gtag/js?id=G-XXXXXXXX", measurementIds: ["G-XXXXXXXX"], // You can find this measurement ID in your Google Analytics account: Administration > Data Stream gtagConfig: { transport_url: "https://metrics.my-commerce.net", first_party_collection: true, }, }; }, script: () => import("@analytics/google-tag-manager"), }, // highlight-end ], }, }; ``` ### Setup CSP Then you'll need to update your CSP to allow your Front-Commerce application to communicate with you addingwell server. ```js title="app/config/cspProvider.ts" const appCSPProvider = () => { return { name: "cspConfiguration", values: { contentSecurityPolicy: { __dangerouslyDisable: false, directives: { // highlight-next-line scriptSrc: ["metrics.my-commerce.net"], frameSrc: [], styleSrc: [], imgSrc: [], fontSrc: [], // highlight-next-line connectSrc: ["metrics.my-commerce.net"], baseUri: [], }, }, }, }; }; export default appCSPProvider; ``` --- # Examples URL: https://developers.front-commerce.com/docs/3.x/guides/analytics/plugins/examples :::info Feel free to browse [**our examples plugins**](https://github.com/front-commerce/examples), and even contribute to them if you have any ideas! 💡 ::: ## Meta Pixel To track in Meta Pixel, you can use our [example plugin](https://github.com/front-commerce/examples/tree/main/analytics/meta-pixel) as a good starting point. ## Matomo To track in Matomo, you can use our [example plugin](https://github.com/front-commerce/examples/tree/main/analytics/matomo) as a good starting point. --- # Google Analytics 4 URL: https://developers.front-commerce.com/docs/3.x/guides/analytics/plugins/google-analytics-4

{frontMatter.description}

## Prerequisites Before configuring Google Analytics 4 in Front-Commerce, ensure that you have: - A GA4 measurement ID. ## Resources - [GA4 Documentation](https://developers.google.com/analytics/devguides/collection/ga4) - [Plugin Documentation](https://getanalytics.io/plugins/google-analytics) ## Installation ```shell pnpm add @analytics/google-analytics ``` ## Configuration ```ts title="app/config/analytics.ts" import { type AnalyticsConfig } from "@front-commerce/core/react"; export default { analytics: { enable: true, debug: process.env.NODE_ENV !== "production", plugins: [ // highlight-start { name: "google-analytics", needConsent: true, settings: (authorization) => { return { measurementIds: ["G-XXXXXXXXXX"], gtagConfig: { anonymize_ip: !authorization, }, }; }, script: () => import("@analytics/google-analytics"), }, // highlight-end ], } satisfies AnalyticsConfig, }; ``` --- # Google Tag Manager URL: https://developers.front-commerce.com/docs/3.x/guides/analytics/plugins/google-tag-manager

{frontMatter.description}

## Prerequisites Before configuring Google Tag Manager in Front-Commerce, ensure that you have: - A GTM container ID. ## Resources - [GTM Documentation](https://developers.google.com/tag-manager) - [Plugin Documentation](https://getanalytics.io/plugins/google-tag-manager/) ## Installation ```shell pnpm add @analytics/google-tag-manager ``` ## Configuration ```ts title="app/config/analytics.ts" import { type AnalyticsConfig } from "@front-commerce/core/react"; export default { analytics: { enable: true, debug: process.env.NODE_ENV !== "production", plugins: [ // highlight-start { name: "google-tag-manager", needConsent: true, settings: (authorization, otherAuthorizations) => { // This ensure an event is pushed with current authorizations // right after the plugin's initialization. window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: "initConsents", userConsents: otherAuthorizations, }); return { containerId: "GTM-XXXXXX", }; }, script: () => import("@analytics/google-tag-manager"), }, // highlight-end ], } satisfies AnalyticsConfig, }; ``` ## Setup CSP You will need to update your CSP to allow your Front-Commerce application to communicate with the GTM servers. ```ts title="app/config/cspProvider.ts" const appCSPProvider = () => { return { name: "cspConfiguration", values: { contentSecurityPolicy: { __dangerouslyDisable: false, directives: { // add-next-line scriptSrc: ["www.googletagmanager.com"], ...[], }, }, }, }; }; export default appCSPProvider; ``` ## User Consent In GTM, you will then be able to leverage several specific things configured in your plugins. First, pushing the `initConsents` event will push the current customer's authorization to your dataLayer as `userConsents` value. You can reference it from a Variable in GTM. Here is an example:
![Screenshot of a GTM Variable configured to expose the user consents](./assets/gtm-datalayer-variable.png)
Then, you can leverage the `UserConsentUpdated` event tracked whenever users update their consent preferences. You could create triggers to enable scripts to load / remove (depending on the `userConsents` value). Here is an example:
![Screenshot of a GTM Trigger configured to detect when users gave their consent to a specific integration](./assets/gtm-trigger-example.png)
Please note that to retrieve the authorized cookies services in GTM's datalayer, services must be [declared in your `analytics.js`](../getting-started#retrieve-authorized-cookie-services-in-analytics). --- # Before going to production URL: https://developers.front-commerce.com/docs/3.x/guides/before-going-to-production

{frontMatter.description}

To make sure you're ready, go through this checklist: - [ ] Style your **Offline page**
:::info This page is accessible when you disable your network while browsing your shop. If you have `FRONT_COMMERCE_ENV !== "production"`, this page will also be available at `/__front-commerce/offline`. You can then [override](./override-a-component.mdx) the [`theme/pages/Offline`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/0cb2bf0bd475e6fcf79d4f4af340636ffcf02821/packages/theme-chocolatine/theme/pages/Offline) component to make sure that this page suits your brand. :::
- [ ] Style your **Maintenance page**
:::info This page is accessible when your backend is in maintenance mode (HTTP 503). If you have `FRONT_COMMERCE_ENV !== "production"`, this page will also be available at `/__front-commerce/maintenance`. You can then [override](./override-a-component.mdx) the [`theme/pages/Maintenance`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/0a9c1d53d22a9cd11d8e4fc9eac016781dfcfa0f/packages/theme-chocolatine/theme/pages/Error/Maintenance) component to make sure that this page suits your brand. And take the opportunity to learn more about maintenance mode with [our guide](./maintenance-mode/01-entering-and-exiting-maintenance-mode.mdx). :::
- [ ] Ensure your server can serve the application in **HTTPS** mode.
:::info Front-Commerce is aimed at being exposed in HTTPS when in production mode (`NODE_ENV === "production"`). It will redirect users to an HTTPS URL if they try to access a page using HTTP. Cookies are also set with secure mode. :::
- [ ] Ensure that your PWA is set up.
:::info It allows users to install your store as a native application on their device. See the [PWA Setup guide](./pwa-setup.mdx). :::
- [ ] Override `robots.txt` if necessary.
:::info By default, your `robots.txt` is generated and looks something like this: ```txt User-agent: * Allow: / (Disallow: / in development) Sitemap: https://shopURL.com/sitemap.xml ``` You can see how it is generated [here](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/5bfd00bfe4c6d7f948418bdc5d018409d5a2d950/packages/remix/seo/robots/index.ts). If you need to customize your `robots.txt`, override `robots[.txt].tsx` : ```tsx title="app/routes/robots[.txt].tsx" import type { LoaderFunctionArgs } from "@remix-run/node"; import { generateRobotsTxt } from "@front-commerce/remix/seo"; import { FrontCommerceApp } from "@front-commerce/remix"; const ONE_HOUR = 60 * 60; const ONE_DAY = 24 * ONE_HOUR; export function loader({ context }: LoaderFunctionArgs) { const app = new FrontCommerceApp(context.frontCommerce); app.services.CacheControl.setCacheable({ sMaxAge: ONE_HOUR, staleWhileRevalidate: ONE_DAY, }); return generateRobotsTxt( [ { type: "sitemap", value: new URL("/sitemap.xml", app.config.shop.url).toString(), }, ], { allowCrawling: true, } ); } ``` :::
--- # Using DataLoaders in GraphQL loaders URL: https://developers.front-commerce.com/docs/3.x/guides/caching-remote-data/using-dataloaders-in-graphql-loaders ## What are DataLoaders? DataLoader is a pattern promoted by Facebook, from their internal implementations, to solve problems with data fetching. We use this name because it is the name of the reference implementation in Javascript: [graphql/dataloader](https://github.com/graphql/dataloader). A DataLoader is instantiated with a **batching function**, which allows data to be fetched in groups (see [Batching](../../05-concepts/common-issues-in-the-data-fetching-layer.mdx#batching)). It also has a caching strategy that prevents fetching the same data twice in the same request or across requests (see [Caching](../../05-concepts/common-issues-in-the-data-fetching-layer.mdx#caching)). :::note For a better understanding of why we use DataLoaders, read the [Common issues in the data fetching layer](../../05-concepts/common-issues-in-the-data-fetching-layer.mdx) guide. ::: By default every DataLoader provides request-level caching. But this can be configured to switch to a persistent caching strategy instead (see [Configure caching strategies](./02-configure-caching-strategies.mdx#caching-strategies)). We encourage you to read the [DataLoader readme](https://github.com/graphql/dataloader/blob/main/README.md) documentation to learn more about how it works. Front-Commerce provides a factory function to create DataLoaders from your GraphQL modules while keeping caching strategies configurable. Under the hood, it is a pure DataLoader instance, so you could use it in a standard manner. ## Using DataLoaders in Front-Commerce When building a GraphQL module, Front-Commerce will inject a `makeDataLoader` factory function in your [module’s `contextEnhancer` function](../../04-api-reference/front-commerce-core/graphql.mdx#contextenhancer-function). ### `makeDataLoader` usage The `makeDataLoader` factory allows developers to build a DataLoader without worrying about the current store scope (in a multi-store environment) or caching concern. Here is an example based on the use case above: ```ts title="extensions/acme/inventory/graphql/index.ts" import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "AcmeInventory", loadRuntime: () => import("./runtime"), typeDefs: /* GraphQL */ ` extend type Product { qty: Int } `, }); ``` ```ts title="extensions/acme/inventory/graphql/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; import StockLoader from "./loader"; import axios from "axios"; export default createGraphQLRuntime({ contextEnhancer: ({ makeDataLoader, config }) => { const axiosInstance = axios.create({ baseURL: config.inventoryApiEndpointUrl, }); return { // create an instance of the loader, to be made available in resolvers Stock: new StockLoader(makeDataLoader, axiosInstance), }; }, resolvers: { Product: { qty: ({ sku }, _, { loaders }) => { // use the loader instance to fetch data // batching and caching is transparent in the resolver return loaders.Stock.loadBySku(sku); }, }, }, }); ``` ```ts title="extensions/acme/inventory/graphql/loader.ts" import { reorderForIds } from "@front-commerce/core/graphql"; import type { DataLoaderScopedFactory } from "@front-commerce/core/cache"; import type { AxiosInstance } from "axios"; class StockLoader { private httpClient: AxiosInstance; private dataLoader: ReturnType>; constructor( makeDataLoader: DataLoaderScopedFactory, httpClient: AxiosInstance ) { // The `Stock` key here must be unique across the project // and is used in cache configuration to determine the caching strategy to use this.dataLoader = makeDataLoader("Stock")( (skus: readonly (string | number)[]) => this.loadStocksBatch(skus) ); this.httpClient = httpClient; } // Our batching function that will be injected in the DataLoader factory // it is important to return results in the same order than the passed `skus` // hence the use of `reorderForIds` (documented later in this page) private loadStocksBatch(skus: readonly (string | number)[]) { return this.httpClient .get("/stocks", { params: { skus } }) .then((response) => response.data.items) .then(reorderForIds(skus, "sku")); } loadBySku(sku: string | number) { return this.dataLoader.load(sku); } } export default StockLoader; ``` The 2nd parameter to `makeDataLoader` are the options to pass to the DataLoader instance. You usually don't have to use it. Please refer to [dataloader's documentation](https://github.com/graphql/dataloader#new-dataloaderbatchloadfn--options) for further information. ### Useful patterns #### Prevent caching errors (data not found) Batching functions will sometimes return `null` or _falsy_ data for nonexistent items. By default, these values will be cached so further data retrieval could return this `null` value instead of doing a remote API call. In some specific cases, you may want to force fetching data from the remote source every time. You can do so by returning an `Error` for the nonexistent item. Here is an example: ```js const fooLoader = makeDataLoader("AcmeFoo")((ids) => loadFooBatch(ids).then((items) => items.map((item) => { if (!item) { return new Error("not found"); } return item; }) ) ); ``` #### Using a predefined TTL In some contexts, cache invalidation could be impossible or difficult to implement in remote systems. You may still want to leverage Front-Commerce's caching features, such as [the Redis persistent cache](../../04-api-reference/front-commerce-core/caching-strategies.mdx#redis), to improve performance of your application. The Redis strategy supports an additional option (to be provided during instantiation) that allows you to create a loader with a specified expiration time for cached items. Here is how you could use it: ```js const fooLoader = makeDataLoader("AcmeFoo")( (ids) => loadFooBatch(ids), { expire: EXPIRE_TIME_IN_SECONDS } // see https://github.com/DubFriend/redis-dataloader ); ``` #### Caching scalar values DataLoaders mostly manipulate objects. Hence, it is safer to design your application to return objects from batching functions. This will ensure a wider range of caching strategies' compatibility (ex: Redis strategy does not support caching of scalar values). ```js const fooLoader = makeDataLoader("AcmeFoo")((ids) => loadFooBatch(ids).then((results) => results.map((result) => ({ value: result })) ) ); // … return fooLoader.load(id).then((data) => data.value); ``` ## Helpers available to create dataLoaders Writing batching functions and loaders could lead to reusing the same patterns. We have extracted some utility functions to help you in this task. You can find them in the [`node_modules/@front-commerce/core/graphql/dataloaderHelpers.ts`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/507d58e1d70368cab9a0ff7b93f11af374823f68/packages/core/graphql/dataloaderHelpers.ts) module. ### `reorderForIds` Batch functions must satisfy two constraints to be used in a DataLoader (from the [graphql/dataloader documentation](https://github.com/graphql/dataloader#batch-function)): 1. The Array of values must be the same length as the Array of keys 2. Each index in the Array of values must correspond to the same index in the Array of keys. `reorderForIds` will ensure that these constraints are satisfied. Signature: `const reorderForIds = (ids, idKey = "id") => data => sortedData;` It will sort `data` by `idKey` to match the order from the `ids` array passed in parameters. In case no matching values is found, it will return `null` and log a message so you could then understand why no result was found for a given id. Example: ```js // skus will very likely be a param of your batch loader const skus = ["P01", "P02", "P03"]; return ( axiosInstance .get("/frontcommerce/price", { params: { skus: skus.join(","), }, }) .then((response) => { const prices = response.data; /* [ {sku: "P02", price: 12}, {sku: "P03", price: 13}, {sku: "P01", price: 11}, ] */ return prices; }) // results will be sorted according to the initial skus passed (P01, P02, P03) .then(reorderForIds(skus, "sku")) ); ``` ### `reorderForIdsCaseInsensitive` As its name implies, it is very similar to `reorderForIds` but ids are compared in a case insensitive way. Example: ```js return axiosInstance .get(`/products`, { params: searchCriteria }) .then((response) => response.data.items.map(convertMagentoProductForFront)) .then(reorderForIdsCaseInsensitive(skus, "sku")); ``` ### `makeBatchLoaderFromSingleFetch` Until now, we created batching functions using a remote API that allowed to request several results at once (`https://inventory.example.com/stocks?skus=PANT-01,PANT-02,…,PANT-10`). When using 3rd party APIs or legacy systems, such APIs might not always be available. Using dataLoaders in this case will not allow you to reduce the number of requests in the absolute, however it could still allow you to prevent most of these requests (or reduce its number in practice) thanks to caching. It is thus very convenient when dealing with a slow service. The `makeBatchLoaderFromSingleFetch` allows you to create a batching function from a single fetching function easily. Pseudo signature: ```js makeBatchLoaderFromSingleFetch = ( function singleFetch, // function that fetches data for a single id function singleResponseMapper = ({ data }) => data // function that transform a response into data ) => ids => Observable(sortedData); ``` Example (from [the Magento2 category loader](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/6ba7ceed3244c47f1c75e03a60c8a5e87a3f5104/src/server/modules/magento1/catalog/categories/categoryLoader.js#L6)): ```js import { makeBatchLoaderFromSingleFetch } from "server/core/graphql/dataloaderHelpers"; // … const loadBatch = makeBatchLoaderFromSingleFetch( (id) => axiosInstance.get(`/categories/${id}`), (response) => convertCategoryMainAttributesForFront(response.data) ); const loader = makeDataLoader("CatalogCategory")( (ids) => loadBatch(ids).toPromise() // <-- note the `toPromise()` here ); ``` :::note `makeBatchLoaderFromSingleFetch` returns an Observable. You must thus convert it to a Promise using the `.toPromise()` method. ::: --- # Configure caching strategies URL: https://developers.front-commerce.com/docs/3.x/guides/caching-remote-data/configure-caching-strategies ## Caching dataLoaders data By default, all dataLoaders use a **per-request in-memory caching strategy**. This means that within the same GraphQL query, the same data is requested only once. Front-Commerce is also shipped with a persistent cache implementation, using a Redis strategy (see [Caching strategies](./02-configure-caching-strategies.mdx#caching-strategies)). You can implement new strategies to support more services (we also can help and support more strategies, please ). The cache is configured using the [cache](../../04-api-reference/front-commerce-core/config.mdx#cache) key in your Front-Commerce configuration. In our default skeleton, this configuration is managed through the `app/config/caching.js` file for easier maintenance. ## Caching strategies This section provides details about the available strategy implementations in Front-Commerce. They can be used by using them in the `implementation` key of your caching strategies configuration. ### Redis See how the Redis strategy is configured [here](../../04-api-reference/front-commerce-core/caching-strategies.mdx). ### Magento 1 specific strategies See how the Magento 1 specific strategies are configured [here](../../03-extensions/e-commerce/magento1/reference/caching-strategies.mdx). ### Magento 2 specific strategies See how the Magento 2 specific strategies are configured [here](../../03-extensions/e-commerce/magento2/reference/caching-strategies.mdx). ### Advanced usage If you need additional implementations or want to leverage strategies for a specific use case, please so we can discuss it and guide you! --- # Invalidating the cache URL: https://developers.front-commerce.com/docs/3.x/guides/caching-remote-data/invalidating-the-cache ## Invalidating the cache For persistent caching, remote systems must invalidate the cache when necessary. Front-Commerce provides several endpoints for it. They respond to `GET` or `POST` requests and are secured with a token to be passed in a `auth-token` header. The expected token must be configured with the [`FRONT_COMMERCE_CACHE_API_TOKEN` environment variable](../../04-api-reference/environment-variables.mdx#cache). {/* TODO: update link in 3.x */} ### `POST` for batched invalidations This is the recommended way to invalidate cache. It allows to invalidate several entries in one HTTP call which is more efficient. - Endpoint: `/_cache` invalidate all data from the scopes sent in the body - Body: list of cache invalidation descriptor with the following object keys - `scope`: shop code (for instance one store) - `key`: loader key to invalidate - `id`: single id to invalidate for the `key` loader (in the given `scope`) For each key of the invalidation descriptor, it is possible to define the value `"all"` (reserved keyword) to invalidate every defined object. See the example below. ```js title="Example" [ { scope: "default", key: "CatalogProduct", id: "VSK12" }, { scope: "default", key: "all", id: "VSK13" }, { scope: "all", key: "CatalogCategory", id: "42" }, { scope: "default", key: "CmsPage", id: "all" }, ]; ``` ### `GET` for atomic invalidations These endpoints were the first ones implemented in Front-Commerce. They are less efficient than batching invalidations, but may be more convenient for webhooks or simple scripts. - `/_cache`: invalidate all data in persistent cache - `/_cache/:scope`: invalidate all data for a given scope (for instance one store) - `/_cache/:scope/:key`: invalidate all data of a given loader (matching `:key`) for a given store - `/_cache/:scope/:key/:id`: invalidate cached data for a single id of a given loader in a given store :::note Our Magento 2 and Magento 1 extensions handle cache invalidation by default, please refer to their respective documentations to learn how to add your own invalidation logic (for custom Magento entities). ::: --- # Troubleshooting URL: https://developers.front-commerce.com/docs/3.x/guides/caching-remote-data/troubleshooting ## Debug cache operations Front-Commerce provides a way to debug several aspects of the caching layer. You can use the [`DEBUG="front-commerce:cache"` environment variable](../../04-api-reference/debug-flags/index.mdx#front-commerce%3Acache) to view information about caching strategies used for a GraphQL query, along with cache invalidation requests received by your Front-Commerce server. ## Manually clearing the cache Clearing the cache manually is possible on some of the supported platforms. Read dedicated pages for details: - [Magento 1 (OpenMage LTS)](../../03-extensions/e-commerce/magento1/how-to/clear-the-cache.mdx) - [Magento 2](../../03-extensions/e-commerce/magento2/how-to/clear-the-cache.mdx) --- # Caching cart data URL: https://developers.front-commerce.com/docs/3.x/guides/caching-remote-data/caching-cart-data

{frontMatter.description}

## How to enable cart caching To enable cart caching, you need to set the environment variable `FRONT_COMMERCE_CART_CACHE_ENABLE` to `true`. Cart will be cached as soon as a call is made to the cart query. :::info Please note that the cart cache functionality is only available for authenticated customers. ::: ## Cache invalidation operations The cart cache has a TTL of 5 minutes, however some mutations will invalidate the cache immediately. Each extension handle invalidation for its native features. ## Automatic cache invalidation using `decorateWithCartCacheExpire` In addition to the automated cache system, you might have manipulate the Cart cache using the `decorateWithCartCacheExpire` function. This function will allow you to decorate a resolver with a function that will invalidate cache. You can see an example of it's usage in the [Gezy Cart Cache Loader](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/33d409e253ceba26de55160e3bfe03be153447a5/packages/gezy/modules/cart/CachedCartLoader.ts#L31) ## Advanced cache invalidation using `CartCache` loader In addition to the automated cache system, you can also manipulate the Cart cache using the `CartCache` loader. Let's explain this with an example, imagine you want to invalidate the cart cache in a mutation. In your resolver, you can call the `CartCache` loader to invalidate the cache: ```ts title="my-extension/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ resolvers: { Mutation: { myCustomMutation: (_, { cartId }, { loaders }) => { loaders.CartCache.expire(cartId); }, }, }, }); ``` In this example, `myCustomMutation` will invalidate the cart cache for the given `cartId`. You can also use the `CartCache` loader to cache cart with custom query: ```ts title="my-extension/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ resolvers: { Query: { myCustomQuery: (_, { cartId }, { loaders }) => { return loaders.CartCache.cache( cartId, "cart", await loaders.CustomCartLoader.loadById(cartId) ); }, }, }, }); ``` --- # Caching customer data URL: https://developers.front-commerce.com/docs/3.x/guides/caching-remote-data/caching-customer-data

{frontMatter.description}

## How to enable customer caching To enable customer caching, you need to set the environment variable `FRONT_COMMERCE_CURRENT_CUSTOMER_CACHE_ENABLE` to `true`. ```title=".env" FRONT_COMMERCE_CURRENT_CUSTOMER_CACHE_ENABLE=true ``` Customer will be cached as soon as a call is made to the customer query. The TTL for the cache is 5 minutes. :::info Please note that the customer cache functionality is only available for authenticated customers. ::: If your Front-Commerce application is configured to use multiple stores, the customer cache is scoped to the current store. EG: - You have a `default` store and a second store named `FR`. - The customer is connected to the `default` store. - Its data will be cached. - The customer switches to the `FR` store, the data cached from the `default` store won't be available for the `FR` store. --- # Change a resolver behavior URL: https://developers.front-commerce.com/docs/3.x/guides/change-resolver-behavior

{frontMatter.description}

As an example, we will change the way the Product `meta_description` field value is generated. ## Create dedicated GraphQL module First, you have to create a GraphQL module. For that, you can follow the process detailed in [Extend the GraphQL schema](./extend-the-graphql-schema#add-a-graphql-module-within-the-extension). In this example, we have a base GraphQL module for `ProductMetaDescription` which does not do anything yet: ```ts title="extensions/acme-extension/modules/productmetadescription/index.ts" import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "ProductMetaDescription", }); ``` ## Set the module dependency Before being able to inject your custom resolver logic, you first need to find the module that defines the resolver for the field. In our example, the Product `meta_description` is resolved [by the resolver provided by the `Magento2/Catalog/Products` module](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/23937b329c11d5a6f7a0e9e631b9ffffeca16bf5/packages/magento2/modules/catalog/products/resolvers.js#L346). As a result, the `Magento2/Catalog/Products` module must be added as a dependency of our custom module: ```ts title="extensions/acme-extension/modules/productmetadescription/index.ts" import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "ProductMetaDescription", // add-next-line dependencies: ["Magento2/Catalog/Products"], }); ``` ## Implement your custom resolver logic Our custom module can now provide a resolver with a custom logic in your extension's runtime. ```ts title="extensions/acme-extension/modules/productmetadescription/runtime.ts" export default createGraphQLRuntime({ resolvers: { Product: { meta_description: (product) => { // your implementation here return "my custom description"; }, }, }, }); ``` For this resolver to be taken into account, you will need to include the `GraphQL Runtime` import in your `GraphQL Module`: ```ts import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "ProductMetaDescription", dependencies: ["Magento2/Catalog/Products"], // add-next-line runtime: () => import("./runtime"), }); ``` This custom resolver will now be merged with the existing ones and the `meta_description` will be resolved with our custom implementation instead of the default one. --- # Adding a configuration provider URL: https://developers.front-commerce.com/docs/3.x/guides/configuration/adding-a-configuration-provider

{frontMatter.description}

## What is a configuration provider? The goal is to give access to a configuration object that contains all the configuration of the store. This configuration object can contain any kind of configuration. It can be flags about a feature activation, some credentials to connect to a remote service, etc. However, not all applications will need every single configuration. For instance, a shop that chose to use [Algolia](https://www.algolia.com/) won't have the same configurations than a shop that chose [Elasticsearch](https://www.elastic.co/) for product search. The goal of the configuration providers is to define the configurations needed for the specific modules you use in your application. On server start, the configuration providers will then be combined to create the global configuration object. This configuration object will then be available on each server request. This can be illustrated by starting your Front-Commerce server in development mode. If your server is running on `http://localhost:3000` and you open the URL [`/__front-commerce/debug`](http://localhost:3000/__front-commerce/debug), you will have a dump of the configuration for this specific request under the section `config`. ```json { "allShops": { "store:default": { "id": "default", "url": "http://localhost:3000", "locale": "en-GB", "magentoStoreCode": "default", "currency": "EUR" }, "store:fr": { "id": "fr", "url": "http://fr.localhost:3000", "locale": "fr-FR", "magentoStoreCode": "fr", "currency": "EUR" } }, "currentShopId": "default", "shop": { "id": "default", "url": "http://localhost:3000", "locale": "en-GB", "magentoStoreCode": "default", "currency": "EUR" }, "cache": { "defaultMaxBatchSize": 100, "strategies": [ { "implementation": "Redis", "supports": "*", "disabledFor": [], "config": { "host": "localhost" } } ] }, "contentSecurityPolicy": { "__dangerouslyDisable": false, "directives": { "scriptSrc": [], "frameSrc": [], "styleSrc": [], "imgSrc": [], "fontSrc": [], "connectSrc": [], "frameAncestors": [], "baseUri": [] }, "reportOnly": { "scriptSrc": false, "frameSrc": false, "styleSrc": false, "imgSrc": false, "fontSrc": false, "frameAncestors": false, "connectSrc": false } }, "cors": { "origin": { "referal-0": "http://magento23.test" } }, "device": { "memoizationMaxAge": 60000, "type": "pc", "viewportWidthInPx": 1400 }, "public": { "analytics": { "disableDevWarning": false }, "deprecations": { "ignore": "", "trace": "" }, "compatEnv": { "FRONT_COMMERCE_WEB_DEV_ANALYTICS_WARNING_DISABLE": "true" }, "password": { "disableHint": false } "shop":{ "id": "default", "url": "http://localhost:3000", "locale": "en-GB", "currency": "EUR" } }, "magento": { "endpoint": "http://magento23.test", "version": "2", "proxiedPaths": [] } } ``` :::tip When the `FRONT_COMMERCE_ENV` environment is not set to `production`, for example in a staging environment, the configuration will still be available in the `__front-commerce/debug` page, but the page will be secured with a token configurable with the `FRONT_COMMERCE_DEBUG_TOKEN` environment variable. Then you can access the endpoint with the token in the URL: `/__front-commerce/debug?token=your-token`. ::: ## Configuration provider definition In practice, a configuration provider is an object with five properties: a `name`, a `schema`, static `values` (available independently from the request), a `slowValuesOnEachRequest` function to resolve values depending on the request and a `fetchRemoteConfiguration` function to fetch values remotely. ```js const serviceConfigurationProvider = { name, schema, values, slowValuesOnEachRequest, fetchRemoteConfiguration, }; ``` See the sections below to understand what each key stands for. export function Label({ children }) { return ( {children} ); } ### `name` The identifier of the configuration provider. This is needed for configuration providers' registration. See [Inject a configuration provider](#inject-a-configuration-provider) for more details. ```js const name = "serviceProvider"; ``` ### `schema` It's a function returning an object representing the definition of the fields that will appear in the global configuration object if the configuration provider is registered. It can define things like field formats, default values or environment variables. The schema definition is based on [convict](https://github.com/mozilla/node-convict), a library developed by Mozilla to validate configurations. For a single field, the schema will have these keys: - `doc` (string): A description of the field and pointers to learn how to get its value if it requires a manual operation - `format` (string, array or function): The formatter used for this field's value. See [how validation works in convict](https://github.com/mozilla/node-convict#validation). - `default` (any): A default value. If you don't have a default value, you still have to set one by using the `null`. If there is no default value, it won't be considered as a field definition. - `env` (string, optional): The name of the environment variable that should be used as the value. If none is given, the configuration won't be configurable by environment. Please keep in mind that environment variables should still match [Front-Commerce's naming convention](/docs/2.x/reference/environment-variables#add-your-own-environment-variables). Thus, if we want to define a configuration named `serviceKey` available in `req.config.serviceKey` that would take its value from the environment variable `FRONT_COMMERCE_SERVICE_KEY`, we would use this schema definition: ```js const schema = () => ({ serviceKey: { doc: "The key to get access to our remote service", format: String, default: null, env: "FRONT_COMMERCE_SERVICE_KEY", }, }); ``` A configuration provider's schema is not limited to a single field though. You can set multiple configuration keys but also nest deeper objects. For instance, if we want to group a `key` and a `secret` into a single `service` key, we would write the following schema: ```js const schema = () => ({ service: { key: { doc: "The key to get access to our remote service", format: String, default: null, env: "FRONT_COMMERCE_SERVICE_KEY", }, secret: { doc: "The secret to get access to our remote service", format: String, default: null, env: "FRONT_COMMERCE_SERVICE_SECRET", }, }, }); ``` You could then access the values with `req.config.service.key` or `req.config.service.secret`. Please note that if another configuration provider's schema also defined a `service` key, it would merge the definitions and in your final `req.config.service` you would have both the keys from the other configuration provider's schema and from the above schema. ### `values` Most of the time `default` values in the schema is sufficient. However, in some cases you may need to fetch some values from an API, a dynamic file, etc. This is what `values` is for. It is an optional promise that should return the missing values in your schema. It will override default values and env values from the schema with the new values. However only keys from the `values` result will take precedence, the default values and env values will be kept for other keys. For instance, if we implemented the following `values` promise, the `secret` would still be `process.env.FRONT_COMMERCE_SERVICE_SECRET` from the above schema. ```js const values = fetch("https://api.example.com/my-service-key") .then((response) => response.json()) .then((key) => ({ service: { key: key, }, })); ``` Please note that this promise is launched only once on server bootstrap. If the configuration changes over time on your API, the `req.config.service.key` value will still be the same. If it needs to change over time, please use `slowValuesOnEachRequest` or `fetchRemoteConfiguration` instead. ### `slowValuesOnEachRequest` :::note `slowValuesOnEachRequest` is a low level API in Front-Commerce. You shouldn't be need it in most cases. ::: `slowValuesOnEachRequest` is a function that extracts configuration values from the current request. For instance, depending on the URL, the configuration `currentShopId` will get a different id and thus display different information. ```js const slowValuesOnEachRequest = (req) => { const url = req.originalUrl; const shopId = getShopIdFromUrl(url); return { currentShopId: shopId, }; }; ``` :::caution This part is named `slowValuesOnEachRequest` because we want to stress the fact that it can have a huge impact on your website performance. Avoid its usage as much as you can and try to use `values` instead. If this is the only way to setup your configuration make sure to memoize its result to limit the performance impact as much as possible. ```js import memoize from "lodash/memoize"; const getShopIdFromHostname = memoize((hostname) => { /* The definition of the `currentShopId` variable should live here */ return { currentShopId: currentShopId, }; }); const slowValuesOnEachRequest = (req) => { const hostname = req.hostname; return getShopIdFromHostname(req.hostname); }; ``` ::: ### `fetchRemoteConfiguration` :::note `fetchRemoteConfiguration` is a low level API in Front-Commerce. You shouldn't need it in most cases. ::: `fetchRemoteConfiguration` is a function that is called at a later stage in Front-Commerce request handling cycle to receive a fully initialized `request` object, so that, for instance, [you can instantiate a loader to retrieve some configuration from Magento](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/commit/f57ccabae32ebe6243fc213485078cb1f98f8a30#9e3c152a167c50222b152728db1b5946d409ba89_0_29). :::caution Like `slowValuesOnEachRequest`, `fetchRemoteConfiguration` can have a huge impact on the performances. If you really need to implement it, please make sure to memoize its result to limit the performance impact as much as possible. ::: ## Inject a configuration provider Assuming that we have defined the following configuration provider: ```ts title="./config-providers/acmeConfigProvider.ts" export default { name: "acme-config", schema: () => ({ service: { key: { doc: "The key to get access to our remote service", format: String, default: null, env: "FRONT_COMMERCE_SERVICE_KEY", }, secret: { doc: "The secret to get access to our remote service", format: String, default: null, env: "FRONT_COMMERCE_SERVICE_SECRET", }, }, }), }; ``` ### From your application configuration You can add the configuration to your [`front-commerce.config.ts`](../../04-api-reference/front-commerce-core/config.mdx) file. ```ts title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; // add-next-line import acmeConfigProvider from "./config-providers/acmeConfigProvider"; export default defineConfig({ extensions: [themeChocolatine()], // add-start configuration: { providers: [acmeConfigProvider], }, // add-end }); ``` ### From an extension Alternatively, you can add a configuration provider from an extension in the [extensions definition](../../04-api-reference/front-commerce-core/defineExtension.mdx). ```ts title="./acme-extension.ts" import { defineExtension } from "@front-commerce/core"; // add-next-line import acmeConfigProvider from "./config-providers/acmeConfigProvider"; export default defineExtension({ name: "acme-extension", // add-start configuration: { providers: [acmeConfigProvider], }, // add-end }); ``` :::info You can learn more about extensions in the [Register an extensions](../register-an-extension.mdx) guide. ::: --- # Extend and read public configurations URL: https://developers.front-commerce.com/docs/3.x/guides/configuration/extend-and-read-public-configurations # Public Configuration

{frontMatter.description}

## Extending the public configuration To extend the public configuration please ensure you have read the [Add a configuration provider](/docs/3.x/guides/configuration/adding-a-configuration-provider) section. The public configuration can be extended by [Adding a configuration provider](/docs/3.x/guides/configuration/adding-a-configuration-provider) which exposes a `public` object in the schema, for example: ```ts title="./my-extensions/acme-extension/configProvider.ts" export default { name: "acme-config", schema: () => ({ // it's import to extend the `public` object public: { acmeValue: { doc: "The public api key for ACME", format: String, default: undefined, env: "FRONT_COMMERCE_WEB_ACME_PUBLIC_KEY", }, }, }), }; ``` You can then add this to your [extension definition](/docs/3.x/api-reference/front-commerce-core/defineExtension): ```ts title="./my-extensions/acme-extension/index.ts" import configProvider from "./configProvider"; import { defineExtension } from "@front-commerce/core"; export default defineExtension({ name: "acme", theme: "./extensions/acme/theme", // add-start configuration: { providers: [configProvider], }, // add-end }); ``` ## Reading the custom public configuration You should now be able to access the value from the client or server as any other public configuration value. ### From the Client To access the public configuration from the client you can use the [`usePublicConfig`](/docs/3.x/api-reference/front-commerce-core/react#usepublicconfig) hook, or the [`getPublicConfig`](/docs/3.x/api-reference/front-commerce-core/react#getpublicconfig) function. - The [`usePublicConfig`](/docs/3.x/api-reference/front-commerce-core/react#usepublicconfig) hook is mainly used to access the public configuration from a React component. - The [`getPublicConfig`](/docs/3.x/api-reference/front-commerce-core/react#getpublicconfig) function is used to access the public configuration from a non-React component. :::tip advanced We additionally attached the public configuration to the `window.__FRONT_COMMERCE__.publicConfig` object. ::: ```tsx title="./my-extensions/acme-extension/theme/components/MyComponent.tsx" import { usePublicConfig } from "@front-commerce/core/react"; const MyComponent = () => { const { acmeValue } = usePublicConfig(); return
ACME value: {acmeValue}
; }; ``` ### From the Server You can access the public configuration from the server using the config from your [FrontCommerceApp](/docs/3.x/api-reference/front-commerce-remix/front-commerce-app#appconfig) ```ts title="./app/routes/my-route.ts" import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node"; import { FrontCommerceApp } from "@front-commerce/remix"; export const loader = ({ context }: LoaderFunctionArgs) => { // highlight-start const app = new FrontCommerceApp(context.frontCommerce); const acmeValue = app.config.public.acmeValue; // highlight-end // do something }; export const action = ({ context }: ActionFunctionArgs) => { // highlight-start const app = new FrontCommerceApp(context.frontCommerce); const acmeValue = app.config.public.acmeValue; // highlight-end // do something }; ``` ### From GraphQL resolvers You can access the public configuration from your GraphQL resolvers: ```ts title="./app/graphql/resolvers/my-resolver.ts" import type { FrontCommerceContext } from "@front-commerce/core"; import type { MyResolverArgs } from "../types"; export default { Query: { myResolver: async (parent, args, context, info) => { // highlight-next-line const acmeValue = context.config.public.acmeValue; // do something }, }, }; ``` --- # Accessing current shop configuration URL: https://developers.front-commerce.com/docs/3.x/guides/configuration/accessing-current-shop-configuration ## From the Client (_Public_) The current shop configuration can be accessed from the client using the [public configuration](./02-extend-and-read-public-configurations.mdx). :::note The public shop configuration (`config.public.shop`) is a subset of the private shop configuration (`config.shop`). This will only contain information that can be exposed to the client. ::: ```mdx-code-block ``` To learn more see [`usePublicConfig`](/docs/3.x/api-reference/front-commerce-core/react#usepublicconfig) documentation. ```ts import { usePublicConfig } from "@front-commerce/core/react"; const MyComponent = () => { const { shop } = usePublicConfig(); return (

{shop.id}

{shop.locale}

); }; ``` ```mdx-code-block
``` To learn more see [`getPublicConfig`](/docs/3.x/api-reference/front-commerce-core/react#getpublicconfig) documentation. ```ts import { getPublicConfig } from "@front-commerce/core/react"; const myFunction = () => { const publicConfig = getPublicConfig(); return { id: publicConfig.shop.id, locale: publicConfig.shop.locale, }; }; ``` ```mdx-code-block
``` ## From the Server (_Private_) The current shop configuration can be accessed from the client using the [app configuration](/docs/3.x/guides/configuration/adding-a-configuration-provider). :::note The private shop configuration (`config.shop`) is a superset of the public shop configuration (`config.public.shop`). This can contain additional information that should not be exposed to the client. ::: ```mdx-code-block ``` ```ts import type { LoaderFunctionArgs } from "@remix-run/node"; export const loader = async ({ context }: LoaderFunctionArgs) => { const app = new FrontCommerceApp(context.frontCommerce); // add-next-line const currentShopConfig = app.config.shop; // ... }; ``` ```mdx-code-block ``` ```ts import type { ActionFunctionArgs } from "@remix-run/node"; export const action = async ({ context }: ActionFunctionArgs) => { const app = new FrontCommerceApp(context.frontCommerce); // add-next-line const currentShopConfig = app.config.shop; // ... }; ``` ```mdx-code-block ``` ```ts export default { Query: { myQuery: async (parent, args, context, info) => { // add-next-line const currentShopConfig = context.config.shop; // ... }, }, }; ``` ```mdx-code-block ``` --- # Configure multiple stores URL: https://developers.front-commerce.com/docs/3.x/guides/configuration/configure-multiple-stores

{frontMatter.description}

## What is a store in Front-Commerce A store in Front-Commerce has the same meaning as [in Magento](https://experienceleague.adobe.com/en/docs/commerce-admin/stores-sales/site-store/stores). Usually, the goal of a store is to display your catalog in multiple languages. If you want to adapt the prices depending on your language, you should use the websites feature. ## Configuring multiple stores A single of Front-Commerce can handle several stores at the same time. This is configurable in `app/config/stores.js`. :::note However, if you need multiple websites, you will need to deploy as many Front-Commerce instances as there are websites. Feel free to if you need more information about this. ::: ### Configuration file example The configuration file will look like this: ```js title="app/config/stores.js" export default { // the key is the code of your store default_en: { // language used in the store (useful for react-intl) locale: "en-GB", // currency code ISO 4217 currency: "GBP", // country code ISO 3166-1 (used for preselecting the country in an address input for instance) default_country_id: "FR", // The url used for this store url: process.env.FRONT_COMMERCE_EN_URL || "https://en.fallback.com", // an optional URL that will be used to serve static assets, if not defined assets are served from the url above. assetsBaseUrl: "http://a.cdn.mybrand.com", }, default_fr: { locale: "fr-FR", currency: "EUR", default_country_id: "FR", url: process.env.FRONT_COMMERCE_FR_URL || "https://fr.fallback.com", }, }; ``` Its main goal is to tell your application how to fetch the correct translations and languages. ## A few pointers about URLs A store is always associated with a single URL. Store is not based on cookies or session but on the fetched URL. This means that you can have either a set of subdomains (`fr.example.com`, `en.example.com`, etc.) or base paths per stores (`www.example.com/fr`, `www.example.com/en`, etc.). These URLs should be defined in the `url` key of the store object specified above. Since you will most likely have a local environment, a staging environment and a production environment, we encourage you to use environment variables. In addition, it's possible to [configure Front-Commerce to serve static assets from a different domain](../serving-assets-from-a-cdn-custom-domain.mdx) .To optimize caching, all stores can share the same domain. ### How should I get the URL in another store? URLs may change between stores. This is the case for the base url of your store but also for the actual path of your page. For instance a `/shirts.html` URL would be changed in `/chemises.html` in french. This can be done by executing the following GraphQL query: ```graphql query StoreViewUrlQuery($url: String!, $otherShop: ID!) { shop { id translatedUrl(url: $url, otherShop: $otherShop) } } ``` It will return in `translatedUrl` the correct URL in the other store for platforms supporting this feature. :::warning Please if you need this feature for your platform. If it isn't yet supported, we will work to make it possible! ::: ## Multiple currencies > This feature has been added for Magento 1. Please if you need > it for another platform. With **Magento1** it is possible to handle multiple currencies for a single store. To do so, you need to define the `availableCurrencies` key for the stores using multiple currencies. This will add a button in your shop's header that lets the user choose which currency to use. By default, a user will be using the currency specified in the `currency` key. ```diff export default { // the key is the code of your store default_en: { // ... currency: "GBP", + availableCurrencies: ["GBP", "EUR"] // ... }, } ``` --- # Content Composition URL: https://developers.front-commerce.com/docs/3.x/guides/content-composition/index ## References For more detailed information, check out: - [Content Composition API](../../04-api-reference/front-commerce-core/content-composition.mdx) - [React Hooks](../../04-api-reference/front-commerce-core/react.mdx#usecompositioncomponentmap) ## Introduction Content Composition is a powerful feature that allows you to create reusable building blocks for your content. It helps you: - Build modular and maintainable content structures - Implement type-safe GraphQL fragments - Create flexible page layouts using composable components This guide will walk you through implementing content composition in your Front-Commerce application. ## Prerequisites Before starting, make sure you: - Have a [Front-Commerce application set up](../../01-get-started/installation.mdx) - Understand [how to register an extension](../../02-guides/register-an-extension.mdx) - Understand [file-based routing in Remix](../../01-get-started/your-first-route.mdx) - Know how to [load data from the GraphQL schema](../../01-get-started/loading-data-from-unified-graphql-schema.mdx) - Are familiar with [GraphQL fragments](https://graphql.org/learn/queries/#fragments) - Understand [React components basics](https://react.dev/learn/thinking-in-react) ## Tutorial ### Define Your Content Types First, let's define the GraphQL types for our content. Here's an example that defines various content types like Carousel and ProductsList: ```ts title="./tutorial-extension/modules/core/index.ts" import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "Tutorial/Core", typeDefs: /* GraphQL */ ` extend type Query { homePage: HomePage } type HomePage{ // highlight-next-line sharedContent: [SharedContent] } // highlight-start union SharedContent = Carousel | ProductsList type Carousel { slides: [CarouselSlide] } type CarouselSlide { title: String image: String cta: CallToAction } type ProductsList { title: String category: Category } // highlight-end `, }); ``` ### Register Content Compositions Next, register your content compositions using the [`composition.register`](../../04-api-reference/front-commerce-core/content-composition.mdx) method in your extension: ```ts title="./tutorial-extension/index.ts" import { createContentComposition } from "@front-commerce/core"; // highlight-next-line // The composed fragment will be named `SharedContentFragment` // highlight-next-line const SharedContentComposition = createContentComposition("SharedContent", [ { // highlight-next-line name: "Carousel", // The fragment name needs to match this name, eg. `CarouselFragment` client: { component: new URL("./components/Carousel.tsx", import.meta.url), fragment: /* GraphQL */ ` // highlight-next-line fragment CarouselFragment on Carousel { slides { title image cta { url text } } } `, }, }, { // highlight-next-line name: "ProductsList", // The fragment name needs to match this name, eg. `ProductsListFragment` client: { component: new URL("./components/ProductsList.tsx", import.meta.url), fragment: /* GraphQL */ ` // highlight-next-line fragment ProductsListFragment on ProductsList { title category { id name } } `, }, }, ]); export default defineExtension({ unstable_lifecycleHooks: { onContentCompositionInit: (composition) => { composition.registerComposition(SharedContentComposition); }, }, }); ``` ### Use the Generated Fragments The registration process automatically generates a composed fragment that includes all individual fragments: ```graphql fragment SharedContentFragment on SharedContent { ...CarouselFragment ...ProductsListFragment } fragment CarouselFragment on Carousel { ... } fragment ProductsListFragment on ProductsList { ... } ``` You can now use this fragment in your page queries: ```graphql title="./tutorial-extension/theme/pages/HomePage/HomePageQuery.gql" query HomePage { homepage { sharedContent { ...SharedContentFragment } } } ``` ### Implement the Page Component Finally, create your page component that uses the composition: ```tsx title="./example-extension/application-extension/routes/_main.index.ts" import { HomePageDocument } from "~/graphql/graphql"; import { FrontCommerceApp } from "@front-commerce/remix"; import { json } from "@front-commerce/remix/node"; import { useLoaderData } from "@front-commerce/remix/react"; import { CompositionComponent } from "@front-commerce/core/react"; import { LoaderFunctionArgs } from "@remix-run/node"; // 1. Fetch the data for your page export const loader = async ({ request, context }: LoaderFunctionArgs) => { const app = new FrontCommerceApp(context); const data = await app.graphql.query(HomePageDocument); return json({ homepage: data.homepage }); }; // 2. Render the content composition export default function HomePage() { const { homepage } = useLoaderData(); return ( {components}} // optional custom rendering that wraps a single react node renderComponent={(component) => { if (component.props.__typename !== "Carousel") { return
{component}
; } return component; }} /> ); } ``` :::tip You can also use the [`useCompositionComponentMap`](../../04-api-reference/front-commerce-core/react.mdx#usecompositioncomponentmap) hook to get the individual composition components and build your own rendering logic: ```tsx const SharedContent = useCompositionComponentMap("SharedContent"); // // ``` :::
## Advanced Usage ### Overriding Existing Compositions You can override existing compositions by registering a new composition with the same name: ```ts composition.register("Wysiwyg", [ { name: "DefaultWysiwyg", client: { component: new URL("./components/DefaultWysiwyg.tsx", import.meta.url), fragment: /* GraphQL */ ` fragment DefaultWysiwygFragment on DefaultWysiwyg { childNodes data { dataId } customField // Add new fields } `, }, }, ]); ``` :::tip When overriding compositions, the last registered extension takes precedence, but all compositions are merged together. ::: --- # Create a Business Component URL: https://developers.front-commerce.com/docs/3.x/guides/create-a-business-component

{frontMatter.description}

In Front-Commerce we have separated our components in two categories: the [**UI** components](./create-a-ui-component) available in the `app/theme/components` folder, and the **Business** components available in the `app/theme/modules` and `app/theme/pages` folders. :::note If you would like to understand why we went for this organization, feel free to refer to [React components structure](../concepts/react-components-structure/) first. ::: ## What is a Business component A Business component will be located in the `app/theme/modules` folder. Those components are not meant to be reused a lot in your application, they are built with a **very specific use in mind**. When creating a custom theme, they should emerge in [your Pages components](../get-started/your-first-route/). To illustrate this, imagine that you are building the homepage for your store. You add a big hero image on top, some product listings for news and sales, a reinsurance banner, etc. Quickly, you will want to **extract** some components from your page to avoid a _big bloated file_. Some of these components will be extracted as **reusable UI components** but some are very specific to your page and there is no reason to put them in the [`components`](./create-a-ui-component) folder. :::note They are often a mix between UI components and custom layout. They may be split into multiple components if they are big enough. ::: Generally, they are related to **your business** and often need backend data like CMS content or store information. We refer to them as **Business components** or even **modules**. :::note Unlike UI components, Business components are often _smart_ and contain logic. We try to extract this logic in **hooks**, but more on that later. ::: ## Creating a store locator To explain the concept and the emergence of modules, we will add a **store locator** to our home page and see how to extract it properly as a module. In the following steps, we are going to build our store locator and we will go through: 1. Displaying a map on the homepage 2. Fetching the position of the store from the backend 3. Link both to have an actual module ### Installing the dependencies To create the map, we are going to use the [**react-leaflet**](https://react-leaflet.js.org/) package. It provides a component that uses leaflet under the hood. It will allow us to display the position of our store within [OpenStreetMap](https://www.openstreetmap.org/search?query=Toulouse#map=12/43.6007/1.4329). This is one of the biggest advantages of using React to build our front-end, we have access to this huge ecosystem. Let's install the required packages (versions are important): ```shell pnpm install leaflet@^1.7 react-leaflet@3.2.5 ``` :::note Front-Commerce also [provides a `` component](./display-a-map) that would be a better candidate for a real project. In order to learn Front-Commerce we prefer to document how to do things yourself. As a stretch goal, you can try to replace your components with the `` one. ::: ### Our new Homepage #### Override the default Home page Before starting to edit the Home page, you first need to extend the theme. If you don't have a module yet, please refer to [Extend the theme](./override-a-component/#understanding-theme-overrides). Once you have one, the goal will be to override the Home component from [`node_modules/@front-commerce/theme-chocolatine/theme/pages/Home/Home.js`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/85bb712e7fbb0b2e74a6db643f5522451a4bdc58/packages/theme-chocolatine/theme/pages/Home/Home.jsx) to `app/theme/pages/Home/Home.js`. :::warning Do not forget to restart your application (`pnpm start`) in case the override does not work. ::: #### Customize it Once you have your own `Home` component in your module, it is time to customize it and add your store locator. You don't need to anticipate every **UI** or **Business** component in your application. Only extract them when your component gets bigger or if you feel the need to extract some of them. To illustrate this point, we are going to create the first version of our map into the homepage directly. We will start with hardcoded values for the store coordinates. Then we will extract the store locator feature in its own module component. And finally, we will fetch the actual coordinates from the **GraphQL schema**. The first working version will look like: ```tsx title="app/theme/pages/Home/Home.tsx" // ... Existing imports import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet"; import "leaflet/dist/leaflet.css"; import "leaflet/dist/images/marker-icon.png"; import "leaflet/dist/images/marker-shadow.png"; const Home = () => (
{/* ... The rest of the homepage */} My awesome store is HERE!
); // ... ``` With that, you should see the map appear in your homepage. {/* TODO: Uncomment when the CSP article is written */} {/* :::warning Important */} {/* You will detect issues with remote assets (images, CSS…) not being loaded. It is */} {/* related to the default CSP headers */} {/* sent by Front-Commerce. To allow new domains, you should modify your */} {/* [`config/website.js`'s `contentSecurityPolicy` key](/docs/2.x/reference/content-security-policy) */} {/* (i.e: define `imgSrc: ["*.openstreetmap.org"]`). */} {/* ::: */}
### Extracting our new component If the `Home` component grows too complex with multiple components, maintaining it becomes challenging. At that point, it's best to extract the Store Locator into its own module for better organization and maintainability. To do so, we will reuse the exact same component but move it into its own module in `app/theme/modules`. ```tsx title="app/theme/modules/StoreLocator/StoreLocator.tsx" import React from "react"; import PropTypes from "prop-types"; import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet"; import "leaflet/dist/leaflet.css"; import "leaflet/dist/images/marker-icon.png"; import "leaflet/dist/images/marker-shadow.png"; const StoreLocator = () => { return ( My awesome store is HERE! ); }; export default StoreLocator; ``` In order to make it consistent with other components in the application, we will add two more files: - `app/theme/modules/StoreLocator/index.ts`: will proxy the `StoreLocator.tsx` file in order to be able to do imports on the folder directly. See [this blog post](http://bradfrost.com/blog/post/this-or-that-component-names-index-js-or-component-js/) for more context about this pattern. ```ts title="app/theme/modules/StoreLocator/index.ts" export { default } from "./StoreLocator"; ``` - `app/theme/modules/StoreLocator/StoreLocator.stories.tsx`: will add a story to the Storybook of your application. The story will serve as living documentation that will allow anyone to understand what the StoreLocator is used for and how to use it. ```tsx title="app/theme/modules/StoreLocator/StoreLocator.stories.tsx" import StoreLocator from "./StoreLocator"; import type { Meta, StoryObj } from "@storybook/react"; const meta: Meta = { component: StoreLocator, }; export default meta; type Story = StoryObj; export const Default: Story = { args: {}, }; ``` :::note We won't focus on the story in this guide. But you can refer to the [Storybook guide](./add-component-to-storybook) to learn how to ::: ### Fetching our data Hardcoded values are perfectly fine. But if the coordinates change over time, it might be a good idea to fetch them dynamically. This is what we will do in this example. Usually, the recommended way to retrieve data in components is to retrieve them from the routes directly, and pass them as `props` to components. However when not possible do to so, or in the case of client-side components like our `StoreLocator`, we can retrieve dynamic data using API routes and fetchers instead. Thus, to fetch our data from GraphQL, we are going to create an **Hook** for our store locator, which will be responsible of fetching the data from a custom API route and transforming the store information to match our needs. This hook will contain the **Business logic** of our component. #### Creating the hook ```ts title="app/theme/modules/StoreLocator/useStoreLocator.ts" import { useApiFetcher } from "@front-commerce/remix/react"; import { loader } from "routes/api.store-locator"; export default function useStoreLocator() { const fetcher = useApiFetcher("/api/store-locator"); const { data, state } = fetcher.load(); return { loading: state === "loading", error: data?.error, store: data?.store, }; } ``` Here, we are using the [`useApiFetcher`](../api-reference/front-commerce-remix/#useapifetcher) hook which allows us to fetch data from an API route. #### Implementing the API route We will now create the API route that will be used to fetch the data. ```ts title="app/routes/api.store-locator.ts" import { json } from "@front-commerce/remix/node"; import { FrontCommerceApp } from "@front-commerce/remix"; import { StoreLocatorQueryDocument } from "~/graphql/graphql"; export const loader = async ({ context }) => { const app = new FrontCommerceApp(context.frontCommerce); const store = await app.graphql.query(StoreLocatorQueryDocument); return json({ store }); }; ``` As you can see, our route needs a **Query** (`StoreLocatorQueryDocument`). This is a `.gql` file that uses the **GraphQL** syntax. In our case, it will look like: ```graphql title="app/theme/modules/StoreLocator/StoreLocatorQuery.gql" query StoreLocator { store { name phone owner { email } coordinates { longitude latitude } } } ``` :::info Front-Commerce will automatically generate the `StoreLocatorQueryDocument` from the `.gql` file when starting the application. You can find it in the `~/graphql/graphql.ts` file. Since our useApiFetcher is typed based on the route loader, it has the benefit of automatically typing the whole hook based on the GraphQL query. ::: To better understand and test your schema, you can use **GraphQL Playground**. It is a web interface for GraphQL, similar to what PhpMyAdmin is for MySQL. ### Making it dynamic Now that we have our hook ready, we are going to use it in our store locator. When dealing with asynchronous resources like fetching data from the backend, you have to handle the loading state and error state. Here we we will show a simple message such as "Loading..." or "Oops, an error occurred." to the user. But in a real life application, you would want to show better messages depending on your context. :::info For error handling, you could take a look at [Error Boundaries](./error-handling-for-routes). ::: ```tsx title="app/theme/modules/StoreLocator/StoreLocator.tsx" import React from "react"; import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet"; import "leaflet/dist/leaflet.css"; import "leaflet/dist/images/marker-icon.png"; import "leaflet/dist/images/marker-shadow.png"; import useStoreLocator from "./useStoreLocator"; const StoreLocator = () => { const { loading, error, store } = useStoreLocator(); if (loading) { return
Loading...
; } if ((!loading && !store) || error) { return
Oops, an error occurred.
; } const coordinates = [store.coordinates.longitude, store.coordinates.latitude]; const defaultZoom = 14; return (
My awesome store ─ {props.store.name} Email: {props.store.owner.email} Phone: {props.store.phone}
); }; export default StoreLocator; ```
## Using it in our App We now have extracted all the store locator logic. We can now use our brand new and shiny module component within the homepage. ```tsx title="app/theme/pages/Home/Home.tsx" import StoreLocator from "theme/modules/StoreLocator"; const Home = () => (
{/* ... */}
); ``` As you can see, we did not use a relative import. This is because in Front-Commerce we have a few aliases that will let you import files without worrying about your current position in the folder structure. In our case, the `Home` component being in `app/theme/pages/Home/Home.js`, we do not have to import the `StoreLocator` by using relative paths `../../modules/StoreLocator` but we can remain with `app/theme/modules/StoreLocator` which is more explicit. This is possible for any file located in the folder `theme/theme` of an extension. And it works! You now have a clean `Home` page component that uses a Business component which could be used anywhere in your application (About us, Contact, etc.). :::info Going further The store locator we just created is very simple and it has a very limited Business impact. The store locator module does not need to know the implementation of the map itself (like the fact of using `react-leaflet`). So a map component could be extracted in a **UI** component. But for the sake of this guide, we kept it simple. ::: As a final note, please keep in mind that splitting code is a difficult task. It needs practice and refinement. But it is also a pretty personal point of view. Thus one team could split code differently. In this guide we have made a choice but feel free to make yours. In any case, we advice you to not overcomplicate things and find a method that matches your needs. --- # Create a custom image adapter URL: https://developers.front-commerce.com/docs/3.x/guides/create-a-custom-image-adapter

{frontMatter.description}

An Image Adapter is a custom function that you can use to deliver enhanced and optimized images for every user with your custom adapter. It is a function that takes an image URL as input and returns a new image URL as output. The new image URL can be the same as the original image URL, or it can be a different image URL that is optimized for the user's device, browser, or network. ## Getting Started To get started, you need to create a custom adapter class. The class must extend the `ImageAdapter` domain which implements a `supportSrc` method, and a `makeImageSrc` method. The `supportSrc` method is used to check if the image URL is valid for the current adapter. If the image URL is valid, the `makeImageSrc` method is called to generate the new image URL. ### Creating an Image Adapter Let's create two Image adapters that extends the base `ImageAdapter` class and implements two key methods: `supportSrc` for URL validation and `makeImageSrc` for URL transformation. For this example, we will create adapters for two popular image services: Unsplash and Lorem Picsum. First, let's look at an adapter for Unsplash images: ```typescript title="my-extension/adapters/unsplash.ts" import { ImageAdapter } from "theme/components/atoms/Image/ImageAdapter"; class UnsplashAdapter extends ImageAdapter { supportSrc(src: string) { return src.includes("unsplash.com"); } makeImageSrc(src: string) { // ... do something with the image URL return src; } } export default new UnsplashAdapter(); ``` And here's a similar adapter for Lorem Picsum: ```typescript title="my-extension/adapters/lorem-picsum.ts" import { ImageAdapter } from "theme/components/atoms/Image/ImageAdapter"; class LoremPicsumAdapter extends ImageAdapter { supportSrc(src: string) { return src.includes("picsum.photos"); } makeImageSrc(src: string) { // ... do something with the image URL return src; } } export default new LoremPicsumAdapter(); ``` ### Registering an Image Adapter To register an Image Adapter, you need to add it to the `imageAdapters` component. You can do this by adding it to a top level route or a layout for example. ```typescript title="my-extension/routes/_main.tsx" // Other imports ... // add-start import imageAdapters from "theme/components/atoms/Image/adapters"; import UnsplashAdapter from "./adapters/unsplash"; import LoremPicsumAdapter from "./adapters/lorem-picsum"; // add-end // Original route code ... export default function MainLayout() { const { headerNavigationMenu, footerNavigationMenu } = useLoaderData(); // add-start imageAdapters.register(UnsplashAdapter); imageAdapters.register(LoremPicsumAdapter); // add-end return ( ); } // ... original route code ... ``` ## Using an Image Adapter All the built-in image components use the ImageAdapters by default to deliver enhanced and optimized images. ### Changing the generated sources for srcSet The ImageAdapter has two optional methods: - `getSupportedExtensions` this allows you to change how the `srcSet` is generated. - `getDefaultExtension` this allows you to change the default format used for the `srcSet`. For example, if we want to change the supported formats for the UnsplashAdapter: ```typescript title="my-extension/adapters/unsplash.ts" import { ImageAdapter } from "theme/components/atoms/Image/ImageAdapter"; class UnsplashAdapter extends ImageAdapter { supportSrc(src: string) { return src.includes("unsplash.com"); } makeImageSrc(src: string) { return src; } // add-start getSupportedExtensions(transparent: boolean) { if (transparent) { return ["webp", "png"]; } return ["webp", "jpeg"]; } getDefaultExtension() { return "jpeg"; } // add-end } ``` You can also opt-out of multiple sources for the `srcSet` by either returning a single format or a null if you don't want to specify a format. ```typescript title="my-extension/adapters/unsplash.ts" getSupportedExtensions(transparent: boolean) { if (transparent) { return "webp" // return a single format } return null // generate one source without a format specification } ``` :::note Not specifying the `getSupportedExtensions` and `getDefaultExtension` methods will fallback to the [default implementation](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/f3147b98aa779bf7da9f25b12de32e293327363e/packages/theme-chocolatine/theme/components/atoms/Image/adapters/index.js#L63-81). ::: For more information on image adapters provided by Front-Commerce, please refer to the [Image Adapters](../category/images) documentation. --- # Create a UI Component URL: https://developers.front-commerce.com/docs/3.x/guides/create-a-ui-component

{frontMatter.description}

import Figure from "@site/src/components/Figure"; In Front-Commerce components are classified under two categories: the **UI** components available in the `theme/components` folder, and the **Business** components {/* TODO: add link to article */} available in the `theme/modules` and `theme/pages` folders. :::info If you feel the need to understand why we went for this organization, feel free to refer to [React components structure](../05-concepts/react-components-structure.mdx) first. ::: As mentioned in the introduction, we will use Storybook {/* TODO: add link to article */} in the process because it is our usual workflow when creating a UI Component. But if you don't need it or prefer to add your stories later, feel free to leave the parts mentioning Storybook later. Front-Commerce’s core UI components follow the same principles and you could dive into the [`node_modules/@front-commerce/theme-chocolatine/theme/components`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/main/packages/theme-chocolatine/theme/components) folder to find examples into our source code. But first, let's define what is an ideal UI Component. ## The ideal UI Component In Front-Commerce we call UI component any component that is: - **Reusable in many contexts**: if a component is used only once in the whole application, it might feel like it doesn't exist purely for UI purposes. The component most likely needs to be moved to the `/app/modules` folder {/* TODO : add Create a business component link */}. That's also the reason why we avoid to give names too close to its business use. For instance, we don't want to have a UI component that would be called `ProductDescription`. It would be better to go for a `Description` that would thus be reusable by a Category. - **Focused on abstracting away UI concerns**: the goal of UI components is to hide styles or [DOM](https://fr.wikipedia.org/wiki/Document_Object_Model) concerns from their parents. It may be hard to achieve sometimes, but it will greatly improve the parent component's readability. For instance, a UI component should not give the opportunity to pass a `className` in its props as it may lead to many style inconsistencies across the theme. ## How to build a UI Component? Alright, that's nice in theory, but how does it translate in practice? First, we’ll try to get a bit more tangible by creating a UI component for adding a Reinsurance Banner on a page similar to the following mockup. ![The reinsurance banner that we will implement](assets/reinsurance.jpg) ### Defining the components First, let's split the mockup in several UI components following the [Atomic Design Principles](http://atomicdesign.bradfrost.com/). ![The various components that will make up our banner](assets/reinsurance-with-areas.jpg) - **`app/theme/components/atoms/Typography/Heading` (green):** enforces consistent font sizes in our theme for any title - **`app/theme/components/atoms/Icon` (purple):** enforces icon sizes and accessibility guidelines - **`app/theme/components/molecules/IllustratedContent` (red):** displays some content illustrated by an image and aligns images consistently across the whole theme - **`app/theme/components/organisms/FeatureList` (orange):** manages a list of cards and inlines them, regardless of the device size. As you can see, each UI component will take place in the `app/theme/components` folder. To better understand why, please refer to our [React components structure](../05-concepts/react-components-structure.mdx) documentation. :::note If you have trouble splitting your mockups, you can refer to [Thinking in React](https://react.dev/learn/thinking-in-react) in the official React documentation or to [Brad Frost's book about Atomic Design](http://atomicdesign.bradfrost.com/). You may want to organize your code differently and that's perfectly fine. The way we splitted things here is one of many possible solutions. Such choices will depend on your project and your team. It may be a better idea to keep things simple. It's often easier to wait and see how it can be reused later. ::: We won't be able to detail each component here. We will focus on `IllustratedContent` instead. But keep in mind that any UI component in Front-Commerce will look similar to what we are going to build here. ### Setup your dev environment Before doing the actual work let's bootstrap our dev environment. To do so, you will need to create three files: - **`IllustratedContent.js` :** will bootstrap the actual component. ```tsx title="app/theme/components/molecules/IllustratedContent/IllustratedContent.tsx" const IllustratedContent = () => { return
Illustrated Content
; }; export default IllustratedContent; ``` - **`index.js` :** will proxy the `IllustratedContent.js` file in order to be able to do imports on the folder directly. See [this blog post](http://bradfrost.com/blog/post/this-or-that-component-names-index-js-or-component-js/) for more context about this pattern. ```tsx title="app/theme/components/molecules/IllustratedContent/index.ts" import IllustratedContent from "theme/components/molecules/IllustratedContent/IllustratedContent"; export default IllustratedContent; ``` - **`IllustratedContent.stories.tsx` :** will add a story to the [Storybook](https://storybook.js.org/) of your application. This will serve as living documentation and will allow anyone to understand what is `IllustratedContent` used for and how to use it. ```tsx title="app/theme/components/molecules/IllustratedContent/IllustratedContent.stories.tsx" import type { Meta, StoryObj } from "@storybook/react"; import IllustratedContent from "./IllustratedContent"; const meta: Meta = { component: IllustratedContent, }; export default meta; type Story = StoryObj; export const Default: Story = { render: () => , }; ``` {/* :::note */} {/* For a more detailed explanation of how Storybook works in the context of */} {/* Front-Commerce, please refer to */} {/* [Add a component to Storybook](./add-component-to-storybook). */} {/* ::: */} {/* TODO: Update with new doc */} Once you've added your component, you must restart the styleguide (`npm run styleguide`). And once it is up and running, you can view your new story in `components > molecules > IllustratedContent`. Now that you've done that, you can edit the `IllustratedContent` component, save, and view changes live in your browser. :::info Learn more - About Storybook itself by reading the [official Storybook documentation](https://storybook.js.org/basics/introduction/) {/* TODO: update - About [our Storybook usage](./add-component-to-storybook) by reading our documentation */} ::: ### Implement your component Now that everything is ready to go, you can do the actual work and implement the component. In the case of the `IllustratedContent`, it would look something like this: ```jsx title="app/theme/components/molecules/IllustratedContent/IllustratedContent.tsx" import type { ReactNode } from "react"; interface IllustratedContentProps { media: ReactNode; children: ReactNode; } const IllustratedContent: React.FC = ({ media, children, }) => { return (
{media}
{children}
); }; export default IllustratedContent; ``` #### Styling your component :::note We are using [Sass](https://sass-lang.com/) (hence the `.scss` extension). We believe that it is easier for developers new to the React Ecosystem to remain with this well-known CSS preprocessor. ::: Create your stylesheet in the same folder as your component to keep your project organized and maintainable. ```scss title="app/theme/components/molecules/IllustratedContent/IllustratedContent.scss" .illustrated-content { display: flex; flex-direction: column; align-items: center; } .illustrated-content__media { width: 30%; max-width: 10em; text-align: center; } ``` And import it in your component. ```tsx title="app/theme/components/molecules/IllustratedContent/IllustratedContent.tsx" import type { ReactNode } from "react"; // highlight-next-line import "./IllustratedContent.scss"; interface IllustratedContentProps { media: ReactNode; children: ReactNode; } // Rest of the component ``` :::note As a side note, we also use [BEM convention](http://getbem.com/naming/) for our CSS code base. It makes it easier to avoid naming conflicts by adding a tiny bit of code convention. However, for your custom components, feel free to code however you like. There is no obligation here. ::: #### Document usage of our component If our component can have different usages, we should also add new stories along the default one. ```tsx title="app/theme/components/molecules/IllustratedContent/IllustratedContent.stories.tsx" import type { Meta, StoryObj } from "@storybook/react"; import IllustratedContent from "./IllustratedContent"; import Icon from "theme/components/atoms/Icon"; import { H3 } from "theme/components/atoms/Typography/Heading"; import { BodyParagraph } from "theme/components/atoms/Typography/Body"; import Image from "theme/components/atoms/Image"; const meta: Meta = { component: IllustratedContent, }; export default meta; type Story = StoryObj; export const Default: Story = { render: () => ( }>

Shipping within 48h

), }; export const WithContent: Story = { name: "With a lot of content", render: () => ( }>

Shipping within 48h

We are using many delivery services to let you choose what is best for you!
), }; export const WithImage: Story = { name: "With an image", render: () => ( } >

Shipping within 48h

), }; ``` It has many major benefits such as: - document edge cases - provide a test suite thanks to snapshot testing ([Storyshots](https://github.com/storybooks/storybook/tree/master/addons/storyshots)) - create a common discussion base for designers, product managers, marketers, etc. {/* TODO: update [Learn more about Storybook.](./add-component-to-storybook) */} ### Use the component Once you are satisfied with your component, you can use it anywhere. In this case, the `IllustratedContent` was to be used in the Reinsurance Banner. Thus, this module's component would look like this: ```tsx import FeatureList from "theme/components/organisms/FeatureList"; import IllustratedContent from "theme/components/molecules/IllustratedContent"; import Icon from "theme/components/atoms/Icon"; import { H3 } from "theme/components/atoms/Typography/Heading"; const ReinsuranceBanner = () => ( }>

Shipping within 48h

}>

Money back guarantee

}>

Secured Payment

); export default ReinsuranceBanner; ``` As you can see, we did not use a relative import. This is because in Front-Commerce we have a few aliases that will let you import files without worrying about your current position in the folder structure. In our case, if the `ReinsuranceBanner` was in `app/theme/modules/ReinsuranceBanner`, we don't have to import the `IllustratedContent` by using relative paths `../../components/molecules/IllustratedContent` but we can remain with `theme/components/molecules/IllustratedContent` which is more explicit. This is possible for any file located in the folder `app/theme` of a module. {/* */} --- # Create a custom HTTP endpoint URL: https://developers.front-commerce.com/docs/3.x/guides/create-custom-http-endpoint

{frontMatter.description}

## Create a custom HTTP endpoint Front-Commerce is built on the top of Remix, it means that you can create custom HTTP endpoint using the [Standard Remix Routing System](https://remix.run/docs/en/main/discussion/data-flow). Routes without a default export won't serve any HTML content. You will have to use [`loader`](https://remix.run/docs/en/main/route/loader) and [`action`](https://remix.run/docs/en/main/route/action) functions to perform custom interaction with backend data. Keep in mind that `loader` is only used for `GET` requests, while `action` is used for any other request type. ### GET request example Let's create a custom HTTP endpoint to serve a simple JSON response. ```tsx title="app/routes/api.hello.tsx" import { json } from "@front-commerce/remix/node"; export async function loader() { return json({ message: "Hello, world!" }); } ``` When accessing this route, you will get a JSON response with the following content: ```bash curl http://localhost:4000/api/hello ``` ```json { "message": "Hello, world!" } ``` You can also use the standard [Javascript Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object to send a response. ```tsx title="app/routes/api.hello.tsx" export async function loader() { return new Response("Hello World!", { status: 200, headers: { "Content-Type": "text/plain" }, }); } ``` ### POST/PUT/PATCH/DELETE request example For POST/PUT/PATCH/DELETE requests, you can use the `action` function. ```tsx title="app/routes/api.hello.tsx" export async function action({ request }: ActionFunctionArgs) { const method = request.method; return new Response(`Hey ! You've made a ${method} request !`); } ``` When acessing this route with a POST request, you will get a response with the following content: ```bash curl -X POST http://localhost:4000/api/hello ``` ``` Hey ! You've made a POST request ! ``` Or with a PUT request: ```bash curl -X PUT http://localhost:4000/api/hello ``` ``` Hey ! You've made a PUT request ! ``` You can leverage this endpoint to perform any custom interaction you need, here are some example of things you can do with these: - Serve content from the filesystem. ```ts title="app/routes/get-document.tsx" import fs from "node:fs"; export async function loader() { const file = fs.readFileSync("/home/server/myfile.pdf"); return new Response(file); } ``` - Return computed data like time ```ts title="app/routes/get-time.tsx" import { json } from "@front-commerce/remix/node"; export async function loader() { return json({ time: new Date().toISOString() }); } ``` - Serve data from a remote API ```ts title="app/routes/get-data.tsx" import { json } from "@front-commerce/remix/node"; export async function loader() { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await response.json(); return json(data); } ``` --- # Cache-Control URL: https://developers.front-commerce.com/docs/3.x/guides/customize-http-headers/cache-control `Cache-Control` headers are implemented to improve user experience by instructing browsers to store parts of the webpage in their internal cache. By default, Front-Commerce already applies `Cache-Control` headers to several important routes: - Products pages - Categories pages - Home page - `/robots.txt` - `/sitemaps.xml` ## How to use Cache-Control headers in Front-Commerce In Front-Commerce, adding `Cache-Control` headers to route is done by leveraging the [`CacheControl`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/core/services/implementations/CacheControl.ts?ref_type=heads) service, available from `FrontCommerceApp`. :::note To learn more about available services in Front-Commerce, see our [documentation](../../04-api-reference/front-commerce-core/services.mdx) ::: In this example, we'll add `Cache-Control` headers on a "Acme" route. ```ts title="app/routes/_main.acme.ts" import Acme from "theme/pages/Acme"; import type { LoaderFunction } from "@remix-run/node"; import { json } from "@front-commerce/remix/node"; import { FrontCommerceApp } from "@front-commerce/remix"; import { AcmeDocument } from "~/graphql/graphql"; export const loader: LoaderFunction = ({ context }) => { const app = new FrontCommerceApp(context.frontCommerce); // highlight-start app.services.CacheControl.setCacheable({ sMaxAge: 60, staleWhileRevalidate: 21600, }); // highlight-end const response = await app.graphql.query(AcmeDocument); return json({ acme: response.acme }); }; export default function Index() { return ; } ``` This snippet will inform the browser that the data resulting from requesting this route can be safely cached for 60 seconds, and that the cached version can still be served for 6 hours while the data is being fetched again in the background. ## Useful commands You can take the actions below to verify that proxy caching is working as you'd expect. First, you must ensure that your application sends correct `Cache-Control` headers for what you want to cache. Check with the following command: ```bash $ curl --silent -I http://localhost:4000 | grep -i cache-control cache-control: public, max-age=32140800 # <-- cache headers defined! ``` Then, check the same headers on a deployed environment. **Production and development environments all support this feature**. If `Cache-Control` headers are also detected, you can then check the `X-Cache` header value: ```bash $ curl --silent -I https://example.com/ | grep -i X-Cache X-Cache: HIT ``` Possible values are: - `HIT`: the resource was returned from the proxy cache (i.e: it didn't hit the NodeJS FC server) - `MISS`: the resource was not in the cache (it will be cached according to `Cache-Control` response headers) - `HIT-STALE`: the resource was returned from the cache but is stale and being revalidated in the background - `EXPIRED`: the resource was in the cache but has expired and needs to be revalidated ## A bash script to automate validation Copy and adapt the script below to analyze several pages of your sites on different environments. Try to list all types of pages available in your project. ```bash #!/usr/bin/env bash BASE_URL=$1 NOW=$(date +"%c") echo "## Cache test for $BASE_URL ($NOW)" getCacheOf() { local url=$BASE_URL$2 echo "" echo "### $1" echo "*$url*" echo ">" $(curl --silent -I $url | grep -i cache-control) } getCacheOf "Home Page" "/" getCacheOf "PLP" "/venia-dresses.html" getCacheOf "PDP" "/petra-sundress.html" getCacheOf "CMS page" "/about-us" getCacheOf "Contact page" "/contact" echo "" ``` Usage: `./test-cache-headers.sh https://magento2.front-commerce.app` 💡 You can pipe the output into a markdown file and have a report to share with teammates. --- # Content Security Policy URL: https://developers.front-commerce.com/docs/3.x/guides/customize-http-headers/content-security-policy This guide explains how to setup {/* prettier-ignore */}CSP in your application using a configuration provider. ## What is CSP? Content Security Policy (CSP) is a security standard that helps prevent various types of attacks including Cross-Site Scripting (XSS) and other code injection attacks. It works by allowing you to specify which content sources are trusted and allowed to be loaded by your web application. CSP is implemented through HTTP headers that tell browsers which resources (scripts, styles, images, etc.) are allowed to be loaded and from where. For example, a basic CSP header might look like this: ```http Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; ``` This policy would: - Only allow resources from the same origin (`'self'`) - Allow inline scripts and `eval()` (though this is generally discouraged) - Allow inline styles - Block all other types of content from external sources When implementing CSP, you might encounter these typical errors in your browser's console: ```http Refused to load the script 'https://third-party.com/script.js' because it violates the following Content Security Policy directive: "script-src 'self'" ``` This occurs when trying to load external resource (here, a JavaScript file) without adding their domain to the related directive (here, `script-src`). In Front-Commerce, CSP is configured through a provider system that allows you to fine-tune these security policies for your specific needs. Typical CSP usage includes: - Allowing payment providers (PayPal, Stripe, etc.) - Allowing tracking providers (Google Analytics, etc.) - Allowing fonts from third parties - Allowing images from third parties - etc. ## Basic configuration For security reasons only URLs from the store's domain are authorized through CSP. However, for tracking and third-party dependencies, you will have to authorize additional domains. If Front-Commerce, the CSP configuration lives in [`config.contentSecurityPolicy`](../configuration/01-adding-a-configuration-provider.mdx). In the skeleton, we provide a default CSP provider that you can extend to add your own domains: ```typescript title="app/config/cspProvider.ts" const appCSPProvider = () => { return { name: "cspConfiguration", values: { contentSecurityPolicy: { __dangerouslyDisable: false, directives: { defaultSrc: [], scriptSrc: [], frameSrc: [], styleSrc: [], imgSrc: [], fontSrc: [], connectSrc: [], baseUri: [], mediaSrc: [], objectSrc: [], workerSrc: [], manifestSrc: [], childSrc: [], }, }, }, }; }; export default appCSPProvider; ``` :::info Each extension can also [define its own CSP provider](../configuration/01-adding-a-configuration-provider.mdx). For example, the [PayPal extension](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/de54dee78a873ee52ea79a9e5fe55fc748b7e7e9/packages/paypal/cspProvider.ts) has its own CSP provider to allow PayPal's domain. ::: Add your custom domains to each directive as needed. When a CSP violation occurs, your browser will log it in the console and it will be recorded using the `security` logger. ## Enable Report-Only :::warning Loose security policies This section involves reducing the security of your application. Please carefully consider your requirements before using this feature. ::: If you are unable to define a restricted list of content providers, you may need to enable all content of a kind. This will allow all content of a kind and log violations to your configuration **without** blocking the contents. In the example below, all frames will be allowed and frames not originating from the domain itself or `mysite.com` will be logged in the `security` logger. ```typescript title="app/config/cspProvider.ts" const appCSPProvider = () => { return { name: "cspConfiguration", values: { contentSecurityPolicy: { __dangerouslyDisable: false, directives: { scriptSrc: [], frameSrc: ["*.mysite.com"], styleSrc: [], imgSrc: [], fontSrc: [], connectSrc: [], baseUri: [], }, // highlight-start reportOnlyDirectives: { frameSrc: true, }, // highlight-end }, }, }; }; export default appCSPProvider; ``` --- # Cross-Origin Resource Sharing (CORS) URL: https://developers.front-commerce.com/docs/3.x/guides/customize-http-headers/cross-origin-resource-sharing According to [Cross-Origin Resource Sharing (CORS) MDN article](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS): > Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that > allows a server to indicate any origins (domain, scheme, or port) other than > its own from which a browser should permit loading resources. In a nutshell, CORS headers instructs browsers and remote services whether they're allowed to load a specific content or not. This check usually happens with [an `OPTIONS` HTTP preflight request](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request). :::danger A typical error you might encounter would be: Access to font at 'https://example.com/public/fonts/my-font.priority.xxxx.woff2' from origin 'https://other.example.org' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. ::: ## Configuring CORS in Front-Commerce **In Front-Commerce, the content of this header can be controlled by creating a new configuration provider.** See the [Adding a configuration provider](../configuration/01-adding-a-configuration-provider.mdx) guide to learn how to register it in your application. ### Example Configuration Here is an example allowing browsers to load your pages from `webcache.googleusercontent.com` and `other.example.org` domains: ```ts title="app/config/appCorsConfigProvider.ts" export default () => { return { name: "cors", values: Promise.resolve({ cors: { // This value is the one transmitted to the cors() middleware // see https://www.npmjs.com/package/cors#configuration-options for details origin: { googleCache: "webcache.googleusercontent.com", otherSite: "other.example.org", }, }, }), }; }; ``` ### Adding the Configuration to Your Project And add it to your `front-commerce.config.ts` file: ```ts title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; import appCSPProvider from "./app/config/cspProvider"; // highlight-next-line import appCorsConfigProvider from "./app/config/appCorsConfigProvider"; export default defineConfig({ // ... configuration: { // highlight-next-line providers: [appCSPProvider(), appCorsConfigProvider()], }, // ... }); ``` ### Testing Your Configuration You can test headers returned from a specific origin by using the `Origin` HTTP header. Example: ```shell curl -sI http://localhost:4000/ \ -H "Origin:webcache.googleusercontent.com" \ | grep -i access-control-allow-origin ``` It should return `Access-Control-Allow-Origin: webcache.googleusercontent.com`. If you don't see this header, it means that the CORS configuration is not correctly set up. --- # Customize Outbound Requests Metrics URL: https://developers.front-commerce.com/docs/3.x/guides/customize-outbound-requests-metrics To customize outbound requests metrics in your Front-Commerce application, you can use the `MetricsService` provided by the core services. This guide will walk you through the steps to set up and use the `MetricsService` to track and observe metrics for outbound requests. ## Register `MetricsUrlConverter` in your extension This converter will define how to extract metrics names from requests. Let's say we have the following outbound request and we want to track them separately in our metrics: - `https://api.example.com/magento` - `https://api.example.com/contentful` We can achieve this by registering a `MetricsUrlConverter` in our extension, - The first parameter is a regex that will be used to match the request host, or a function that returns a boolean. - The second parameter is a function that will be used to extract the metrics name from the request. ```typescript title="src/extensions/acme-extension/index.ts" import { MetricsUrlConverter } from "@front-commerce/core/services"; export default function acmeExtension() { const basePath = "extensions/acme-extension"; return defineRemixExtension({ meta: import.meta, name: "acme-extension", unstable_lifecycleHooks: { onServerServicesInit: async (services) => { // outbound requests "Example API: Magento (https://api.example.com/magento/product/1)" services.MetricsService.registerMetricsUrlConverter( new MetricsUrlConverter( /.*api\.example\.com\/magento/, (req) => `Example API: Magento (${req.host})` ) ); // outbound requests "Example API: Contentful (https://api.example.com/contentful/rest)" services.MetricsService.registerMetricsUrlConverter( new MetricsUrlConverter( (req) => req.path.startsWith("/contentful"), (req) => `Example API: Contentful (${req.host})` ) ); }, }, }); } ``` --- # Customize routes programmatically URL: https://developers.front-commerce.com/docs/3.x/guides/customize-routes-programmatically

{frontMatter.description}

## Overview When building a Front-Commerce application, you might need to customize routes for various reasons: - Filter out unwanted routes from extensions - Dynamically defining routes without creating files in app/routes - Renaming routes to match your business requirements (e.g., translating URLs) - Ensure only necessary API routes are available - Customize the routing behavior for your specific use case ## Basic configuration Route customization happens in your `vite.config.ts` file by providing a `defineAppRoutes` function to the Front-Commerce plugin. This function receives three parameters: - `defineRoutes`: The Remix route definition function - `extensionRoutes`: Routes defined by Front-Commerce extensions - `fileRoutes`: Routes defined in your `app/routes` directory ```ts title="vite.config.ts" export default defineConfig((env) => { return { plugins: [ remixDevTools({ client: { defaultOpen: false, }, }), frontCommerce({ env, defineAppRoutes: async (defineRoutes, extensionRoutes, fileRoutes) => { // Your custom route logic here }, }), ], }; }); ``` ## Common use cases ### Disable unused routes You can prevent specific routes from being registered by filtering them out: ```ts defineAppRoutes: async (defineRoutes, extensionRoutes, fileRoutes) => { // Remove unwanted routes from extensions const routesToDisable = ["routes/account", "routes/wishlist"]; const filteredRoutes = Object.entries(extensionRoutes).reduce( (acc, [key, route]) => { if (!routesToDisable.includes(route.id)) { acc[key] = route; } return acc; }, {} ); return filteredRoutes; }, ``` ### Create additional routes You can define new routes that don't exist in your app/routes directory: ```ts defineAppRoutes: async (defineRoutes, extensionRoutes, fileRoutes) => { return { "routes/not-in-routes-dir": { id: "routes/not-in-routes-dir", path: "/not-in-routes-dir", file: "custom/not-in-routes-dir.tsx", // This file will be created in the app/custom directory }, }; }, ``` ### Rename routes You can change the URL path of existing routes while keeping the original implementation: ```ts defineAppRoutes: async (defineRoutes, extensionRoutes, fileRoutes) => { return { "routes/wishlist": { ...extensionRoutes["routes/wishlist"], path: "/your-custom-name", // Change URL path from /wishlist to /your-custom-name }, }; }, ``` :::warning Renaming routes only changes the URL path. It does not automatically update links in your theme or components. You'll need to update those manually. ::: ## Route priority When customizing routes, keep in mind the following priority order: 1. File-based routes (app/routes/\*) 2. Programmatically defined routes (via defineAppRoutes) 3. Extension routes This means that if you define a route that conflicts with a file-based route, the file-based route will take precedence. --- # Customize the sitemap URL: https://developers.front-commerce.com/docs/3.x/guides/customize-the-sitemap ## Introduction This guide provides step-by-step instructions on how to customize the sitemap for your Front-Commerce application. Customizing your sitemap can help improve SEO by ensuring search engines can easily crawl and index your site's content. ## Static Pages Customization ### `getSitemapEntries` Function To customize the sitemap for static pages, use the `getSitemapEntries` function in the SEO handle. This function allows you to add custom URLs, set the last modification date, change frequency, and priority for each URL. #### Example: ```tsx title="./extensions/acme/routes/blog.tsx" import { createHandle } from "@front-commerce/remix/handle"; export const handle = createHandle({ getSitemapEntries: ({ request, app }) => [ { url: "/blog", lastmod: new Date(), changefreq: "daily", priority: 0.5, images: ["/blog-image.jpg"], }, ], }); ``` :::caution Ensure server-only logic in `getSitemapEntries` is implemented using [`vite-env-only`](https://remix.run/docs/en/main/discussion/server-vs-client#vite-env-only) to prevent leaking into client-side code. ::: ## Dynamic Pages Customization Dynamic pages can also be customized using `getSitemapEntries` or a custom `sitemapFetcher`. ### `getSitemapEntries` Function ```tsx title="./extensions/acme/routes/todo.$id.tsx" import { createHandle } from "@front-commerce/remix/handle"; import { serverOnly$ } from "vite-env-only"; export const handle = createHandle({ getSitemapEntries: serverOnly$(async ({ request, app }) => { const posts = await fetch( "https://jsonplaceholder.typicode.com/posts" ).then((res) => res.json()); return posts.map((post) => ({ url: `/todo/${post.id}`, })); }), }); ``` ### `sitemapFetcher` Option Refer to [Registering Sitemap Fetcher](#creating-and-registering-custom-fetchers) for custom fetcher registration. ```tsx title="./extensions/acme/routes/todo.$slug.tsx" import { createHandle } from "@front-commerce/remix/handle"; export const handle = createHandle({ sitemapFetcher: "todoSitemapFetcher", }); ``` ## Creating and Registering Custom Fetchers Before creating new fetchers let's dive into the `SitemapService` service to get a better understanding of how it works. The `SitemapService` is composed of two main parts: - **Composite**: A list of fetchers that are executed in parallel to generate the sitemap. - **FetcherLeaf**: A function that returns an array of sitemap entries. Here is a diagram to help you visualize the Sitemap Service: ![visualize-sitemap-service](assets/sitemap-service.svg) As you can see, each service is able to register it's own fetchers for a composition, this will help with customisation later on in the guide. ### Register a composition Compositions are generally registered by the extensions which are responsible for adding the `handle` in the routes. In theme chocolatine, we have already have a few compositions (`products`, `category`, `cms`). You can register your own composition in your [extension definition](../04-api-reference/front-commerce-core/defineExtension.mdx): ```ts title="./extensions/acme/index.ts" import { createSitemapFetcher } from "@front-commerce/core"; export default defineExtension({ name: "acme", meta: import.meta, unstable_lifecycleHooks: { onServerServicesInit: async (services, request, config) => { // highlight-next-line services.Sitemap.registerComposition("acmeComposition"); }, }, }); ``` ### Registering a Fetcher First we will learn how to register a fetcher, the next section will cover how to create a fetcher. Registering a fetcher is similar to registering a composition, you can do it in your [extension definition](../04-api-reference/front-commerce-core/defineExtension.mdx). :::tip If you register a fetcher to a non-existent composition, the composition will be added automatically. ::: ```ts title="./extensions/acme/index.ts" import { defineExtension } from "@front-commerce/core"; export default defineExtension({ name: "acme", meta: import.meta, unstable_lifecycleHooks: { onServerServicesInit: async (services, request, config) => { // highlight-start services.Sitemap.registerFetcher("todoComposition" "AcmeTodo", () => import("./sitemap/todo.ts") ); // highlight-end // We can also register other fetchers for the `todoComposition`, for example: // services.Sitemap.registerFetcher("todoComposition" // "ContentfulTodo", // () => import("./sitemap/contentful/todo.ts") // ); }, }, }); ``` ### Creating a Fetcher [🔗 documentation](../04-api-reference/front-commerce-core/createSitemapFetcher.mdx) The fetcher is runtime logic, which will be resolved when a request to the `sitemap.xml` page is made. Here is an example of a fetcher that fetches a list of todos from a remote API, and generates the sitemap entry for each todo. ```ts title="./extensions/acme/sitemap/todo.ts" import { createSitemapFetcher } from "@front-commerce/core"; export default createSitemapFetcher(async () => { const posts = await fetch("https://jsonplaceholder.typicode.com/posts").then( (res) => res.json() ); return posts.map((post) => ({ route: `/todo/${post.id}`, changefreq: "monthly", lastmod: new Date(post.updatedAt), priority: 0.5, images: [post.image], data: post, // we add the full post data which allows for custom filter logic })); }); ``` ### Filtering sitemap entries from a fetcher To filter specific entries from a fetcher, you can register a filter in your extension definition. Filters are applied directly to the `FetcherLeaf`, so this allows different filters based on different `FetcherLeaf`'s in the same composition. ```ts title="./extensions/acme/index.ts" import { defineExtension } from "@front-commerce/core"; export default defineExtension({ name: "acme", meta: import.meta, unstable_lifecycleHooks: { onServerServicesInit: async (services, request, config) => { // We can add the data types through the generic <{ status: boolean }> typing services.Sitemap.registerFetcherFilter<{ status: boolean }>( "AcmeTodo", // The entries returned are only those of the `AcmeTodo` fetcher leaf. async (entries) => { return entries.filter((entry) => entry.data?.status === "published"); } ); }, }, }); ``` ### Extending the type declarations For TypeScript support, you'll need to extend the `SitemapCompositionList` and `SitemapFetcherList` interfaces from `@front-commerce/types` to include your custom composites and fetchers. First ensure you have the `@front-commerce/types` package installed in your project, as it provides the necessary types for the `SitemapService`. ```bash pnpm add -D @front-commerce/types ``` Then you can create a new types declaration file in your extension: ```ts title="./extensions/acme/types/sitemap.d.ts" declare module "@front-commerce/types" { export interface SitemapCompositionList { todoComposition: "todoComposition"; } export interface SitemapFetcherList { AcmeTodo: "AcmeTodo"; } } ``` These types will be merged with the existing types, so you can do this multiple times across multiple extensions. :::important important The declaration file needs to be included in your `tsconfig.json` before typescript can pick it up. ```json title="tsconfig.json" { // remove-next-line "include": ["types/**/*"] // add-next-line "include": ["extensions/**/types/**/*", "types/**/*"] } ``` ::: ## Opting Out of Sitemap Generation for static pages Static pages are automatically included in the sitemap. To exclude a page from the sitemap, return `null` or an empty array from `getSitemapEntries`. ```ts title="./extension/acme/routes/todo.tsx" import { createHandle } from "@front-commerce/remix/handle"; export const handle = createHandle({ getSitemapEntries: () => null, }); ``` --- # Customize the styles URL: https://developers.front-commerce.com/docs/3.x/guides/customize-the-styles

{frontMatter.description}

import Figure from "@site/src/components/Figure"; It is not something that you can do in just a few minutes because your identity shines in the details. However, you can do all the heavy lifting pretty quickly by customizing what is called **Design Tokens**. And it's a great way to familiarize yourself with Front-Commerce! ## Get your Design Tokens This concept is often closely related to Design Systems. Basically Design Tokens are the _core styles_ of your design: colors, font families, font sizes, borders, shadows, etc. Together they are what make your brand unique. There even exist tools that extract those tokens from existing websites. In this example, we will use [CSS Stats](https://cssstats.com/) to extract Design Tokens from [Smashing Magazine](https://www.smashingmagazine.com/) and use them later.
![CSS Stats lists all the 62 colors and 59 unique background colors of Smashing Magazine](./assets/smashingmagazine-cssstats.png)
:::info If you want to learn more about it, you can have a look at [Design tokens for dummies](https://specifyapp.com/blog/introduction-to-design-tokens) which is a very nice introduction. ::: ## Apply these tokens to your theme Now that we've got our Design Tokens, let's apply them to Front-Commerce's chocolatine theme. Since we use the Atomic Design principles, the tokens are within atoms of our theme. From your application, you will find components in the theme directory and atoms under its `theme/components/atoms` subdirectory. :::tip For example, files for [theme-chocolatine](../03-extensions/theme-chocolatine/_category_.yml), will be in [`@front-commerce/theme-chocolatine/theme`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/main/packages/theme-chocolatine/theme) The theme-chocolatine will be used as an example for this guide ::: In this guide, we'll focus on the colors and the typography settings. But feel free to go further and edit buttons, form inputs, etc. ### Colors In order to style our HTML, we use [Sass](https://sass-lang.com/), the well-known CSS preprocessor. Thus, the design tokens often translate to Sass variables. For instance, if we want to edit the colors of our application, we need to override the one defined in the core. To do so: 1. override the `_colors.scss` theme file in your theme: ```shell mkdir -p app/theme/components/atoms/Colors/ cp node_modules/@front-commerce/theme-chocolatine/theme/components/atoms/Colors/_colors.scss \ app/theme/components/atoms/Colors/_colors.scss ``` 2. edit the colors as needed In Smashing Magazine's case it would be: ```diff title="theme/components/atoms/Colors/_colors.scss" -$brandPrimary: #fbb03b; -$brandSecondary: #818199; +$brandPrimary: #d33a2c; +$brandSecondary: #2da2c5; -$fontColor: #131433; +$fontColor: #333; ``` ### Typography We could change the fonts to match Smashing Magazine's in a similar way. Fonts are defined in `theme/components/atoms/Typography/_typography.scss`. The difference here is that we will also introduce a different font for headings and that we will have to allow the remote font domain in the CSP headers. Follow the same steps than for colors: 1. override the `_typography.scss` and `Heading.scss` theme files in your theme: ```shell mkdir -p app/theme/components/atoms/Typography/Heading cp node_modules/@front-commerce/theme-chocolatine/theme/components/atoms/Typography/_typography.scss \ app/theme/components/atoms/Typography/_typography.scss cp node_modules/@front-commerce/theme-chocolatine/theme/components/atoms/Typography/Heading/_Heading.scss \ app/theme/components/atoms/Typography/Heading/_Heading.scss ``` 2. restart the application so the override is detected 3. edit the fonts as needed ```diff title="app/theme/components/atoms/Typography/_typography.scss" - $fontFamily: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, - Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; + @font-face { + font-family: "Elena"; + font-display: swap; + src: url("https://d33wubrfki0l68.cloudfront.net/a978f759fa0230c1e590d1bdb5a1c03ceb538cec/fed6b/fonts/elenawebregular/elenawebregular.woff2") + format("woff2"); + } + + @font-face { + font-family: "Mija"; + font-display: swap; + src: url("https://d33wubrfki0l68.cloudfront.net/b324ee03d5048d2d1831100e323b0b6336ffce68/0445e/fonts/mijaregular/mija_regular-webfont.woff2") + format("woff2"); + } + + $fontFamily: Elena, Georgia, serif; + $titleFontFamily: Mija, Arial, sans-serif; ``` ```diff title="app/theme/components/atoms/Typography/Heading/Heading.scss" h1, .h1 { font-size: 2rem; + font-family: $titleFontFamily; font-weight: normal; margin-top: 0.67em; margin-bottom: 0.67em; line-height: 1.5; } ``` 4. allow `d33wubrfki0l68.cloudfront.net` (the domain we have included fonts from) in your CSP `font-src` header value configured in `app/config/website.js` ```diff title="app/config/website.js" imgSrc: [], - fontSrc: [], + fontSrc: ["d33wubrfki0l68.cloudfront.net"], connectSrc: [], ``` ### Icons To introduce new icons or [override existing](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/theme-chocolatine/theme/components/atoms/Icon/icons/app-icons.js) icons you can add an `app-icons` file including your icons, for example: ```js title="theme/components/atoms/Icon/icons/app-icons.js" import { FcHome, FcPhone } from "react-icons/fc"; /** @type {Record} */ const componentIcons = { home: FcHome // this will override the `IoIosHome` icon from the core implementation phone: FcPhone // this will add a new icon }; /** @type {string[]} */ const svgIcons = []; export default { componentIcons, svgIcons, }; ``` ## A new look Front-Commerce would then now look like this:
![Before](./assets/product_page_chocolatine.jpg)
![After](./assets/product_page_smash.jpg)
Sure it still needs tweaking, but as you can see, it is already far better. Furthermore, it is an easy first step to start convincing your team and clients that using modern front-end technologies is for the best. You can experiment further by changing other tokens such as spacing, form inputs, buttons… ## Expand your brand theme Please note that all we've been talking about until now was to adapt the existing to your convenience. However, that is not the only benefit of having a Design System already in place. It is actually a perfect canvas to help you creating new components and styles matching to your brand. See the [override component guide](./override-a-component.mdx) to learn more about this. --- # Customize WYSIWYG Platform URL: https://developers.front-commerce.com/docs/3.x/guides/customize-wysiwyg-platform

{frontMatter.description}

If you want to learn how the core WYSIWYG component works instead, please refer to [Display WYSIWYG content](./display-wysiwyg-content.mdx). Each platform has a specific type of WYSIWYG. This allows to change how your content is rendered depending on its origin. For instance, a content from WordPress might have some specific media shortcodes while Magento will have some widgets to display a category name. In the following section you will learn about the one implemented in Front-Commerce:
Definitions: - a _shortcode_ is a specific string structure that is meant to be transformed into actual content - a _transform_ is a function that replaces an HTML tag with a React Component ## `DefaultWysiwyg` The goal of this WYSIWYG type is to remain as simple as possible. It is not meant to fetch data from additional services. **No shortcodes** **Default transforms** - `` tags are transformed into `theme/components/atoms/Typography/Link` components when the `href` attribute does not contain a domain. ## `MagentoWysiwyg` The MagentoWyswiyg's goal is to support all the default features in Magento 1 & 2. If you notice that some features are missing, please and we'll look into implementing it. **Supported shortcodes** - `{% raw %}{{media url="*"}}{% endraw %}` - `{% raw %}{{store url="*"}}{% endraw %}` - `{% raw %}{{widget type="*" attribute="value"}}{% endraw %}` **Default transforms** - `` tags are transformed into `theme/components/atoms/Typography/Link` components when the `href` attribute does not contain a domain. - `` tags are transformed into `theme/modules/Wysiwyg/MagentoWysiwyg/Widget/Widget.js` components. However, you shouldn't write a `` tag manually. It comes from the `{% raw %}{{widget}}{% endraw %}` shortcode. - ` // highlight-end

Lorem ipsum…

{content}

); }; export default MyComponent; ``` It will generate an inline ` ``` That's it! 🎉 Everything explained previously is the core behavior of the WYSIWYG implemented in Front-Commerce. It is _very_ flexible as it was implemented over a lot of iterations and feedbacks from our integrators. However, with this flexibility comes a bit of a complexity. Please keep in mind that you don't need to fully understand all of it to get started. However, if you've understood most of it, you will be able to dive into our code and understand how we have implemented platform specific WYSIWYG components. You could even use those components as an inspiration to develop your own specific behaviors. --- # Dynamic Routing URL: https://developers.front-commerce.com/docs/3.x/guides/dynamic-routing

{frontMatter.description}

## What is dynamic routing? Dynamic routes are generated based on the data available in your application, allowing for more meaningful and SEO-friendly URLs. For example, instead of a generic URL such as `/product/sku-6`, you can have a more descriptive URL like `/acme-product.html`. Consider a Remix route for your products defined as `routes/product.$id.tsx`. By leveraging dynamic routes, you can replace generic product URLs with SEO-friendly alternatives. #### Generic Route Example - URL: `/product/sku-6` #### SEO-Friendly Route Example - URL: `/acme-product.html` By implementing dynamic routes, you enhance the readability and search engine optimization (SEO) of your URLs, which can improve your site's visibility and user experience. ## How to create dynamic routes? ### Create a `DynamicRouteUrlMatcher` First, define a class that implements the `DynamicRouteUrlMatcher` interface to match URLs to your application's routes. ```ts title="extension/UrlMatcher.ts" import type { DynamicRouteUrlMatcher, MatchedDynamicRouteUrl, } from "@front-commerce/core"; // Define hardcoded routes for demonstration purposes const hardcodedRoutes = [ { type: "product" path: "/acme-product.html", identifier: "sku-6", }, { type: "product" path: "/baz-product.html", redirectTo: "/acme-product.html", redirectType: 301 }, ]; export default class UrlMatcher implements DynamicRouteUrlMatcher { matchUrl(path: string): MatchedDynamicRouteUrl | undefined { // Find a matching route in the hardcoded list, ideally this will be from a remote resource const match = hardcodedRoutes.find((route) => route.path === path); if (!match) { return undefined; } return { path: match.path, identifier: match.identifier, params: { id: match.identifier, // The product ID to pass to the route }, }; } } ``` This class checks if a provided URL matches any of the predefined paths and returns the corresponding route details. ### Register a `DynamicRouteUrlMatcher` Next, integrate the UrlMatcher into your application by registering it with the DynamicRoutes registry. ```ts title="extension/index.ts" import { defineExtension } from "@front-commerce/core"; import UrlMatcher from "./UrlMatcher"; export default defineExtension({ name: "acme-extension", meta: import.meta, // highlight-start unstable_lifecycleHooks: { onServerServicesInit: async (services, request, config) => { services.DynamicRoutes.registerUrlMatcher( "AcmeUrlMatcher", () => new UrlMatcher() ); }, }, // highlight-end }); ``` This configuration ensures that your UrlMatcher is included in the server's initialization process, making it active for incoming requests. ### Add `handle` export to Remix route Update your Remix route to use the dynamic route identifier by adding it to the `handle` export. ```tsx title="routes/product.$id.tsx" import { createHandle } from "@front-commerce/remix/handle"; export const handle = createHandle({ dynamicRoute: { type: "product", // to match the type of your url matcher priority: 1, // higher priority routes are matched first }, }); ``` If you have multiple pages that use the same `type` for their dynamic routes, you can use the `priority` field to determine the order in which they are matched. Higher priority routes are matched first. for example the following `routes/acme-product.$id.tsx` will be matched before `routes/product.$id.tsx`: ```tsx title="routes/acme-product.$id.tsx" import { createHandle } from "@front-commerce/remix/handle"; export const handle = createHandle({ dynamicRoute: { type: "product", priority: 2, }, }); ``` ### Replace `useLoaderData` with `useDynamicRouteLoaderData` Finally, replace the `useLoaderData` hook with the `useDynamicRouteLoaderData` which provides the dynamic route data. ```tsx title="routes/product.$id.tsx" // remove-next-line import { useLoaderData } from "@front-commerce/remix/react"; // add-next-line import { useDynamicRouteLoaderData } from "@front-commerce/remix/react"; import { createHandle } from "@front-commerce/remix/handle"; export const loader = () => {...} export const handle = createHandle(...); export default function Product() { // remove-next-line const data = useLoaderData(); // add-next-line const data = useDynamicRouteLoaderData(); return
Product: {data.product.name}
; } ```
### Visiting the Dynamic Route When you navigate to `/acme-product.html`, the system uses your `UrlMatcher` to resolve the URL to the product page corresponding to `sku-6`. This approach allows you to manage URLs dynamically based on your application's data requirements.
## Batching and Prioritizing URL Matchers When multiple URL matchers are registered without any `matcherOptions`, it will default the batchOrder and priority to 0. This means that the order in which the URL matchers are registered will determine the order in which they are executed. Let's say we have the following list of URL Matchers: ```bash A → fast API (cached) B → no API (static) C → slow API (not cached) D → no API (static) ``` We would ideally want this to first try to run `B` and `D` then `A` and finally `C`. To achieve this we can set the `batchOrder` and `priority` fields in the `matcherOptions` object. ```ts title="extension/index.ts" services.DynamicRoutes.registerUrlMatcher("A", () => new UrlMatcherA(), { // highlight-start batchOrder: 1, priority: 1, // highlight-end }); services.DynamicRoutes.registerUrlMatcher("B", () => new UrlMatcherB(), { // highlight-start batchOrder: 0, priority: 2, // highlight-end }); services.DynamicRoutes.registerUrlMatcher("C", () => new UrlMatcherC(), { // highlight-start batchOrder: 2, // only run if no other batches have matched priority: 1, // highlight-end }); services.DynamicRoutes.registerUrlMatcher("D", () => new UrlMatcherD(), { // highlight-start batchOrder: 0, priority: 1, // highlight-end }); ``` This would result in the following order of execution: ```bash # batch 0 D → no API (static) B → no API (static) # batch 1 A → fast API (cached) # batch 2 (only run if no other batches have matched) C → slow API (not cached) ``` ## Extending the type declarations For TypeScript support of the `type` field in your handle export, you can extend the `DynamicRoutesCompositionList` interface from the `@front-commerce/types` package to include your custom types. #### Install the `@front-commerce/types` package Ensure you have the `@front-commerce/types` package installed in your project, as it provides the necessary types for the `DynamicRoutes`. ```bash $ pnpm add @front-commerce/types ``` #### Create declaration file This declaration can be placed in a `dynamic-routes.d.ts` file within your extension's types directory. ```ts title="extension/types/dynamic-routes.d.ts" declare module "@front-commerce/types" { export interface DynamicRoutesCompositionList { contentful: "contentful"; // example of another type } } ``` Existing types can be found in the [theme-chocolatine package](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/3c31a7d63ba6fa4726bcf4997188d38be7254248/packages/theme-chocolatine/types/dynamic-routes.d.ts#L1-7). :::note TypeScript will merge all the found module declarations, so you can add your own without redefining the whole module. ::: #### Ensure your declaration is included in the tsconfig.json This declaration can be placed in a `dynamic-routes.d.ts` file within your extension's types directory. ```json title="tsconfig.json" { "include": [ "./**/*.jsx", "./**/*.js", "./**/*.ts", "./**/*.tsx", // add-next-line "./extensions/**/types/*.d.ts" ] } ``` #### Verify your handle export ```tsx title="routes/contentful.$id.tsx" import { createHandle } from "@front-commerce/remix/handle"; export const handle = createHandle({ dynamicRoute: { type: "contentful", // this should now autocomplete in your IDE. }, }); ``` ## Known Limitations ### Matching Catch-All Routes (Splat Routes) In Remix, you can create a catch-all route that matches any path, for example: | URL | Matched Route | | -------- | ------------------ | | `/foo/a` | `routes/foo.$.tsx` | | `/bar/a` | `routes/bar.$.tsx` | However, it's not possible to create a URL Matcher for catch-all routes because the URL will still be matched by Remix. Attempting to match `/example` to `routes/foo.$.tsx` using a URL matcher will result in the following error: ``` Error: Route "routes/foo.$" does not match URL "/example" ``` ### Dynamic Route Catch-All Placeholder To implement CSR for dynamic routes, the application requires specific internal logic in the [`routes/_main.$.tsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/integration/fc-1646-dynamic-routes/packages/theme-chocolatine/routes/_main.$.tsx?ref_type=heads) file. If you have overwritten this route, you will need to manually apply and maintain this logic to ensure proper functionality. ### HMR Reloads with Multiple Dynamic Route Tabs When working in development mode with Hot Module Replacement (HMR), you might encounter issues when loading multiple dynamic routes in parallel tabs. This can cause the dynamic route manifest to become out of sync, resulting in pages not loading correctly. #### Symptoms Pages with dynamic routes stop working after HMR reloads ```bash TypeError: Cannot read properties of undefined (reading 'module') ``` #### Solutions You can resolve this issue by either: 1. Restarting your development server 2. Using client-side navigation to the affected pages, which will re-populate the manifest and restore functionality --- # Error Handling for routes URL: https://developers.front-commerce.com/docs/3.x/guides/error-handling-for-routes # Comprehensive Error Handling Front-Commerce provides a robust error handling system that allows you to gracefully manage errors at different levels of your application. This guide will walk you through the process of implementing error boundaries and customizing error pages to enhance your application's resilience and user experience. ## Understanding Error Boundaries Error boundaries in Front-Commerce are based on Remix's error handling mechanism. They allow you to catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed. Front-Commerce offers three main types of error boundaries: 1. Root Error Boundary 2. Layout Error Boundary 3. Route-specific Error Boundary ## Root Error Boundary The Root Error Boundary is the top-level error handler for your entire application. It's typically defined in your `app/root.tsx` file. To implement the Root Error Boundary: 1. Import the `RootErrorBoundary` component from Front-Commerce's theme: ```tsx import { RootErrorBoundary } from "theme/pages/Error"; ``` 2. Add the ErrorBoundary export to your `root.tsx`: ```tsx export const ErrorBoundary = RootErrorBoundary; ``` This will catch any unhandled errors in your application and display a generic error page. :::note The `RootErrorBoundary` is wrapped in the `SimpleLayout` component. ::: ## Layout Error Boundary For routes that use a custom layout, you can implement a Layout Error Boundary. This allows you to maintain your layout structure even when an error occurs. Here is an example of how we use the LayoutErrorBoundary for the `_main.tsx` layout: 1. Import the `LayoutErrorBoundary` component: ```tsx title="app/routes/_main.tsx" import { LayoutErrorBoundary } from "theme/pages/Error"; ``` 2. Use it within your layout component's ErrorBoundary: ```tsx title="app/routes/_main.tsx" export function ErrorBoundary() { // If this loader fails, it will be caught by the RootErrorBoundary const data = useRouteLoaderData("routes/_main"); const { headerNavigationMenu = [], footerNavigationMenu = [] } = data || {}; return ( ); } ``` This approach ensures that your layout remains intact while displaying the error message. :::note The `LayoutErrorBoundary` is not wrapped in any layout as this is meant to be used inside a route which is already wrapped in a layout. ::: ## Customizing Error Pages Front-Commerce allows you to customize error pages for specific HTTP status codes. This is done through the `RouteResponseError` component and the [`appErrorPages`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/5f23e92ac2008298dad36b72ae0066433d0311ae/packages/theme-chocolatine/theme/pages/Error/appErrorPages.ts) object. ### Adding Custom Error Pages 1. Create a new component for your custom error page, e.g., `CustomNotFound.tsx`. 2. Add your custom error page to the `appErrorPages` object in [`theme/pages/Error/appErrorPages.ts`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/5f23e92ac2008298dad36b72ae0066433d0311ae/packages/theme-chocolatine/theme/pages/Error/appErrorPages.ts): ```typescript title="theme/pages/Error/appErrorPages.ts" import CustomNotFound from "./CustomNotFound"; export const appErrorPages = { 404: CustomNotFound, } satisfies AppErrorPages; ``` 3. The `RouteResponseError` component will now use your custom component for 404 errors. ### Default Error Pages Front-Commerce provides default error pages for common status codes: - [**429**: Rate Limit Exceeded](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/2886bd9de6413411f89c7267252a95f45a55dd94/packages/theme-chocolatine/theme/pages/Error/RateLimit/RateLimit.tsx) - [**503**: Maintenance](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/83fe19855d18ed1a21693cb31c401e61f4ce8c79/packages/theme-chocolatine/theme/pages/Error/Maintenance/Maintenance.tsx) - [**404**: Not Found](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/67393729953ea2dabbd8ac8dae14e7e82fbdbcb5/packages/theme-chocolatine/theme/modules/PageError/NotFound/NotFound.jsx) You can override these by adding your own components to the [`theme/pages/Error/appErrorPages.ts`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/5f23e92ac2008298dad36b72ae0066433d0311ae/packages/theme-chocolatine/theme/pages/Error/appErrorPages.ts) object. ## Customizing Meta Tags To extend the meta tags for your route error pages, you can register new meta tags in the [`root-error-meta`](../04-api-reference/front-commerce-remix/features.mdx#root-error-meta) feature. ### Adding Custom Meta Tags You can refer to the [example demo extension](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/a025ba1e1ba82a4a56609e58d537d03d74317df3/skeleton/example-extensions/error-boundary-demo/index.ts) for implementation details. ```ts title="example-extension/application-extension/index.ts" hooks.registerFeature("root-error-meta", { // ... unstable_lifecycleHooks: { onFeaturesInit: (hooks) => { hooks.registerFeature("root-error-meta", { config: { messages: () => { return (intl: IntlShape) => { "500": { // with intl translations title: intl.formatMessage({ id: "500.title" }), description: intl.formatMessage({ id: "500.description" }), }, "418": { // without intl translations title: "Custom - Teapot", description: "I'm a teapot", }, }; }, }, }); }, }, }); ``` ### Default Meta Tags Front-Commerce provides default meta tags for common status codes: (_see exhaustive list in [`root-error-meta.ts`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/a025ba1e1ba82a4a56609e58d537d03d74317df3/packages/theme-chocolatine/features/root-error-meta.ts)_) - **429**: Rate Limit Exceeded - **503**: Maintenance - **404**: Not Found ## Best Practices 1. **Granular Error Handling**: Use route-specific error boundaries for handling errors that are unique to certain parts of your application. 2. **Informative Error Messages**: Provide clear and helpful error messages to guide users on what went wrong and what they can do next. 3. **Logging**: Implement proper error logging to help with debugging and monitoring application health. 4. **Graceful Degradation**: Design your error pages to maintain as much functionality as possible, allowing users to navigate away from the error or retry their action. 5. **Consistency**: Maintain a consistent look and feel in your error pages to align with your application's overall design. By following these guidelines and utilizing Front-Commerce's error handling components, you can create a robust and user-friendly error management system for your e-commerce application. --- # Extend layout route URL: https://developers.front-commerce.com/docs/3.x/guides/extend-layout-route

{frontMatter.description}

## What is a layout route? Layout routes are used to apply a the same layout to multiple pages, generally these are pathless routes. For more information how routing works in Remix, please see the [Nested Routes](https://remix.run/docs/en/main/file-conventions/routes#nested-routes) documentation. In Front-Commerce some of the main layout routes are: - [`_main.tsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/theme-chocolatine/routes/_main.tsx) - [`_main.user.tsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/theme-chocolatine/routes/_main.user.tsx) - [`_main.user.orders.tsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/theme-chocolatine/routes/_main.user.orders.tsx) - [`checkout.tsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/theme-chocolatine/routes/checkout.tsx) To extend these routes you can either create your own layout route or re-use an existing one. ## Create a new layout route To create a new layout route you can create a new file in the `routes` folder ```tsx title="routes/_main.tsx" import { Outlet } from "@remix-run/react"; const Layout = () => { return (
{...}
{...}
); }; export default Layout; ``` :::warning This will override the existing `_main.tsx` layout in the original theme, it doing so it might also override required actions, loaders, meta, etc. Please refer to the original layout to ensure you are not missing anything. It is at the moment not possible to reuse functions from the original route without copying and pasting its logic over to your new route file. ::: --- # Extend the GraphQL schema URL: https://developers.front-commerce.com/docs/3.x/guides/extend-the-graphql-schema

{frontMatter.description}

When developing an e-commerce store, you might at some point need to expose new data in your unified GraphQL schema to support new features and allow frontend developers to use them. Front-Commerce’s GraphQL modules is the mechanism allowing to extend and override any part of the schema defined by other modules. The Front-Commerce core and platform integrations (such as Magento2) are implemented as GraphQL modules. They leverage features from the GraphQL Schema Definition Language (SDL). This page will guide you through the process of exposing a new feature in your GraphQL schema. We will create an extension with a GraphQL module that allows to maintain a counter of clicks for a product. ## Create an extension To extend the GraphQL schema and implement your own logic, you first have to create an extension. An extension can be created by adding a new folder under `extensions` and by creating a `index.ts` file (the extension definition) with the following content: ```typescript title="example-extensions/click-counter/index.ts" import { defineExtension } from "@front-commerce/core"; export default function clickCounter() { return defineExtension({ name: "click-counter", meta: import.meta, }); } ``` Then you need to [register the extension into your Front-Commerce project](./register-an-extension.mdx). ## Add a GraphQL module within the extension For now, the `click-counter` extension does not provide any feature. To extend the GraphQL schema and implement our own logic, we need to add a GraphQL module to the extension containing a schema and some resolvers. ### Add an empty GraphQL module For that, you can create the GraphQL module definition in the file `example-extensions/click-counter/graphql/index.ts` with the following content: ```typescript title="example-extensions/click-counter/graphql/index.ts" import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "ClickCounter", }); ``` This is a minimal GraphQL module definition which namespace is `ClickCounter` and that does nothing for now. Then, the extension needs to declare that it provides a GraphQL module by modifying the extension's index.ts: ```typescript title="Changes to example-extensions/click-counter/index.ts" import { defineExtension } from "@front-commerce/core"; import clickCounter from "./example-extensions/click-counter/graphql"; export default function clickCounter() { return defineExtension({ name: "click-counter", configuration: { providers: [], }, // highlight-start graphql: { modules: [clickCounter], }, // highlight-end }); } ``` At this point, your Front-Commerce project is configured with an extension called `click-counter`, this extension provides a GraphQL module which namespace is `Clickcounter`. The extension is now ready to bring changes to your Graph. ### Extend the GraphQL Schema Front-Commerce lets you describe your schema using the expressive [GraphQL Schema Definition Language (SDL)](https://graphql.org/learn/schema/#type-language). We would like to: 1. add a `clickCounter` field to the existing `Product` type 2. add an `incrementProductCounter` mutation For that, you can add the type definitions as a `typeDefs` value in your GraphQL module definition. ```ts title="example-extensions/click-counter/graphql/index.ts" import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "ClickCounter", // add-start typeDefs: /** GraphQL */ ` extend type Product { clickCounter: Int } extend type Mutation { incrementProductCounter(sku: String!, incrementValue: Int): MutationSuccess! } `, // add-end }); ``` :::info The `Product` type is part of Front-Commerce’s `Front-Commerce/Product` module. The `MutationSuccess` type, used as the response for the mutation, is part of Front-Commerce’s core. ::: At this point, you should be able to see in the GraphQL playground `http://localhost:4000/graphql` that the type `Product` has a `clickCounter` field and that a mutation `incrementProductCounter` exists. You can try to execute the query below in your GraphQL playground `http://localhost:4000/graphql`: ```graphql title="http://localhost:4000/graphql" { product(sku: "WH09") { sku clickCounter } } ``` Our GraphQL module does not yet have any code for sending content. This means that even if you can request the `clickCounter` field, it won't actually return any data. So you should see the following response: ```json { "data": { "product": { "sku": "WH09", "clickCounter": null } } } ``` ### Define Resolvers and Runtime Logic The latest step is to implement the logic, which we will refer to as the `GraphqlRuntime`, to retrieve the `Product.clickCounter` value and behind the `incrementProductCounter` mutation. For that, you have to add resolvers for the field and the mutation. This is where most of the _real work_ is done, for instance by fetching remote data sources or transforming data. Resolvers are exposed using the `resolvers` key of the GraphQL runtime definition. It should be a [**Resolver map**](https://www.graphql-tools.com/docs/resolvers): an object where each key is a GraphQL type name, and values are mapping between field names and resolver function. Resolver functions may return a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) for asynchronous operations. :::note To learn more about resolvers and their internals, we recommend reading [GraphQL Tools resolvers documentation](https://www.graphql-tools.com/docs/resolvers). ::: First, let's update the module definition to register the runtime API: ```typescript title="Changes to example-extensions/click-counter/graphql/index.ts" import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "ClickCounter", // add-next-line loadRuntime: () => import("./runtime"), // as this only executes in during runtime, it needs to be an import statement. }); ``` And then let's create the GraphqlRuntime so that we can add the `resolvers` map as below: ```typescript title="example-extensions/click-counter/graphql/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ resolvers: { Product: { clickCounter: (product) => { console.log(`click count retrieval for product ${product.sku}`); // your own retrieval logic here return 1; }, }, Mutation: { incrementProductCounter: (_root, params) => { const { sku, incrementValue } = params; console.log(`incrementing counter for product "${sku}"`); console.log(`counter + ${incrementValue ?? 1}`); // your own increment logic here return { success: true, }; }, }, }, }); ``` It brings a very basic implementation for both the `Product.clickCounter` field resolution and the `incrementProductCounter` mutation. After restarting Front-Commerce, the `clickCounter` field of a product should now return `1` instead of `null`. Likewise, the mutation can also be called from the playground. As stated above, a resolver function can also return a Promise and thus handle asynchronous result. A more real life resolver map would like look like: ```typescript title="example-extensions/click-counter/graphql/runtime.ts" export default createGraphQLRuntime({ resolvers: { Product: { clickCounter: (product) => { // add-start const clickCountPromise = fetchClickCountForSku(product.sku); return clickCountPromise; // add-end }, }, Mutation: { incrementProductCounter: async (_root, params) => { // add-start const { sku, incrementValue } = params; try { await incrementProductCount(sku, incrementValue); return { success: true, }; } catch (error: any) { return { success: false, errorMessage: error.message, }; } // add-end }, }, }, }); ``` ## Adding custom `scalars` to the schema To add [custom scalars](https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#scalars) to your schema, you can define them in your GraphQL module as shown below. ```ts import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "example-module", typeDefs: /** GraphQL */ ` Query { acmeEntry(identifier: AcmeScalar!): String! } `, // highlight-start scalars: { AcmeScalar: "string | number", }, // highlight-end }); ``` --- # Markdown and MDX URL: https://developers.front-commerce.com/docs/3.x/guides/extend-the-vite-configuration/markdown-and-mdx

{frontMatter.description}

:::tip You can check the [`example-extension/mdx-blog`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/main/skeleton/example-extensions/mdx-blog) example to see how MDX can be implemented. ::: ## Setup ### Install MDX bundler for vite ```sh pnpm add @mdx-js/rollup @mdx-js/react ``` ### Update `vite.config.ts` **Please note that `mdx` plugin must be declared before the `frontCommerce` plugin** to ensure that MDX files have already been transformed to standard React components before going through the Front-Commerce–Remix–React toolchain. ```diff import { defineConfig } from "vite"; import { vitePlugin as frontCommerce } from "@front-commerce/remix/vite"; +import mdx from "@mdx-js/rollup"; export default defineConfig((env) => { return { plugins: [ + mdx({ providerImportSource: "@mdx-js/react" }), frontCommerce({ env }) ], }; }); ``` ## Adding custom components To specify which component to render for a specific tag, you can use the `MDXProvider`, please check out [demo example](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/skeleton/example-extensions/mdx-blog/routes/_main.blog.tsx#L8) to see how to use it. ## Demo You can load this demo extension to check if your setup is working by adding these lines to your `front-commerce.config.ts` file: ```diff import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; import storesConfig from "./app/config/stores"; import cacheConfig from "./app/config/caching"; import rateLimiterConfig from "./app/config/rateLimiter"; import appCSPProvider from "./app/config/cspProvider"; import serverEventsConfig from "./app/config/serverEvents"; import pwaConfig from "./app/config/pwa"; +import mdxBlog from "./example-extensions/mdx-blog"; export default defineConfig({ extensions: [ themeChocolatine(), + mdxBlog() ], stores: storesConfig, cache: cacheConfig, rateLimiter: rateLimiterConfig, serverEvents: serverEventsConfig, configuration: { providers: [appCSPProvider()], }, v2_compat: { useApolloClientQueries: true, useFormsy: true, }, pwa: pwaConfig, }); ``` Then run your Front-Commerce app and go to [http://localhost:4000/blog](http://localhost:4000/blog) ## References - https://mdxjs.com/docs/ - https://mdxjs.com/packages/rollup/ --- # Remix Development Tools URL: https://developers.front-commerce.com/docs/3.x/guides/extend-the-vite-configuration/remix-development-tools

{frontMatter.description}

Since Front-Commerce is based on Remix, you can use any related Remix extension to enhance your development experience and your project. In this guide, we will guide you through the setup of [Remix Development Tools](https://remix-development-tools.fly.dev/). :::tip Remix Development Tools isn't affiliated with Front-Commerce. Since we use it to work on Front-Commerce, we decided to share it with you to enhance your experience too! ::: ## Quick start ### Install the extension ```sh pnpm add --save-dev remix-development-tools ``` ### Add the extension to your `vite.config.ts` **Please note that `remixDevTools` plugin must be declared before the `frontCommerce` plugin** ```diff import { defineConfig } from "vite"; import { vitePlugin as frontCommerce } from "@front-commerce/remix/vite"; +import { remixDevTools } from "remix-development-tools"; export default defineConfig((env) => { return { plugins: [ + remixDevTools({ + client: { + defaultOpen: false, + }, + }), frontCommerce({ env }) ], }; }); ``` --- # Tailwind URL: https://developers.front-commerce.com/docs/3.x/guides/extend-the-vite-configuration/tailwind

{frontMatter.description}

:::tip You can check the [`example-extension/tailwind`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/main/skeleton/example-extensions/tailwind) example to see how tailwind can be implemented. ::: ## Install Tailwind ```sh pnpm add -D tailwindcss ``` ## Generate Tailwind configuration ```sh pnpm dlx tailwindcss init -p --ts ``` ## Update your tailwind config Update your `tailwind.config.ts` to include files from `app/theme`: ```diff import type { Config } from 'tailwindcss' export default { - content: [] + content: ["./app/**/*.{ts,tsx,js,jsx}"], theme: { extend: {}, }, plugins: [], } satisfies Config ``` If you have components in some of your extensions, you need to add the related extension paths to the `content` directive. ## Setup PostCSS Add the `tailwindcss` directive to the `postcss.config.js` file: ```diff export default { plugins: { autoprefixer: {}, + tailwindcss: {}, }, }; ``` ## Inject Tailwind stylesheet Create a `tailwind.css` file in the `app` folder with the following content: ```css @tailwind base; @tailwind components; @tailwind utilities; ``` Then, import this file in your `root.tsx` like so: ```diff ... + import "./tailwind.css"; ... ``` You can now start your project and tailwind will be used for all your styles. # Usage You can test it by loading this module into your app by inject it in your `front-commerce.config.ts` file: ```diff ... +import tailwind from "./example-extensions/tailwind"; export default defineConfig({ extensions: [ themeChocolatine(), + tailwind() ], ... ``` And update the `tailwind.config.js` with this: ```diff /** @type {import('tailwindcss').Config} */ export default { content: [ "./app/theme/**/*.{ts,tsx,js,jsx}", + "./example-extensions/tailwind/theme/**/*.{ts,tsx,js,jsx}", ], theme: { extend: {}, }, plugins: [], } ``` Now you can head to [`http://localhost:4000/tailwind`](http://localhost:4000/tailwind) and check that Tailwind is working. ## References - https://tailwindui.com/documentation - https://tailwindcss.com/docs/guides/remix --- # Components Map URL: https://developers.front-commerce.com/docs/3.x/guides/extension-features/components-map ## What is the components map?

{frontMatter.description}

For instance, component map can be useful for: - implementing a user menu with links contributed from different extensions - displaying platform-specific components in the application features specific to the platform - customizing how polymorphic GraphQL data are displayed in an application ## How to register a component map? ### Registering a new component map To register a new component map, you can use the [`registerFeature`](/docs/3.x/api-reference/front-commerce-core/extension-features#registerfeature) hook. ```ts title="./example-extension/acme-extension/index.ts" export default defineExtension({ ... unstable_lifecycleHooks: { onFeaturesInit: (hooks) => { // highlight-next-line hooks.registerFeature("acme-feature", { ui: { componentsMap: { // This should resolve to a valid file relative to the current import.meta.url // highlight-next-line Header: new URL("./components/AcmeHeader.tsx", import.meta.url), Toolbar: new URL("./components/AcmeToolbar.tsx", import.meta.url), Footer: new URL("./components/AcmeFooter.tsx", import.meta.url), }, }, }); }, }, }); ``` ### Override an existing component map Let's say our application needs to disable the previously registered `Toolbar` component which is by default registered in the `acme-extension`. We can achieve this by registering a new component map with the same name, but replacing the `Toolbar` with `null` ```ts title="./example-extension/application-extension/index.ts" export default defineExtension({ ... unstable_lifecycleHooks: { onFeaturesInit: (hooks) => { // this will extend the `acme-feature` from `acme-extension` hooks.registerFeature("acme-feature", { ui: { // we only override the `Toolbar` component, the others will still resolve from `acme-extension` // highlight-next-line componentsMap: { Toolbar: null }, }, }); }, }, }); ``` :::tip When overriding an existing component map, the last registered extension will be the components used for the feature. All the components are merged together. Not including a component, will maintain the component registered before. To _unregister_ it, you must explicitly override it with `null`. So in the above example, the final components map will be resolved as: ```ts { Header: AcmeHeader, Toolbar: () => null, Footer: AcmeFooter, } ``` ::: ## How to use the components map? The components map is used through the [`useExtensionComponentMap`](/docs/3.x/api-reference/front-commerce-core/react#useextensioncomponentmap) hook. ```tsx import { useExtensionComponentMap } from "@front-commerce/core/react"; const App = () => { const AcmeFeature = useExtensionComponentMap("acme-feature"); return (
); }; ``` --- # Feature Flags URL: https://developers.front-commerce.com/docs/3.x/guides/extension-features/feature-flags ## Overview

{frontMatter.description}

For instance feature flags can be useful for display or not a banner in your application. ## Using feature flags ### Declaring a feature flag To declare a feature flag, you must include a `featureFlags` property in your extension declaration. ```ts title="extension/acme/index.ts" export default defineExtension({ ... unstable_lifecycleHooks: { onServerServicesInit: async (services, _request, config, user) => { onFeaturesInit: (hooks) => { hooks.registerFeature("order", { flags: { canOrderFooBar: true, } }); } }, } ... ``` This example will allow you to know if you can order a `FooBar` product. ### Using a feature flag To use a feature flag, you must use the `useExtensionFeatureFlags` hook. The `useExtensionFeatureFlags` hook takes three arguments: - The extension name - The feature flag name - The default value ```tsx title="extension/acme/pages/OrderFooBarPage.tsx" import { useExtensionFeatureFlags } from "@front-commerce/core/react"; export function OrderFooBarPage() { const canOrderFooBar = useExtensionFeatureFlags( "order", "canOrderFooBar", false ); return (

Order FooBar

{canOrderFooBar ? ( ) : (

You cannot order FooBar

)}
); } ``` ## Implemented Features and Flags ### `newsletter` Feature | Flag name | Description | | --------- | ----------------------------------------------- | | `enabled` | Is the newsletter related actions are available | ### `accountInformation` Feature | Flag name | Description | | --------------- | --------------------------------------------------------------------- | | `phoneRequired` | Should the phone number must be required in the user information form | | `canEditEmail` | Can the user edit it's email address | --- # Legacy feature flags URL: https://developers.front-commerce.com/docs/3.x/guides/extension-features/legacy-feature-flags Some modules in Front-Commerce use feature flags to enable or disable its features. Feature flags are resolved by the [FeatureFlagLoader](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/9498c0781170f7eb3a0b26a364f2e93681858943/packages/core/graphql-core-schema/core/loaders.js) which is typically overridden in the [`contextEnhancer`](../../api-reference/front-commerce-core/graphql#contextenhancer-function) of each module supporting a feature flag. ## Creating a Feature Flag Module To implement a feature flag in your module, you'll need to create and configure several files. In this guide, we will implement a feature flag that disable the wishlist feature, which is enabled by default by Front-Commerce's backend extensions (Magento2, Magento1 and Gezy). ### Create the GraphQL Module Definition First, create a module definition file that defines your feature's namespace and dependencies. You will need to add the dependencies of other modules that control the feature flag you want to disable: ```typescript title="my-extension/index.ts" import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "MyExtension/Wishlist", dependencies: ["Front-Commerce/Core", "Magento2/Wishlist"], // or "Magento1/Wishlist" or "Gezy/Wishlist" loadRuntime: () => import("./runtime"), }); ``` ### Implement the Runtime Configuration Next, create a runtime file that sets up the feature flag loader and any necessary services: ```typescript title="my-extension/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; import WishlistFeatureFlagLoader from "./featureFlagLoader"; export default createGraphQLRuntime({ contextEnhancer: ({ loaders }) => { return { FeatureFlag: WishlistFeatureFlagLoader(loaders.FeatureFlag), }; }, }); ``` ### Create the Feature Flag Loader Then, implement the feature flag loader that determines whether your feature is enabled: ```typescript title="my-extension/featureFlagLoader.ts" const WishlistFeatureFlagLoader = (loader) => { return { ...loader, isActive: (name) => { if (name === "wishlist") { // Control whether the wishlist feature is enabled return Promise.resolve(false); } else { // Check other modules return loader.isActive(name); } }, }; }; export default WishlistFeatureFlagLoader; ``` The `if (name === "wishlist")` above is needed as the feature flag query is queried by module name. The `return loader.isActive(name);` is needed to propagate the query to other modules. :::tip If a feature is not explicitly configured in the feature flags, it will be enabled by default. ::: --- # Flash messages URL: https://developers.front-commerce.com/docs/3.x/guides/flash-messages

{frontMatter.description}

## When to use flash messages? Flash messages are used to display informations to the user. These messages are meant to be displayed once and are stored in the session. When consumed, they are removed from the session. ## Flash message data type Flash messages are represented by an object the following properties: | Property | Type | Description | | --------- | ------ | -------------------------------------------------------------------------------------------------------------------------------- | | `type` | string | The type of the flash message. It can be one of the types declared in the [flash messages component](#flash-messages-component). | | `message` | string | The message to display. | | `data` | object | An object that can be used to pass additional data to the endering component. | ## Flash messages component The default theme provides a default implementation for flash messages depending on the `type` property of the flash message object. It can be customized by overriding the [`theme/modules/FlashMessages/getFlashMessageComponent.tsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/9cc7010e5d3c3ade454cdeaad90f28c769916695/packages/theme-chocolatine/theme/modules/FlashMessages/getFlashMessageComponent.jsx) file. ## Adding flash messages Flash messages can be added in your code from the user session object. Here's an example on how to do it in a route loader via the `FrontCommerceApp` instance: ```typescript import { FrontCommerceApp } from "@front-commerce/remix"; export const action = async ({ context }: ActionFunctionArgs) => { const app = new FrontCommerceApp(context.frontCommerce); app.user.session.addFlashMessage({ type: "success", message: "Payment succeeded", }) //... }); ``` ## Consume flash messages Flash messages can be consumed in your code from the user session too. They usually are returned like any other data in a route loader: ```tsx import { FrontCommerceApp } from "@front-commerce/remix"; import { json } from "@front-commerce/remix/node"; export const loader = async ({ context }: LoaderFunctionArgs) => { const app = new FrontCommerceApp(context.frontCommerce); const flashMessages = app.user.session.getFlashMessages(); return json({ flashMessages }); }); ``` ## Flash message component In the default theme, we provide a ready-to-use flash message component. It can be used to display flash messages. Here is an usage example: ```typescript import { FrontCommerceApp } from "@front-commerce/remix"; import { json } from "@front-commerce/remix/node"; export const loader = async ({ context }: LoaderFunctionArgs) => { const app = new FrontCommerceApp(context.frontCommerce); const flashMessages = app.user.session.getFlashMessages(); return json({ flashMessages }); }); export default PaymentPage() { const { flashMessages } = useLoaderData(); return (
); } ``` --- # Implement a Front-Commerce payment method URL: https://developers.front-commerce.com/docs/3.x/guides/implement-a-custom-front-commerce-payment-method ## Choose the payment workflow wisely Payment can be handled synchronously or asynchronously. If Front-Commerce allows both strategies to work, we highly recommend you to implement an asynchronous payment method (using {/* prettier-ignore */}IPN) whenever it is possible. This will prevent your payments from being rejected later within the provider process without your backend application knowing about it. In this guide, we'll implement an asynchronous Front-Commerce payment method. ## Implement the server logic Front-Commerce allows you to implement your own payment method. New embedded payment methods have to be registered from a GraphQL module. Let’s create a new "PWAy" payment module for a fictive payment provider. ### [Create a new GraphQL module](./extend-the-graphql-schema) ### Register the payment method in the module’s contextEnhancer ```ts title="my-extension/runtime.ts" export default createGraphQLRuntime({ contextEnhancer: ({ loaders }) => { // [...] initialization here const loader = new MyPaymentLoader(); const METHOD_CODE = "pway_awesomecheckout"; const METHOD_TITLE = "PWAy"; loaders.Payment.registerEmbeddedPaymentMethod( METHOD_CODE, METHOD_TITLE, // this method is called following the onAuthorize call in the AdditionalDataComponent (see below) // e.g. to trigger the payment authorization on the provider (validation and capture will be handled asynchronously by IPN) (paymentData, orderId, orderPaymentDetails) => { return loader.order(paymentData, orderId, orderPaymentDetails); }, null, // The notification processor will handle IPN notifications from the payment provider new MyPaymentNotificationProcessor(hipayConfig) ); return {}; // you may export loaders here in case the payment provides custom Queries (to fetch a payment token for instance) }, }); ``` ### Implement your loader's `order` method to process a payment ```ts title="my-extension/loaders/MyPaymentLoader.ts" export default class MyPaymentLoader { constructor(/* ... */) { /* ... */ } async order(paymentData, orderId, orderPaymentDetails) { // process the paymentData and call the payment provider const paymentStatus = call(/* ... */); // return payment DomainEvents representing what happened in the remote payment system. // These events are broadcasted by Front-Commerce to the registered external event listeners // that can use this information to update other remote systems (e.g: adding an Order comment in Magento) switch (paymentStatus.status) { case SUCCESS: // as the IPN handler will handle final authorisation it is often only an authorisation at this step return new PaymentAuthorized( new PurchaseIdentifier({ orderId }), paymentStatus.paymentReference ); case REFUSED: return new PaymentRefused( new PurchaseIdentifier({ orderId }), paymentStatus.reason ); } } } ``` ### Implement the notification processor (for IPN handling) See [the existing `DomainEvent` classes to update the order](../api-reference/front-commerce-core/payment-domain-events). See [the existing `EarlyNotificationAck` classes below](#early-notification-returns) ```ts title="my-extension/loaders/MyPaymentLoader.ts" import { AcknowledgeNotification, RefuseNotification, NotificationProcessor, type PaymentCommandDispatcher } from "@front-commerce/core/graphql/payment"; export default class MyPaymentNotificationProcessor extends NotificationProcessor { constructor(...) { ... } async getEarlyNotificationAck(notificationData: {notificationPayload: any}) { const data = notificationData.notificationPayload; // ensure the notification payload is valid (IPN often uses checksums and hashes with a key to allow you to ensure the notifications can be trusted) if (valid) { return new AcknowledgeNotification("Accepted"); } else { return new RefuseNotification("Invalid notification"); } } async process(notificationData: {status: string}, paymentCommandDispatcher: PaymentCommandDispatcher) { // process the notification properly here depending on the status switch(notificationData.status) { case "IN_PROGRESS": // create the order in the backend application using the paymentCommandDispatcher const paymentDetails = new PaymentDetails( cartId, // this is needed to know which cart is to be placed as an order orderTotalAmount, orderCurrency ); const orderId = await paymentCommandDispatcher.execute( new PlaceOrderCommand( paymentDetails, // payment details will be used to ensure the payment corresponds to the cart value and avoid "cart jacking" (insertion of an item in the cart while the payment is processing) { code: METHOD_CODE, additionalData: [ // additionnal data to be stored in the order { key: "transactionId", value: transactionId }, ], }, guestCartId // for unlogged customer, the guestCartId is needed as it can differ from a logged in user's cartId ) ); // return an array of DomainEvent that will be stored into the order in the backend, the last event will define the order's status return [ new PaymentCaptureStarted(new PurchaseIdentifier({ orderId }), { transactionId, }), ]; case "PAID": return [ new PaymentCaptured( new PurchaseIdentifier({ cartId }) ), ]; case "ERROR": // process error cases and return the relevant DomainEvent return [...]; } } } ``` ### Early Notification returns Early notifications can be used by notification processors to acknowledge or reject a notification for security reasons (invalid authenticity proof). - [See the acknowledgment implementations here](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/aa129d1d19173d90fc85bd97a73227c846171830/packages/core/graphql-core-schema/payment/domain/notificationAckValues.js) #### AcknowledgeNotification When a notification is acknowledged the payment provider is answered with a status `200 - OK` #### RefuseNotification When a notification is refused the payment provider is answered with a status `403 - Forbidden` ## Handle a sublist of payment methods You may need to dynamically set the list of payment methods displayed for your module. This is achieved by registering a replacement handler ```ts title="my-extension/runtime.ts" export default createGraphQLRuntime({ contextEnhancer: ({ loaders }) => { /* ... */ loaders.Payment.registerEmbeddedPaymentMethod( /* ... */ ); loaders.Payment.registerMultiplePaymentMethods({ isMethodToReplace: (method) => method.code === METHOD_CODE, getReplacementMethods: async () => { try { // custom call to fetch the allowed payment methods list const paymentMethods = call(/* ... */); return paymentMethods.map((method) => ({ // this is the method code provided to the checkout workflow // note that all payment methods must have a common prefix code: `${prefix}_${method.id}`, // the displayed payment method name on the checkout page title: method.description, // this method is called following the onAuthorize call in the AdditionalDataComponent (see bellow) instead of the default method defined with registerEmbeddedPaymentMethod callback: (paymentData, orderId, orderPaymentDetails) => return loader.order(paymentData, orderId, orderPaymentDetails); })), } catch (error) { // handle the error here return []; } }, }); /* ... */ }, }); ``` ## Display the new payment method on the UI with additional information While payment methods provided by Front-Commerce don't need any additional information, you may require some, for example, if you want to let users enter a comment related to the payment method/checkout. :::note See [the payment workflows specificities](#payment-workflows-specificities) for changes to the current example for each workflow. ::: #### Display a custom input for a payment method You should register a new component in `getAdditionalDataComponent.js` by following this template: ```js title="my-extension/theme/modules/Checkout/Payment/AdditionalPaymentInformation/getAdditionalDataComponent.js" import MyCustomComponent from "theme/modules/CustomComponent/CustomComponent"; const ComponentMap = { "": MyCustomComponent, }; // ... the rest of the code should be kept intact ``` The Custom component referenced here should be created within your `my-extension` extension, and display the additional information of your payment method to the user. ```tsx title="my-extension/theme/modules/CustomComponent/CustomComponent.tsx" import SubmitPayment from "theme/modules/Checkout/Payment/SubmitPayment"; const CustomComponent = ({ onAuthorize, value, gscAccepted, error, method, }: { onAuthorize: (additionalData: Record) => void; value: Record; gscAccepted: boolean; error?: string | null; method: { code: string; title: string; }; }) => { const [comments, setComments] = useState(value?.comments ?? ""); return (
setComments(event.target.value)} value={comments} type="text" /> { onAuthorize(gscAccepted ? { comments } : null); }} error={error} />
); }; export default CustomComponent; ``` There are a few things to understand here: - The `SubmitPayment` component is a common component across all payment methods to ensure consistency in how methods are managed. - `method` the current selected method. - `value` contains initial additional data (usually it is always null). - `onAuthorize` should be called with the additional data on ``'s on submit method. - `gscAccepted` whether or not the user have accepted the general sales conditions (checkbox above the "Place my order" button). Just forward this to the `` component. - `error` an error object if there is an error. Just forward this to the `` component. In this example we're only setting a `comments` as additional information, but you could as well implement a more complex form, by fetching a list for the user to pick from GraphQL and displaying them here for example.
#### Updating an existing method Additional Data Sometimes you may need to add an extra field to the additional data of an existing payment method. You first need to check if this payment method already has an `AdditionalDataComponent` registered. - [x] If `AdditionalDataComponent` has been registered, you should then override the `AdditionalDataComponent` and add your custom modifications. - [ ] If no `AdditionalDataComponent` is registered, you can then follow the [Display a custom input for a payment method](#display-a-custom-input-for-a-payment-method) docs, using the existing method's `method_code`. #### Adding a field to all payment methods In case you require adding a field to all existing payment methods, you should enhance the `AdditionalDataComponent` by following this template: ```js title="my-extension/theme/modules/Checkout/Payment/AdditionalPaymentInformation/getAdditionalDataComponent.js" import CustomEnhancer from 'theme/modules/CustomModule/CustomEnhancer' const ComponentMap = { ..., // registered additional data }; const getAdditionalDataComponent = (method) => { const Component = ComponentMap[method.code]; return CustomEnhancer(Component); }; export default getAdditionalDataComponent; ``` The CustomEnhancer referenced here should be created in your own extension. It should display the base component sent to it and add additional data fields and override the `onAuthorize` method. ```tsx title="my-extension/theme/modules/CustomModule/CustomEnhancer.tsx" import SubmitPayment from "theme/modules/Checkout/Payment/SubmitPayment"; const CustomEnhancer = (BaseComponent) => ({ onAuthorize, value, gscAccepted, error, method, }: { onAuthorize: (additionalData: Record) => void; value: Record; gscAccepted: boolean; error?: string | null; method: { code: string; title: string; }; }) => { const [comments, setComments] = useState(value?.comments ?? ""); return (
setComments(event.target.value)} value={comments} type="text" /> {BaseComponent ? ( { onAuthorize({ ...additionalData, comments }); }} value={value} gscAccepted={gscAccepted} error={error} method={method} /> ) : ( { onAuthorize(gscAccepted ? { comments } : null); }} error={this.props.error} /> )}
); }; export default CustomEnhancer; ```
## Payment workflows specificities Front-Commerce provides different hooks allowing you to use the payment method of your choice. This section explains the implementation specificities of each payment workflows. :::note The documentation here only explains changes to apply to the default implementation example for each workflow. - See [the basic implementation tutorial](#implement-the-server-logic) for a detailed implementation - See [the high-level workflow explanation](../concepts/understanding-payment-workflows) to choose the right implementation for your usecase. ::: ### Async Order This workflow is recommended for payments that are fully front-end (e.g. Payzen/Lyra, Paypal) #### Server changes Ensure the direct payment processor call does nothing in the extension's [`contextEnhancer`](../api-reference/front-commerce-core/graphql#contextenhancer-function): ```diff title="my-extension/runtime.ts" export default createGraphQLRuntime({ contextEnhancer: ({ loaders }) => { // ... loaders.Payment.registerEmbeddedPaymentMethod( METHOD_CODE, METHOD_TITLE, - (paymentData, orderId, orderPaymentDetails) => { - return loader.order(paymentData, orderId, orderPaymentDetails); - }, + () => { + throw new Error( + "The payment method should only be handled by IPN notifications" + ); + }, null, new MyPaymentNotificationProcessor(hipayConfig) ); return {}; // you may export loaders here in case the payment provides custom Queries (to fetch a payment token for instance) }, }); ``` :::note This implies the `order` method in the module’s loader is not needed for this method (your may need to implement other methods instead to be used by the `NotificationProcessor`) ```diff title="my-extension/loaders/MyPaymentLoader.ts" export default class MyPaymentLoader { - async order(paymentData, orderId, orderPaymentDetails) { - // ... - } } ``` ::: #### Theme changes Override `theme/pages/Checkout/checkoutFlowOf.js` to indicate the use of the `asyncOrder` workflow for the `pway_awesomecheckout` method code ```diff title="app/theme/pages/Checkout/checkoutFlowOf.js" const checkoutFlowOf = (method) => { ... + if (method === "pway_awesomecheckout") return "asyncOrder"; return "directOrder"; }; export default checkoutFlowOf; ``` ### Direct Order A direct order handles directly the payment on `onAuthorize` call in the `CustomComponent` you created (`my-extension/theme/modules/CustomComponent/CustomComponent.js`). This `onAuthorize` call will provide the information to `loader.order()` implemented server-side. This last method is responsible to handle the full payment process. ### Sub workflow : Direct Order with additional action You may need to handle a second payment step with a Direct order workflow (3DS validation, etc). In this case, perform the following steps: #### Reference the payment workflow Override `theme/pages/Checkout/checkoutFlowOf.js` to indicate the use of `directOrderWithAdditionalAction` ```diff title="app/theme/pages/Checkout/checkoutFlowOf.js" const checkoutFlowOf = (method) => { ... + if (method === "pway_awesomecheckout") return "directOrderWithAdditionalAction"; return "directOrder"; }; export default checkoutFlowOf; ``` #### Create an additional action component to handle the addtional payment step ```tsx title="my-extension/theme/AdditionalAction/MyAdditionalAction.tsx" import { useIntl } from "react-intl"; // Other imports... const MyAdditionalAction = ({ onOrderPlacedArgs, originalOnOrderPlaced }) => { const intl = useIntl(); const orderId = onOrderPlacedArgs[0]; // call originalOnOrderPlaced() when the additionnal action is successfull return (/* ... */); }; export default MyAdditionalAction; ``` #### Reference the additional action component ```js title="my-extension/theme/modules/Checkout/PlaceOrder/getAdditionalActionComponent.js" import None from "theme/modules/Checkout/PlaceOrder/AdditionalAction/None"; import MyAdditionalAction from "theme/AdditionalAction/MyAdditionalAction"; const ComponentMap = {}; const getAdditionalActionComponent = (paymentCode, paymentAdditionalData) => { if (paymentCode === "pway_awesomecheckout")) { return MyAdditionalAction; } return ComponentMap?.[paymentCode] ?? None; }; export default getAdditionalActionComponent; ``` ### Redirect Before Order :::warning This workflow is only supported for Magento2, please [contact us](mailto:contact@front-commerce.com) if you need this workflow with another backend server ::: A redirect before order workflow allows to interact with payment providers handling the complete payment workflow on their side. Front-Commerce will only redirect to the payment provider and ensure the order creation afterwards. #### Server changes ##### Register the payment method payment processor with `registerRedirectBeforeOrderPaymentMethod` module’s [`contextEnhancer`](../api-reference/front-commerce-core/graphql#contextenhancer-function) Replace the extension's contextEnhancer with the following ```ts title="my-extension/runtime.ts" export default createGraphQLRuntime({ contextEnhancer: ({ loaders, config }) => { const shopConfig = config.shop; const METHOD_CODE = "pway_awesomecheckout"; const METHOD_TITLE = "PWAy"; // [...] initialization here const loader = new MyPaymentLoader(shopConfig, METHOD_CODE, ...); loaders.Payment.registerRedirectBeforeOrderPaymentMethod( METHOD_CODE, METHOD_TITLE, loader, ); return {}; // you may export loaders here in case the payment provides custom Queries (to fetch a payment token for instance) }, }); ``` ##### Implement the three **required** methods in the loader ```ts title="my-extension/loaders/MyPaymentLoader.ts" type PaymentCaptured = { transactionId: string; // the transactionId from the payment provider, it allows the merchant to make the link between an order and a payment success: boolean; // the capture succeed cancel: boolean; // the capture was canceled // NOTE: if both success and cancel are false, the capture is considered failed (throwing an error will lead to the same event) } export default class MyPaymentLoader { constructor(shopConfig, methodCode, ...) { ... this.callbackUrl = `${shopConfig.url}/checkout/payment/${methodCode}`; } /** * @param {{paymentId: any, cart: any}} paymentData the paymentId is the paymentMethod registered previously * @returns {PaymentCaptured} the captured payment status */ mapCartPaymentToRedirect({ paymentId, cart }) { return { url: true, // set it to true for URL redirection html: true, // set it to true for HTML code insertion value: `...?callbackUrl=${this.callbackUrl}`, // the URL or HTML code to handle the redirection to the payment provider } } /** * @param {any} data : callbackUrl query parameters * @returns {boolean} is the payment authorized or not, returning false will lead to the checkout cancellation */ async isReturnSuccess(data) { return true | false; } /** * proceed to the payment capture in the payment provider * NOTE: this stage follows the order creation in Magento, you should provide the orderId to the payment provider for the transaction to be easily found by the merchant * @param {any} additionalData : callbackUrl query's additionalData parameter -> "https://callbackUrl?additionalData=..." * @param {string} orderId : magento order Id * @param {{ currency:string, totalInclTax: number }} paymentDetails : the currency and value of the order (please ensure the payment has the same value and currency to prevent cart hijack) * @returns {PaymentCaptured} the captured payment status */ async capturePayment(additionalData, orderId, paymentDetails) { return { transactionId, success: true, cancel: true, }; } } ``` #### Theme changes :::warning No `AdditionalDataComponent` is required for this implementation as there is no frontend component displayed by Front-Commerce ::: Using this workflow, you will need to override `theme/pages/Checkout/checkoutFlowOf.js` to indicate the use of `redirectBeforeOrder`: ```diff title="app/theme/pages/Checkout/checkoutFlowOf.js" const checkoutFlowOf = (method) => { ... + if (method === "pway_awesomecheckout") return "redirectBeforeOrder"; return "directOrder"; }; export default checkoutFlowOf; ``` :::note We encourage you to have a look at the payment extensions' source code from Front-Commerce to learn about advanced patterns: - [Adyen](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/main/packages/adyen) - [Hipay](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/main/packages/hipay) - [PayPal](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/main/packages/paypal) - [Payzen](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/main/packages/payzen) - [Stripe](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/main/packages/stripe) ::: --- # Improve your Core Web Vitals URL: https://developers.front-commerce.com/docs/3.x/guides/improve-your-core-web-vitals

{frontMatter.description}

> We will keep improving this page over time. ## Resources - https://webperf.tools/ - [Web Vitals (official documentation page)](https://web.dev/vitals/) - [Everything we know about Core Web Vitals and SEO](https://simonhearne.com/2021/core-web-vitals-seo) - Understanding the [Chrome User Experience Report (CrUX)](https://developers.google.com/web/tools/chrome-user-experience-report/) - An analysis of CrUX data: [What do Lighthouse Scores look like across the web?](https://www.tunetheweb.com/blog/what-do-lighthouse-scores-look-like-across-the-web/) - [Three Techniques for Performant Custom Font Usage](https://css-tricks.com/three-techniques-performant-custom-font-usage/) - [Detect the element associated with LCP using the DevTools](https://web.dev/optimize-lcp/#developer-tools) - Understand assets impact on different devices, to help you set budgets: [The Mobile Performance Inequality Gap, 2021](https://infrequently.org/2021/03/the-performance-inequality-gap/) ## Front-Commerce tips {/* TODO FC-XXXX drastically improve the core web vitals documentation */} {/* see EPIC https://docs.google.com/document/d/1h6Uf1H7DLYm10NYZUs300sENZnfjobMcpPoO-NwI1Es/edit?tab=t.0 */} ### Mark above-the-fold images as priority Using: `` ### Improve your fonts performance - [Self-host your custom (Google) fonts](https://www.zdnet.com/article/chromes-new-cache-partitioning-system-impacts-google-fonts-performance/) - Use WOFF2 format - Use `font-display: optional` - Preload key fonts by naming them with the suffix: `*.priority.woff2` ### One query per route You must aim at One GraphQL query per Front-Commerce route. Keep additional queries for secondary content. Ensure that this query is fetched in the `loader` route function from `@remix-run/node` (exemple [here](https://developers.front-commerce.com/docs/3.x/get-started/loading-data-from-unified-graphql-schema#loading-data)). #### `Promise.all()` When multiple API requests are required, using `Promise.all()` to fetch data in parallel rather than sequentially can significantly improve your website's loading performance. Fetching requests one after another increases total wait time, as each request depends on the previous one to complete. In contrast, `Promise.all()` executes them concurrently, reducing overall latency and making your application feel much faster and more responsive. So, make sure to leverage it in your `loader` route functions or directly within your resolvers to optimize efficiency and enhance user experience. --- # In-Stock Alert URL: https://developers.front-commerce.com/docs/3.x/guides/in-stock-alert import { Button } from "react-infima"; import Link from "@docusaurus/Link";

{frontMatter.description}

Enabling this feature will add a button on a product's detail page for every product that is out of stock. :::info This feature is available for Magento 1 (OpenMage LTS) and Magento 2 platforms. :::
![Example with the default theme's component](./assets/in-stock-alert-sample.png)
## Enable In-Stock Alerts in your project To use in-stock alerts in your project, you must first make sure the feature is enabled in your backend configuration. ### Magento 1 (OpenMage LTS) Navigate to `System > Configuration > Catalog > Catalog`. Within the `Product Alerts` menu, make sure `Allow Alert When Product Comes Back in-Stock` is set to `Yes`. ### Magento 2 Navigate to `Stores > Configuration > Catalog > Catalog`. Within the `Product Alerts` menu, make sure `Allow Alert When Product Comes Back in Stock` is set to `Yes`. ## Customize In-Stock Alert texts By default, having the configuration enabled for in-stock alerts in your backend will automatically add the default component (`SubscribeToInStockAlert`) on each out-of-stock product page. The placeholders and messages displayed by the `SubscribeToInStockAlert` component have translation keys prefixed with `modules.SubscribeToInStockAlert`. You can customize these texts by following our [translation guide](./translate-your-application). --- # Auto Refresh URL: https://developers.front-commerce.com/docs/3.x/guides/magic-button/auto-refresh import BrowserWindow from "@site/src/components/BrowserWindow"; import SinceVersion from "@site/src/components/SinceVersion"; ## How to activate Auto-refresh To activate the **Auto-refresh** feature, click on the Magic Button and select the Auto-refresh mode.
When enabled, the page is continuously updated and then contributors see their changes as soon as possible. --- # Contribution Mode URL: https://developers.front-commerce.com/docs/3.x/guides/magic-button/contribution-mode import SinceVersion from "@site/src/components/SinceVersion"; import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem";

{frontMatter.description}

## API Reference The ContributionMode instance is exposed in the `user` context, see [app.user.contributionMode](/docs/3.x/api-reference/front-commerce-remix/front-commerce-app?tab=ContributionMode#appuser) for more information. ## How to activate Contribution Mode The **Magic Button** is only visible when the **Contribution Mode** is active. One can force the contribution mode by using [Front-Commerce's `contributionMode.force` configuration](/docs/3.x/api-reference/front-commerce-core/config#contributionmode). In a default Front-Commerce application, this is achieved using the `FRONT_COMMERCE_CONTRIBUTION_MODE_FORCE` environment variable in the [`app/config/contributionMode.ts` configuration file](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/update-contribution-mode-demos/skeleton/app/config/contributionMode.ts), but you can implement your own logic. :::warning This should only be used for development purposes. ::: The `ContributionMode` state should be updated using the `setContributionMode` function. ```ts user.contributionMode.setContributionMode({ enabled: true, previewMode: true, // optional and will only change if contribution mode is enabled xRayMode: true, // optional and will only change if contribution mode is enabled }); ``` Here are some examples of how to activate the **Contribution Mode**. ```mdx-code-block ``` An action can be used, for example to activate from a user interface. ```ts title="app/routes/enable-contribution-mode.ts" export const action = ({ context, request }: ActionFunctionArgs) => { const app = new FrontCommerceApp(context.frontCommerce); const contributionMode = app.user.contributionMode; if (!contributionMode) { throw new Response("Not Found", { status: 404, statusText: "Not Found", }); } const formData = await request.formData(); context.user.contributionMode.setContributionMode({ enabled: true, previewMode: formData.get("previewMode") === "on", xRayMode: formData.get("xRayMode") === "on", }); return json({ contributionMode: contributionMode.current(), }); }; ``` ```mdx-code-block ``` This is also possible via remix loaders, for example a custom preview route. ```ts title="app/routes/preview.ts" export const loader = ({ context, request }: LoaderFunctionArgs) => { const url = new URL(request.url); if (url.searchParams.get("token") !== "MySuperSecretToken") { throw new Response("Invalid token", { status: 401 }); } app.user.contributionMode.setContributionMode({ enabled: true, previewMode: true, }); return redirect("/"); }; ``` ```mdx-code-block ``` This is also possible via GraphQL, for example a custom mutation or from a GraphQL loader. ```js export default { namespace:"MyModule" dependencies:["Front-Commerce/Contribution-Mode"] resolvers:{ Mutation:{ enableContributionMode: async (root, args, context) => { context.user.contributionMode.setContributionMode({enabled: true}) return true } } } } ``` ```mdx-code-block ``` ## Check if Contribution Mode is active You can check if the **Contribution Mode** is active by using the `enabled` field in the `ContributionMode` instance. ```ts if (user.contributionMode.enabled) { // Contribution Mode is active } ``` alternatively you can get the full state of contribution mode using the `current` method ```ts const contributionMode = user.contributionMode.current(); // contributionMode.enabled - true if contribution mode is active // contributionMode.previewMode - true if contribution mode is active and preview mode is enabled // contributionMode.xRayMode - true if contribution mode is active and xRay mode is enabled ``` --- # Preview Mode URL: https://developers.front-commerce.com/docs/3.x/guides/magic-button/preview-mode import BrowserWindow from "@site/src/components/BrowserWindow"; import SinceVersion from "@site/src/components/SinceVersion"; **Preview Mode** is an integral feature of Magic Button, specifically designed to enable content creators to preview their drafts from systems supporting this feature before publishing changes. ## How to activate Preview Mode ### Using the Magic Button Activate the **Preview Mode** directly from the Editorial Toolbox. Click on the Magic Button and select the Preview Mode option.
### Programmatically Alternatively, **Preview Mode** can be activated programmatically by leveraging the [`ContributionMode`](/docs/3.x/api-reference/front-commerce-remix/front-commerce-app?tab=ContributionMode#appuser). instance exposed by the `user.contributionMode` context. ### `setPreviewMode` The `setPreviewMode` method is typically used for safe activation of Preview Mode. This means that if Contribution Mode is not enabled, Preview Mode will not be activated. ```ts title="app/routes/preview.ts" export const loader = ({ context, request }: LoaderFunctionArgs) => { const url = new URL(request.url); if (url.searchParams.get("token") !== "MySuperSecretToken") { throw new Response("Invalid token", { status: 401 }); } app.user.contributionMode.setContributionMode({ enabled: true, previewMode: true, }); return redirect("/"); }; ``` ### Check if Preview Mode is enabled ```ts title="app/routes/preview.ts" export const loader = ({ context, request }: LoaderFunctionArgs) => { const url = new URL(request.url); if (url.searchParams.get("token") !== "MySuperSecretToken") { // highlight-next-line return json({ enabled: app.user.contributionMode.previewMode }); } // [..] }; ``` --- # X-Ray URL: https://developers.front-commerce.com/docs/3.x/guides/magic-button/x-ray import BrowserWindow from "@site/src/components/BrowserWindow"; import SinceVersion from "@site/src/components/SinceVersion"; import ContactLink from "@site/src/components/ContactLink"; The **X-Ray** feature enhances your stores functionality by providing an edit button alongside each content. Content creators can directly access the source for editing, thereby improving content management efficiency. ## How to activate X-Ray To activate the **X-Ray** feature, click on the Magic Button and select the X-Ray option.
When enabled, content composing the page are outlined with a user interface allowing the contributor to know the source of it and providing a quick access to the corresponding administration interface. ## X-Ray on custom types By default, the X-Ray feature is available for the main GraphQL types provided by Front-Commerce (Product, Categories, …). It is also possible to implement X-Ray for custom types and/or for existing types that are extended with data coming from another source. ### Add the `@storefrontContent` directive in the schema referencing a metadata extractor First, when defining a custom type, you have to use the `@storefrontContent` directive to instruct the X-Ray feature that an object of that custom type comes from an external service and can be edited. The directive will dynamically add an internal resolver to track usage of any field of the type. Your `schema.gql` would look like: ```graphql title="src/my-module/schema.gql" type MyCustomType @storefrontContent(extractorIdentifier: "identifier") { ID id! String name! } ``` The `extractorIdentifier` identifies a _content metadata extractor_ that must be registered in the application. This is typically done in the `contextEnhancer` of [the GraphQL module](/docs/2.x/essentials/extend-the-graphql-schema#create-a-new-graphql-module): ```javascript title="./my-extension/extractors/custom-type-extractor.js" import { ContentMetadata, ContentMetadataExtractor, } from "@front-commerce/core/graphql/contribution-mode"; export default class MyCustomTypeExtractor extends ContentMetadataExtractor { getIdentifier() { return "identifier"; // the same value as in schema.gql } async extract(resolvedData, source, args, context) { return new ContentMetadata( source.id, "MyCustomType", // it can be any string identifying a source, we use `magento` or // `contentful` for instance. It is used to customize the color and the icon // of the X-Ray user interface. "aCustomSource", `https://a-remote-service.example.com/edit/${source.id}` ); } } ``` ### Register the extractor in the `ContentMetadataExtractorRegistry` To integrate your custom extractor into the system, you must register it with the `ContentMetadataExtractorRegistry` service. This process involves using [lifeCycle hooks](/docs/3.x/api-reference/front-commerce-core/defineExtension#unstable_lifecyclehooks) available in Front-Commerce [extension definition](/docs/3.x/guides/register-an-extension). ```javascript title="./my-extension/index.ts" // Import your custom extractor import MyCustomTypeExtractor from "./extractors/custom-type-extractor"; export default defineExtension({ name: "my-extension", // Additional extension configuration can go here // Setup lifecycle hooks unstable_lifecycleHooks: { // Hook for server initialization onServerServicesInit: async (services, request, config) => { // Example of configuring your extractor with dynamic parameters from configProviders const adminUrl = `${config.someConfig.endpoint}/${config.someConfig.adminPath}`; // Register your custom extractors // highlight-start services.ContentMetadataExtractorRegistry.register([ new MyCustomTypeExtractor(adminUrl), ]); // highlight-end }, }, }); ``` ### Add `` in your React components After that, you can enrich React components responsible for displaying a `MyCustomType` object by using ``, that way when X-Ray is enabled, the user interface can be enriched: ```jsx title="src/web/theme/modules/MyCustomType/MyCustomType.js" import React from "react"; import StorefrontContent from "theme/modules/StorefrontContent"; const MyCustomType = ({ aMyCustomType }) => { return (

{aMyCustomType.name}

); }; ``` By default, the X-Ray view will be scoped as **block**. You can make it global to the whole page by defining the `scope="page"` prop. ```js ``` The screenshot below illustrates a page scope for the category (1) and block scopes for product items (2): [![Example of X-Ray scopes on the same page](./assets/x-ray-blocks-types.png)](./assets/x-ray-blocks-types.png) ### Style a custom source If your content comes from a specific source, you can configure a dedicated color and icon for that source. For that, you can can override `app-sources`: ```js title="theme/modules/StorefrontContent/app-sources.js" const anSvgIcon = /* … */ const customStyle = { name: "aCustomSource", color: "rgb(147, 74, 97)", icon: anSvgIcon, } export default [customStyle]; ``` --- # Entering and exiting the maintenance mode URL: https://developers.front-commerce.com/docs/3.x/guides/maintenance-mode/entering-and-exiting-maintenance-mode # Maintenance Mode You may want to put one or more of your stores in maintenance mode while you do some maintenance work/deployment tasks on your store(s). Front-Commerce comes with an API that allows you to put/remove a store in maintenance mode. To enable the maintenance mode API you need to set the `FRONT_COMMERCE_MAINTENANCE_MODE_AUTHORIZATION_TOKEN` environment variable. ```shell title=".env" FRONT_COMMERCE_MAINTENANCE_MODE_AUTHORIZATION_TOKEN=a-secret-token ``` Once you have setup the `FRONT_COMMERCE_MAINTENANCE_MODE_AUTHORIZATION_TOKEN` environment variable the maintenance mode API will be available after restarting the server ## The Maintenance Mode API ### Activating the maintenance mode To activate the maintenance mode on a store which URL is `https://example.com/` use the following HTTP request: ```shell curl --location --request POST 'https://example.com/api/maintenance-mode' \ --header 'Content-Type: application/json' \ --data-raw '{ "token": "a-secret-token" }' ``` The `token` value must match the secret defined in your application's `FRONT_COMMERCE_MAINTENANCE_MODE_AUTHORIZATION_TOKEN` environment variable. It is also possible to set a duration (in milliseconds) after which the maintenance mode will be automatically reset: ```shell curl --location --request POST 'https://example.com/api/maintenance-mode' \ --header 'Content-Type: application/json' \ --data-raw '{ "token": "a-secret-token", "duration": 3600000 }' ``` Use this if you have a rough idea of how much time the maintenance task would take or to prevent a user or automated process to forget to deactivate the maintenance mode. ### Deactivating the maintenance mode To deactivate the maintenance mode use the following HTTP request: ```shell curl --location --request DELETE 'https://example.com/api/maintenance-mode' \ --header 'Content-Type: application/json' \ --data-raw '{ "token": "a-secret-token" }' ``` ### To check the maintenance mode To check the maintenance mode use the following HTTP request: ```shell curl --location --request GET 'https://example.com/api/maintenance-mode' ``` ## Bypassing Maintenance Mode ### Bypassing with Authorized IPs To bypass the maintenance mode for certain IP addresses, you can configure the `maintenance.authorizedIps` in your `front-commerce.config.ts` file. ```ts title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core"; export default defineConfig({ maintenance: { authorizedIps: ["127.0.0.1", "83.65.12.111", "2ce8:c427::7156:ad8e"], }, }); ``` :::caution Please configure **both v4 and v6** IP addresses as much as possible. To retrieve your IP address, you can use [What Is My IP](https://www.whatismyip.com/) or via the curl command: ```shell $ curl ifconfig.me -6 # 2ce8:c427::7156:ad8e $ curl ifconfig.me -4 # 83.65.12.111 ``` ::: ### Bypassing with Authorized Header If you want to bypass the maintenance mode using a specific HTTP header, you can configure the `maintenance.authorizedHeader` in your `front-commerce.config.ts` file. This allows the maintenance mode to be bypassed if the request contains the specified header with a non-empty value. ```ts title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core"; export default defineConfig({ maintenance: { authorizedHeader: "X-Maintenance-Mode-Bypass", }, }); ``` In your request, include the header as follows: ```shell curl --location --request GET 'https://example.com/acme-page' \ --header 'X-Maintenance-Mode-Bypass: true' ``` This will allow the request to bypass the maintenance mode if the header is present. :::caution You are responsible for ensuring that the header is persisted in the request. When navigating to a new page, the header might not be included automatically. Consider implementing a solution to consistently add the header to all requests if you need persistent maintenance mode bypass, such as: - Using an HTTP client that allows setting default headers - Configuring your reverse proxy to add the header - Setting up browser extensions that can inject headers ::: --- # Automatic detection with service Health Checks URL: https://developers.front-commerce.com/docs/3.x/guides/maintenance-mode/automatic-detection-with-service-health-checks # Health Checks Health checks allow you to monitor the availability of your services and automatically put your store in maintenance mode if a service is down. This ensures that your users are not affected by a service outage. With health checks, you can define a set of services that you want to monitor and provide a function to check the health of each service. Front-Commerce will periodically run these health check functions and if a service is down, it will automatically put the store in maintenance mode. Once the service is back up, the Front-Commerce will automatically remove the store from maintenance mode. Additionally, you can configure the scheduling of the health checks using a Cron pattern. This allows you to control how often the health checks are run and how quickly your store is put in maintenance mode if a service is down. :::info When Maintenance Mode has been activated manually via [the maintenance mode API](./entering-and-exiting-maintenance-mode#the-maintenance-mode-api), the health checks will not remove the store from maintenance mode even after a service has recovered. To remove the store from maintenance mode, you must [manually deactivate](./entering-and-exiting-maintenance-mode#deactivating-the-maintenance-mode) it. ::: :::tip demo For a more detailed example of how to use this feature, you can refer to the [Maintenance Mode Demo Extension](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/master/skeleton/example-extensions/maintenance-mode-demo). This extension defines a health check that checks a hypothetical "acme" service, and allows to mutate the status in a custom `/maintenance` route. ::: ## The Health Check API ### Adding a Health Check service To add a health check, you need to define a function that checks the health of your service and returns: - `true`: when the service is `up` - `false`: when the service is `down` - `thrown Error`: assumes the service is `down` This function is then passed to `addHealthCheckService` in the `onServerInit` hook in your extension definition. Here is an example: ```ts title="extensions/health-check.ts" import { defineExtension } from "@front-commerce/core"; export default defineExtension({ name: "health-check", meta: import.meta, unstable_lifecycleHooks: { onServerInit: async (services, request, config) => { // highlight-start services.MaintenanceMode.addHealthCheckService("acme", async () => { const response = await fetch("https://acme.com/api/health-check"); return response.status === 200; }); // highlight-end }, }, }); ``` ### Configuring the global Cron interval The health checks are run at a fixed interval determined by a [Cron pattern](https://en.wikipedia.org/wiki/Cron). By default, the interval is `*/10 * * * * *`, which means "every 10 seconds". To configure a different interval, you can provide a pattern in your `front-commerce.config.ts` file. For example, if you want to check the health of your services every 30 seconds, you can do it like this: ```ts title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core"; export default defineConfig({ maintenance: { healthChecks: { schedule: "*/30 * * * * *", }, }, }); ``` :::important The Cron interval applies to all the services being monitored. Each service defined in `services.MaintenanceMode.addHealthCheckService` will use the same interval. ::: ### Manually disable all Health Checks If you're experiencing issues with your health checks, for instance, if a change in the health check URLs requires a manual intervention, you might want to manually disable health checks to avoid your application automatically switching to maintenance mode. This can be done by calling the `/api/maintenance-mode/health` endpoint via a curl request using the same [`FRONT_COMMERCE_MAINTENANCE_MODE_AUTHORIZATION_TOKEN`](./entering-and-exiting-maintenance-mode) which you have configured to enable the maintenance mode feature: ```shell curl --location \ --request DELETE http://localhost:4000/api/maintenance-mode/health \ --header "Content-Type: application/json" \ --data-raw '{ "token": "a-secret-token" }' ``` ### Manual enable all Health Checks Once you have resolved any issues and you want to restart the health checks, you can run the following curl request to enable all health checks, using the same [`FRONT_COMMERCE_MAINTENANCE_MODE_AUTHORIZATION_TOKEN`](./entering-and-exiting-maintenance-mode) which you have configured to enable the maintenance mode feature: ```shell curl --location \ --request POST http://localhost:4000/api/maintenance-mode/health \ --header "Content-Type: application/json" \ --data-raw '{ "token": "a-secret-token" }' ``` --- # Use and customize the maintenance page URL: https://developers.front-commerce.com/docs/3.x/guides/maintenance-mode/use-and-customize-the-maintenance-page

{frontMatter.description}

## Entering and exiting the maintenance mode The maintenance mode is activated by setting the `FRONT_COMMERCE_MAINTENANCE_MODE_FORCE_ENABLED` environment variable to `true` in your Front-Commerce project. Once that is done, any request done in your application will show the maintenance page instead. :::tip The skeleton provides an [maintenance mode demo example extension](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/main/skeleton/example-extensions/maintenance-mode-demo?ref_type=heads) with three custom routes to show the interactions with the maintenance mode. ::: ## Testing and customizing the maintenance page The maintenance page can be changed by overriding the `theme/pages/Error/Maintenance.tsx` file. In order to facilitate the edition of this page, you can display it in development mode by [activating the maintenance mode](/docs/3.x/guides/maintenance-mode/entering-and-exiting-maintenance-mode). Alternatively you can navigate to the [`__front-commerce/maintenance`](http://localhost:4000/__front-commerce/maintenance) route in your application. ## Controlling Access During Maintenance To bypass the maintenance mode for certain IP addresses you can configure the `FRONT_COMMERCE_MAINTENANCE_MODE_AUTHORIZED_IPS` environment variable. Please note that you can have multiple IPs separated by a comma like: ```shell title=".env" FRONT_COMMERCE_MAINTENANCE_MODE_AUTHORIZED_IPS=127.0.0.1,83.65.12.111,2ce8:c427::7156:ad8e ``` :::caution Please configure **both v4 and v6** IP as much as possible. You can retrieve your own IP addresses with https://www.whatismyip.com/ ::: --- # Mutate Data Using Client Side Fetcher URL: https://developers.front-commerce.com/docs/3.x/guides/mutate-data-using-client-side-fetcher # Mutate Data Using Client Side Fetcher When building Front-Commerce applications with Remix, you have multiple options for handling data mutations. While the [`
`](https://remix.run/docs/en/main/components/form) component works well for traditional form submissions that navigate to a new page, [`useFetcher`](https://remix.run/docs/en/main/hooks/use-fetcher) is better suited for mutations that should happen without navigation or when you need more control over the mutation lifecycle. ## When to Use useFetcher You should consider using [`useFetcher`](https://remix.run/docs/en/main/hooks/use-fetcher) when: 1. You want to perform a mutation without navigating away from the current page 2. You need to handle multiple concurrent mutations (like deleting multiple items) 3. You want fine-grained control over the loading and error states 4. You need to programmatically trigger mutations (not just from form submissions) ## Basic Usage Here's a basic example of using [`useFetcher`](https://remix.run/docs/en/main/hooks/use-fetcher) for a mutation: ```tsx import { useFetcher } from "@front-commerce/remix/react"; function DeleteButton({ itemId }) { const fetcher = useFetcher(); const handleDelete = () => { fetcher.submit(null, { method: "DELETE", action: `/api/items/${itemId}`, }); }; return ( ); } ``` ## Using [`fetcher.Form`](https://remix.run/docs/en/main/hooks/use-fetcher#fetcherform) For form-based mutations, [`fetcher.Form`](https://remix.run/docs/en/main/hooks/use-fetcher#fetcherform) provides a convenient way to handle submissions without navigation. It works similarly to Remix's [``](https://remix.run/docs/en/main/components/form) component but doesn't trigger page transitions. This makes it ideal for inline edits, like toggling favorites or updating settings: ```tsx import { IconButton } from "theme/components/atoms/Button"; import { useFetcher } from "@front-commerce/remix/react"; type RemoveFromWishlistProps = { wishlistId: string | number; itemId: string | number; }; export default function RemoveFromWishlist({ wishlistId, itemId, }: RemoveFromWishlistProps) { const fetcher = useFetcher(); return ( ); } ``` ## Advanced Usage with Status Handling For more complex scenarios, you might want to handle different states and responses. Here's a more complete example inspired by the [`useCartItemRemoveForm`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/f3a080bc3740e4b03b33f154fc240037875b0ec9/packages/theme-chocolatine/theme/modules/Cart/CartItem/CartItemRemoveForm/useCartItemRemoveForm.ts) hook: ```tsx import { useFetcher } from "@front-commerce/remix/react"; import { useEffect, useMemo } from "react"; import { useRevalidator } from "@remix-run/react"; function useItemRemove() { const fetcher = useFetcher(); const revalidator = useRevalidator(); // Trigger the mutation const removeItem = (itemId) => { fetcher.submit(null, { method: "DELETE", action: `/api/items/${itemId}`, }); }; // Revalidate data after successful mutation useEffect(() => { if (fetcher.state === "idle" && fetcher.data) { revalidator.revalidate(); } }, [fetcher.state]); // Handle different response states const status = useMemo(() => { if (!fetcher?.data) { return { hasMessage: false, message: "", success: false, }; } if ("error" in fetcher.data) { return { hasMessage: true, message: fetcher.data.error, success: false, }; } return { hasMessage: Boolean(fetcher.data.errorMessage), message: fetcher.data.errorMessage, success: fetcher.data.success, }; }, [fetcher.data]); return { status, submissionState: fetcher.state, removeItem, }; } ``` ## Fetcher States The [`fetcher.state`](https://remix.run/docs/en/main/hooks/use-fetcher#fetcherstate) can be one of three values: - `idle` \- No submission in progress - `submitting` \- Submission in progress - `loading` \- Submission completed, but page data is being reloaded ## Form vs. Fetcher Comparison Here's when to use each approach: ### Use [``](https://remix.run/docs/en/main/components/form) - When the mutation should result in a navigation - For traditional form submissions with multiple fields - When you want the browser's native form behavior - When you don't need fine-grained control over the mutation state ```tsx import { Form } from "@front-commerce/remix/react"; function CheckoutForm() { return ( {/* form fields */} ); } ``` :::info For a detailed guide on handling mutations with the [`
`](https://remix.run/docs/en/main/components/form) component, please refer to our [Mutate Data Using Forms](./mutate-data-using-forms.mdx) guide. ::: ### Use [`useFetcher`](https://remix.run/docs/en/main/hooks/use-fetcher) - For mutations that shouldn't trigger navigation - When handling multiple concurrent mutations - When you need programmatic control over the submission - For optimistic UI updates - When you need detailed control over loading and error states ```tsx function CartItemRemove({ itemId }) { const { removeItem, status, submissionState } = useItemRemove(); return (
{status.hasMessage && (

{status.message}

)}
); } ``` ## Best Practices 1. **Type Safety** : Use TypeScript to ensure type safety with your mutations: ```tsx const fetcher = useFetcher(); ``` 2. **Revalidation** : Use [`useRevalidator`](https://remix.run/docs/en/main/hooks/use-revalidator) to refresh page data after successful mutations: ```tsx useEffect(() => { if (fetcher.state === "idle" && fetcher.data) { revalidator.revalidate(); } }, [fetcher.state]); ``` 3. **Error Handling** : Always handle both success and error states: ```tsx if ("error" in fetcher.data) { // Handle error case } ``` 4. **Loading States** : Provide feedback during mutations: ```tsx ``` By following these patterns, you can create robust and user-friendly mutation handling in your Front-Commerce applications. ## To learn more: - [Mutate Data Using Forms](./mutate-data-using-forms.mdx) - [Form vs. Fetcher](https://remix.run/docs/en/main/discussion/form-vs-fetcher) - [Fullstack Data Flow](https://remix.run/docs/en/main/discussion/data-flow) - [useFetcher](https://remix.run/docs/en/main/hooks/use-fetcher) - [useRevalidator](https://remix.run/docs/en/main/hooks/use-revalidator) --- # Mutate Data Using Forms URL: https://developers.front-commerce.com/docs/3.x/guides/mutate-data-using-forms # Mutate Data Using Forms In Front-Commerce, you can handle data mutations using Remix's [``](https://remix.run/docs/en/main/components/form) component combined with Front-Commerce's GraphQL mutations. This guide explains how to implement form submissions and handle data mutations effectively. ## Using Remix Forms Remix uses [``](https://remix.run/docs/en/main/components/form) component as the primary way to mutate data. When a form is submitted, Remix will: 1. Call the route's [`action`](https://remix.run/docs/en/main/route/action) function 2. Process the form data 3. Execute any necessary mutations 4. Return a response (redirect or data) ### Basic Form Structure Here's a basic example of a password reset form using our theme components: ```tsx title="app/theme/components/PasswordResetForm.tsx" import { Form } from "@remix-run/react"; import Stack from "theme/components/atoms/Layout/Stack"; import Fieldset from "theme/components/atoms/Forms/Fieldset"; import FormItem from "theme/components/molecules/Form/Item"; import { Password } from "theme/components/atoms/Forms/Input"; import FormActions from "theme/components/molecules/Form/FormActions"; import { SubmitButton } from "theme/components/atoms/Button"; export default function PasswordResetForm() { return (
Change password
); } ``` ## Handling Form Submissions When the form is submitted, Remix calls the route's [`action`](https://remix.run/docs/en/main/route/action) function. Here's how to handle the form submission and perform mutations: ```tsx title="app/routes/reset-password.tsx" import { redirect, type ActionFunctionArgs } from "@remix-run/node"; import { json } from "@front-commerce/remix/node"; import { FrontCommerceApp } from "@front-commerce/remix"; import { YourMutationDocument } from "~/graphql/graphql"; import PasswordResetForm from "../theme/components/PasswordResetForm"; export async function action({ request, context }: ActionFunctionArgs) { const app = new FrontCommerceApp(context.frontCommerce); const formData = await request.formData(); const userInput = Object.fromEntries(formData.entries()); if (userInput.password !== userInput.password_confirm) { return json({ errorMessage: "Passwords do not match" }, { status: 400 }); } try { // Execute GraphQL mutation const result = await app.graphql.mutate(YourMutationDocument, { newPassword: userInput.password, }); if (result.resetPassword?.success) { return redirect("/success-page"); } return json({ success: true, message: "Password changed successfully" }); } catch (e) { return json({ errorMessage: "An error occurred" }, { status: 500 }); } } export default function AccountPasswordResetPage() { return ; } ``` :::info In the `PasswordResetForm` component above, we used the `action` property to specify which route will handle the form submission. However, when using Remix, if your form is defined in the same route that handles its submission (in this case, `reset-password.tsx`), you can omit the `action` property. Remix will automatically submit the form to the current route's `action` function. For more details, see the [Form API documentation](https://remix.run/docs/en/main/components/form#action). ::: ## Key Points 1. Use Remix's [`
`](https://remix.run/docs/en/main/components/form) component 2. Access form data using `request.formData()` in the route's [`action`](https://remix.run/docs/en/main/route/action) function 3. Use [`app.graphql.mutate()`](../04-api-reference/front-commerce-remix/front-commerce-app.mdx) to execute GraphQL mutations 4. Return appropriate responses (redirect, json data or throw an error) ## See also - [Chocolatine Theme Form Components](../03-extensions/theme-chocolatine/reference/forms.mdx) - [Form Validation](https://remix.run/docs/en/main/guides/form-validation) - [Form Resubmissions](https://remix.run/docs/en/main/discussion/resubmissions) - [Form vs. fetcher](https://remix.run/docs/en/main/discussion/form-vs-fetcher) --- # Optimize your image assets URL: https://developers.front-commerce.com/docs/3.x/guides/optimize-your-image-assets

{frontMatter.description}

import ContactLink from "@site/src/components/ContactLink"; One of the tough things to do when doing responsive websites is to have images that match your user's screen. Especially when content is contributed by a wide range of people who aren't fully aware of the impact their actions can have on the user's experience. To solve this issue, Front-Commerce has what we call a media middleware. It is a proxy that will fetch your media from your upload server (Magento, Wordpress, etc.) resize it to the requested size, cache it, and send it back to the user request. This is somewhat similar to [Cloudinary](https://cloudinary.com/documentation/responsive_images)’s service. This method has two advantages: 1. you no longer need to expose your backend since it will be the Front-Commerce server that will fetch the image on your backend server 2. you get better performance with correctly cached and sized images ## How to configure it? :::note This section explains how to use default proxy. To create your own one, refer to the [Add your own media proxy endpoint](#add-your-own-media-proxy-endpoint) section below. ::: You need to configure the different formats that your server is willing to accept. ```js title="app/config/images.js" const config = { defaultBgColor: "FFFFFF", presets: { swatch: { width: 26, height: 26, bgColors: [] }, thumbnail: { width: 50, height: 50, bgColors: [] }, small: { width: 136, height: 168, bgColors: [] }, galleryPreview: { width: 136, height: 136, bgColors: [] }, medium: { width: 474, height: 474, bgColors: [] }, large: { width: 1100, height: 1100, bgColors: [] }, newsletter: { width: 1280, height: 242, bgColors: [] }, carousel: { width: 1280, height: 600, bgColors: [] }, pushBlock: { width: 284, height: 354, bgColors: [] }, pushBlockWide: { width: 568, height: 354, bgColors: [] }, cartPreview: { width: 50, height: 50, bgColors: [] }, wishlistPreview: { width: 50, height: 50, bgColors: [] }, zoomable: { width: 1100, height: 1100, bgColors: [], sizes: [2] }, }, /** @type {"original"} */ originalPresetCode: "original", }; export const { defaultBgColor, presets, originalPresetCode } = config; export default config; ``` ## How to query an image? Once you have configured your media middleware, you will be able to actually request a proxied image. To do so, you need to build your URL as follow: ```wiki http://localhost:4000/media/?format=&bg=&cover=&dpi=x2 ``` With actual values, it would look like this: ```wiki http://localhost:4000/media/path/to/my/image.jpg?format=small&bg=e7e7e7&cover=true ``` - `format`: must be one of the keys available in your `presets` configuration or `original` to request the image without any transformations - `bg` (optional): must have one of the values in the array `bgColors` of your preset. If you don't set it, it will be the `defaultBgColor` - `cover` (optional): crops the image so that both dimensions are covered, making parts of the image hidden if necessary. If this option is not set, the image will fit in the dimensions without hiding any of its content. The space left in your image will be filled with the `bgColor` attribute. ### `` component However, this can be troublesome to setup manually. This is why in Front-Commerce you should rather use the `` React component. ```jsx import Image from "theme/components/atoms/Image"; a suited description of the image; ``` Here as well `bg` and `cover` are optional. :::info Important The src of the image here is the path of the image on the proxy. ::: To learn more about this component and what you can achieve with it, **we highly recommend you** to read the [`Image` component reference](/docs/2.x/reference/image-component). ## Image Sizes :::info Prerequisites Be sure you have a good understanding of how the browser selects which image to load. It is a process that depends on the rendered image width and the image widths available in its `srcset` attribute (the [srcset + sizes = AWESOME!](https://ericportis.com/posts/2014/srcset-sizes/#part-2) article is a good explanation). ::: We have added sensible defaults to image sizes on key components in the principal pages. Figuring the correct `sizes` to set can be a daunting task. You need to know the different sizes available for your image. You must also take into account the size it will be rendered as, in relation to media breakpoints on the page and parent containers' `max-width`, `padding` and `margin` properties. ### A method to determine image sizes To simplify this process we devised a smart method to determine these numbers in a very straightforward way. 1. First open the page you want to setup the `sizes` property of. 2. Open the developers tools and paste the below snippet in the `console` tab. 3. Before you hit the enter key edit the condition(s) indicated in the snippet to match the image you want. 4. Hit the enter key. 5. Now you have 4 new global functions you can use `getImage`, `resetIsMyImage`, `getSizesStats`, `clearSizesStats`. - Use the `getImage` function to test the condition you set if it returns the correct image. - In case `getImage` was returning the wrong image. Use `resetIsMyImage` to change the condition. - `getSizesStats` returns the sizes stats collected so far. - `clearSizesStats` clears the stats collected so far. 6. To start collecting stats of how your image width changes with viewport width start resizing your browser window from small (say 300px) to extra large. Be gentle with the resizing so as to capture more data points. Be extra gentle around break points to capture the breakpoint perfectly. 7. Run `getSizesStats()` in your `console` tab. It will print a long string. 8. Copy the string in 7. (everything in between the double quotes). 9. Paste the string you copied in 8. In to a spreadsheet. 10. Now you can plot how the image width changes with viewport width. 11. Using the above information and the different sizes available for your image, you can build a `sizes` value that matches your scenario. Check [example below](#image-sizes-example) for a hands-on exercise. ```js let { getSizesStats, getImage, resetIsMyImage, clearSizesStats } = (( isMyImage = (img) => { return ( // IMPORTANT UPDATE THE CONDITION BELOW TO MATCH THE IMAGE YOU WANT TO TRACK (img.alt || "").trim().toLowerCase() === "your-image-alt".trim().toLowerCase() || (img.src || "") .trim() .toLowerCase() .indexOf("your-image-src".trim().toLowerCase()) >= 0 || (img?.attributes?.some_custom_prop?.value || "").trim().toLowerCase() === "your-custom-image-prop-value".trim().toLowerCase() || (img.className || "").toLowerCase().indexOf("your-class-name") >= 0 ); } ) => { const getImage = () => { return Array.prototype.filter.call( document.getElementsByTagName("img"), isMyImage )[0]; }; const stats = []; window.addEventListener("resize", () => { const windowSize = window.innerWidth; const img = getImage(); if (!stats.find(([winSize]) => winSize === windowSize)) { stats.push([windowSize, img.offsetWidth]); } }); return { getSizesStats: () => { stats.sort(([winSize1], [winSize2]) => winSize1 - winSize2); return console.log(stats.map((itm) => itm.join("\t")).join("\n")); }, getImage, clearSizesStats: () => { stats.splice(0, stats.length); }, resetIsMyImage: (newIsMyImage) => { isMyImage = newIsMyImage; }, }; })(); ``` ### Image Sizes Example: Let's say the data you collected in the [A method to determine image sizes section above](#a-method-to-determine-image-sizes) is as follow: ![Alt](./assets/sizes-graph.png "Sizes Graph") And let's further assume that the image sizes available are [68, 136, 272, 544]. Notice from the above: 1. For the viewport width of 1320 the size of the image becomes larger than 272 (the 272 sized image is not enough in this case). This means for viewport widths above 1320 the 544 image size is needed. 2. For viewport width between 1120 and 1320 the image size is always between 136 and 272. This means for viewport widths above between 1120 and 1320 the 272 image size is sufficient. 3. For viewport width between 1020 and 1120 the image size is larger than 272 again. This means for viewport widths between 1020 and 1120 the 544 image size is needed. 4. For viewport width less than 1020 the image size on the image is always between 136 and 272 again. This means for viewport widths less than 1020 the 272 image size is sufficient. 5. All this translates to the below sizes attribute (p.s. we gave it a 10px buffer): ```jsx ``` If you look at the `CategoryConstants.js` under `./theme-chocolatine/web/theme/pages/Category/` folder. You will notice the exact same `sizes` as we have deduced above. No magic numbers! 🧙‍♂️ ### Image Sizes Defaults We have used the method explained above to set default `sizes` across the theme. Those defaults found in the Constants file of the respective page are related to the image presets in the `app/config/images.js` and the default values of some SCSS variables like `$boxSizeMargin`, `$smallContainerWidth` and `$containerWidth` in the `theme/_variables.scss` file. So if you have customized any of the default configurations that affect how the image sizes change with viewport width, **you should definitely consider adapting the `sizes` values in the Constants files.** ## Add your own media proxy endpoint The example above leveraged the built-in Magento media proxy. However, one could add a new media proxy for virtually any remote source thanks to Front-Commerce core libraries. Implementing the media proxy is possible by combining the following mechanisms: - [adding custom HTTP endpoint](/docs/3.x/guides/create-custom-http-endpoint) (with Remix Router) - Front-Commerce's [`createResizedImageResponse`](/docs/3.x/api-reference/front-commerce-core/create-resized-image-response) method To learn more about how to implement this, please refer to the [`createResizedImageResponse`](/docs/3.x/api-reference/front-commerce-core/create-resized-image-response) documentation. ## Image caching While this feature is super handy, it comes with a cost: images are resized on the fly. To minimize this issue, we've added some guards. The first one as you might have noticed in the previous section is limiting the available formats by using presets. But that is not enough. This is why we have added caching: if an image is proxied once, the resized image will be put directly in your server's file system to avoid a resize upon each request of the image. This folder is in `.front-commerce/cache/images/`. ## Ignore caching through a regular expression While the proxy and caching functionality is really useful you may want to disable it for certain routes or files. In Front-Commerce we have implemented a mechanism to bypass the cache for routes that matches specified RegExp. Use `FRONT_COMMERCE_BACKEND_IGNORE_CACHE_REGEX` environment variable to specify a pattern that you want to bypass the cache for. This pattern will be matched against the file full URL without the query string (e.g. `https://www.example.com/path/to/the/file.png`). Usage examples: - if you want to allow files under `/media/excel` to be available without modifications, you can set `FRONT_COMMERCE_BACKEND_IGNORE_CACHE_REGEX` to `/media/excel`, - if you want to allow `.svg` and `.mp4` files to be available without modifications, you can set `FRONT_COMMERCE_BACKEND_IGNORE_CACHE_REGEX` to `\.(svg|mp4)$` Setting `FRONT_COMMERCE_BACKEND_IGNORE_CACHE_REGEX` will set the `ignoreCacheRegex` config of the [`cacheConfigProvider`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/e126e3d38f784ea362df5e7261f01c261414779c/packages/core/cache/config/cacheConfigProvider.ts#L63). Consequently it will be available on `config.cache.ignoreCacheRegex` should you ever need it. Please note to [escape regular expression special characters](https://javascript.info/regexp-escaping) when needed. ### Ignore caching for build files The former `FRONT_COMMERCE_BACKEND_IGNORE_CACHE_REGEX` only handles assets and external URLs caching. In case you want to prevent caching build time compiled files, you can set the `FRONT_COMMERCE_BACKEND_IGNORE_BUILD_CACHE_REGEX` variable instead. This will set the `ignoreBuildCacheRegex` config of the [`cacheConfigProvider`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/e126e3d38f784ea362df5e7261f01c261414779c/packages/core/cache/config/cacheConfigProvider.ts#L63). Consequently it will be available on `config.cache.ignoreBuildCacheRegex` should you ever need it. Please note to [escape regular expression special characters](https://javascript.info/regexp-escaping) when needed. :::warning This regex can heavily impact the response time of your site, please consider carefully its usage beforehand ::: --- # Override a component URL: https://developers.front-commerce.com/docs/3.x/guides/override-a-component

{frontMatter.description}

## Understanding theme overrides Front-Commerce web application is a fully featured e-commerce [universal](https://cdb.reacttraining.com/universal-javascript-4761051b7ae9) React application aimed at providing a sane base for your own theme. Even though you could reuse pages and components individually in a totally different theme, most of the time you might find easier and faster to start with the a theme and iterate from there. **Theme override is what allows you to:** - customize existing themes: - [theme-chocolatine](../03-extensions/theme-chocolatine/_category_.yml) - upgrade Front-Commerce and benefit from the latest component improvements. - or create a slightly different theme for events like Black Friday with confidence! Having an understanding of [themes in Magento](https://developer.adobe.com/commerce/frontend-core/guide/templates/), [child themes in Wordpress](https://developer.wordpress.org/themes/advanced-topics/child-themes/) / [Prestashop](https://devdocs.prestashop-project.org/8/themes/reference/template-inheritance/parent-child-feature/), [templates override in Drupal](https://www.drupal.org/docs/develop/theming-drupal/twig-in-drupal/working-with-twig-templates), [themes in CakePHP](https://book.cakephp.org/3.0/en/views/themes.html) will help you understand Front-Commerce’s theme overrides because they all share the same philosophy. If you have no previous experience with these other implementations, let’s see how it works! Your own theme will be located in its own folder and will use default components from parent theme(s). You would then copy the files you want to override in your theme folder by maintaining an identical file structure. Your component will then be used instead of the one in your parent theme(s). This translates in those three steps: 1. configure your custom theme and use it in your application 2. copy the file (`jsx`, `scss` or `gql`) you want to override in the `theme` folder of the `app` directory 3. customize its content in your extension directory ## Creating the `theme` folder :::info Whatever method you use, we will refer to this folder as "theme" for the rest of this documentation ::: ### Using the skeleton as base First we need to create the `theme` directory in your `app` folder ``` my-project └── app └── theme ``` That’s it! You have successfully created the folder that will be the root of your custom theme! ### In your own extension You can also extend theme in your extension, please read [Register an extension](./register-an-extension.mdx) guide to know how to create your own extension. First in your extension definition file, add the `theme` entry: ```javascript title="extension/theme-acme/index.ts" import path from "path"; import { defineExtension } from "@front-commerce/core"; export default function themeAcme() { return defineExtension({ name: "AcmeTheme", // highlight-next-line theme: "extensions/theme-acme/theme", configuration: { providers: [], }, }); } ``` And now theme file will be injected in your project! ## Override a component Let's add the description of a `Product` to a `ProductItem` as an example of overriding the base theme. The original file is: [`node_modules/@front-commerce/theme-chocolatine/theme/modules/ProductView/ProductItem/ProductItem.jsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/theme-chocolatine/theme/modules/ProductView/ProductItem/ProductItem.jsx) :::info Please refer to the [Front-Commerce’s folder structure documentation page](../05-concepts/react-components-structure.mdx) to get a better understanding of how components are organized in the base theme. ::: 1. copy it to: `app/theme/modules/ProductView/ProductItem/ProductItem.jsx` 2. add the description after the `` component in your `ProductItem` with `{props.description}`. **But you are not done yet!** The `description` information is not included in the GraphQL fields fetched by the application in the base theme. You will thus need to update the fragment related to `ProductItem`. :::info In this case, the data fetching is made using a GraphQL request. Since our component use data fetched by a GraphQL request, we need to update this fragment to add the data we want to the request. We invite you to read the [request data flow](../05-concepts/a-request-data-flow.mdx) to learn more about how data are fetched in Front-Commerce. ::: The original fragment file is collocated with the original component at: 1. copy the [`original ProductItemFragment`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/theme-chocolatine/theme/modules/ProductView/ProductItem/ProductItemFragment.gql?ref_type=heads) to the equivalent location in your extension. 2. add the field `description` to the fragment. ```diff title="app/theme/modules/ProductView/ProductItem/ProductItemFragment.gql" fragment ProductItemFragment on Product { imageUrl + description ...ProductOverviewFragment ...ProductItemActionsFragment } ``` 3. restart the application The data will now be fetched from GraphQL every time the `ProductItem` is used (product listings, upsells…) and will be available for you to render it as wanted. ### Reuse the original component Front-Commerce has been designed with small components having a single responsibility. **We believe that theme overrides should cover most of your use cases**. For some features though, it appeared that reusing the base component could be really useful. For instance, if you want to add a small feature or wrapper without changing the core feature of a component. It is possible to import a component from the `@front-commerce/theme/` module instead of `theme`. It is similar to if you were importing components from other libraries. :::note **Use with care!** We don't think that this method should be the default one, because it can make your updates more painful. ::: In the file that we've created in the previous section, instead of copying the original source file you could set it up like this: ```jsx title="theme/modules/ProductView/ProductItem/ProductItem.jsx" import BaseProductItem from "@front-commerce/theme/modules/ProductView/ProductItem/ProductItem.js"; const ProductItem = (props) => (
{/* Add your feature here */}
); export default ProductItem; ``` --- # Password fields URL: https://developers.front-commerce.com/docs/3.x/guides/password-fields

{frontMatter.description}

Front-Commerce contains some default components and mechanisms for providing a good user experience while ensuring that passwords match the security criteria you may have. Here is how to adapt them to your needs. ## Configure password validity Front-Commerce expects a certain level of complexity for the password entered by users. The default is: - A minimum of 8 characters - At least 3 types of characters among: lowercase letters, uppercase letters, digits and special characters You can customize those default rules by overriding [`theme/components/atoms/Forms/Input/Password/passwordConfig.ts`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/a8480398906aac97a3e6f999dcec65debe1393e5/packages/theme-chocolatine/theme/components/atoms/Forms/Input/Password/passwordConfig.ts) The configuration expects a list of rules to be satisfied and status to be displayed to users. It should follow the format below: ```js title="theme/components/atoms/Forms/Input/Password/passwordConfig.ts" import { defineMessages } from "react-intl"; const messages = defineMessages({ invalid: { id: "components.atoms.Form.Input.PasswordStrengthHint.Status.invalid", defaultMessage: "This is an invalid password", }, tooShort: { id: "components.atoms.Form.Input.PasswordStrengthHint.Status.tooShort", defaultMessage: "Too short", }, }); export default { rules: [ { // technical ID, must be unique id: "a-unique-technical-id", // optional, enforces this status if isValid returns false invalidStatus: "TOO_SHORT", // optional, label to display for this rule in the hint. If it is not defined, the rule is not displayed. label: messages.invalid, // validation method for this rule isValid: (password) => true, }, ], status: { // status key, used by rule.invalidStatus TOO_SHORT: { // message to display for this status label: messages.tooShort, // the status to be used from the ProgressStatus component status: "error", // minimum number of valid criterias for this status to be display, only the first valid status sorted by minCriterias will be shown minCriterias: 0, // is this status sufficient to validate the password isValid: false, }, }, }; ``` ## Disable password strength hints Front-Commerce provides a `` component to provide detailed feedback to users about the expected password complexity. You can deactivate this feature by adding the variable `FRONT_COMMERCE_WEB_PASSWORD_HINT_DISABLE=true` to your environment file. --- # Parallelize data fetching URL: https://developers.front-commerce.com/docs/3.x/guides/performance/data-fetching

{frontMatter.description}

If you need to execute multiple GraphQL queries in a single loader, you can parallelize them using [`Promise.all`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all). You can use any promise that returns data, not only GraphQL queries. Here's an example of a loader that fetches data from a GraphQL query and an external API package: **Before:** ```tsx title="app/routes/data-route.tsx" import { FrontCommerceApp } from "@front-commerce/remix"; import { json } from "@front-commerce/remix/node"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { MyFirstQueryDocument, MySecondQueryDocument } from "~/graphql/graphql"; import externalApi from "@example-package/external-api"; export async function loader({ context }: LoaderFunctionArgs) { const app = new FrontCommerceApp(context.frontCommerce); const firstData = await app.graphql.query(MyFirstQueryDocument); const secondData = await app.graphql.query(MySecondQueryDocument); const externalData = await externalApi.fetchData(); return json({ firstData, secondData, externalData, }); } ``` **After:** ```tsx title="app/routes/data-route.tsx" import { FrontCommerceApp } from "@front-commerce/remix"; import { json } from "@front-commerce/remix/node"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { MyFirstQueryDocument, MySecondQueryDocument } from "~/graphql/graphql"; import externalApi from "@example-package/external-api"; export async function loader({ context }: LoaderFunctionArgs) { const app = new FrontCommerceApp(context.frontCommerce); const [firstData, secondData, externalData] = await Promise.all([ app.graphql.query(MyFirstQueryDocument), app.graphql.query(MySecondQueryDocument), externalApi.fetchData(), ]); return json({ firstData, secondData, externalData, }); } ``` :::tip Please note that in this case, our API calls are independent from each other, so they will be executed in parallel. But you can also play with promises, to chain API calls. ::: :::warning This is a simple example. In a real-world scenario, make sure to handle errors properly. ::: --- # Optimize GraphQL queries URL: https://developers.front-commerce.com/docs/3.x/guides/performance/optimize-graphql-queries

{frontMatter.description}

## Query only what you need GraphQL allows you to query only the data you need, you may be tempted to have a single query that fetch all fields of an entity of your schema. Instead, query only the fields you need. Let's demonstrate this with an example on Category entity. Category entity is composed of datas related to the category itself and has a datalayer to retrieve products of this category. You may be tempted to have a single query in a GQL file that retrieve all datas of the category like this one: ```graphql query MyBigCategoryQuery($id: ID!) { category(id: $id) { id name layer { products { name sku } } } } ``` This query will in background call the Magento API twice: - First call to get the category datas - Second call to get the products datas That's a good call if you're on a category page and you want to display the category and its products. However, let's says that you want to retrieve only the category infos and use Algolia to display products. In this case, you don't need to retrieve products datas, you can query only the category datas like this one: ```graphql query MyFastCategoryQuery($id: ID!) { category(id: $id) { id name } } ``` Here, only one call will be done to the Magento API, and you will save some precious time on the server. --- # Prevent excessive usage with rate limits URL: https://developers.front-commerce.com/docs/3.x/guides/prevent-excessive-usage-with-rate-limits

{frontMatter.description}

## Rate Limiter Service The Rate Limiter service provides a robust solution for managing and preventing excessive usage by imposing limits on the number of requests that users can make within a specified period. This service utilizes Redis for storage and supports integration into both HTTP contexts and GraphQL operations. ### Features - **Redis-backed Storage**: Utilizes Redis to efficiently track request counts and support high concurrency. - **Flexible Rate Limits**: Configure max requests and duration per rate limit namespace. - **HTTP and GraphQL Support**: Easily integrate rate limiting into Remix actions/loaders and GraphQL resolvers. - **Automated Response Handling**: Automatically sends HTTP 429 Too Many Requests responses when a limit is exceeded. ### Service Configuration The rate limiter requires a Redis configuration to function. Upon instantiation, it subscribes to a Redis instance and sets up namespaces for different rate limits. To configure the service, you can add the following to your `front-commerce.config.ts`: ```ts title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; export default defineConfig({ // ...other configuration options rateLimiter: { // feel free to extract this configuration in a separate file to ease maintenance redis: { host: process.env.FRONT_COMMERCE_CLOUD_REDIS_SESSIONS_HOST, port: process.env.FRONT_COMMERCE_CLOUD_REDIS_SESSIONS_PORT || 6379, db: process.env.FRONT_COMMERCE_CLOUD_REDIS_SESSIONS_DB || 2, }, }, }); ``` :::tip We've suggested a configuration which shares the same Redis DB than sessions. While it's not required, we think it makes sense because both share the same kind of responsibilities. ::: ## Implementing Rate Limits The service provides two main methods for limiting rates: - `limitHTTPResource(namespace, requestId, options)`: Limits rates for HTTP requests by namespace and request ID. - `limitGraphQLResource(fieldPath, requestId, options)`: Limits rates for GraphQL queries based on the resolver field path. There is also an additional exported method in the `graphql` exports: - `limitRateByGraphQLResolver(options, resolver)`: Wraps a GraphQL resolver with rate limiting. ### Remix Loader In the Remix framework, the rate limiter can be employed within a loader to control request frequency based on URL path and user IP. ```ts import { FrontCommerceApp } from "@front-commerce/remix"; import type { LoaderFunctionArgs } from "@remix-run/node"; export const loader = async ({ context, request }: LoaderFunctionArgs) => { const app = new FrontCommerceApp(context.frontCommerce); await app.services.RateLimiter.limitHTTPResource( new URL(request.url).pathname, app.user.clientIp, { max: 5, duration: "1m", } ); // Perform loader-specific logic here }; ``` ### Remix Action Similarly, for actions in Remix, the rate limiter ensures that excessive requests are controlled at the action level, again based on URL path and user IP. ```ts import { FrontCommerceApp } from "@front-commerce/remix"; import type { ActionFunctionArgs } from "@remix-run/node"; export const action = async ({ context, request }: ActionFunctionArgs) => { const app = new FrontCommerceApp(context.frontCommerce); await app.services.RateLimiter.limitHTTPResource( new URL(request.url).pathname, app.user.clientIp, { max: 5, duration: "1m", } ); // Perform action-specific logic here }; ``` ### GraphQL Resolver For GraphQL resolvers, the rate limiter can be directly applied to individual queries or mutations to manage resource consumption effectively. ```ts import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ resolvers: { Query: { helloWorld: async (parent, args, { user, services }) => { await services.RateLimiter.limitGraphQLResource( "Query:helloWorld", user.clientIp, { max: 5, duration: "1m", } ); // Perform resolver logic here }, }, }, }); ``` Or you can use the `limitRateByGraphQLResolver` method to wrap the resolver with a rate limit: ```ts import { createGraphQLRuntime, limitRateByGraphQLResolver, } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ resolvers: { Query: { helloWorld: limitRateByGraphQLResolver( { max: 5, duration: "1m", }, async (parent, args, context, info) => { // Perform resolver logic here } ), }, }, }); ``` :::info This method will automatically apply a namespace based on the resolver path, for example `Query:helloWorld`. ::: ## Additional Tips - **Handling Overlimits**: The service automatically handles cases where the limit is exceeded by sending a `429` status code, but this can be customized further if different handling is required. - **Monitoring and Logs**: Integration with `winston` for logging allows tracking down issues and misuse effectively. - **Custom namespace and requestId**: The `namespace` and `requestId` parameters can be customized to suit your application's requirements. --- # Proxyfing invoices URL: https://developers.front-commerce.com/docs/3.x/guides/proxyfing-invoices

{frontMatter.description}

In a project, you may want to change the default source to retrieve invoices or prefer to provide a downloadable PDF document for them. This section documents how that could be achieved. :::info This guide will help you to reproduce the [pdf-invoice example extension](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/6770c4b61a55f47eeb54acfe62efae7f983bb465/skeleton/example-extensions/pdf-invoice). Feel free to use this example as a starting point to implement your own solution. ::: ## Front-Commerce Invoice The `front-commerce/invoice` GraphQL module defines shared interfaces to represent Invoices: - `FcInvoice`: a generic invoice entity - `FcDownloadableInvoice`: invoices that can be downloaded by the Customer (implements `FcInvoice`) - `FcDisplayableInvoice`: invoices that can be displayed on the web by the Customer (implements `FcInvoice`) ## Replace default invoices with downloadable invoice files We will illustrate how one can replace the default Front-Commerce implementation to provide PDF files for invoices. It will highlight the existing extension points that might be useful to implement any other specific use cases you may face. Customizing how invoices are resolved in your application consists in 3 steps: - adding a new Invoice type to the graphql schema - overriding the `Order.invoices` resolver to fetch invoices from your endpoint - making the file downloadable for the Customer ### Add your new Invoice type to the graphql schema _This section assumes that you know how to [Extend the GraphQL schema](./extend-the-graphql-schema)._ Since the invoice is aimed at being downloadable, the new invoice type will have to implement the `FcDownloadableInvoice` interface. ```ts title="extensions/pdf-invoice/graphql/index.ts" import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ // ... rest of the module typeDefs: /* GraphQL */ ` type AcmeInvoice implements FcInvoice { id: ID } type AcmeInvoice implements FcDownloadableInvoice { id: ID download: DownloadLink } `, // ... rest of the module }); ``` ### Fetch invoices from your endpoint Override the `Order.invoices` resolver by providing a list coming from your own API (there can be multiple invoices for each order). In this example, we consider that all invoices are `AcmeInvoice` but Front-Commerce supports heterogeneous types: ```ts title="extensions/pdf-invoice/graphql/resolvers.ts" export default { Order: { invoices: ({ entity_id }, _, { loaders }) => { return loaders.AdminInvoice.loadByOrderId(entity_id); }, }, FcInvoice: { __resolveType: ({ __typename }) => { return "AcmeInvoice"; }, }, AcmeInvoice: { download: ({ id, entity_id }, _, { loaders }) => { return loaders.AcmeInvoice.loadPdf(entity_id); // should return { name: "The downloaded file name.pdf", url: "/invoices/fileXXX.pdf" } }, }, }; ``` ### Make invoice files downloadable for Customers You must implement [a custom route](./create-custom-http-endpoint) to expose the PDF file on a URL. The implementation of this route is up to you and depends on your context. It might be a simple proxy, but if your remote data source does not implement access control you may have to roll your own authorization checks to ensure that Customers cannot access invoices that don't belong to them. Here is an example of a custom route that proxy a Magento remote url with additional checks: ```ts title="extensions/pdf-invoice/routes/invoices.download.$id.tsx" import { FrontCommerceApp } from "@front-commerce/remix"; import { type LoaderFunctionArgs } from "@remix-run/node"; export async function loader({ params, context }: LoaderFunctionArgs) { const app = new FrontCommerceApp(context.frontCommerce); const magentoEndpoint = app.config.magento.endpoint; const invoiceId = params.id; const invoiceUrl = `${magentoEndpoint}/invoices/${invoiceId}`; try { const invoice = await fetch(invoiceUrl); if (invoice.status !== 200) { return new Response("Not found", { status: 404 }); } const invoiceFile = await invoice.arrayBuffer(); return new Response(invoiceFile); } catch (error) { return new Response("Internal server error", { status: 500 }); } } ``` :::note When the user is not authorized or the final path does not work, it will display a 404 page instead. This is kind of for a security reason but mostly because we don't want to force people to style a new error page! ::: --- # PWA Setup URL: https://developers.front-commerce.com/docs/3.x/guides/pwa-setup import Figure from "@site/src/components/Figure";

{frontMatter.description}

## What is a PWA [PWA stands for Progressive Web App](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps). This technology allows the user to install your store as a native application on their device. It also brings advanced caching capabilities, offline support and much more. Front-Commerce integrates PWA capabilities and is available with a few configuration. ## Setup PWA ### `webmanifest` configuration The [`webmanifest`](https://developer.mozilla.org/en-US/docs/Web/Manifest) defines meta information about your store. To setup this webmanifest, you need to add the following configuration in your `front-commerce.config.ts` file: ```typescript title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; import magento2 from "@front-commerce/magento2"; import storesConfig from "./app/config/stores"; import cacheConfig from "./app/config/caching"; export default defineConfig({ extensions: [magento2({ storesConfig }), themeChocolatine()], stores: storesConfig, cache: cacheConfig, configuration: { providers: [], }, // highlight-start pwa: { appName: "ACME Store", shortName: "ACME Store", themeColor: "#fbb03b", icon: "public/favicon.svg", // we suggest using a svg which can be resized without losing quality maskableIcon: "public/maskable.svg", // we suggest using a svg which can be resized without losing quality offline: { pageFallback: "__front-commerce/offline", imageFallback: "images/Logo.svg", // image url (relative to public folder) }, }, // highlight-end }); ``` Here is a list of the available options: | Option | Type | Description | | -------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `appName` | `string` | The name of your PWA | | `shortName` | `string` | The short name of your PWA, it will be displayed on the home screen of your customer | | `themeColor` | `string` | The theme color of your PWA, it will define theme for your App, we recommend you to use your website primary color | | `icon` | `string` | The icon of your PWA, it will be displayed on the home screen of your end user device, it must be at least 512x512 | | `maskableIcon` | `string` | The maskable icon of your PWA, it will be displayed on the home screen of your customer, it must be at lease 512x512 | | `offline` | [`OfflineFallbackOptions`](https://developer.chrome.com/docs/workbox/modules/workbox-recipes/#type-OfflineFallbackOptions) | The offline configuration of your PWA | ### Icons setup A PWA requires multiple icons to be defined in your project that will be used by the device to identify the application on the end-user device screen. Two types of icons are needed: #### `icon` This icon is a standard icon that will be displayed over a background color depending on the end-user device configuration and OS.
![Example on standard icons displayed on android](./assets/standard_icons.png)
> Source: [web.dev](https://web.dev/maskable-icon/) #### `maskableIcon` This icon must be a squared icon and will be cropped in a shape defined by the device
![Example of maskable icons displayed on android](./assets/maskable_icons.png)
> Source: [web.dev](https://web.dev/maskable-icon/) You can read more about icons here : [https://web.dev/maskable-icon/](https://web.dev/maskable-icon/) ## Install PWA Once everything is setup, you will be able to install your website as an application. Depending on the browser, a prompt inviting the customer to install the application is different: - Chrome: https://support.google.com/chrome/answer/9658361 - Safari: https://web.dev/learn/pwa/installation/#ios-and-ipados-installation - Firefox: https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Installing - Edge: https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/ux :::tip Note that you can control PWA installation dialog using [Installation prompt](https://web.dev/learn/pwa/installation-prompt/) ::: --- # Quick orders URL: https://developers.front-commerce.com/docs/3.x/guides/quick-orders

{frontMatter.description}

The `` component adds a user interface allowing your customers to directly order items based on their SKU
![Example with the component added to the default's theme](./assets/quickorder-sample.png)
## Integrate QuickOrder into your project You must include the component in your page with the following lines: ```jsx import React from "react"; // highlight-next-line import QuickOrder from "theme/modules/QuickOrder"; const MyComponent = () => { return (
// highlight-next-line
); }; ``` If you need to customize the component, you can, of course, [override it](./override-a-component.mdx). --- # Register an extension URL: https://developers.front-commerce.com/docs/3.x/guides/register-an-extension

{frontMatter.description}

All extensions of a Front-Commerce application are registered through the `front-commerce.config.ts` configuration file at the root of the project by importing the extension definitions (or functions returning an extension definition) and adding them the `extensions` list. For instance, in a project configured to be connected to a Magento2 instance and with the theme chocolatine, [the Magento2 extension](./register-an-extension.mdx) and [the Theme Chocolatine extension](../03-extensions/theme-chocolatine/_category_.yml) are registered in `front-commerce.config.ts`: ```typescript title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; import magento2 from "@front-commerce/magento2"; import storesConfig from "./app/config/stores"; import cacheConfig from "./app/config/caching"; export default defineConfig({ // highlight-next-line extensions: [magento2({ storesConfig }), themeChocolatine()], stores: storesConfig, cache: cacheConfig, configuration: { providers: [], }, }); ``` In that project, if you want to add an extension, you could add the following changes to `front-commerce.config.ts`: ```typescript title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; import magento2 from "@front-commerce/magento2"; // highlight-next-line import aCustomExtension from "./extensions/custom-extension"; import storesConfig from "./app/config/stores"; import cacheConfig from "./app/config/caching"; export default defineConfig({ // highlight-next-line extensions: [ magento2({ storesConfig }), themeChocolatine(), aCustomExtension(), ], stores: storesConfig, cache: cacheConfig, configuration: { providers: [], }, }); ``` where `./extensions/custom-extension/index.ts` would be something like: ```typescript title="extensions/custom-extension/index.ts" import { defineExtension } from "@front-commerce/core"; export default function aCustomExtension() { return defineExtension({ name: "custom-extension", configuration: { providers: [], }, }); } ``` ---

{frontMatter.description}

# Expose metrics for prometheus Front-Commerce provides a `/__front-commerce/metrics` endpoint with data about the node process and response times. ## Configure it To enable this endpoint, you need to define the environment variable `FRONT_COMMERCE_CLOUD_METRICS_KEY` with a basic authentication token (base64 encoded, e.g. `dXNlcjpwYXNzd29yZA==` for `user:password`) ## Access it Call `https:///__front-commerce/metrics` with the defined header `Authorization: Basic ` ## Standard metrics Front-Commerce is exposing some standard metrics about the node process like memory / CPU usage, please check [OpenTelemetry Node.js SDK documentation](https://opentelemetry.io/docs/languages/js/) for more information. ## Other metrics ### Measure external services call times Once the `/__front-commerce/metrics` endpoint is enabled, you can count the time spent calling external services. Example with a magento backend service: ```shell # TYPE outbound_requests_duration histogram outbound_requests_duration_bucket{le="0.1",target="magento.location.net"} 0 outbound_requests_duration_bucket{le="0.25",target="magento.location.net"} 51 outbound_requests_duration_bucket{le="0.5",target="magento.location.net"} 58 outbound_requests_duration_bucket{le="1",target="magento.location.net"} 59 outbound_requests_duration_bucket{le="2",target="magento.location.net"} 74 outbound_requests_duration_bucket{le="+Inf",target="magento.location.net"} 75 outbound_requests_duration_sum{target="magento.location.net"} 31.227999999999998 outbound_requests_duration_count{target="magento.location.net"} 75 ``` --- # FAQ URL: https://developers.front-commerce.com/docs/3.x/guides/server-side-events/faq

{frontMatter.description}

## What events are implemented in Front-Commerce? :::info Currently, Front-Commerce only implement the OrderPlaced events, but more events will be added in the future. ::: | Event | Description | | ------------- | ------------------------------------------------ | | `OrderPlaced` | This event is triggered when an order is placed. | ## How can I test Server events in local environment? To test server events in a local environment, you need a Redis server, and you must configure redis host and port in your `front-commerce.config.ts` file: ```ts title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; export default defineConfig({ extensions: [themeChocolatine()], serverEvents: { integrations: [], // highlight-start redis: { host: "127.0.0.1", port: 6379, }, // highlight-end }, }); ``` Then you need to start the worker by running the following command from your project : `pnpm run worker` If you would like to obtain additional information and enable debug mode, you can do one of the following: - Run the command with the DEBUG flag : `DEBUG=front-commerce:server-events:cli pnpm run worker` - Add the debug flag to your `.env` file : `DEBUG=front-commerce:server-events:cli` ## What data is published for a Server Event? The data returned by the events is an object with the following properties : ```json { event_type: string, // Type of the event defined in the Event emitter created_at: string // Timestamp of when the event was created payload: object // The data of the event metadata: object // The metadata of the event } ``` --- # Create an event URL: https://developers.front-commerce.com/docs/3.x/guides/server-side-events/how-to/create-an-event

{frontMatter.description}

## What is an event emitter The events are dispatched through the use of an event emitter. The role of the event emitter is to push events to the server event pipeline to be processed by the server. ## Prerequisites Before diving into the code, ensure you have the following prerequisites in place: - A Front-Commerce project configured and ready to use :::info The outcome of this guide is already available in the [FAQ example extension](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/main/skeleton/example-extensions/faq-demo). ::: ## Create a Domain event class Let's create a file in our project, called `QuestionVotedEvent.ts`: ```tsx title="app/server-events/events/QuestionVotedEvent.ts" import { ServerEvent } from "@front-commerce/core/events"; const QuestionVotedEventName = "QuestionVotedEvent"; // This is the payload data that we want to gather for the event type QuestionVotedEventPayload = { questionSlug: string; upvote: boolean; }; // We define an event class that extends the ServerEvent class // This class will contain data to be pushed to the server event pipeline export default class QuestionVotedEvent extends ServerEvent { static event_type = QuestionVotedEventName; // See https://developers.front-commerce.com/docs/3.x/api-reference/front-commerce-core/ServerEvent#arguments constructor(questionSlug: string, upvote: boolean, shopId: string) { super( QuestionVotedEventName, new Date(), { questionSlug, upvote }, // This are the event metadata, you can use it to set infos about the event // which are not part of the event payload { shopId } ); } } ``` :::info You can check the [`ServerEvent` API Reference](/docs/3.x/api-reference/front-commerce-core/ServerEvent) out to learn more about the usage of ServerEvent class. ::: ## Use the Event Now that we've created a Domain event class, let's use it in a route's `action`: ```tsx title="app/routes/_main.faq.$slug.tsx" //... export const action = async ({ context, params, request, }: ActionFunctionArgs) => { const { slug } = params; const app = new FrontCommerceApp(context.frontCommerce); // ... Here should be the "voting" mechanism (probably a mutation) // We call the `ServerEventBus` to dispatch an event. // In this example, we use it to dispatch a `QuestionVotedEvent` event created above. app.services.ServerEventBus.publish( new QuestionVotedEvent( String(slug), formData.get("isFaqUseful") === "true", app.config.currentShopId ) ); // ... The rest of the action, return, etc. }; //... ``` ## Test our emitter Now that everything is ready, let's test it! To do so, start the Front-Commerce Worker by running: ```shell pnpm run worker ``` The worker will boot: ``` 2024-02-23 14:55:21 [info]: Font-Commerce Server Events Worker 2024-02-23 14:55:21 [info]: Please wait booting... 2024-02-23 14:55:24 [info]: Worker started ``` And now let's head to the faq question page: [http://localhost:4000/faq/probleme-colis](http://localhost:4000/faq/probleme-colis) and vote. You will see new output to the worker: ``` [ConsoleIntegration]: (1708696560088-0) {"event_type":"QuestionVotedEvent","created_at":"2024-02-23T13:56:00.080Z","payload":"{\"questionSlug\":\"probleme-colis\",\"upVote\":true}","metadata":"{\"shopId\":\"default\"}"} ``` And that's it! You now know how to create and emit your own server events. --- # Create an integration URL: https://developers.front-commerce.com/docs/3.x/guides/server-side-events/how-to/create-an-integration import BrowserWindow from "@site/src/components/BrowserWindow";

{frontMatter.description}

We will create a new integration to dispatch events to [Webhook.cool](https://webhook.cool/). ## Create an integration file First you need to create a file for the integration. For this example, we will name our integration `Coolwebhook` and will create a new file in `app/server-events/integrations/CoolWebhookIntegration.ts`. Integrations classes must implement the `Integration` interface from `@front-commerce/core/server-events/integrations`. Copy this code in the newly created file: ```ts title="app/server-evets/integrations/CoolWebhookIntegration.ts" import type { Integration } from "@front-commerce/core/server-events/integrations"; export default class CoolWehbookIntegration implements Integration { compatibleEvents: string[] | "*" = "*"; webhookId: string; // this is the id of the unique WebhookCool url to use (e.g: `old-kitchen-32` for https://webhook.cool/at/old-kitchen-32) constructor(webhookId: string) { this.webhookId = webhookId; } handle(messageId: string, payload: any) { console.log(messageId); console.log(payload); } } ``` In this class, we have two entities : - `compatibleEvents`: this indicates which events the integration will handle, it can be `*` to handle all events or an array of string that contains [compatible events](/docs/3.x/guides/server-side-events/faq#what-events-are-implemented-in-front-commerce). - `handle`: this is the method that will be called when an event is received. For your integrations, only `compatibleEvents` and `handle` are required, the `webhookId` and the constructor is an example of custom method and properties that you can implement into your integrations. ## Write specific code for your integration Now we need to implement to logic which sends the event to external service Since Webhook.cool is a minimalist services, we can use a single `fetch` call to send the event : ```ts title="app/server-evets/integrations/CoolWebhookIntegration.ts" import type { Integration } from "@front-commerce/core/server-events/integrations"; export default class CoolWehbookIntegration implements Integration { compatibleEvents: string[] | "*" = "*"; webhookId: string; constructor(webhookId: string) { this.webhookId = webhookId; } handle(messageId: string, payload: any) { // highlight-start fetch(`https://${this.webhookId}.webhook.cool`, { method: "POST", body: JSON.stringify(payload), }); // highlight-end } } ``` :::info Here, we are posting the raw event details to the webhook, but feel free to manipulate it's data freely to adapt it to your specific integration. You can find the data format for the payload object in the [FAQ](/docs/3.x/guides/server-side-events/faq#what-data-is-published-for-a-server-event) section. ::: ## Register your integration Now that our integration is created, we have to register it in Front-Commerce. To do this in a newly created app, you can register your new integration in the `app/config/serverEvents.ts` configuration file: ```ts title="app/config/serverEvents.ts" import { ConsoleIntegration } from "@front-commerce/core/server-events/integrations"; // highlight-next-line import CoolWehbookIntegration from "../server-events/integrations/CoolWehbookIntegration"; export default { redis: { host: process.env.FRONT_COMMERCE_SERVER_EVENT_REDIS_HOST || process.env.FRONT_COMMERCE_CLOUD_REDIS_HOST || process.env.FRONT_COMMERCE_REDIS_HOST || "127.0.0.1", port: process.env.FRONT_COMMERCE_SERVER_EVENT_REDIS_PORT || process.env.FRONT_COMMERCE_CLOUD_REDIS_PORT || process.env.FRONT_COMMERCE_REDIS_PORT || 6379, }, integrations: [ new ConsoleIntegration(), // highlight-next-line new CoolWehbookIntegration("old-kitchen-32"), ], }; ``` :::info You need to replace `old-kitchen-32` with your Webhook ID, you can find it on the [Webhook.cool](https://webhook.cool/) page ![Webhook cool code](assets/webhook_cool_code.gif) ::: ## Test your integration We are now ready to test your integration ! Please be sure to read the [run local Server Event Worker guide](/docs/3.x/guides/server-side-events/faq#how-can-i-test-server-events-in-local-environment) before testing your integration. Then let's try to send an event by going to the server event test page [http://localhost:4000/\_\_front-commerce/test/events](http://localhost:4000/__front-commerce/test/events) and check out the [Webhook.cool](https://webhook.cool/) page.
--- # Google Analytics URL: https://developers.front-commerce.com/docs/3.x/guides/server-side-events/integrations/google-analytics

{frontMatter.description}

## Introduction Google Analytics provides a JS SDK to push events to their servers. To do server-to-server communication, you need to use Google Analytics Measurement Protocol. ## Prerequisites To use Google Analytics Measurement Protocol, you need to get your API Secret and your measurement ID. - Measurement ID - Measurement Protocol API Secret :::info Please follow the [Google Analytics Measurement Protocol Documentation](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#payload_query_parameters) to learn how to get them. ::: ## Setup Front-Commerce Update your `app/config/serverEvents.ts` file: ```ts title="app/config/serverEvents.ts" import { ConsoleIntegration, AnalyticsIntegration, } from "@front-commerce/core/server-events/integrations"; // higlihjt-next-line import analytics from "@front-commerce/core/analytics"; export default { //... integrations: [ // ... // highlight-start new AnalyticsIntegration("front-commerce", [ analytics.GA4MeasurementProtocol( "G-XXXXXXXX", // Your Analytics Measurement ID "XXXXXXXXXXXXXXXXXXXXXX" // Your Analytics Measurement Protocol Secret ), ]), // highlight-end ], }; ``` Then in analytics configuration, enable server tracking in analytics configuration: ```ts title="app/config/analytics.ts" //... export default { analytics: { enable: true, debug: process.env.NODE_ENV !== "production", // highlight-start serverTracking: { enabled: true, }, // highlight-end //.. } satisfies AnalyticsConfig, }; ``` And that's it! Now every event will be submitted to Google Analytics by your server! ## Compatible events | Front-Commerce event | Google Analytics event | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `Products Searched` | [search](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events#search) | | `Product List Viewed` | [view_item_list](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events#view_item_list) | | `Product Clicked` | [select_content](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events#select_content) | | `Product Viewed` | [view_item](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events#view_item) | | `Product Added` | [add_to_cart](https://developers.google.com/analytics/devguides/collection/ga4/reference/events?client_type=gtag#add_to_cart) | | `Product Removed` | [remove_from_cart](https://developers.google.com/analytics/devguides/collection/ga4/reference/events?client_type=gtag#remove_from_cart) | | `Cart Viewed` | [view_cart](https://developers.google.com/analytics/devguides/collection/ga4/reference/events?client_type=gtag#view_cart) | | `Checkout Started` | [begin_checkout](https://developers.google.com/analytics/devguides/collection/ga4/reference/events?client_type=gtag#begin_checkout) | | `Payment Info Entered` | [add_payment_info](https://developers.google.com/analytics/devguides/collection/ga4/reference/events?client_type=gtag#add_payment_info) | | `Order Completed` | [purchase](https://developers.google.com/analytics/devguides/collection/ga4/reference/events?client_type=gtag#purchase) | | `User Logged in` | [login](https://developers.google.com/analytics/devguides/collection/ga4/reference/events?client_type=gtag#login) | | `User Created` | [sign_up](https://developers.google.com/analytics/devguides/collection/ga4/reference/events?client_type=gtag#sign_up) | | `Product Added to Wishlist` | [add_to_wishlist](https://developers.google.com/analytics/devguides/collection/ga4/reference/events?client_type=gtag#add_to_wishlist) | --- # Server Side Rendering (SSR) URL: https://developers.front-commerce.com/docs/3.x/guides/server-side-rendering

{frontMatter.description}

Front-Commerce uses **SSR** - as opposed to **CSR** - to improve {/* prettier-ignore */}SEO and {/* prettier-ignore */}UX by serving HTML pages to users on their first hit to the server. You can read [a request data-flow in Front-Commerce](../05-concepts/a-request-data-flow.mdx) to get a better understanding of this process. {/* TODO: Update link to 3.x */} To generate this HTML page, Front-Commerce will render the React application as a string. During this process, your React components will trigger GraphQL queries to fetch data from the GraphQL layer. This may involve hitting remote APIs to retrieve the necessary data, which is then used to generate the HTML content of your page. **In a Front-Commerce project, server-side rendering is managed by Remix. We utilize Remix's native features to simplify the process for developers, so you don’t have to worry about the technical details. However, it's not a black box, and there are some important aspects you should be aware of while creating your theme.** This section details everything frontend developers must keep in mind when authoring components in a SSR context ## There is no `window` on the server! Even though it may seem obvious, **you are very likely to come across this problem at some point!** Browser APIs are not available during SSR. Keep it in mind when using them, and provide a graceful fallback. Sometimes, it could be as simple as a default value or returning earlier from your function without any side effects. Examples: ```js const doSomethingWithIntersectionObserver = () => { if (typeof window === "undefined" || !window.IntersectionObserver) { return; } // … }; ``` ```js const initializePosition = (setPosition) => { if (typeof navigator === "undefined" || !("geolocation" in navigator)) { setPosition(DEFAULT_LATITUDE, DEFAULT_LONGITUDE); } else { navigator.geolocation.getCurrentPosition((position) => { setPosition(position.coords.latitude, position.coords.longitude); }); } }; ``` ## `useEffect` to the rescue! There may be situations where rendering a totally different component during server-side rendering might be relevant. For instance, instead of displaying a map centered on the user's geolocation, it might be better to render a placeholder (page skeleton) on the server and load the map with additional data during CSR. Another use case would be to avoid unnecessary overhead by not rendering some components during SSR: a social media feed may not make sense on the server. You can use `useEffect` to conditionally render a component or populate it with data on the client side after the initial server-side render. Example: ```tsx import React, { useEffect, useState } from "react"; interface Location { latitude: number; longitude: number; } const MyMapComponent = () => { const [location, setLocation] = useState(null); useEffect(() => { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (position) => { setLocation({ latitude: position.coords.latitude, longitude: position.coords.longitude, }); }, (error) => { console.error("Error getting geolocation:", error); } ); } }, []); if (!location) { return
Loading map...
; // Placeholder during SSR } return (

Map centered at your location

Latitude: {location.latitude}, Longitude: {location.longitude}
{/* Here you would render your map component using the location */}
); }; export default MyMapComponent; ``` Please keep in mind that by default the server version will always be displayed first. This means that if you are on a category page and navigate to a product page, it will first display the server version of the product page. When it's done loading, it will then display the client version. The goal here is to enable faster navigation and display only critical information on page mount. If you want to change this behavior and display the client version of the component immediately, you can use `useEffect` to handle the client-side logic and state updates. ## A viewport as consistent as possible When using server-side rendering, standard CSS media queries are honored and should be used as much as possible. However, be aware that browser-specific APIs aren't available on the server. For more advanced use cases and solutions in our default theme, please refer to the [Adapting Content to the Viewport](../03-extensions/theme-chocolatine/how-to/adapt-content-to-the-viewport.mdx) guide in the Theme Chocolatine area. ## Learn more If you want to learn more about SSR in Front-Commerce, we recommend you to understand how [Server timings](../02-guides/adding-your-own-server-timings.mdx) could help you spot performance issues and have a look at [hydration](). --- # Serving assets from a CDN/custom domain URL: https://developers.front-commerce.com/docs/3.x/guides/serving-assets-from-a-cdn-custom-domain

{frontMatter.description}

## Configuration Serving assets from a custom domain can be done with a configuration change: 1. for each store in `app/config/stores`, you can add a `assetsBaseUrl` entry so that static assets and images are served from it. For instance: ```js module.exports = { default: { url: process.env.FRONT_COMMERCE_URL, // highlight-next-line assetsBaseUrl: "http://a.cdn.example.com", locale: "en-US", currency: "EUR", default_country_id: "GB", countries: (IsoCountries) => IsoCountries.registerLocale( require("i18n-iso-countries/langs/en.json") ), }, }; ``` In a multiple store setup, it's possible to use the same `assetsBaseUrl` for all stores. 2. [Configure the CSP](./customize-http-headers/content-security-policy.mdx) so that assets can be loaded from this external URL 3. You might also have to [allow this domain in CORS origins](./customize-http-headers/cross-origin-resource-sharing.mdx) to prevent "_has been blocked by CORS policy_" errors After restarting Front-Commerce, your assets should be loaded from this custom domain. --- # Start Front-Commerce with PM2 URL: https://developers.front-commerce.com/docs/3.x/guides/start-front-commerce-with-pm2

{frontMatter.description}

:::note [PM2](https://pm2.keymetrics.io/) is not the only way to deploy Front-Commerce in production, however, we often get asked about how PM2 should be configured for Front-Commerce. This page is here to get you started but we recommend that you discuss it with your system administrator. ::: ## `ecosystem.config.cjs` We recommend to use an `ecosystem.config.cjs` file so you could track changes to your deployment configuration, however these settings can also be passed to PM2 CLI. Below is an annotated example of configuration: ```js title="ecosystem.config.cjs" module.exports = { apps: [ { // Name of the app name: "front-commerce", // Path to the script to run script: "./server.mjs", // Path to your front-commerce app cwd: "/path/to/your/frontcommerce/app", // Node arguments node_args: "--import tsx/esm", // Environment variables env: { NODE_ENV: "production", // You may also prefer to use this configuration file // to define environment variables }, // collocate PM2 logs with default Front-Commerce logs error_file: "./logs/pm2-error.log", out_file: "./logs/pm2-out.log", log_file: "./logs/pm2-combined.log", // It is safe to let PM2 restart your processes in case of // an unexpected failure, or if they consume too much memory. // Memory leaks on long-running processes such as node servers // are common, and it is safe to have a watchdog for them autorestart: true, restart_delay: 1000, kill_timeout: 15000, wait_ready: true, max_memory_restart: "2G", // PM2's cluster mode allows your application to leverage all // the CPUs available on your server. It is a good way to make // your app faster. exec_mode: "cluster", instances: "max", }, ], }; ``` :::note This page only focuses on Front-Commerce related configurations. You should look into additional PM2 configuration keys and ensure they are adapted to your deployment environment for an optimal configuration. ::: ## Start your application If an `ecosystem.config.cjs` file is versioned at the root of your project, you could then start or replace your Front-Commerce app in production with a new version using the command below: ```shell pm2 reload ecosystem.config.cjs ``` It will allow you to deploy the new version of your application with zero downtime! For other questions about using PM2, we recommend that you read [their official documentation](https://pm2.keymetrics.io/docs/usage/pm2-doc-single-page/). --- # Translate your application URL: https://developers.front-commerce.com/docs/3.x/guides/translate-your-application

{frontMatter.description}

import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; :::note If you want to configure your application to support multiple languages, please refer to [Configure multiple stores](./configuration/04-configure-multiple-stores.mdx). ::: Front-Commerce manages your translations by using [react-intl](https://formatjs.io/docs/getting-started/application-workflow), a standard library in the React ecosystem. This library works by transforming your static values in React Components. These components will then fetch your translations and display them in your application. ## Declare translations in your application :::info You can find the full set of components in the [React Intl Components documentation](https://formatjs.io/docs/react-intl/components/). ::: For instance, let's see how to transform your values in `react-intl` Components. ### Strings ```tsx import { FormattedMessage } from "react-intl"; ; ``` :::note To take full advantage of the `translate` cli in Front-Commerce, a default message is required for formatted messages. This will be used to extract the messages for the default locale and ensure there are no dead translations, or missing translations. ::: ### Dates ```tsx import { FormattedDate } from "react-intl"; ; ``` ### Prices ```tsx import { FormattedNumber } from "react-intl"; ; ``` :::note However, for this particular use case we have abstracted this in Front-Commerce with the [theme/components/atoms/Price](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/c8fc25465ab7228098bf45c3cbc712c8d2c32529/packages/theme-chocolatine/theme/components/atoms/Typography/Price/index.js) component (and its variants: `ProductPrice`, `PriceOrFree`). We recommend you to use it instead, so you could benefit from better integration with its related GraphQL Type. ::: ### Without React Component These components will be enough in most cases. However, the issue with these is that it wraps your string within a React Element, which may be troublesome when you actually need to handle the string itself. For instance, this is the case when you want to add some label attributes to your DOM elements. ```jsx ``` Fortunately, this is correctly handled by `react-intl` if you use [`defineMessages`](https://formatjs.io/docs/react-intl/api/#definemessagesdefinemessage) combined with the [`useIntl`](https://formatjs.io/docs/react-intl/api/#useintl-hook) hook or the [`injectIntl`](https://formatjs.io/docs/react-intl/upgrade-guide-3x/#new-useintl-hook-as-an-alternative-of-injectintl-hoc) HOC. ```jsx import { defineMessages, useIntl } from "react-intl"; const messages = defineMessages({ ariaLabel: { id: "screen-reader-icon-title", defaultMessage: "Icon title displayed for screen readers", }, }); const MyComponentWithHook = (props) => { const intl = useIntl(); return ( ); }; ``` :::caution IMPORTANT You should always define a `defaultMessage`, and preferably in your default locale which you will use with the `translate` command. This will allow `FormatJS` to extract the existing messages for your application. ::: ## Translate what's in your components Now that you have defined what should be translated in your application, you actually need to translate your application. This is a two-step process: 1. Run the following script that will fetch all your translatable strings in your application and gather them in a JSON file located in `lang/[locale].json` ```shell $ front-commerce translate --locale ``` :::tip ProTip™ The script has already been defined in the [skeleton template](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/c8fc25465ab7228098bf45c3cbc712c8d2c32529/skeleton/package.json#L16), and can be used directly with `pnpm run translate`. You can change the default locale to accommodate your store. ::: 2. Translate the strings available in those files, and you are good to go 🙂 If your default locale is `en` and you want to add support for `fr` you can create a `fr.json` in your `lang` folder and translate the strings for each key. :::tip ProTip™ You can use tools like [BabelEdit](https://www.codeandweb.com/babeledit) to help translate and keep your translations in sync. ::: ### Translations fallback With `react-intl` translations are usually grouped into a single file. In our case, we would expect them to be in `lang/[locale].json`. but we don't want you to be troubled by translations that are handled by the core. That's why Front-Commerce uses a mechanism called **translations fallbacks**. Instead of relying on a single file for translations, Front-Commerce will look out for translations that are defined by extensions #### From your extension [declared in `front-commerce.config.js`](./register-an-extension.mdx) If you are developing an extension, you will need to add the path to your translations in the extension configuration file, for example lets say your translations are located in `/lang/[locale].json`. You can then run the following command command to extract the translations from your extension: ```bash $ pnpm run front-commerce translate /**/*.{js,jsx,ts,tsx} --locale ``` And then in your extension definition file you should add the `translations` option: ```ts title="extensions/acme-extension/index.ts" import { defineExtension } from "@front-commerce/core"; export default defineExtension({ // ... translations: `extensions/acme-extension/lang`, // path to your translations }); ``` This will extract the translations in your extension to the following file: - `/lang/.json` It will also allow it to be injected into the generated translation file. #### From your project - `lang/[short-locale].json` or - `lang/[locale].json` `[short-locale]` here means that we are only taking the first particle of the `locale`. E.g. if the locale was `en-GB`, the short-locale would be `en`. That's how `en-GB` would load translations from both `en.json` and `en-GB.json` files. If a translation key is defined in multiple files, the last one (according to the above list) will be be used. This is especially useful if you want to change the core's translations. :::tip ProTip™ You can see exactly which translation files are used by opening the files located in .front-commerce/compiled-lang/. ::: With `react-intl` translations are usually grouped into a single file. In our case, we would expect them to be in `lang/[locale].json`, we don't want you to be troubled by translations that are handled by the core. Please keep in mind that when you run `pnpm run translate`, new keys will be added to `lang/[locale].json` as the script only detects locales in the `app` folder. ## About dynamic content `react-intl` lets you translate your static content. But what about dynamic content? Things like product title and description, CMS pages, etc. This is done on the GraphQL side. The principle is that when a user is connected to Front-Commerce, they will be assigned a `storeViewCode` in their session (see [Configure multiple stores](./configuration/04-configure-multiple-stores.mdx) for more details). This code will then be used by your GraphQL loaders to retrieve the correct content from your backend. ## Loaders and Meta function The [`FrontCommerceApp`](../04-api-reference/front-commerce-remix/front-commerce-app.mdx) instance exposes the [`intl` object](https://formatjs.io/docs/intl) which can be used to translate strings in your loaders, which can then be used in in the meta function. ```tsx title="app/routes/my-route.ts" import { json } from "@front-commerce/remix/node"; import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { FrontCommerceApp } from "@front-commerce/remix"; import { defineMessage } from "react-intl"; const documentTitle = defineMessage({ id: "pages.my-route.document-title", defaultMessage: "My Route", }); export const loader = async ({ context }: LoaderFunctionArgs) => { const app = new FrontCommerceApp(context.frontCommerce); return json({ // highlight-next-line title: app.intl.formatMessage(messages.documentTitle), }); }; export const meta: MetaFunction = (args) => { // This will contain the translated title returned from the loader function. // highlight-next-line return [{ title: args.data.title }]; }; ``` --- # Use custom shipping information URL: https://developers.front-commerce.com/docs/3.x/guides/use-custom-shipping-information

{frontMatter.description}

:::info The method described in this guide is using the `shippingMethodAdditionalData` feature from `ExtensionComponentMap` to register a new component, which was introduced in Front-Commerce v3.10.0. If you are using an older version, you should use the legacy method described in the [v2 documentation](/docs/2.x/advanced/shipping/custom-shipping-information/). ::: Before tackling this documentation, please make sure that you've understood the [essentials](../category/get-started) of Front-Commerce and that you've registered a shipping method in your backend (Magento2…). All the code that will be created in this page should be put in a specific extension for the shipping method. If you create a specific one, please keep in mind that you need to register it in `front-commerce.config.ts`. ## Identify your shipping method If your shipping method is correctly registered in your backend, the following GraphQL query should return your method's codes. ```graphql { checkout { availableShippingMethodList( address: { country_id: "FR", postcode: "31400" } ) { data { carrier_code method_code } } } } ``` > Make sure to use an address that is eligible for your shipping method. ## Display a custom input in your shipping method Once you've done this request, you will be able to get the `method_code` of your shipping method. You should use those to register a new component by registering it through the [`ExtensionComponentMap`](../api-reference/front-commerce-core/extension-features): ```ts title="my-extension/index.ts" import { defineExtension } from "@front-commerce/core"; export default defineExtension({ name: "myShippingMethodExtension", meta: import.meta, unstable_lifecycleHooks: { onFeaturesInit: (hooks) => { hooks.registerFeature("shippingMethodAdditionalData", { ui: { componentsMap: { "": new URL( "theme/modules/CustomMethod/CustomMethod.jsx", import.meta.url ), }, }, }); }, }, }); ``` The custom component referenced here should display the additional information of your shipping method to the user. ```jsx title="theme/modules/CustomMethod/CustomMethod.jsx" import React, { useState } from "react"; import SubmitShippingMethod from "theme/modules/Checkout/ShippingMethod/SubmitShippingMethod/SubmitShippingMethod"; const CustomMethod = ({ pending, error, submitAdditionalData }) => { const [customValue, setCustomValue] = useState(); return (
setCustomValue(event.target.value)} value={customValue} name="customValue" type="text" /> { submitAdditionalData([{ key: "customValue", value: customValue }]); }} />
); }; CustomMethod.handlesNextStepButton = () => true; CustomMethod.AddressRecapLine = (props) => (
You can change how the address is displayed in the Address Recap of the checkout by declaring this component.
); export default CustomMethod; ``` Things to understand here: - The `SubmitShippingMethod` component is a common component across all shipping methods to ensure consistency in how methods are managed. - `CustomMethod.handlesNextStepButton = () => true;` means that you are indeed using `SubmitShippingMethod` in your component. This should always be `true` as `false` is the legacy behavior. - `CustomMethod.AddressRecapLine` (optional) is used to change how the shipping address is displayed in the checkout recap once you've selected your custom shipping method. For instance this is what you will use to display a pickup address instead of the shipping address filled by the user. - `submitAdditionalData` sends an array of objects with a `key` and `value` to your backend. This means that when the shipping method will be set, the additional data will be sent to your backend at the same time. We'll see in the next section how to act on them. In this example we're only setting a text input, but you can do fancier things like fetching a list of pickup points from GraphQL and displaying them here. To do so, you need to use `` component that allows to select a pickup. See [Add a shipping method with pickup points](./add-a-shipping-method-with-pickup-points) documentation for more information. ## Send the additional data to your backend Once you've registered a shipping method with additional data, the `setCheckoutShippingInformation` mutation will be sent to Front-Commerce. Front-Commerce will then send the additional data provided in this mutation to your backend. For instance, in Magento1, it'll be passed to the API at `/api/rest/frontcommerce/cart/mine/shipping-information` by sending a JSON looking like this: ```json { "shipping_carrier_code": "mondialrelaypickup", "shipping_method_code": "24R", "additional_data": [ { "key": "customValue", "value": "custom" } ] } ``` This means that you have two solutions to act upon these values in your backend: - you can either override the API in your backend (recommended way) by registering an observer (`frontcommerce_api_set_shipping_information_before_save` in Magento1's case) - or you can trigger a function in your GraphQL module before calling the backend's API by using the following method: ```js loaders.ShippingMethod.registerShipmentMethodsHandler({ method: "_", updateShippingInformation: (shippingMethod, data) => { // By registering this method, this means that you can transform the data or send a different request before calling the backend's API. // The resolved data here will be the data sent to your backend in the base API return Promise.resolve(data); }, }); ``` --- # Use temporary shipping address URL: https://developers.front-commerce.com/docs/3.x/guides/use-temporary-shipping-address

{frontMatter.description}

## Classic checkout :::info This feature is only available for Magento1 and Magento2 classic checkout. ::: When this feature is enabled, a logged in customer's address is passed to the `setCheckoutShippingInformation` mutation without an id will be used as a temporary address and not be saved in the address book. This example sets an address in customer's address book as the shipping address: ```graphql mutation { setCheckoutShippingInformation( cartId: "189" shippingMethod: { carrier_code: "flatrate", method_code: "flatrate" } shippingAddress: { id: 12 } ) { success } } ``` This example sets a shipping address that will not be saved in the address book: ```graphql mutation { setCheckoutShippingInformation( cartId: "209" shippingMethod: { carrier_code: "flatrate", method_code: "flatrate" } shippingAddress: { address: { firstname: "Testing" lastname: "Front-Commerce" street: ["42 Street of test"] postcode: "31000" city: "Toulouse" country_id: "FR" } } ) { success errorMessage } } ``` :::tip You can test these queries in the [GraphQL Playground](../api-reference/front-commerce-remix/graphql-over-http/#graphql-playground) ::: To achieve this, you will need to remove the check for addresses ids for logged in customers in the `theme/pages/Checkout/stepsDefinition.jsx`: ```diff title="theme/pages/Checkout/stepDefinitions.jsx" const steps = [ { renderProgressItem: () => renderProgressItem("address", "user", messages.address), renderStep: (props) => (
{ props.checkoutTransaction(() => { props.setEmail(email); props.setBillingAddress(billing); if (shipping) { props.setShippingAddress(shipping); } }); }} shouldAskShipping={props.checkoutState.isShippable ?? true} /> ), isValid: (checkoutState) => { - const isValidAddress = (address) => - checkoutState.checkoutType === ACCOUNT_TYPE.GUEST - ? address - : address?.id; const hasGuestInfo = checkoutState.checkoutType !== ACCOUNT_TYPE.GUEST || checkoutState.email; return ( isValidAddress(checkoutState.billingAddress) && (!checkoutState.isShippable || - isValidAddress(checkoutState.shippingAddress)) && hasGuestInfo ); }, isRelevant: () => true, isDisplayable: () => true, }, ``` You must be sure to pass a shipping address without an id to the `ChooseShippingMethod` component in the `stepsDefinition.js` file by editing the `Address` component that handles address selection. ```jsx title="theme/pages/Checkout/stepDefinitions.jsx" { const { additionalData, ...rest } = method; props.checkoutTransaction(() => { props.setShippingMethod(rest); props.setShippingAdditionalInfo(additionalData || {}); }); }} onChangeShippingAddress={() => props.gotoStepNumber(0)} /> ``` --- # Use Wishlist provider URL: https://developers.front-commerce.com/docs/3.x/guides/use-wishlist-provider

{frontMatter.description}

:::info This documentation describes the legacy `WishlistProvider` implementation that was introduced in version 2 of Front-Commerce. This approach will be updated in future versions of Front-Commerce to provide a more streamlined wishlist usage. While the current implementation is still functional, we recommend being mindful that the API and usage patterns described here may change in upcoming releases. ::: The wishlist provider was introduced in Front-Commerce to unify, simplify and optimise wishlist related queries. The wishlist provider supports the following functionalities: ## Check if wishlist is enabled This is achieved by the use of the `useIsWishlistEnabled` hook. The hook will return a boolean indicating if the wishlist feature is enabled or not. A common usage example: ```tsx const MyComponent = () => { const isWishlistEnabled = useIsWishlistEnabled(); if (isWishlistEnabled) { return
Wishlist is enabled!
; } return
Wishlist is NOT enabled :(
; }; ``` ## Load wishlist This is achieved by the use of the `useLoadWishlist` hook. The hook will return an object including either `loading`, `error` or `wishlist` depending on the query status. A common usage example: ```tsx const MyComponent = () => { const { loading, error, wishlist } = useLoadWishlist(); if (loading) { return
Loading Wishlist...
; } if (error) { return
Error Loading Wishlist :
; } if (!wishlist) { return
Wishlist feature is disabled
; } return
you have {wishlist.items.length} items in your wishlist
; }; ``` ## Load wishlist item by sku This is achieved by the use of the `useLoadWishlistItem` hook. The hook will return an object including either `loading`, `error`, or `isInWishlist` and `wishlistItem` depending on the query status. A common usage example: ```tsx const MyComponent = ({ sku }: { sku: string }) => { const { loading, error, isInWishlist, wishlistItem } = useLoadWishlistItem(sku); if (loading) { return
Loading...
; } if (error) { return
Error Loading Wishlist :(
; } if (!isInWishlist) { return
Product is NOT in your wishlist
; } return
Product is in your wishlist
; }; ``` --- # Add Content Blocks URL: https://developers.front-commerce.com/docs/3.x/extensions/content/contentful/how-to/add-content-blocks # Content Blocks

{frontMatter.description}

## Prerequisites To create a Block library, you will require a library of exposed [ContentTypes](./add-content-types) in your GraphQL schema. ## How to use Blocks? Integrating a Content Blocks Zones into your project is a 2 steps process: - [Create a Block library](#create-a-block-library) - [Independent components for each Block type](#independent-components-for-each-block-type) - [Define the Block library](#define-the-block-library) - [Retrieve and display the content](#retrieve-and-display-the-content) ## Create a Block library To ease the creation of a Block Library, Front-Commerce provides the `makeContentBlockLibrary` function. It allows developers to create a library of Blocks for the GraphQL types declared with [ContentTypes](./add-content-types). ### Independent components for each Block type First, ensure that you have created one or more components for each Block type to display. Each one of them must have a GraphQL fragment defining the data they require. A component **must accept** data of the fragment attached to it in `props.data`. Here is an example for a `BlockTitleText` to be used the Home Block library ```jsx title="theme/modules/Blocks/BlockTitleText/BlockTitleText.js" const BlockTitleText = (props) => { const { subtitle, title, align = "center" } = props.data; return (
{title && ( // we always check if the value is defined as "draft" data might not yet have the title set.

{title}

)} {subtitle && ( // same here, we should always ensure it is defined before using it.

{subtitle}

)}
); }; export default BlockTitleText; ``` and the related GraphQL fragment describing data dependencies: ```graphql title="theme/modules/Blocks/BlockTitleText/BlockTitleTextFragment.gql" fragment BlockTitleTextFragment on BlockTitleText { title subtitle align } ``` Then we can export these in the `index.js` file of the block: ```js title="theme/modules/Blocks/BlockTitleText/index.js" export { default as BlockTitleText } from "./BlockTitleText"; // We will also need the fragment to create the block library, explained below export { BlockTitleTextFragmentFragmentDocument } from "~/graphql/graphql"; ``` ### Define the Block library Several components and their fragments can now be assembled together in a Block library. Here is how to achieve this with the `makeContentBlockLibrary` factory: ```js title="theme/modules/Blocks/Home/index.js" import { makeContentBlockLibrary } from "@front-commerce/contentful/react"; import { BlockTitleText, BlockTitleTextFragmentFragmentDocument, } from "theme/modules/Blocks/BlockTitleText"; import { BlockTextImage, BlockTextImageFragmentFragmentDocument, } from "theme/modules/Blocks/BlockTextImage"; // "HomePageBlock" is the name of the GraphQL union type exposing the Block Library const blockLibrary = makeContentBlockLibrary("HomePageBlock", [ { component: BlockTitleText, fragment: BlockTitleTextFragment }, { component: BlockTextImage, fragment: BlockTextImageFragmentFragmentDocument, }, ]); // This export must be left unchanged. It allows the file to be imported // as a Fragment in a `*.gql` file. // The generated fragment will have the GraphQL type name, suffixed with "Fragment". // In this example, it will be: HomePageBlockFragment export const definitions = blockLibrary.fragmentDefinitions; // This ContentBlock component is the one that will display data correctly export default blockLibrary.ContentBlock; ``` :::info important The `export const definitions` is required to allow importing this file as a GraphQL fragment (see below). ::: ## Retrieve and display the content Now that you have defined a Block library, you could use it wherever you need to display a BlockLibrary with these Blocks. Let's continue our example and update our hypothetical `` component to display the Block library. First, update the GraphQL query to add the Block library field (`mainContent` in our example) ```graphql title="theme/pages/Home/HomeQuery.gql" #import "theme/modules/Blocks/Home" query HomeQuery { homepage { mainContent { ...HomePageBlockFragment } } } ``` Then, update the `` component to display the content: ```jsx title="theme/pages/Home/Home.js" // add-next-line import HomeBlockLibrary from "theme/modules/Blocks/Home"; // […] const Home = (props) => { const homepageData = props.data.homepage; return (

{homepageData.title}

// add-start {blocks}} /> // add-end
); }; ``` :::note The `renderBlocks` prop is optional. it allows to add a container around the blocks. In this example, we use the `Stack` component from Front-Commerce's `web/theme/components/atoms/Layout/Stack` to display the blocks vertically. ::: **That's it!** 🎉 You can now try to create new Blocks in Contentful or reorder them, and your page should reflect these changes after publication. --- # Add Content Types URL: https://developers.front-commerce.com/docs/3.x/extensions/content/contentful/how-to/add-content-types # Content Types

{frontMatter.description}

## Expose Data in GraphQL Initially, you must **update your GraphQL Schema** to mirror the ContentType definitions, which will be structured as a [GraphQL union type](https://graphql.org/learn/schema/#union-types). ```graphql type AcmeHomePage { title: String blocks: [AcmeHomePageBlock] seo: Seo } type AcmeSeo { title: String description: String } union AcmeHomePageBlock = BlockTitleText | BlockTextImage type BlockTitleText { title: String subtitle: String align: String } type BlockTextImage { title: String picture: String picturePosition: String } ``` ## Crafting `ContentTypes` To commence, create your initial ContentType for **`AcmeHomepage`**, which will be utilized to populate our homepage. The constructor of the [ContentType](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/contentful/graphql/core/domain/ContentType.js) class accepts four arguments: - `id` - The Content type id as displayed on the Contentful model detail screen. - `fcGraphQLType` - The GraphQL type defined in your schema. - `dataFormatter` - A function transforming Contentful data to fit your schema. ```js title="./my-extension/cms/contentType/Homepage.js" import { ContentType } from "@front-commerce/contentful"; import gql from "graphql-tag"; const formatContentfulData = (contentfulData) => { return { title: contentfulData.pageTitle, slug: contentfulData.pageSlug, }; }; class Homepage extends ContentType { constructor() { super("homepage", "AcmeHomepage", formatContentfulData); } get contentfulFragment() { return gql` fragment HomepageFragment on Homepage { pageTitle pageSlug } `; } } export default Homepage; ``` ## Loading Data with the `ContentfulLoader` With the ContentfulLoader, you can implement a **`contextEnhancer`** in your module to retrieve homepage data from Contentful. Should you require a list of content items rather than a singular item, the [`loadAllContentMatching`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/contentful/graphql/core/ContentfulLoader.js#L82-128) loader method is at your service. It accepts parameters similar to [`loadFirstContentMatching`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/contentful/graphql/core/ContentfulLoader.js#L82-128), with added pagination arguments: - `skip` - Number of items to bypass. - `limit` - Quantity of items to fetch. - `order` - Ordering criteria, accepted as a string or string[]. For more, see [Contentful GraphQL API](https://www.contentful.com/developers/docs/references/graphql/#/reference/collection-order). First, create the `GraphqlModule` file: ```ts title="./my-extension/cms/graphql.ts" import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "Acme/MyModule", dependencies: ["Contentful"], loadRuntime: () => import("./runtime"), }); ``` Then we will create the `GraphqlRuntime` file: ```ts title="./my-extension/cms/runtime.ts" import Homepage from "./contentType/Homepage"; import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ resolvers: { Query: { loadHomepages: async ( parent, args, { loaders: { ContentfulHomePage, Contentful } } ) => { const response = await Contentful.loadAllContentMatching( ContentfulHomePage, undefined, 0, 5 ); return response.items.map(HomePage.dataFormatter); }, }, }, contextEnhancer: ({ req, loaders }) => { return { ContentfulHomePage: new Homepage(), }; }, }); ``` ## Creating an Independent `ContentType` Our present `AcmeHomepage` is fairly basic. Enhance it by integrating an independent ContentType, such as the `seo` data for our homepage. ```js title="./my-extension/cms/contentType/shared/Seo.js" import { ContentType } from "@front-commerce/contentful"; import gql from "graphql-tag"; const formatContentfulData = (contentfulData) => { return { title: contentfulData.seoTitle, description: contentfulData.seoDescription, }; }; class Seo extends ContentType { constructor() { super("seo", "AcmeSeo", formatContentfulData); } get contentfulFragment() { return gql` fragment SeoFragment on Seo { seoTitle seoDescription } `; } } export default Seo; ``` :::note `Seo` ContentType has its distinct formatter. We transfer it to the homepage formatter and utilize it for formatting the seo data. ::: ## Integrating `ContentTypes` with `BlocksContentType` The `BlocksContentType` is a unique ContentType that facilitates the creation of a repository of ContentTypes. These can be harnessed to generate reusable [UI components](./add-content-blocks). Construct the `AcmeHomePageBlock` ContentType, which embodies the collection of ContentTypes constituting our union type. ```js title="./my-extension/cms/contentType/AcmeHomePageBlock.js" import { BlocksContentType } from "@front-commerce/contentful"; const AcmeHomePageBlock = new BlocksContentType( "homepage", "PageContentBlocksItem", [], "HomePageBlock" ); export default AcmeHomePageBlock; ``` Now, establish the `BlockTextTitle` ContentType, intended for utilization by the `AcmeHomePageBlock` ContentType. ```js title="./my-extension/cms/contentType/BlockTitleText.js" import { ContentType } from "@front-commerce/contentful"; import gql from "graphql-tag"; const formatContentfulData = (contentfulData) => { return { title: contentfulData.blockTitle, subtitle: contentfulData.excerpt, }; }; class BlockTextTitle extends ContentType { constructor() { super("blockTextTitle", "BlockTextTitle", formatContentfulData); } get contentfulFragment() { return gql` fragment BlockTextTitleFragment on BlockTextTitle { blockTitle excerpt } `; } } ``` :::note The `BlocksContentType` has a specialized formatter. We import it to the homepage formatter for blocks data formatting. ::: --- # Add X-Ray Compatibility URL: https://developers.front-commerce.com/docs/3.x/extensions/content/contentful/how-to/add-x-ray-compatibility # Storefront Content

{frontMatter.description}

## Prerequisites To use [X-Ray](https://developers.next.front-commerce.com/docs/2.x/magic-button/x-ray), ensure you have followed the [`Magic Button`](https://developers.front-commerce.com/docs/2.x/category/magic-button) documentation to setup `Contribution Mode`. ## Adding the `@storefrontContent` directive To enable the `@storefrontContent` directive, you need to add it to your graphql schema, if we take the example from [`content-types`](./add-content-types) documentation, it would look like this: ```graphql type AcmeCmsPage @storefrontContent(extractorIdentifier: "AcmeCmsPage") { title: String slug: String seo: Seo } type AcmeHomePage @storefrontContent(extractorIdentifier: "AcmeHomePage") { title: String blocks: [AcmeHomePageBlock] seo: Seo } type AcmeSeo { title: String description: String } union AcmeHomePageBlock = BlockTitleText | BlockTextImage type BlockTitleText @storefrontContent(extractorIdentifier: "BlockTitleText") { title: String subtitle: String align: String } type BlockTextImage @storefrontContent(extractorIdentifier: "BlockTextImage") { title: String picture: String picturePosition: String } ``` ## Using the `StorefrontContentType` to define your content types Previously you had to define your content types using the `ContentType` class, now you need to use the `StorefrontContentType` class instead, this class will allow you to add the `sys` to your contentfulFragment and define the `identifierValue` to map a unique identifier for this content type. ```js // remove-next-line import { ContentType } from "@front-commerce/contentful"; // add-next-line import { StorefrontContentType } from "@front-commerce/contentful"; // remove-next-line class CmsPage extends ContentType {...} // add-next-line class CmsPage extends StorefrontContentType {...} ``` ### Adding the `sys` field to you query In order to this will eventually be used to build a url to the contentful editor, the `sys` fragment is exposed in the `StorefrontContentType` class and can be called with `this.sys`. ```js get contentfulFragment() { return gql` fragment HomepageFragment on Homepage { pageTitle pageSlug ${this.sys} } `; } ``` ### Defining the `identifierValue` The `StorefrontContentType` adds an additional parameter to the constructor: the `identifierValue`. This parameter will allow `@storefrontContent` directive to extract the correct identifier from the content. > **IMPORTANT:** The `identifierValue` must be a scalar value or a function > returning one. In the above example, the slug field cannot be an array or an > object. #### Using a function to extract the identifierValue In the case were you have a content type with multiple entries, you need to define a function to extract the identifierValue, for example the `AcmeCmsPage` content type can be identified with the unique `slug` field: ```js import { StorefrontContentType } from "@front-commerce/contentful"; class CmsPage extends StorefrontContentType { constructor() { super( "cmsPage", "AcmeCmsPage", (contentfulData) => ({ title: contentfulData.pageTitle, slug: contentfulData.pageSlug, }), (formattedData) => formattedData.slug // this is the identifierValue which will be used by the @storefrontContent directive ); } get contentfulFragment() { return gql` fragment HomepageFragment on Homepage { pageTitle pageSlug ${this.sys} } `; } } ``` #### Using a scalar value as the identifierValue In the case where you have a unique content type, you can directly use a scalar value as the `identifierValue`, for example a `Homepage` content type: ```js import { StorefrontContentType } from "@front-commerce/contentful"; class Homepage extends StorefrontContentType { constructor() { super( "homepage", "AcmeHomepage", (contentfulData) => ({ title: contentfulData.pageTitle, }), "home" // Here we mapped home a scalar value as the identifierValue ); } get contentfulFragment() { return gql` fragment HomepageFragment on Homepage { pageTitle ${this.sys} } `; } } ``` ## Register your `StorefrontContentType` You need to register your defined `StorefrontContentType` via the Contentful loader. This is typically done in a loader, or a context enhancer for simpler use cases. First ensure you have a `GraphQLModule` definition: ```ts import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "Example/Contentful/Homepage", dependencies: ["Front-Commerce/Contentful/Core"], loadRuntime: () => import("./runtime"), typeDefs: /** GraphQL */ ` type AcmeCmsPage @storefrontContent(extractorIdentifier: "AcmeCmsPage") { title: String slug: String seo: Seo } type AcmeHomePage @storefrontContent(extractorIdentifier: "AcmeHomePage") { title: String blocks: [AcmeHomePageBlock] seo: Seo } type AcmeSeo { title: String description: String } union AcmeHomePageBlock = BlockTitleText | BlockTextImage type BlockTitleText @storefrontContent(extractorIdentifier: "BlockTitleText") { title: String subtitle: String align: String } type BlockTextImage @storefrontContent(extractorIdentifier: "BlockTextImage") { title: String picture: String picturePosition: String } `, }); ``` Then you can add your `GraphqlRuntime` definition to register your `StorefrontContentType`: ```js import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ contextEnhancer: ({ loaders }) => { const sharedBlocks = { TitleText: new BlockTitleText(), TextImage: new BlockTextImage(), }; const Homepage = new HomepageLoader(sharedBlocks); // add-next-line loaders.Contentful.registerStorefrontContentType(Homepage); // add-start for (const block of Object.values(sharedBlocks)) { if (block instanceof StorefrontContentType) { loaders.Contentful.registerStorefrontContentType(block); } } // add-end return { Homepage, }; }, }); ``` :::info IMPORTANT You need to register all the content types which use the `@storefrontContent` directive, and they should all extend the `StorefrontContentType` class. ::: ## Adding the `StorefrontContent` component in your application You need to add the [`StorefrontContent` component](https://developers.next.front-commerce.com/docs/2.x/magic-button/x-ray#add-storefrontcontent--in-your-react-components) in your application, this component will be used to wrap the contentful data and add the edit link. ```js export default function CmsPage(props) { const { data } = props; // represents the data from your query; return (

{data.title}

); } ``` --- # Configure Preview Mode URL: https://developers.front-commerce.com/docs/3.x/extensions/content/contentful/how-to/configure-preview-mode

{frontMatter.description}

:::caution Alpha Feature This is currently an alpha feature. The API could potentially undergo significant changes in the future. ::: ## Prerequisites To use Contentful Preview, ensure you have followed the [`Magic Button`](https://developers.front-commerce.com/docs/2.x/category/magic-button) documentation to setup `Contribution Mode`. You need to add a `FRONT_COMMERCE_CONTENTFUL_PREVIEW_ACCESS_TOKEN` environment variable to your `.env` file. This token will be used to authenticate the preview requests. ## How to Configure Contentful space To take advantage of Contentful's Preview Mode, you need to set up URL configurations for the [preview environment](https://www.contentful.com/help/setup-content-preview#preview-content-in-your-online-environment) in your Contentful space. Follow these steps to configure your Contentful settings: 1. Navigate to `Settings` > `Content Preview` in your Contentful space. 2. Click the `Add content preview` button. You'll need to provide the following information: - `Name`: Enter the name for this particular content type preview. - `Description`: Provide a brief description of this content type preview. - `Content preview URLs`: Enter a list of URLs that you'd like to use to preview different content types. In Front-Commerce, we provide a specific endpoint, `/contentful/preview/:space/:contentType/:id`, that you can use as a `Content preview URL`. Here is a sample format to follow when configuring the URL: ``` https://acme.app/contentful/preview/{env_id}/{entry.sys.contentType.sys.id}/{entry.sys.id}?secret=YOUR_PREVIEW_ACCESS_TOKEN ``` Where: - `env_id` corresponds to the environment of the current entry. - `entry.sys.contentType.sys.id` will resolve to the content type id of the current entry. - `entry.sys.id` will resolve to the entry id. :::tip Due to technical limitations in Contentful, to preview a local environment, you must either use `ngrok` to expose your local server to the internet and benefit from the Live Preview feature (iframe), or open the preview in another window. Replace `acme.app` in the sample URL above with your ngrok or localhost URL accordingly. ::: ## Configuring Your Application By default, the preview route will direct you to the landing page (`/`) of your application. However, you can customize this behavior by registering an _URL matcher_. The `registerUrlMatcher` method is accessible through the Contentful loader's `routes` object, it accepts two parameters: - `contentType`: The contentful GraphQL type name of the entry you want to match. - `urlFormatter`: A function that receives the entry id and the entry metadata as arguments. This function should return a string or null. The `urlFormatter` function determines the URL to which the preview route will redirect: - If `urlFormatter` returns a string, the preview route will redirect to the URL defined by that string. - If `urlFormatter` returns null, the preview route will default to the landing page. Here's an example of how you might register a `urlMatcher`: ```js import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ [..], contextEnhancer: ({loaders}) => { loaders.Contentful.routes.registerUrlMatcher("Page", (id, metadata) => { if (id === "dntmgP2QV6uRuRChrdLFj") { return `/contact-us`; } return null; }); loaders.Contentful.routes.registerUrlMatcher("BlogPost", async (id) => { const blogpost = await loaders.ContentfulClient.getEntry(id); if (!blogpost.fields.slug) { return null; } return `/blog/${blogpost.fields.slug}`; }); } }) ``` This allows you to control the behavior of your application's preview mode on a per-content-type basis, enhancing flexibility and customization. --- # Contentful URL: https://developers.front-commerce.com/docs/3.x/extensions/content/contentful/index

{frontMatter.description}

## Prerequisites Before you can use the Contentful extension, you need to have the following: - Access to a [Contentful](https://www.contentful.com/) instance - A working [Front-Commerce environment](/docs/3.x/get-started/installation) To set up a Contentful instance, please refer to the [Contentful documentation](https://www.contentful.com/developers/docs/). To set up a Front-Commerce environment, please refer to the [Front-Commerce documentation](/docs/3.x/get-started/installation). ## Installation To install the Contentful extension, run the following command: ```bash $ pnpm install @front-commerce/contentful@latest ``` ## Configuration To use the Contentful extension, you need to configure your environment and your Front-Commerce application. ### Environment variables In the `.env` file, you need to define the following environment variables: - `FRONT_COMMERCE_CONTENTFUL_SPACE_ID`: The ID of your Contentful space - `FRONT_COMMERCE_CONTENTFUL_ACCESS_TOKEN`: The access token for your Contentful space - `FRONT_COMMERCE_CONTENTFUL_PREVIEW_ACCESS_TOKEN`: The preview access token for your Contentful space Optionally, you can also configure a different environment using `FRONT_COMMERCE_CONTENTFUL_ENVIRONMENT=xxx`. You can also explicitly override the locale used when retrieving Contentful content by using the `FRONT_COMMERCE_CONTENTFUL_DANGEROUSLY_FORCED_LOCALE` environment variable or with the contentful.locale configuration from a [configuration provider](/docs/2.x/advanced/server/configurations#what-is-a-configuration-provider). Please note however that this is not recommended by Front-Commerce. ### Front-Commerce application To use the Contentful extension in your Front-Commerce application, you need to register the extension in your `front-commerce.config.ts` file: ```ts title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; import magento2 from "@front-commerce/magento2"; // add-next-line import contentful from "@front-commerce/contentful"; import storesConfig from "./app/config/stores"; import cacheConfig from "./app/config/caching"; export default defineConfig({ extensions: [ magento2({ storesConfig }), themeChocolatine(), // add-next-line contentful(), ], stores: storesConfig, cache: cacheConfig, configuration: { providers: [], }, }); ``` --- # API Reference URL: https://developers.front-commerce.com/docs/3.x/extensions/content/contentful/reference/api import Description from "@site/src/components/Description"; ## Server API ### `ContentType` For more details, see: - [ContentType](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/contentful/graphql/core/domain/ContentType.js) API - [Add Content Types](../how-to/add-content-types) Documentation ### `BlocksContentType` For more details, see: - [BlocksContentType](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/contentful/graphql/core/domain/BlocksContentType.js) API - [Integrating ContentTypes with BlocksContentType](../how-to/add-content-types#integrating-contenttypes-with-blockscontenttype) Documentation ### `StorefrontContentType` For more details, see: - [StorefrontContentType](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/contentful/graphql/core/domain/StorefrontContentType.js) API - [X-Ray compatibility](../how-to/add-x-ray-compatibility) Documentation ## Client API ### `ContentBlock` For more details, see: - [ContentBlock](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/contentful/react/ContentBlock.jsx) API - [Add Content Blocks](../how-to/add-content-blocks) Documentation ### `makeContentBlockLibrary` For more details, see: - [makeContentBlockLibrary](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/contentful/react/makeContentBlockLibrary.jsx) API - [Define the Block library](../how-to/add-content-blocks#define-the-block-library) Documentation --- # Environment variables URL: https://developers.front-commerce.com/docs/3.x/extensions/content/contentful/reference/environment-variables

{frontMatter.description}

| Name | Description | | ----------------------------------------------------- | ----------------------------------------------------------------------- | | `FRONT_COMMERCE_CONTENTFUL_SPACE_ID` | **Required** The Contentful space id | | `FRONT_COMMERCE_CONTENTFUL_ACCESS_TOKEN` | **Required** The Contentful access token | | `FRONT_COMMERCE_CONTENTFUL_PREVIEW_ACCESS_TOKEN` | **Required** The Contentful preview access token | | `FRONT_COMMERCE_CONTENTFUL_ENVIRONMENT` | The Contentful environment (default: `master`) | | `FRONT_COMMERCE_CONTENTFUL_DANGEROUSLY_FORCED_LOCALE` | The Contentful locale | | `FRONT_COMMERCE_CONTENTFUL_DOMAIN` | The Contentful domain URL (default: `"https://graphql.contentful.com"`) | --- # Adding Content Slices URL: https://developers.front-commerce.com/docs/3.x/extensions/content/prismic/how-to/adding-content-slices

{frontMatter.description}

import TOCInline from "@theme/TOCInline"; With [Content Slices](https://prismic.io/docs/slice), Front-Commerce embraces the **"Don't Ship Pages, Ship a Page Builder"** vision brought by Prismic. See how to make slices a part of your authoring strategy. ## How to use Slices? Integrating a Prismic Slice Zone and its Slices into your project is a 3 steps process:
!["how-to-use-slices"].includes(id))} maxHeadingLevel={4} />
## Expose data in GraphQL In this example, we will add a Slice Zone with 3 types of Slices to an existing home page. First, [**update your GraphQL Schema**](../../../../02-guides/extend-the-graphql-schema.mdx) to reflect the Slice Zone definition. A Slice Zone will be designed as a [GraphQL union type](https://graphql.org/learn/schema/#union-types). ```graphql title="my-extension/schema.gql" type Homepage { title: String // add-next-line mainContent: [HomePageSlice] } // add-start union HomePageSlice = Carousel | Push type Carousel { slides: [CarouselSlide] } type Push { blocks: [PushBlock] } type PushBlock { title: String image: String cta: CallToAction format: String cellSize: String } #[…] type CallToAction { url: String text: String } // add-end ``` In your resolvers, load your content as usual [with the Prismic loader](./loading-prismic-content.mdx#loading-content). In addition to [the `fieldTransformers` parameter](./loading-prismic-content.mdx#transforming-fields) in the [defineContentTransformers](./loading-prismic-content.mdx#transforming-fields), the definition accepts a `supportedSlices` key. It allows to define to a data structure for the Slices available in the Custom Type. For a Custom `Homepage` type containing a title and a Slice Zone with the slices declared above (`HomePageSlice`), the change would look like this: ```js title="my-extension/runtime.ts" import { type PrismicLoaders } from "@front-commerce/prismic/graphql"; import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ contextEnhancer: ({ config, loaders }) => { const PrismicLoader = (loaders as PrismicLoaders).Prismic; const { GroupTransformer, TitleTransformer, ImageTransformer, LinkTransformer, RichtextToTextTransformer } = PrismicLoader.transformers; const contentTransformOptions = { fieldTransformers: { title: new TitleTransformer(), }, // add-start supportedSlices: [ new Slice("carousel", { graphQLType: "Carousel", fieldTransformers: { items: new GroupTransformer({ slide_title: new TitleTransformer(), slide_image: new ImageTransformer(), slide_cta_url: new LinkTransformer(), slide_cta_text: new RichtextToTextTransformer(), }), }, }), new Slice("push", { graphQLType: "Push", fieldTransformers: { items: new GroupTransformer({ push_title: new TitleTransformer(), push_block_image: new ImageTransformer(), push_url: new LinkTransformer(), push_link_text: new RichtextToTextTransformer(), }), }, }), // ... add other Slice definitions here ], // add-end } PrismicLoader.defineContentTransformers( "home_page", contentTransformOptions ); return {}; }, }); ``` The resolved content will now contain Slices contributed for the Slice Zone in the `content.body` field. Repeatable fields in a Slice are always made available under the `content.items` field of the Slice content. Here is an example showcasing how resolvers could allow to adapt Prismic content to the schema you've designed (and overcome the technical naming constraint brought by Prismic): ```ts title="my-extension/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ contextEnhancer: ({ config, loaders }) => { // [...] }, resolvers: { // highlight-start Homepage: { mainContent: (content) => content.body, }, Carousel: { slides: (content) => content.items, }, Push: { blocks: (content) => content.items, }, PushBlock: { cta: (content) => ({ url: content.push_url, text: content.push_link_text, }), }, // highlight-end }, }); ``` That's it! You should now be able to view data from Prismic when requesting your application's GraphQL server: ```graphql query HomeQuery { homepage { title mainContent { __typename ... on Carousel { slides { title } } ... on Push { blocks { title image cta { url text } } } } } } ``` Let's now see how to map this data to frontend components. ## Render your Slices To ease the creation of a Slice Zone, Front-Commerce provides the [`Content Composition`](../../../../02-guides/content-composition/index.mdx) API, which allows you to create reusable building blocks for your content. ### Create your Content Composition To see a full implementation please refer to the [Content Composition Tutorial](../../../../02-guides/content-composition/index.mdx#tutorial). This will be used later to register the composition in your extension config. ```ts title="my-extension/slices/HomePageSlice.ts" import { createContentComposition } from "@front-commerce/core"; // The composed fragment will be named `HomePageSliceFragment` export const HomePageSliceComposition = createContentComposition( "HomePageSlice", [ { name: "Carousel", // The fragment name needs to match this name, eg. `CarouselFragment` client: { // Component path relative to the current file via the import.meta.url component: new URL("./components/Carousel.tsx", import.meta.url), fragment: /* GraphQL */ ` fragment CarouselFragment on Carousel { slides { title image cta { url text } } } `, }, }, { name: "Push", // The fragment name needs to match this name, eg. `ProductsListFragment` client: { // Component path relative to the current file via the import.meta.url component: new URL("./components/Push.tsx", import.meta.url), fragment: /* GraphQL */ ` fragment PushFragment on Push { blocks { title image cta { url text } } } `, }, }, ] ); ``` A component **must accept** props of it's fragment type and have a **default export**. Here is an example for a `Carousel` slice to be used the `Home` content composition: ```tsx title="my-extension/slices/components/Carousel.tsx" // […] import type { Carousel as CarouselProps } from "~/graphql/graphql"; // component needs to be a default export export default function Carousel(props: CarouselProps) { return (
{props.slides.map((slide, index) => (
{slide.title}
))}
); } ```
A component **must accept** props of it's fragment type and have a **default export**. Here is an example for a `Push` slice to be used the `Home` content composition: ```tsx title="my-extension/slices/components/Push.tsx" // […] import type { Push as PushProps } from "~/graphql/graphql"; // component needs to be a default export export default function Push(props: PushProps) { return (
{props.blocks.map((block, index) => (

{block.title}

{block.cta.text}
))}
); } ```
### Register your Content Composition Next we need to register the composition in your extension config. ```ts title="my-extension/index.ts" import { defineExtension } from "@front-commerce/core"; import { HomePageSliceComposition } from "./slices/HomePageSlice"; export default defineExtension({ name: "my-module", meta: import.meta, // highlight-start unstable_lifecycleHooks: { onContentCompositionInit: (composition) => { composition.registerComposition(HomePageSliceComposition); }, }, // highlight-end }); ``` This will ensure that all the components and fragments are generated for your runtime. ### Retrieve and display content Now that you have registered your composition, you could use it wherever you need to display your `HomePageSlice`. Let's continue our example and update our hypothetical `` component to display the Slices. First, update the GraphQL query to add the Slice zone field (`mainContent` in our example): ```graphql query HomeQuery { homepage { title mainContent { # Fragment name = {Composition name}Fragment // add-next-line ...HomePageSliceFragment } } } ``` Then, pass these data to the `CompositionComponent` which has the generated components components map: ```tsx title="my-extension/theme/pages/Home.tsx" import { CompositionComponent } from "@front-commerce/core/react"; import { HomePage } from "~/graphql/graphql"; // […] const Home = ({ homepage }: HomePage) => { return (

{homepage.title}

{components}} renderComponent={(component) => { if (component.props.__typename !== "Carousel") { return null; // only display the carousel component } return component; }} />
); }; ``` :::note Field data must be passed as the `content` prop. The `renderComponents` prop is optional, it allows you to customize the rendering of the array of components. The `renderComponent` prop is optional, it allows you to customize the rendering of a single component. :::
--- **This is it!** 🎉 You can now try to create new Slices in Prismic or reorder them, and your page should reflect these changes after publication. --- # Adding Embed Fields URL: https://developers.front-commerce.com/docs/3.x/extensions/content/prismic/how-to/adding-embed-fields

{frontMatter.description}

import Figure from "@site/src/components/Figure"; In Front-Commerce we expose the embed fields in two ways; - Standalone Embed fields with an EmbedTransformer - Embed Fields in [`Wysiwyg`](../../../../02-guides/customize-wysiwyg-platform.mdx) with [`PrismicWysiwyg`](./customizing-prismic-wysiwyg.mdx) In this section we will cover how to implement both of these methods, and how to implement your own custom embed fields. :::note You can read more about the oEmbed format and view a list of supported providers on the [oEmbed website](https://oembed.com/). ::: ## Standalone Embed Fields You can learn how to [configure an embed field](https://prismic.io/docs/field#embed) in the prismic documentation. For this example let's say we want to create an album cover from an embed field. ### Adding an Embed Field in your GraphQL Module First add an embed field in your schema using the [`oEmbedContent`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/784684ce56cca69ca5c2e42d5d421a8c0b4bb9c3/modules/prismic/server/modules/prismic/core/schema.gql) type, for example: ```graphql type MyAlbum { title: String cover: oEmbedContent } ``` Then you can add the [`EmbedTransformer`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/784684ce56cca69ca5c2e42d5d421a8c0b4bb9c3/modules/prismic/server/modules/prismic/core/loaders/transformers/Embed.js) to your album definition which will parse the embed response from Prismic as a rich [`oEmbedContent`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/784684ce56cca69ca5c2e42d5d421a8c0b4bb9c3/modules/prismic/server/modules/prismic/core/schema.gql) type. ```tsx title="my-extension/graphql/album/runtime.js" import { type PrismicLoaders } from "@front-commerce/prismic/graphql"; import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ contextEnhancer: ({ loaders }: { loaders: PrismicLoaders }) => { // remove-next-line const { TitleTransformer } = loaders.Prismic.transformers; // add-next-line const { TitleTransformer, EmbedTransformer } = loaders.Prismic.transformers; loaders.Prismic.defineContentTransformers("album", { fieldTransformers: { title: new TitleTransformer() // add-next-line cover: new EmbedTransformer(), }, }); return {}; } }) ``` ### Adding an Embed Field on your Client Side We have added a [`PrismicEmbed`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/b187d9592558b34058e958102118c10bcfdea1af/packages/prismic/theme/modules/Prismic/PrismicEmbed/PrismicEmbed.jsx) component to handle the embed response in you client side. To simplify the usage between the component and the backend data we created a [`PrismicEmbedFragment`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/b187d9592558b34058e958102118c10bcfdea1af/packages/prismic/theme/modules/Prismic/PrismicEmbed/PrismicEmbedFragment.gql) which contains the minimum required data for the component, for example: ```graphql #import "theme/modules/Prismic/PrismicEmbed/PrismicEmbedFragment.gql" fragment AlbumFragment on Album { title cover { ...PrismicEmbedFragment } } ``` The fragment will expose several fields from the [`oEmbedContent`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/784684ce56cca69ca5c2e42d5d421a8c0b4bb9c3/modules/prismic/server/modules/prismic/core/schema.gql) type. ```tsx title="my-extension/theme/modules/Prismic/PrismicEmbed/Embed/Album.tsx" import type { Album as AlbumProps } from "~/graphql/graphql"; const Album = (props: AlbumProps) => { return
/* Custom Embed Logic */
; }; ``` You can override the default components for each embed type in your own theme | Type | Component | | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `photo` | [`theme/modules/Prismic/PrismicEmbed/Embed/Photo.jsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/b187d9592558b34058e958102118c10bcfdea1af/packages/prismic/theme/modules/Prismic/PrismicEmbed/Embed/Photo.jsx) | | `video` | [`theme/modules/Prismic/PrismicEmbed/Embed/Video.jsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/b187d9592558b34058e958102118c10bcfdea1af/packages/prismic/theme/modules/Prismic/PrismicEmbed/Embed/Video.jsx) | | `link` | [`theme/modules/Prismic/PrismicEmbed/Embed/Link.jsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/b187d9592558b34058e958102118c10bcfdea1af/packages/prismic/theme/modules/Prismic/PrismicEmbed/Embed/Link.jsx) | | `rich` | [`theme/modules/Prismic/PrismicEmbed/Embed/Rich.jsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/b187d9592558b34058e958102118c10bcfdea1af/packages/prismic/theme/modules/Prismic/PrismicEmbed/Embed/Rich.jsx) | As mentioned above, the fragment only exposes the minimum required fields for the PrismicEmbed component, if you would like to create your own custom embed component you can refer to all the available fields on the [`oEmbedContent`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/7180bc44200042064b38f716f6a9038604693057/packages/prismic/modules/core/index.js#L6-64) type.
## Wysiwyg Embed Fields ### Using the `PrismicWysiwyg` type Update your schema to use the [`PrismicWysiwyg`](./customizing-prismic-wysiwyg.mdx) type instead of the [`DefaultWysiwyg`](../../../../02-guides/customize-wysiwyg-platform.mdx#defaultwysiwyg). ```graphql type MyAlbum { title: String cover: oEmbedContent // remove-next-line content: DefaultWysiwyg // add-next-line content: PrismicWysiwyg } ``` Then use the `RichtextToWysiwygTransformer` which will transform the Prismic Richtext to the Wysiwyg format. ```tsx title="my-extension/graphql/album/runtime.js" import { type PrismicLoaders } from "@front-commerce/prismic/graphql"; import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ contextEnhancer: ({ loaders }: { loaders: PrismicLoaders }) => { // remove-next-line const { TitleTransformer, EmbedTransformer } = loaders.Prismic.transformers; // add-next-line const { TitleTransformer, EmbedTransformer, RichtextToWysiwygTransformer } = loaders.Prismic.transformers; loaders.Prismic.defineContentTransformers("album", { fieldTransformers: { title: new TitleTransformer() cover: new EmbedTransformer(), // add-next-line content: new RichtextToWysiwygTransformer(loaders.Wysiwyg), }, }); return {}; } }) ``` ### Adding additional Embed loading scripts We have added default loading scripts for the following embeds: - `Twitter` - `Facebook` - `Instagram` You can add your own by overriding the [`theme/modules/Wysiwyg/PrismicWysiwyg/Components/EmbedScript/appEmbeds.js`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/b187d9592558b34058e958102118c10bcfdea1af/packages/prismic/theme/modules/Wysiwyg/PrismicWysiwyg/Components/EmbedScript/appEmbeds.js#L5-13) file. - This file accepts a record with the key as the embed [provider](https://oembed.com/providers.json) (case sensitive). - The value contains a `load` function and optionally an `src` if you would like to use a custom script. ```js title="theme/modules/Wysiwyg/PrismicWysiwyg/Components/EmbedScript/appEmbeds.js" const appEmbeds = { // Add you custom embed script here eg: Facebook: { // this will override the `Facebook` script included by Front-Commerce src: "https://example.com/my-custom-facebook-script.js", // optional src (it's not recommended to change this) load: (ref) => { // this is only triggered once the script load event is fired if (typeof window !== "undefined" && window.FB) { window.FB.XFBML.parse(ref); } }, }, }; export default appEmbeds; ``` ## Custom HTML and Raw Embed Fields To add Custom HTML or Raw Embed Fields you would need to make some customization to the `PrismicWysiwyg` component. ### Setup your Prismic Rich text You need to setup your Prismic Rich text field to allow the `pre` tag in your Prismic dashboard.
![Enable pre tags in Rich Text](./assets/embed-fields/rich-text-pre-tags.png)
### Create your custom component This component will be used to inject the html from the `pre` tag into the dom. ```jsx title="theme/modules/Wysiwyg/PrismicWysiwyg/Components/Pre.jsx" const Pre = (props) => { const html = props.children[0].props.children || ""; return html ?
: null; }; export default Pre; ``` To improve the loading of scripts you can implement your own `useEffect` to lazy load the specific script by detecting the provider in the HTML raw text, you might also be interested in implementing a helper like [`dangerously-set-html-content`](https://github.com/christo-pr/dangerously-set-html-content) which can better handle to loading of injected scripts. ### Register your custom component You can register the component by overriding the [`theme/modules/Wysiwyg/PrismicWysiwyg/appComponentsMap.js`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/b187d9592558b34058e958102118c10bcfdea1af/packages/prismic/theme/modules/Wysiwyg/PrismicWysiwyg/appComponentsMap.js) file. ```js title="theme/modules/Wysiwyg/PrismicWysiwyg/appComponentsMap.js" import Pre from "./Components/Pre"; const appComponentsMap = { pre: Pre, }; export default appComponentsMap; ``` ### Example of a custom embed Here is an example of a custom embed field in a Prismic document. ```html ``` Here is a preview of how the content will look in the rendered Wysiwyg component.
![Raw Embed Field](./assets/embed-fields/raw-embed-field.png)
:::tip This can also be done in slices, see the [Add Custom Embed or HTML code](https://community.prismic.io/t/add-custom-embed-or-html-code/5455) article for more information. ::: --- # Adding Integration Fields URL: https://developers.front-commerce.com/docs/3.x/extensions/content/prismic/how-to/adding-integration-fields

{frontMatter.description}

## What are Integration Fields? [Integration fields](https://prismic.io/feature/integration-field) are a Prismic feature that allows content managers to reference external data in their content. In Front-Commerce, this means they can select products, categories, or any other data from your application directly in the Prismic writing room. Key benefits: - Content managers can build landing pages with real product data without leaving Prismic - Developers can access the referenced data through GraphQL, reusing existing fragments and components - Changes in your application's data are automatically reflected in Prismic ## Understanding Integration Fields Front-Commerce provides two main ways to implement Integration Fields: ### Custom Integration Fields The `IntegrationField` class is the foundation for creating custom integration fields. It allows you to: - Define how to fetch and paginate your data - Transform your data into [Prismic's expected format](https://prismic.io/docs/integration#custom-api-format) - Add custom metadata through the `blob` property - Control pagination behavior ```typescript interface UserData { userId: string; name: string; email: string; updated_at: string; avatar_url?: string; } const userIntegrationField = new IntegrationField({ // Function to fetch paginated results from your data source loadPullResults: async (page, shopUrl) => { const users = await fetchUsers(page); return { result_size: users.total, // By default `autoPaginate` is true, so we don't need to paginate manually // If you prefer to paginate manually, set `autoPaginate` to false. // Prismic is limited to 50 results per page. results: users.items.map((user) => ({ id: user.userId, title: user.name, description: user.email, image_url: user.avatar_url ? new URL(user.avatar_url, shopUrl).toString() : undefined, last_update: new Date(user.updated_at).valueOf(), blob: user, // Original data available in resolveEntity })), }; }, // Function to resolve the entity in your GraphQL context resolveEntity: (blob, context) => { return context.loaders.User.load(blob.userId); }, // Optional: control pagination behavior of results. // By default `autoPaginate` is true, so we don't need to paginate manually // If you prefer to paginate manually, set `autoPaginate` to false autoPaginate: true, }); ``` ### Sitemap Integration Fields The `SitemapIntegrationField` is a specialized implementation that leverages your existing sitemap data. See our [Customize the sitemap](../../../../02-guides/customize-the-sitemap.mdx) guide for details. It's perfect for exposing products, categories, and other URL-based content: ```typescript const productIntegrationField = new SitemapIntegrationField< MagentoSitemapProduct, Product >({ // Name of your sitemap fetcher sitemapFetcher: "products", // Convert sitemap entry to Prismic's format convertSitemapEntry: (entry, shopUrl) => { if (!entry.data) return null; let imageUrl = entry.images?.[0]; if (imageUrl) { imageUrl = new URL(imageUrl, shopUrl).toString(); } return { id: entry.data.sku, title: entry.data.name, description: entry.data.short_description, image_url: imageUrl, last_update: new Date(entry.data.updated_at).valueOf(), }; }, // Resolve the entity in your GraphQL context resolveEntity: (blob, context) => { return context.loaders.Product.load(blob.id); }, }); ``` ## Implementation Guide ### Register Your Integration Fields Here's a complete example of how to implement and register integration fields: ```typescript import { defineExtension } from "@front-commerce/core/extension"; import { ProductIntegrationField } from "./integration-fields/product"; import { CategoryIntegrationField } from "./integration-fields/category"; export default defineExtension({ unstable_lifecycleHooks: { afterServerServicesInit: async (services) => { const { integrationFields } = services.DI.get("prismic"); // Register both product and category integration fields integrationFields.registerSitemapField( "Product", ProductIntegrationField ); integrationFields.registerSitemapField( "Category", CategoryIntegrationField ); }, }, }); ``` ```typescript import { SitemapIntegrationField } from "@front-commerce/prismic"; import type { MagentoSitemapProduct } from "@front-commerce/magento1"; import type { Product } from "~/graphql/graphql"; export const ProductIntegrationField = new SitemapIntegrationField< MagentoSitemapProduct, Product >({ sitemapFetcher: "products", convertSitemapEntry: (sitemapEntry, shopUrl) => { if (!sitemapEntry.data) { return null; } let imageUrl = sitemapEntry.images?.[0]; if (imageUrl) { imageUrl = new URL(imageUrl, shopUrl).toString(); } return { id: sitemapEntry.data.sku, title: sitemapEntry.data.name, description: sitemapEntry.data.short_description, image_url: imageUrl, last_update: new Date(sitemapEntry.data.updated_at).valueOf(), }; }, resolveEntity: (blob, context) => { return context.loaders.Product.load(blob.id); }, }); ``` ```typescript import { SitemapIntegrationField } from "@front-commerce/prismic"; import type { MagentoSitemapCategory } from "@front-commerce/magento1"; import type { Category } from "~/graphql/graphql"; export const CategoryIntegrationField = new SitemapIntegrationField< MagentoSitemapCategory, Category >({ sitemapFetcher: "category", convertSitemapEntry: (sitemapEntry, shopUrl) => { if (!sitemapEntry.data) { return null; } let imageUrl = sitemapEntry.images?.[0]; if (imageUrl) { imageUrl = new URL(imageUrl, shopUrl).toString(); } let description = sitemapEntry.data.meta_description; if (!description) { description = new URL(sitemapEntry.data.route, shopUrl).toString(); } return { id: sitemapEntry.data.entity_id, title: sitemapEntry.data.name, description: description, image_url: imageUrl, last_update: new Date(sitemapEntry.data.updated_at).valueOf(), }; }, resolveEntity: (blob, context) => { return context.loaders.Category.load(blob.id); }, }); ``` ### Configure in Prismic 1. Go to **Settings > Integration Fields** in your Prismic repository 2. Create a new **Custom API** Integration Field 3. Configure the endpoint: - URL: `https://your-store.example.com/prismic/integration/{name}` - Access Token: Your `FRONT_COMMERCE_PRISMIC_WEBHOOK_SECRET` value ![Integration field creation form](./assets/integration-fields/new-integration-field.jpg) ### Add to Your Custom Types Add the Integration Field to your Prismic Custom Types: ![Custom type field from Integration field](./assets/integration-fields/configure-integration-field.jpg) ### Expose in GraphQL 1. Define your GraphQL type: ```graphql type MyPrismicContent { uid: ID title: String fc_product: Product # References your existing Product type fc_category: Category # References your existing Category type } ``` 2. Add the transformer: ```typescript import { ProductIntegrationField } from "./integration-fields/product"; import { CategoryIntegrationField } from "./integration-fields/category"; export default createGraphQLRuntime({ contextEnhancer: ({ loaders }) => { const { IntegrationFieldTransformer } = loaders.Prismic.transformers; loaders.Prismic.defineContentTransformers("my-type", { fieldTransformers: { fc_product: new IntegrationFieldTransformer(ProductIntegrationField), fc_category: new IntegrationFieldTransformer( CategoryIntegrationField ), }, }); return {}; }, // add-end }); ``` 3. Create resolvers: ```typescript import { ProductIntegrationField } from "./integration-fields/product"; import { CategoryIntegrationField } from "./integration-fields/category"; // This is a helper for the types of the integration fields transformed by the IntegrationFieldTransformer import { IntegrationFieldResolverContent } from "@front-commerce/prismic/graphql"; export default createGraphQLRuntime({ contextEnhancer: (context) => { // [..] }, // add-start resolvers: { MyPrismicContent: { fc_product: ( content: IntegrationFieldResolverContent< "product", typeof ProductIntegrationField > ) => { return content.product.loadValue().catch(() => null); }, fc_category: ( content: IntegrationFieldResolverContent< "category", typeof CategoryIntegrationField > ) => { return content.category.loadValue().catch(() => null); }, }, }, // add-end }); ``` ## Testing Your Integration You can test your integration field endpoint using curl: ```bash curl --user 'your-webhook-secret:' http://localhost:4000/prismic/integration/Product curl --user 'your-webhook-secret:' http://localhost:4000/prismic/integration/Product?page=2 ``` ```bash curl --user 'your-webhook-secret:' http://localhost:4000/prismic/integration/Category curl --user 'your-webhook-secret:' http://localhost:4000/prismic/integration/Category?page=2 ``` :::important Integration fields are limited to 50 results per page. Implement proper pagination in your `loadPullResults` method if you have more data. The Sitemap Integration Field automatically paginates for you. ::: ## Troubleshooting If your integration isn't working: 1. Enable debug logs with `DEBUG=front-commerce:prismic:integration-fields` 2. Verify your endpoint URL and access token 3. Check the response format matches [Prismic's Custom API Format](https://prismic.io/docs/core-concepts/integration-fields-setup#pull-data-into-prismic-from-a-custom-api) 4. Ensure your resolvers correctly handle the `blob` data ## Next Steps - Learn how to [customize the Prismic WYSIWYG](./customizing-prismic-wysiwyg.mdx) - Explore [adding content slices](./adding-content-slices.mdx) - Understand [using the resolver cache](./using-resolver-cache.mdx) --- # Customizing PrismicWysiwyg URL: https://developers.front-commerce.com/docs/3.x/extensions/content/prismic/how-to/customizing-prismic-wysiwyg

{frontMatter.description}

### Customizing `PrismicWysiwyg` with [`Content Composition API`](../../../../02-guides/content-composition/index.mdx) To customize how the PrismicWysiwyg renders content, you can register your own components and data fetching logic using the Content Composition API. :::tip If you don't need to extend the GraphQL Fragment, you can simplify override the [`appComponentsMap.js`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/b187d9592558b34058e958102118c10bcfdea1af/packages/prismic/theme/modules/Wysiwyg/PrismicWysiwyg/appComponentsMap.js) file with your custom components, and skip the following steps. ::: #### Register your custom components Create a new extension and register your customizations for the PrismicWysiwyg: ```ts title="extensions/acme-extension/index.ts" export default defineExtension({ unstable_lifecycleHooks: { onContentCompositionInit: (composition) => { composition.register("Wysiwyg", [ { name: "PrismicWysiwyg", client: { component: new URL( "./theme/components/CustomPrismicWysiwyg.tsx", import.meta.url ), }, fragment: /* GraphQL */ ` fragment CustomPrismicWysiwygFragment on PrismicWysiwyg { childNodes data { dataId # Add any additional fields you need } } `, }, ]); }, }, }); ``` Create your custom PrismicWysiwyg component with your own component mappings: ```tsx title="extensions/acme-extension/components/CustomPrismicWysiwyg.tsx" import { useMemo } from "react"; import renderChildNodes from "theme/modules/Wysiwyg/renderChildNodes"; import { defaultComponentsMap } from "theme/modules/Wysiwyg/DefaultWysiwyg"; import CustomHeading from "./CustomHeading"; import CustomImage from "./CustomImage"; export const customComponentsMap = { h2: CustomHeading, img: CustomImage, // Add more custom component mappings here }; const CustomPrismicWysiwyg = ({ content, componentsMap }) => { const componentsMapWithCustomComponents = useMemo(() => { return { ...defaultComponentsMap, ...customComponentsMap, ...(componentsMap || {}), }; }, [componentsMap]); return useMemo(() => { return renderChildNodes( content.childNodes, content.data, componentsMapWithCustomComponents ); }, [content, componentsMapWithCustomComponents]); }; export default CustomPrismicWysiwyg; ``` --- # Loading Prismic Content URL: https://developers.front-commerce.com/docs/3.x/extensions/content/prismic/how-to/loading-prismic-content

{frontMatter.description}

## Overview The Prismic module provides a powerful loader and transformers to expose Prismic content in your Front-Commerce GraphQL API. This guide will show you how to: 1. Load different types of content from Prismic 2. Transform Prismic fields into usable formats 3. Expose the content through your GraphQL API :::info Prerequisites - [Install the Prismic module](../index.mdx) - [Create a GraphQL module](../../../../02-guides/extend-the-graphql-schema.mdx) - Understand [Prismic Core Concepts](https://prismic.io/docs/core-concepts) ::: ## Loading Content The Prismic loader provides several methods to load content: ### Single Type Documents For [Single Type](https://prismic.io/docs/core-concepts/custom-types#difference-between-single-and-repeatable) documents: ```typescript title="my-extension/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ resolvers: { Query: { homepage: async (_, __, { loaders }) => { return loaders.Prismic.loadSingle("homepage"); }, }, }, }); ``` ### Documents by UID For documents with a [UID field](https://prismic.io/docs/core-concepts/uid): ```typescript title="my-extension/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ resolvers: { Query: { article: async (_, { slug }, { loaders }) => { return loaders.Prismic.loadByUID("article", slug); }, }, }, }); ``` ### Lists of Documents To load multiple documents with filtering and pagination: ```typescript title="my-extension/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ resolvers: { Query: { articles: async (_, { page, search }, { loaders }) => { const ListQuery = loaders.Prismic.queries.ListQuery; const query = new ListQuery(10, page || 1); query.type("article").sortBy("document.last_publication_date", "desc"); if (search) { query.search(search); } return loaders.Prismic.loadList(query); }, }, }, }); ``` ## Transforming Fields Prismic fields often need transformation before they can be used effectively. The module provides several transformers: ### Basic Field Transformers ```typescript import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "MyModule", dependencies: ["Prismic/Core", "Front-Commerce/Wysiwyg"], typeDefs: /* GraphQL */ ` type Article { title: String content: DefaultWysiwyg image: String imageAlt: String lastUpdate: String } extend type Query { article(slug: String!): Article } `, loadRuntime: () => import("./runtime"), }); ``` ```typescript import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ contextEnhancer: ({ loaders }) => { const { TitleTransformer, RichtextToWysiwygTransformer, ImageTransformer, DateTransformer, } = loaders.Prismic.transformers; loaders.Prismic.defineContentTransformers("article", { fieldTransformers: { title: new TitleTransformer(), content: new RichtextToWysiwygTransformer(loaders.Wysiwyg), image: new ImageTransformer(), lastUpdate: new DateTransformer(), }, }); return {}; }, resolvers: { Query: { article: async (_, { slug }, { loaders }) => { return loaders.Prismic.loadByUID("article", slug); }, }, Article: { imageAlt: (article) => article.image.main.alt, }, }, }); ``` ### Link Transformation The `LinkTransformer` can rewrite Prismic links to work with your Front-Commerce routes: ```typescript title="my-extension/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ contextEnhancer: ({ loaders }) => { const linkTransformer = new LinkTransformer([ "localhost", "staging.example.com", "production.example.com", ]); loaders.Prismic.defineContentTransformers("article", { fieldTransformers: { content: new RichtextToWysiwygTransformer( loaders.Wysiwyg, linkTransformer ), link: linkTransformer, }, }); return {}; }, }); ``` ## Error Handling Always handle potential errors when loading content: ```typescript title="my-extension/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ resolvers: { Query: { article: async (_, { slug }, { loaders }) => { try { return await loaders.Prismic.loadByUID("article", slug); } catch (error) { console.error(`Failed to load article ${slug}:`, error); return null; } }, }, }, }); ``` ## See Also - [API Reference](../reference/api.mdx) for detailed API documentation - [Adding Integration Fields](./adding-integration-fields.mdx) for connecting external data - [Adding Content Slices](./adding-content-slices.mdx) for dynamic content blocks - [Using Prismic Preview](./using-prismic-preview.mdx) for content previews --- # Simulating Prismic Webhook URL: https://developers.front-commerce.com/docs/3.x/extensions/content/prismic/how-to/simulating-prismic-webhook

{frontMatter.description}

## Overview In production, webhooks ensure that Front-Commerce always fetches the latest version of your Prismic content. However, in a development environment, you can: 1. Skip webhook configuration initially - content will refresh based on your `FRONT_COMMERCE_PRISMIC_API_CACHE_TTL_IN_SECONDS` setting 2. Manually simulate webhooks when needed to fetch the latest content without restarting the application ## Simulating a Webhook You can trigger a manual content refresh by sending a `POST` request to the webhook endpoint: ``` http://localhost:4000/prismic/webhook ``` The request must include: - Method: `POST` - Content-Type: `application/json` - Body: A JSON object with the following structure: Example using cURL: ```shell curl --request POST 'http://localhost:4000/prismic/webhook' \ --header 'Content-Type: application/json' \ --data-raw '{ "secret": "the value of FRONT_COMMERCE_PRISMIC_WEBHOOK_SECRET", "type": "api-update", "masterRef": true }' ``` :::tip Convenient One-liner From your Front-Commerce project root, you can use this convenient one-liner that automatically uses your environment variables: ```shell source .env ; PAYLOAD=`printf '{"secret": "%s", "type": "api-update", "masterRef": true}' $FRONT_COMMERCE_PRISMIC_WEBHOOK_SECRET` ; curl -H "Content-Type: application/json" -d "$PAYLOAD" $FRONT_COMMERCE_URL/prismic/webhook ``` ::: --- # Using Prismic Preview URL: https://developers.front-commerce.com/docs/3.x/extensions/content/prismic/how-to/using-prismic-preview

{frontMatter.description}

import Figure from "@site/src/components/Figure"; :::caution Known Limitation {/* TODO FC-3048: Remove this once routable types have been implemented */} Prismic preview currently redirects content writers to your store's homepage when they click on the preview button, they will then have to navigate to the preview page to see the preview. ::: ## Configure previews in your Prismic repository In your repository, go to Settings > Previews > Manage your Previews and select Create a preview. Then fill in the new preview configuration: - **Site Name:** The display name to identify the website in the preview. - **Domain for Your Application:** The URL of your site, such as https://example.com/ or http://localhost:3000/ - **Link Resolver:** The route where prismic resolves the preview url. In Front-Commerce we handle this route at `/prismic/preview`.
![Screenshot of a repository's preview configuration](./assets/previews/create-new-preview.jpg)
## Configure previews in your app Once you have configured Previews in your repository, you need to implement the preview functionality in your project code. ### Update your Content Security Policy You need to add these domains to enable the scripts and the iframe to be loaded in your website. ```diff title="src/config/website.js" { contentSecurityPolicy: { directives: { - scriptSrc: [], + scriptSrc: [ + "static.cdn.prismic.io", + "prismic.io", + "https://html2canvas.hertzen.com/dist/html2canvas.min.js", + ], - frameSrc: [], + frameSrc: ["*.prismic.io"], }, }, } ``` - `static.cdn.prismic.io` is the prismic cdn where the script is loaded from - `https://html2canvas.hertzen.com/dist/html2canvas.min.js` is a third-party script which allows a sharable prismic link - `*.prismic.io` is the prismic domain of your repository --- # Using Resolver Cache URL: https://developers.front-commerce.com/docs/3.x/extensions/content/prismic/how-to/using-resolver-cache

{frontMatter.description}

The `PrismicCachedResolver` is a powerful tool that helps you cache Prismic content at the resolver level. It automatically handles cache invalidation based on document references and supports different caching strategies. ## Prerequisites Before implementing resolver caching, ensure you have: - A Front-Commerce application with the [Prismic module installed](../index.mdx) - [GraphQL resolvers](../../../../01-get-started/loading-data-from-unified-graphql-schema.mdx) that return [Prismic content](./loading-prismic-content.mdx) ## Implementation Guide ### Create a GraphQL Module First, create a GraphQL module that will contain your cached resolvers: ```typescript title="my-extension/graphql/index.ts" import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "MyPrismicModule", typeDefs: /* GraphQL */ ` extend type Query { cachedPage(uid: String!): Page cachedPages: [Page!]! paginatedPages(page: Int!): PageConnection! } type PageConnection { items: [Page!]! totalCount: Int! pageInfo: PageInfo! } type PageInfo { currentPage: Int! } `, loadRuntime: () => import("./runtime"), }); ``` ### Implement the Runtime with Cached Resolvers Create a runtime file to implement your resolvers with caching: ```typescript title="my-extension/graphql/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; import { PrismicCachedResolver, NullContent, } from "@front-commerce/prismic/graphql"; export default createGraphQLRuntime({ resolvers: { Query: { // Single document cache example cachedPage: PrismicCachedResolver( async (_, { uid }, { loaders }) => { return loaders.Prismic.loadByUID("page", uid); }, { mapParamsToId: (_, { uid }) => uid, } ), // List cache example cachedPages: PrismicCachedResolver(async (_, __, { loaders }) => { const query = new loaders.Prismic.queries.ListQuery(10, 1) .type("page") .sortBy("document.first_publication_date", "desc"); return loaders.Prismic.loadList(query); }), // Nested content cache example paginatedPages: PrismicCachedResolver( async (_, { page }, { loaders }) => { const query = new loaders.Prismic.queries.ListQuery(10, page) .type("page") .sortBy("document.first_publication_date", "desc"); const results = await loaders.Prismic.loadList(query); return { items: results, totalCount: results.length, pageInfo: { currentPage: page }, }; }, { contentProperty: "items", } ), }, }, }); ``` ### Register the Module in Your Extension Add the GraphQL module to your extension: ```typescript title="my-extension/index.ts" import { defineExtension } from "@front-commerce/core"; import prismicModule from "./graphql"; export default function myExtension() { return defineExtension({ name: "my-prismic-extension", meta: import.meta, graphql: { modules: [prismicModule], }, }); } ``` ## Cache Invalidation The cache is automatically invalidated when: - The referenced Prismic documents are updated - The cache TTL expires (default: 5000ms) For more details about the cache configuration and API, see the [API Reference](../reference/api.mdx#resolver-cache-api). --- # Using X-Ray with Prismic URL: https://developers.front-commerce.com/docs/3.x/extensions/content/prismic/how-to/using-x-ray

{frontMatter.description}

## Overview The Prismic module provides built-in support for Front-Commerce's X-Ray feature through the `PrismicMetadataExtractor`. This allows content editors to: 1. See which parts of the page come from Prismic 2. Click directly through to edit the content in Prismic 3. Understand the structure of their content ![X-Ray in Prismic](./assets/x-ray/x-ray-in-prismic.jpg) ## Implementation Steps ### Create Metadata Extractors There are three ways to create metadata extractors in Prismic: 1. Singleton (static identifier) 2. Custom Identifier (dynamic identifier) 3. Document/Slice ID (dynamic identifier) A singleton extractor is used for content types that have a static identifier that will not change. ```typescript title="my-extension/extractors/HomePageExtractor.ts" import { createPrismicMetadataExtractor } from "@front-commerce/prismic"; export const HomePageExtractor = createPrismicMetadataExtractor( "PrismicHomePage", // The identifier used in @storefrontContent directive "home_page" // For single types, use a static identifier ); ``` A Custom identifier extractor is used for content types that have a dynamic identifier that will change. ```typescript title="my-extension/extractors/BlogPostExtractor.ts" import { createPrismicMetadataExtractor } from "@front-commerce/prismic"; export const BlogPostExtractor = createPrismicMetadataExtractor( "PrismicBlogPost", (data) => data.uid // Use a function to extract the identifier ); ``` A metadata identifier extractor is used for content types that have a dynamic identifier that will change. The `documentMetadata` will include a `sliceID` when it has been defined as a slice, otherwise it will only include a `documentID`. ```typescript title="my-extension/extractors/SliceExtractor.ts" import { createPrismicMetadataExtractor } from "@front-commerce/prismic"; export const SliceExtractor = createPrismicMetadataExtractor( "PrismicSlice", (content, source) => source.documentMetadata.sliceID ?? "" ); ``` ### Update GraphQL Schema Add the `@storefrontContent` directive to your types: ```graphql type HomePage @storefrontContent(extractorIdentifier: "PrismicHomePage") { title: String content: DefaultWysiwyg } type BlogPost @storefrontContent(extractorIdentifier: "PrismicBlogPost") { uid: String! title: String content: DefaultWysiwyg } union SharedSlice = ProductsSlice | FeaturedProductSlice | RichTextSlice type ProductsSlice @storefrontContent(extractorIdentifier: "PrismicSlice") { sliceID: String title: String products: [Product] } type FeaturedProductSlice @storefrontContent(extractorIdentifier: "PrismicSlice") { sliceID: String title: String product: Product } type RichTextSlice @storefrontContent(extractorIdentifier: "PrismicSlice") { sliceID: String content: DefaultWysiwyg } ``` ### Register Extractors Register your extractors in your extension's lifecycle hooks: ```typescript title="my-extension/index.ts" import { defineExtension } from "@front-commerce/core/extension"; import { HomePageExtractor } from "./extractors/HomePageExtractor"; import { BlogPostExtractor } from "./extractors/BlogPostExtractor"; import { SliceExtractor } from "./extractors/SliceExtractor"; export default defineExtension({ name: "my-module", unstable_lifecycleHooks: { onServerServicesInit: async (services) => { services.ContentMetadataExtractorRegistry.register([ HomePageExtractor, BlogPostExtractor, SliceExtractor, ]); }, }, }); ``` ### Implement Slice Resolvers Resolve `sliceID` to identify the slice with the correct identifier: ```typescript title="my-extension/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; import { withSliceFields } from "./extractors/SliceExtractor"; type IntegrationFieldContent = { [key in field]: { blob: { id: string }; }; }; // Helper HOF to add sliceID to resolvers export function withSliceFields>(resolvers: T) { return { sliceID: (content: any) => content.documentMetadata.sliceID, ...resolvers, }; } export default createGraphQLRuntime({ resolvers: { // Add sliceID and resolve products for ProductsSlice ProductsSlice: withSliceFields({ title: (content) => content.primary.title, products: async ( content: IntegrationFieldContent<"products">, _, { loaders } ) => { return Promise.all( content.items.map((item: IntegrationFieldContent<"product">) => { return loaders.Product.load(item.product.blob.id); }) ).then((products = []) => products.filter(Boolean)); }, }), // Add sliceID and resolve single product for FeaturedProductSlice FeaturedProductSlice: withSliceFields({ title: (content) => content.primary.title, product: async ( content: IntegrationFieldContent<"product">, _, { loaders } ) => { return loaders.Product.load(content.primary.product.blob.id); }, }), // Add sliceID and transform rich text content RichTextSlice: withSliceFields({ content: ({ content }) => content, }), }, }); ``` The `withSliceFields` helper: 1. Automatically adds the `sliceID` field required for X-Ray 2. Preserves your existing resolver implementations 3. Ensures proper typing with TypeScript 4. Works with any slice type :::note The `withSliceFields` helper is an example for this guide and not a public API, you can implement your own helper to extract the identifier. To ease reuse across different resolvers or applications, you can relocate as an export from the `SliceExtractor.ts` module. ::: ### Add StorefrontContent Components Wrap your React components with `StorefrontContent`: ```tsx title="my-extension/components/HomePage.tsx" import { StorefrontContent } from "theme/modules/StorefrontContent"; export function HomePage({ data }) { return (

{data.title}

{data.content}
{data.slices.map((slice) => ( ))}
); } function SliceComponent({ slice }) { switch (slice.__typename) { case "ProductsSlice": return (

{slice.title}

); case "FeaturedProductSlice": return (

{slice.title}

); case "RichTextSlice": return (
{slice.content}
); default: return null; } } ``` :::tip If you are using the [`Content Composition API`](../../../../02-guides/content-composition/index.mdx) to register your slices, then the above code can be simplified to: ```tsx title="my-extension/components/HomePage.tsx" import { CompositionComponent } from "@front-commerce/core/react"; export function HomePage({ data }) { return ( { const sliceID = component.props.sliceID; if (sliceID) { return (
{component}
); } return
{component}
; }} />
); } ``` Also see the [Adding content slices](./adding-content-slices.mdx) documentation for more details. :::
## Testing X-Ray Integration 1. Start your development server 2. Enable X-Ray mode using the [Magic Button](../../../../02-guides/magic-button/x-ray.mdx) 3. Verify that: - Prismic content is highlighted with the Prismic brand color - Clicking the edit button opens the correct page in Prismic - Both page-level and block-level content are properly identified - Slice content shows the correct edit button and links ## See Also - [X-Ray Overview](../../../../02-guides/magic-button/x-ray.mdx) for general X-Ray concepts - [API Reference](../reference/api.mdx#x-ray-support) for PrismicMetadataExtractor details - [Adding Content Slices](./adding-content-slices.mdx) for more about Prismic slices --- # Prismic URL: https://developers.front-commerce.com/docs/3.x/extensions/content/prismic/index

{frontMatter.description}

## Prerequisites Before you can use the Prismic extension, you need to have the following: - Access to a [Prismic](https://prismic.io/dashboard/signup) project - A working [Front-Commerce environment](../../../01-get-started/installation.mdx) ## Installation To install the Prismic extension, run the following command: ```bash $ pnpm install @front-commerce/prismic@latest ``` ## Configuration To use the Prismic extension, you need to configure your environment and your Front-Commerce application. ### Environment variables In the `.env` file, you have to define the following environment variables: ```shell FRONT_COMMERCE_PRISMIC_REPOSITORY_NAME=your-repository FRONT_COMMERCE_PRISMIC_ACCESS_TOKEN=the-very-long-access-token-from-prismic FRONT_COMMERCE_PRISMIC_WEBHOOK_SECRET=a-secret-defined-in-webhook-configuration #FRONT_COMMERCE_PRISMIC_API_CACHE_TTL_IN_SECONDS=60 ``` - `FRONT_COMMERCE_PRISMIC_REPOSITORY_NAME` is the Prismic repository name - `FRONT_COMMERCE_PRISMIC_ACCESS_TOKEN` is the access token for the repository, go to _Settings > API & Security_ and create an application and copy the _Permanent access token_ generated for this application - `FRONT_COMMERCE_PRISMIC_WEBHOOK_SECRET` is a secret key used to clear caches in Front-Commerce and to secure [Integration Fields API endpoints](./how-to/adding-integration-fields.mdx). To define it, go to _Settings > Webhook_ and create a webhook pointing to your Front-Commerce URL `https://my-shop.example.com/prismic/webhook`. In the webhook form, you can configure a _Secret_. This is the one you should use in this environment variable. - `FRONT_COMMERCE_PRISMIC_API_CACHE_TTL_IN_SECONDS` is an optional configuration that allows to customize the TTL of Prismic API cache. Shortening it allows to prioritize data freshness in environments not targeted by a Prismic webhook over performance. **It defaults to 23h in production environments and 1min in staging and dev environments.** :::tip In case of trouble, `front-commerce:prismic` (or `front-commerce:prismic*` to include more specific namespaces) can be added to the `DEBUG` environment variable value, this value turns the debug on for Prismic module and make it verbose. For more information see [Debug Flags](../../../04-api-reference/debug-flags/index.mdx) ::: ### Front-Commerce application To use the Prismic extension in your Front-Commerce application, you need to register the extension in your `front-commerce.config.ts` file: ```ts title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; import magento2 from "@front-commerce/magento2"; // add-next-line import prismic from "@front-commerce/prismic"; import storesConfig from "./app/config/stores"; import cacheConfig from "./app/config/caching"; export default defineConfig({ extensions: [ magento2({ storesConfig }), themeChocolatine(), // add-next-line prismic(), ], stores: storesConfig, cache: cacheConfig, configuration: { providers: [], }, }); ``` The module is ready! **You can now use the `Prismic` loader to [Expose Prismic Content in your project](./how-to/loading-prismic-content.mdx).** --- # API Reference URL: https://developers.front-commerce.com/docs/3.x/extensions/content/prismic/reference/api import Description from "@site/src/components/Description"; ## Core API ### `prismic()` The main function to initialize the Prismic extension in your Front-Commerce application. ```typescript title="front-commerce.config.ts" import prismic from "@front-commerce/prismic"; export default { extensions: [prismic()], }; ``` ### Integration Fields The Prismic module provides two main ways to implement Integration Fields: #### `IntegrationField` A class to create custom integration fields with full control over data fetching and transformation. ```typescript import { IntegrationField } from "@front-commerce/prismic"; interface UserData { userId: string; name: string; email: string; updated_at: string; avatar_url?: string; } const userIntegrationField = new IntegrationField({ // Function to fetch paginated results from your data source loadPullResults: async (page, shopUrl) => { const users = await fetchUsers(page); return { result_size: users.total, results: users.items.map((user) => ({ id: user.userId, title: user.name, description: user.email, image_url: user.avatar_url ? new URL(user.avatar_url, shopUrl).toString() : undefined, last_update: new Date(user.updated_at).valueOf(), blob: user, // Original data available in resolveEntity })), }; }, // Function to resolve the entity in your GraphQL context resolveEntity: (blob, context) => { return context.loaders.User.load(blob.userId); }, // Optional: control pagination behavior autoPaginate: true, }); ``` **Configuration Options:** - `loadPullResults`: Function to fetch and format data for Prismic - Parameters: - `page`: Current page number - `shopUrl`: Base URL of the shop - Returns: Promise or direct value of `IntegrationFieldApiResponse` - `resolveEntity`: Function to resolve the entity from blob data - Parameters: - `blob`: The blob data stored in Prismic - `context`: GraphQL context with loaders - Returns: Promise of the resolved entity or null - `autoPaginate`: Boolean to control automatic pagination (defaults to true) See: [Adding Integration Fields](../how-to/adding-integration-fields.mdx) for implementation examples. #### `SitemapIntegrationField` A class to create integration fields that leverage your existing sitemap data. ```typescript import { SitemapIntegrationField } from "@front-commerce/prismic"; const productIntegrationField = new SitemapIntegrationField< MagentoSitemapProduct, Product >({ // Name of your sitemap fetcher sitemapFetcher: "products", // Convert sitemap entry to Prismic's format convertSitemapEntry: (entry, shopUrl) => { if (!entry.data) return null; let imageUrl = entry.images?.[0]; if (imageUrl) { imageUrl = new URL(imageUrl, shopUrl).toString(); } return { id: entry.data.sku, title: entry.data.name, description: entry.data.short_description, image_url: imageUrl, last_update: new Date(entry.data.updated_at).valueOf(), }; }, // Resolve the entity in your GraphQL context resolveEntity: (blob, context) => { return context.loaders.Product.load(blob.id); }, }); ``` **Configuration Options:** - `sitemapFetcher`: Name of the sitemap fetcher to use - `convertSitemapEntry`: Function to convert sitemap entries to Prismic format - Parameters: - `entry`: The sitemap entry with data - `shopUrl`: Base URL of the shop - Returns: Integration field entry without blob or null - `resolveEntity`: Function to resolve the entity from blob data - Parameters: - `blob`: The blob data stored in Prismic - `context`: GraphQL context with loaders - Returns: Promise of the resolved entity or null See: [Adding Integration Fields](../how-to/adding-integration-fields.mdx) for implementation examples. ### Content Loading The Prismic module provides several methods to load content from your Prismic repository: #### `loadSingle(typeIdentifier)` Loads a single document of a specific type. ```typescript const homepage = await loaders.Prismic.loadSingle("homepage"); ``` See: [Loading Prismic Content](../how-to/loading-prismic-content.mdx) for usage examples. #### `loadByUID(typeIdentifier, uid)` Loads a document by its Unique ID (UID). ```typescript const article = await loaders.Prismic.loadByUID("article", "my-article-slug"); ``` See: [Loading Prismic Content](../how-to/loading-prismic-content.mdx) for usage examples. #### `loadByID(id)` Loads a document by its ID. ```typescript const document = await loaders.Prismic.loadByID("XYZ123"); ``` See: [Loading Prismic Content](../how-to/loading-prismic-content.mdx) for usage examples. #### `loadList(query)` Loads a list of documents matching specific criteria. ```typescript const ListQuery = loaders.Prismic.queries.ListQuery; const query = new ListQuery(10, 1) // pageSize, page .type("article") .sortBy("document.last_publication_date", "desc"); const articles = await loaders.Prismic.loadList(query); ``` See: [Loading Prismic Content](../how-to/loading-prismic-content.mdx) for usage examples. ### Content Transformers The Prismic module provides several transformers to convert Prismic field values into more usable formats: - `TitleTransformer`: Transforms Prismic Title fields - `RichtextToWysiwygTransformer`: Transforms Rich Text fields to Wysiwyg format - `ImageTransformer`: Transforms Image fields with support for views - `LinkTransformer`: Transforms Link fields with support for local URL rewriting - `IntegrationFieldTransformer`: Transforms Integration Field references Example usage: ```typescript const { RichtextToWysiwygTransformer, ImageTransformer } = loaders.Prismic.transformers; loaders.Prismic.defineContentTransformers("my-type", { fieldTransformers: { content: new RichtextToWysiwygTransformer(loaders.Wysiwyg), image: new ImageTransformer(), }, }); ``` See: [Loading Prismic Content](../how-to/loading-prismic-content.mdx) for more examples of using transformers. ### X-Ray Support The Prismic module provides built-in support for Front-Commerce's X-Ray feature through metadata extractors. #### `createPrismicMetadataExtractor()` Creates a metadata extractor for enabling X-Ray support on Prismic content. ```typescript import { createPrismicMetadataExtractor } from "@front-commerce/prismic"; // For single types with a static identifier const HomePageExtractor = createPrismicMetadataExtractor( "PrismicHomePage", // The identifier used in @storefrontContent directive "home_page" // Static identifier ); // For types with dynamic identifiers (e.g., using UIDs) const BlogPostExtractor = createPrismicMetadataExtractor( "PrismicBlogPost", (data) => data.uid // Function to extract identifier ); // For Prismic Slices const SliceExtractor = createPrismicMetadataExtractor( "PrismicSlice", (content, source) => source.documentMetadata.sliceID ?? "" ); ``` **Parameters:** - `extractorIdentifier`: The identifier string used to match Prismic content with GraphQL types via the `@storefrontContent` directive - `identifierValue`: Either a static string identifier or a function to extract the identifier from the content - If `string`: Used as-is (good for single types) - If `function`: Called with `(fieldData, source)` to compute the identifier dynamically The extractor automatically: - Generates edit URLs for Prismic's writing room - Handles both document and slice-level content - Integrates with Front-Commerce's X-Ray UI See: [Using X-Ray with Prismic](../how-to/using-x-ray.mdx) for implementation examples. ## Types ### Core Types The module exports several TypeScript types for type safety: ```typescript import type { // The prismic client defined in the dependency injection PrismicClient, // The integration field type IntegrationField, // The sitemap integration field type SitemapIntegrationField, // The metadata extractor type PrismicMetadataExtractor, } from "@front-commerce/prismic"; ``` ### GraphQL Types The module also exports types specific to GraphQL integration: ```typescript import type { // The main Prismic loader type PrismicLoader, // Available content transformers Transformers, // Options for configuring content transformers ContentTransformOptions, // Type definitions for Prismic loaders PrismicLoaders, } from "@front-commerce/prismic/graphql"; ``` ## Resolver Cache API See: [Using Resolver Cache](../how-to/using-resolver-cache.mdx) for implementation examples. ### `PrismicCachedResolver(resolver, options?)` A higher-order resolver that caches Prismic content at the resolver level. It automatically handles cache invalidation based on Prismic document references. ```typescript import { PrismicCachedResolver } from "@front-commerce/prismic/graphql"; const cachedResolver = PrismicCachedResolver(originalResolver, { mapParamsToId: (source, args, context) => args.slug, contentProperty: "data", }); ``` **Parameters:** - `resolver`: The original resolver function that returns Prismic content - `options`: Optional configuration object - `mapParamsToId`: Function to generate a unique cache key from resolver parameters - `contentProperty`: String indicating which property contains the Prismic content (for nested results) **Returns:** A new resolver function that caches its results based on Prismic document references. **Cache Types:** The resolver supports three types of caching: - `singleton`: For single document results - `list`: For arrays of documents - `null`: For explicitly cached null results using `NullContent` ### `NullContent(contentOrDocumentId)` A utility class for caching null results while maintaining Prismic document references. ```typescript import { NullContent } from "@front-commerce/prismic/graphql"; const resolver = PrismicCachedResolver((parent, args, { loaders }) => { const content = await loaders.Prismic.loadByUID("page", args.uid); return content ? content : new NullContent(args.uid); }); ``` **Parameters:** - `contentOrDocumentId`: Either a Prismic Content object or a document ID string **Returns:** A `NullContent` instance that can be cached by `PrismicCachedResolver`. --- # Environment variables URL: https://developers.front-commerce.com/docs/3.x/extensions/content/prismic/reference/environment-variables

{frontMatter.description}

| Name | Description | | ------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `FRONT_COMMERCE_PRISMIC_REPOSITORY_NAME` | The Prismic repository name | | `FRONT_COMMERCE_PRISMIC_ACCESS_TOKEN` | The Prismic access token | | `FRONT_COMMERCE_PRISMIC_WEBHOOK_SECRET` | The Prismic webhook secret | | `FRONT_COMMERCE_PRISMIC_API_CACHE_TTL_IN_SECONDS` | The Prismic API cache TTL in seconds (default: `23h`) | | `FRONT_COMMERCE_PRISMIC_IMAGES_ENDPOINT` | Images endpoint when prismic is sending back images (default: `https://images.prismic.io`) | --- # Set-up Magento Module URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/adobe-b2b/how-to/setup-magento-module

{frontMatter.description}

Here is the list of currently supported features: - Display details of the Company a customer belongs to - Company users handling (list, create, modify and deactivate company users) - [_Payment on account_](./use-payment-on-account.mdx) payment method - Display company credit history - Requisition list - Negotiable quotes (_require Adobe Commerce 2.4.5+_) :::info Those features are only available with [Adobe Commerce and its B2B module](https://docs.magento.com/user-guide/getting-started.html#b2b-features) and require at least Adobe Commerce 2.4.3. ::: ## Enable B2B support ### Requirements On Magento2 side, you need to [install the Front-Commerce Magento2 Commerce module](../../magento2/how-to/setup-magento-module.mdx). You must then install the [`front-commerce/magento2-b2b-module-front-commerce`](https://gitlab.blackswift.cloud/front-commerce/magento2-b2b-module-front-commerce) module: ```shell composer config repositories.front-commerce-magento2-b2b git \ git@gitlab.blackswift.cloud:front-commerce/magento2-b2b-module-front-commerce.git composer require front-commerce/magento2-b2b-module bin/magento setup:upgrade ``` :::tip We recommend to use a specific version of this module and not to blindly rely on the latest version. ::: ## Magento2 Commerce module installation You need to install the [`front-commerce/magento2-commerce-module` module](https://gitlab.blackswift.cloud/front-commerce/magento2-commerce-module-front-commerce/): ```shell composer config repositories.front-commerce-magento2-commerce git \ git@gitlab.blackswift.cloud:front-commerce/magento2-commerce-module-front-commerce.git composer require front-commerce/magento2-commerce-module php bin/magento setup:upgrade ``` :::tip We recommend to use a specific version of this module and not to blindly rely on the latest version. ::: ## Negotiable quotes In order to use Front-Commerce's Negotiable quotes module, you will need to enable the feature in your Magento back office. Please note that you'll need an Adobe Commerce B2B version >= 2.4.5. Navigate to `Stores > Configuration > General > B2B Features`, and set both `Enable Company` and `Enable B2B quote` to `Yes`. Quotes also have to be enabled _per-company_. To do so, navigate to a company's settings (`Customers > Companies`, then open a company), and under `Advanced settings` switch `Allow Quotes` on. :::info Negotiable quotes are only available to users that have a Company with Quotes enabled. More in [Adobe documentation](https://experienceleague.adobe.com/docs/commerce-admin/b2b/quotes/configure-quotes.html). ::: That's it, you should now be able to use Adobe Commerce's Negotiable quotes with Front-Commerce! ## Known issues ### Bundle products can't be used in requisition lists Due to an issue (documented below) with Adobe B2B GraphQL layer, we don't recommend to use Requistion List with bundle items with the default Adobe B2B codebase. Please contact your Adobe support team if you plan to use this feature. ```mdx-code-block
Reproduction details and technical insights
``` The issue can be reproduced by not selecting the first value for the first option of the bundle. It's related to Magento GraphQL layer, because: - it can be reproduced when reading a requisition list whose item was added from the Magento frontend - when a requisition list is broken, the standard Magento frontend displays the correct value When the first choice is selected for the first option, the requisition list can be read but has inconsistent data: the "non-first" option having a "non-first" choice will be resolved as the previous option. E.g: "Sprite Foam Yoga Brick" is returned instead of "Sprite Yoga Strap 10 foot"): ![Wrong option returned (frontend)](./assets/requisition-list-item-wrong-option-front.png) ![Wrong option returned (GraphQL)](./assets/requisition-list-item-wrong-option-graphql.png) It seems to be due to some kind of mismatch between and index and its value for selected options id in the Magento resolver, as the call made natively from Magento is the same as the one made from Front-Commerce: ![The same call is made from Front-Commerce and from Magento](./assets/requisition-list-same-call.png) ![Related resolver on Magento's code](./assets/requisition-list-magento-code.png) ```mdx-code-block
``` --- # Use payment on account URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/adobe-b2b/how-to/use-payment-on-account

{frontMatter.description}

Payment on account is a payment method for B2B contexts. It allows companies to place orders using the credit limit that is specified in their account. ## Enable Payment on account This feature is directly provided by Adobe Commerce. Please refer to [the corresponding documentation page to properly configure it on Magento side](https://docs.magento.com/user-guide/payment/payment-on-account.html). In addition, this feature is also controlled by the roles and the associated permissions assigned to a user within the company. Those roles can be set programmtically or through the Magento frontend. In local environments, In local environments, to make sure a company user can use the _Payment on account_ method you need to: 1. login as the administrator of the company or as a user having the _Manage roles and permissions_ permission. 1. go to `https://magento-front.example.com/company/role/` 1. for the role assigned to the user, check the permissions _Sales / Allow Checkout / Use Pay On Account method_ and _Company Credit / View_ ### Register the Payment on account payment component Override the file that lets you register additional payments components in Front-Commerce to register `PaymentOnAccount` to be used for `companycredit` payments in `getAdditionalDataComponent.js` ```diff title='theme/modules/Checkout/Payment/AdditionalPaymentInformation/getAdditionalDataComponent.js' +import PaymentOnAccount from "theme/modules/Checkout/Payment/AdditionalPaymentInformation/PaymentOnAccount"; const ComponentMap = { + companycredit: PaymentOnAccount, }; ``` And that's it. After having restarted Front-Commerce, a company customer having _Payment on account_ enabled should be able to use this payment method. --- # Adobe B2B URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/adobe-b2b/index

{frontMatter.description}

## Prerequisites 1. On Magento 2 side, you need to [install and configure the module](./how-to/setup-magento-module.mdx) 2. On Front-Commerce side, you need to setup and install the [@front-commerce/magento2 extension](/docs/3.x/extensions/e-commerce/magento2) ## Installation First ensure you have installed the package: ```bash $ pnpm add @front-commerce/adobe-b2b@latest ``` ## Setup Adobe B2B Extension Update your `front-commerce.config.ts` to include the Adobe B2B Extension : ```typescript title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; import magento2 from "@front-commerce/magento2"; // add-next-line import adobeB2B from "@front-commerce/adobe-b2b"; import storesConfig from "./app/config/stores"; import cacheConfig from "./app/config/caching"; export default defineConfig({ extensions: [ themeChocolatine(), magento2({ storesConfig }), // add-next-line adobeB2B(), ], stores: storesConfig, cache: cacheConfig, }); ``` ## Feature Flags
The Adobe B2B extension supports the following feature flags: Click to expand. - `Cart` (default: `true`) - Enable the Cart feature - `Company` (default: `true`) - Enable the Company feature - `CompanyStructure` (default: `true`) - Enable the CompanyStructure feature - `NegotiableQuotes` (default: `true`) - Enable the NegotiableQuotes feature - `RequisitionList` (default: `true`) - Enable the RequisitionList feature - ⚠️ Requires component overrides - `StoreCredit` (default: `true`) - Enable the StoreCredit feature
All these features are active by default. To disable a feature you should return a falsy value for the feature flag in your extension options: ```typescript title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; import magento2 from "@front-commerce/magento2"; import adobeB2B from "@front-commerce/adobe-b2b"; import storesConfig from "./app/config/stores"; import cacheConfig from "./app/config/caching"; export default defineConfig({ extensions: [ magento2({ storesConfig }), adobeB2B({ // add-start features: { Company: false, // Company feature will be disabled NegotiableQuotes: false, // NegotiableQuotes feature will be disabled RequisitionList: false, // RequisitionList feature will be disabled // all other features will be enabled by default }, // add-end }), themeChocolatine(), ], stores: storesConfig, cache: cacheConfig, configuration: { providers: [], }, }); ``` :::tip If a feature is not defined in the feature flags, it will be enabled by default. ::: :::caution The `RequisitionList` feature requires some component overrides as the graphql documents will no longer be available if this feature is disabled - [`theme/modules/Cart/CartTitle/CartTitle.jsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/adobe-b2b/theme/modules/Cart/CartTitle/CartTitle.jsx) → 📎 [example override](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/theme-chocolatine/theme/modules/Cart/CartTitle/CartTitle.jsx) - [`theme/modules/ProductView/ProductItem/ProductItemActions/ProductItemActions.jsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/adobe-b2b/theme/modules/ProductView/ProductItem/ProductItemActions/ProductItemActions.jsx) → 📎 [example override](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/theme-chocolatine/theme/modules/ProductView/ProductItem/ProductItemActions/ProductItemActions.jsx) - [`theme/modules/ProductView/Synthesis/AddProductToCart.jsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/adobe-b2b/theme/modules/ProductView/Synthesis/AddProductToCart.jsx) → 📎 [example override](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/theme-chocolatine/theme/modules/ProductView/Synthesis/AddProductToCart.jsx) ::: ## Known issues ### Bundle products can't be used in requisition lists Due to an issue (documented below) with Adobe B2B GraphQL layer, we don't recommend to use Requistion List with bundle items with the default Adobe B2B codebase. Please contact your Adobe support team if you plan to use this feature. ```mdx-code-block
Reproduction details and technical insights
``` The issue can be reproduced by not selecting the first value for the first option of the bundle. It's related to Magento GraphQL layer, because: - it can be reproduced when reading a requisition list whose item was added from the Magento frontend - when a requisition list is broken, the standard Magento frontend displays the correct value When the first choice is selected for the first option, the requisition list can be read but has inconsistent data: the "non-first" option having a "non-first" choice will be resolved as the previous option. E.g: "Sprite Foam Yoga Brick" is returned instead of "Sprite Yoga Strap 10 foot"): ![Wrong option returned (frontend)](./assets/requisition-list-item-wrong-option-front.png) ![Wrong option returned (GraphQL)](./assets/requisition-list-item-wrong-option-graphql.png) It seems to be due to some kind of mismatch between and index and its value for selected options id in the Magento resolver, as the call made natively from Magento is the same as the one made from Front-Commerce: ![The same call is made from Front-Commerce and from Magento](./assets/requisition-list-same-call.png) ![Related resolver on Magento's code](./assets/requisition-list-magento-code.png) ```mdx-code-block
``` ### Advanced shipping methods and online payments aren't fully supported In its current state, Front-Commerce's module only supports shipping methods without additional information to provide for customers. The negotiable quote checkout fully supports offline payments such as payment on account, check/money or bank transfer. It only has a partial support for online payments and requires the Magento 2 B2B Front-Commerce module in version [>= 1.2.0](https://gitlab.blackswift.cloud/front-commerce/magento2-b2b-module-front-commerce/-/releases/1.2.0). Stripe is the only supported payment method (since version 2.25.0): please refer to [Stripe's documentation page](../../payment/stripe) to enable this feature. Please if you have a specific requirement. ### Negotiable quotes minimum price is not enforced nor exposed in the GraphQL API Front-Commerce's module allows the creation of negotiable quotes from _any amount_, regardless of the settings in Magento. ### Negotiable quotes GraphQL API does not support file-related features File upload and download in negotiable quote comments are not available in Front-Commerce. ### "Catalog" total price is not exposed in negotiable quotes The GraphQL API only exposes the quote's total price with applied discounts from the sales representative, so the catalog prices cannot be easily displayed. ### Magento does not expose quote owners' user ID For this reason, Front-Commerce compares first and last names of a user to detect ownership. Please note it could lead to subtle edge cases for homonyms in the same company, where a non-owner could see additional UI components without being able to act on them. ### Front-Commerce does not take into account per-company feature deactivation We do support the global B2B feature toggle and honor user permissions attached to company roles. However, Front-Commerce doesn't support the per-company feature deactivation. The reason is because the information isn't exposed in Magento's API, and company ACLs don't change depending on this option. As a consequence, it is possible that some users belonging to a company with negotiable quote deactivated (on a shop with the feature globally active) will view some negotiable quotes UI. --- # Environment variables URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/adobe-b2b/reference/environment-variables | Environment Variable | Description | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `ADOBE_B2B_SCHEMA_PATH` | URL to your Adobe B2B instance's GraphQL endpoint or schema file. Used to regenerate the GraphQL schema types. This can be any value supported by [graphql-codegen's schema field](https://the-guild.dev/graphql/codegen/docs/config-reference/schema-field) | --- # Test API calls to Gezy URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/gezy/how-to/test-api-calls-to-gezy

{frontMatter.description}

### Overview Front-Commerce provides a Gezy call testing tool, accessible at [http://localhost:4000/\_\_front-commerce/gezy/playground](http://localhost:4000/__front-commerce/gezy/playground) in development and staging environments. This tool allows developers and testers to simulate HTTP requests to the configured Gezy instance, as done by Front-Commerce internally. ### Key Features - Recap of the Gezy configuration - Create a request using a form - Prompt for confirmation before sending the request - Share URLs to teammates, with pre-filled request settings that can be customized by the recipient - Show details of the sent request (including signature parameters generated by Front-Commerce) and the Gezy response (HTTP code, payload...) ### Using the Tool 1. Access the [http://localhost:4000/\_\_front-commerce/gezy/playground](http://localhost:4000/__front-commerce/gezy/playground) URL in your development or staging environment. 2. Choose the HTTP method (GET, POST, PUT, etc.). 3. Specify the Gezy endpoint you want to call. It is pre-filled with documented API endpoints from your Gezy instance. 4. Add query parameters (optional). 5. Write the request body (optional). 6. Click the "Send" button to simulate the call. 7. The tool will display information about the sent request (signature, etc.) and the Gezy response (HTTP code, payload...). The URL in the browser's address bar will be updated with the request settings every time you submit a request. You can share this URL with your teammates to reproduce the same request. ### Feedback welcome If you have any feedback or feature requests for this tool, please let us know how we can make it more useful for day-to-day development tasks. --- # Gezy URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/gezy/index

{frontMatter.description}

## Prerequisites You need to have a [Gezy](https://www.lundimatin.co.uk/product/e-commerce) backend running, with a e-commerce API user created. ## Installation First ensure you have installed the package: ```bash $ pnpm install @front-commerce/gezy@latest ``` ## Setup Gezy Extension Update your `front-commerce.config.ts` to include the extension : ```typescript title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; // add-next-line import gezy from "@front-commerce/gezy"; import storesConfig from "./app/config/stores"; import cacheConfig from "./app/config/caching"; export default defineConfig({ extensions: [ // add-next-line gezy(), themeChocolatine(), ], stores: storesConfig, cache: cacheConfig, configuration: { providers: [], }, }); ``` Add the following variables to your `.env` file, to configure your project with the technical information provided by the Gezy team: ```shell title=".env" FRONT_COMMERCE_GEZY_ENDPOINT=https://YOUR_GEZY_INSTANCE.lundimatin.biz FRONT_COMMERCE_GEZY_TECHNICAL_ACCOUNT_LOGIN=edi123456 FRONT_COMMERCE_GEZY_TECHNICAL_ACCOUNT_PASSWORD_HASH='$2a$10$YalJINpbjkZqYVbaMsPBeOZSIXuRMEKULvCCgkcxAGlzru3Daq8nq' ``` :::tip You can find the technical account credentials in your Gezy administration area, at the following URL: `/interface_admin/#/page.php/Admin/Standard/Webservices/Index`. _Example: https://YOUR_GEZY_INSTANCE.lundimatin.biz/interface_admin/#/page.php/Admin/Standard/Webservices/Index_ ::: --- # Axios instances URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/gezy/reference/axios-instances

{frontMatter.description}

## Overview The Front-Commerce's Gezy extension exposes preconfigured HTTP client instances for interacting with Gezy's API. These instances are exposed via the [Dependency Injection](/docs/3.x/guides/dependency-injection) system, under the `gezy` HTTP namespace. You can access them via `services.DI.get("gezy").http.*` in your code. ## `publicRestAPI` The `publicRestAPI` instance is an `axios` instance that can be used to interact with Gezy's API as a customer.
It will sign requests depending on the current user's session. Example: ```ts title="path/to/graphql/module/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ contextEnhancer: ({ services, makeDataLoader }) => { // add-next-line const gezyClient = services.DI.get("gezy").http.publicRestAPI; return { MyFeature: new MyFeatureLoader(gezyClient, makeDataLoader), }; }, }); ``` --- # Environment variables URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/gezy/reference/environment-variables

{frontMatter.description}

## Available variables | Name | Description | | ----------------------------------------------------- | -------------------------------------------------------------------------------- | | `FRONT_COMMERCE_GEZY_ENDPOINT` | **Required** The URL of your Gezy instance, **without** trailing slash | | `FRONT_COMMERCE_GEZY_TECHNICAL_ACCOUNT_LOGIN` | **Required** The technical account login (usually starting with `edi`) | | `FRONT_COMMERCE_GEZY_TECHNICAL_ACCOUNT_PASSWORD_HASH` | **Required** The technical account password hash (usually starting with `$2y$`) | | `FRONT_COMMERCE_GEZY_TIMEOUT_IN_MS` | The timeout in milliseconds for Gezy requests (defaults to 30 seconds) | | `FRONT_COMMERCE_GEZY_IMAGE_PROXY_TIMEOUT_IN_MS` | The timeout in milliseconds for image proxy requests (defaults to 5 seconds) | | `FRONT_COMMERCE_GEZY_CART_CAN_EDIT_CUSTOM_OPTIONS` | Whether customer can edit custom options from the cart page (defaults to `true`) | ## Sample `.env` file This section contains a sample `.env` file with all the possible environment variables, with their default values (for optional values). ```shell title=".env" # Gezy: required configurations FRONT_COMMERCE_GEZY_ENDPOINT=https://YOUR_GEZY_INSTANCE.lundimatin.biz FRONT_COMMERCE_GEZY_TECHNICAL_ACCOUNT_LOGIN=edi123456 FRONT_COMMERCE_GEZY_TECHNICAL_ACCOUNT_PASSWORD_HASH='$2y$10$YalJINpbjkZqYVbaMsPBeOZSIXuRMEKULvCCgkcxAGlzru3Daq8nq' # Gezy: optional configurations FRONT_COMMERCE_GEZY_TIMEOUT_IN_MS=30000 FRONT_COMMERCE_GEZY_CART_CAN_EDIT_CUSTOM_OPTIONS=true ``` --- # Add your custom endpoint URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento1/how-to/add-custom-endpoint

{frontMatter.description}

import Figure from "@site/src/components/Figure"; Fetching data is done through a Magento REST endpoint that will be used in a [GraphQL module](../../../../guides/extend-the-graphql-schema). Implementing a new REST endpoint is not something specific to Front-Commerce module and you can follow the Magento tutorial [How to Extend the Magento REST API to Use Coupon Auto Generation](https://devdocs.magento.com/guides/m1x/other/ht_extend_magento_rest_api.html) if you need to understand more about the Magento 1 REST API. This can be done by completing the following steps: 1. Add and complete `api2.xml` file 2. Implement the API method(s) ## Add and complete `api2.xml` `api2.xml` is your main API config file, you can add this file on your local module in `etc` directory. This is a basic structure of this config file: ```xml <{ACL_UNIQUE_NAME} translate="title" module="{MODULE_NAME}"> {ACL_NAME} <{API_UNIQUE_NAME} translate="title" module="{MODULE_NAME}"> {ACL_UNIQUE_NAME} {MAGENTO_MODEL} {API TITLE} 1 1 1 1 1 1 1 1 1 1 1 1 Entity ID /entity/:id entity /entities collection ``` - Below is an example of how such a configuration could appear in the admin area when configuring API Resources authorizations:
![Resources](./assets/resources.png)
- `resource_groups` declares new resource groups. - `resources` declares a new custom endpoint. Every resource needs to have a group (see `resource_groups`), a model and a title. - `privileges` defines the HTTP Method allowed for `customer`, `guest` and `admin`. ([Rest roles configuration Magento 1](https://devdocs-openmage.org/guides/m1x/api/rest/permission_settings/roles_configuration.html)) - `create` = POST - `retrieve` = GET - `update` = UPDATE - `delete` = DELETE - `attributes` lists the attributes that can be retrieved or sent ([Rest attributes configuration Magento 1](https://devdocs-openmage.org/guides/m1x/api/rest/permission_settings/attributes_configuration.html)) - `routes` configure your URL endpoint - Example: This following example is for a basic social network API, which can retrieve list of social networks posts and specific post thanks to 2 endpoints `/social-network-post/:id` and `/social-network-posts` ```xml Social network 100 social_network module_network/api2_post Social network posts 10 1 1 1 Post ID Title Description Link Image /social-network-post/:id entity /social-network-posts collection ``` ## Implement API methods ### Directory First, your file structure should look like this: `[MODULE]/[MODEL]/Rest/[Guest / Customer / Admin]/V1.php` In the social network example above, the directory would be: - `[MODULE]/Model/Api2/Post/Rest/Guest/V1.php` (for guest mode) - `[MODULE]/Model/Api2/Post/Rest/Customer/V1.php` (for customer mode) This file is your API entrypoint. :::warning Never forget to add Customer endpoint. If it is the same as the Guest endpoint, you can extend `Guest/V1.php` in `Customer/V1.php`. But if you don't do so, logged in users won't be able to fetch data from Magento, breaking your feature once logged in. ::: ### Methods to implement - [GET] `protected function _retrieve()` call for `entity` action type - [GET] `protected function _retrieveCollection()` call for `collection` action type - [POST] `protected function _create()` call for `collection` action type, body is mandatory - [PUT] `protected function _update()` call for `entity` action type, body is mandatory - [PUT] `protected function _multiUpdate()` call for `collection` action type, body is mandatory - [DELETE] `protected function _delete()` call for `entity` action type, body is mandatory - [DELETE] `protected function _multiDelete()` call for `collection` action type, body is mandatory ### Example ```php _getCollection(); $this->_applyCollectionModifiers($collection); $this->_loadCollection($collection); $this->addCacheHeaders($collection->getCacheLifetime()); $data = $collection->walk('toArray'); return array_values((array) $data); } /** * Retrieve post collection * */ protected function _getCollection() { $collection = Mage::getResourceModel('module_network/post_collection'); return $collection; } /** * Retrieve information about specified socialize * * @throws Mage_Api2_Exception * @return array */ protected function _retrieve() { $entityId = this->getRequest()->getParam('id'); // param name is defined on your route node, for this example is :id $post = Mage::getModel('module_network/post')->load($entityId); if (!$post || !$post->getId() || $post->getId() != $entityId) { $this->_critical(self::RESOURCE_NOT_FOUND); } return $post->getData(); } } ``` ### Sending requests to your API In case you need to ensure your endpoints work as expected, you can either use cURL or [Postman](https://www.getpostman.com/). But advanced clients like Postman will allow you to have credentials and will make it easier to run requests as logged in customers. - Guest testing: If your endpoint can be accessed in guest mode, you can simply send GET / POST / DELETE / UPDATE request to your endpoint to see the response - Customer testing: You need to add credentials and token for your request. You can retrieve all of this information in your database - Consumer Key = `key` in table `oauth_consumer` - Consumer Secret = `secret` in table `oauth_consumer` - Access Token = `token` of user in table `oauth_token` - Token secret = `secret` of same user in table `oauth_token`
![The Authorization tab of a Postman request with OAuth 1.0 selected.](./assets/testing.png)
## Good to know - If you can, extend `FrontCommerce_Integration_Model_Api2_Abstract` in your own API class. This class adds useful helper functions such as: - `public function getCustomer()`: Retrieve current customer and save it in customer session. - `protected function _initStore()`: Set current store with default store view or store set in request params. - `protected function _getStore()`: Rewrite Magento's base method to memoize store value. Retrieve current store according to request and API user type. - `protected function _getCurrency()`: Retrieve current currency. - ... - If you apply `_applyCollectionModifiers($collection)` to your own `$collection` you can use dynamic API collection filter (see [Magento documentation](https://devdocs-openmage.org/guides/m1x/api/rest/get_filters.html)) - If you need to send back an API error, use `$this->_critical(ERR_CODE);` --- # Add headers in Magento API calls URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento1/how-to/add-headers-in-magento-api-calls Front-Commerce allows you to send additional headers in all API calls. To do so, you must define the `magento.api.extraHeaders` (for storefront API) and/or `magento.api.extraAdminHeaders` (for admin API) configuration values from a [configuration provider](/docs/2.x/advanced/server/configurations#what-is-a-configuration-provider). These additional headers could be useful if you want to add additional context to your queries, depending on the request or to detect Front-Commerce requests from your Magento server. :::tip You can refer to our [Quanta module example](https://github.com/front-commerce/examples) for a working example using this feature. ::: --- # Clear the cache URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento1/how-to/clear-the-cache To clear the Front-Commerce cache, you can flush cache storage from the admin interface. To do this go to **System > Cache Management** then click on **Flush Cache Storage** button on the top right corner. (see below screenshot for more details). ![Clear Front-Commerce cache](./assets/clear-fc-cache.jpg) --- # Exposing additional attributes URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento1/how-to/exposing-additional-attributes

{frontMatter.description}

An example of what this page will show you is how to expose newly created attributes in the endpoints already used in Front-Commerce. For instance, when retrieving products, Front-Commerce uses the `/api/rest/products` URL. ## Overview In cases where you need to expose new attributes on the existing endpoint, you can add this attribute in your `api2.xml` file. For that, you need to retrieve the resource name (`API_UNIQUE_NAME` value if you follow the basic structure shown in the "Add custom endpoint" section) and add your attribute(s) code into the `attributes` node. ## Retrieve the resource name You can find this data on all `api2.xml` files. ## Update your `api2.xml` file In this section, you will learn how to expose an attribute whose code is `my_custom_attribute` for the most common Magento resources. ### Customer data ```xml title="api2.xml" ... ... ``` ### Product data ```xml title="api2.xml" ... ... ``` ### Category data ```xml title="api2.xml" ... ... ``` ## For attributes excluded by another module Sometimes, you will stumble upon modules that force attribute exclusion. You can find an example in the `app/code/community/Clockworkgeek/Extrarestful/etc/api2.xml` file: ```xml title="api2.xml" ... ... 1 1 1 1 1 1 1 1 1 1 1 ... ... ``` To revert this exclusion, you should retrieve the node you want to "disable" in your own `api2.xml` file and set its value to `0`. For instance, to allow Guests and Customers to access categories images you would do as below: ```xml title="api2.xml" ... 0 0 ... ``` If you have any questions, please do not hesitate to ask us. --- # Generate API documentation URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento1/how-to/generate-api-doc

{frontMatter.description}

You can find the script used below to generate the documentation at: https://github.com/PH2M/Magento1-Generate-Api-Doc ## Generate your API doc input files - Download the following PHP script https://github.com/PH2M/Magento1-Generate-Api-Doc/blob/develop/generateApiDoc.php - Paste it in your Magento's `shell` directory - Create `api/input` and `api/doc` directories in your Magento's root directory - Ensure your Magento is available and working fine (database configured, etc.) - Run the following script `php shell/generateApiDoc.php` This script generates PHP and JSON files in your `api/input` directory, these files help you to generate the [apiDoc](https://github.com/apidoc/apidoc). ## Generate your API doc For this step you need to install https://github.com/apidoc/apidoc or have [Docker](https://www.docker.com/) - With Docker: - In your Magento's root directory, run `docker run --rm -v $(pwd)/api:/home/node/apidoc apidoc/apidoc -o doc -i input` ($(pwd) is for Linux, for other system see https://stackoverflow.com/questions/41485217/mount-current-directory-as-a-volume-in-docker-on-windows-10#answer-41489151 - With Npm: - You need to install https://github.com/apidoc/apidoc - In your Magento's root directory, run `apidoc -i api/input/ -o api/doc/` Your API doc is now available in `api/doc/index.html` ## Tips - Link with anchor: to share a link to a specific endpoint's documentation, select the endpoint from the sidebar and copy the URL --- # Implement headless payments URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento1/how-to/implement-headless-payments

{frontMatter.description}

Front-Commerce's Magento module provides a generic way to expose Magento payment modules headlessly and supports [the relevant Front-Commerce payment workflows](../../../../05-concepts/understanding-payment-workflows.mdx). ## Supported Payment platforms Our Magento1 integration currently provides native adapters for the platforms below, learn how to install each one of them from the related documentation page: - [Paypal](../../../payment/paypal/index.mdx#magento1-openmagelts-module) - [PayZen](../../../payment/payzen/index.mdx) :::info If you want to use a Payment module not yet listed above, please so we can provide information about a potential upcoming native support for it. ::: ## Implement a new Magento1 Payment method :::caution WIP This section will be documented in the future. In the meantime, please so we can show you the steps to create your own headless payment in Magento1. ::: ### Using the "redirect after order" flow **Payment flows:** [Redirect after order](../../../../05-concepts/understanding-payment-workflows.mdx#redirect-after-order) 1. Add your own payment instance on `config.xml` file First, you need to create your own model implementing the `FrontCommerce_Integration_Model_Headlesspayment_Interface` interface and register it in your `config.xml` file with the following XML node: ```xml <{payment_method_code}>{class_name} ``` :::note Don't forget to replace `{payment_method_code}` by your payment method code and `{class_name}` by the class names you've just created ::: 2. Implement the interface methods in your own model: - `isHtml(): Boolean` if your payment return type is HTML content or not - `isUrl(): Boolean` if your payment return type is URL or not - `getValue(Mage_Sales_Model_Order $order): String` return URL or HTML when this payment method is call on front - `getResponseSuccess(String $action, [String] $additionalData): Boolean` Failed or success payment action - `getResponseMessage(String $action, [String] $additionalData): [String]` Failed action message - in success case, return an array of `quote_id` and `order_id`: ```js return [ (int) $additionalData['quote_id'], (int) $additionalData['order_id'] ]; ``` - in case of errors, return an array of error messages: ```js return [{error message}] ``` - `returnAction(String $action, [String] $additionalData): Boolean` Callback after payment, for example, you can add your custom code here for retrieve customer cart after cancel payment :::tip REMINDER Don't forget to override or update `checkoutFlowOf.js` to ensure your payment code will use the correct client-side checkout flow: ```diff title="theme/pages/Checkout/checkoutFlowOf.js" const checkoutFlowOf = (method) => { if (method === "ops_cc") return "redirectAfterOrder"; if (method === "ops_cc_redirect") return "redirectAfterOrder"; if (method === "payzen_standard") return "redirectAfterOrder"; if (method === "paypal_standard") return "redirectAfterOrder"; + if (method === "payment_method_code") return "redirectAfterOrder"; if (method === "paypal_express") return "redirectBeforeOrder"; if (method === "buybox") return "redirectBeforeOrder"; if (method.startsWith("adyen_")) return "directOrderWithAdditionalAction"; if (method.startsWith("hipay_")) return "directOrderWithAdditionalAction"; if (method === "payzen_embedded") return "asyncOrder"; return "directOrder"; }; export default checkoutFlowOf; ``` ::: --- # Set-up Magento Module URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento1/how-to/setup-magento-module

{frontMatter.description}

## Module compatibility We recommend to use [OpenMage LTS](https://github.com/OpenMage/magento-lts) (1.9.4.x - v19.x) with at least PHP 7.3 (with OpenSSL) and MySQL 8.0. It is also compatible with Magento CE >= 1.7 and Magento EE 1.12.x. ## Installation You can use 2 ways for installation, we recommend to use composer. ### Install with composer #### Prerequisites - Have a working install of [composer](https://getcomposer.org/download/). - You need to create a (or retrieve an existing) token from your Gitlab account and replace `$FC_GITLAB_TOKEN` by your token on the next step. #### Composer usage For a community edition version: ```shell composer config minimum-stability dev composer config repositories.front-commerce git https://gitlab.blackswift.cloud/front-commerce/magento1-module-front-commerce.git composer config repositories.front-commerce-restful git https://github.com/PH2M/Magento-Extra-RESTful composer config http-basic.gitlab.blackswift.cloud token $FC_GITLAB_TOKEN composer require front-commerce/magento1-module:"^1.3" ``` For an enterprise edition version: ```shell composer config minimum-stability dev composer config repositories.front-commerce-ee git https://gitlab.blackswift.cloud/front-commerce/magento1-module-enterprise-front-commerce composer config repositories.front-commerce git https://gitlab.blackswift.cloud/front-commerce/magento1-module-front-commerce.git composer config repositories.front-commerce-restful git https://github.com/PH2M/Magento-Extra-RESTful composer config http-basic.gitlab.blackswift.cloud token $FC_GITLAB_TOKEN composer require front-commerce/magento1-module-enterprise:"dev-master" ``` :::note `front-commerce/magento1-module-enterprise` is currently in beta, requiring `dev-master` allows to get the last version. After this beta phase, it is strongly recommended to use a fixed version. ([see available release for Enterprise Edition](https://gitlab.blackswift.cloud/front-commerce/magento1-module-enterprise-front-commerce/-/releases)) ::: ### Install directly in your Magento app folder - Community edition version: 1. Clone or download https://gitlab.blackswift.cloud/front-commerce/magento1-module-front-commerce.git 2. Copy `app` directory and paste it in your Magento's root directory 3. Clone or download https://github.com/PH2M/Magento-Extra-RESTful 4. Copy `modules/Clockworkgeek_Extrarestful.xml` file and paste it in your Magento's `app/etc/modules` directory 5. Create `app/code/community/Clockworkgeek/Extrarestful` directory in your Magento's 6. Copy all directories inside `code` in your Magento's `app/code/community/Clockworkgeek/Extrarestful` directory - Enterprise edition version: 1. Clone or download https://gitlab.blackswift.cloud/front-commerce/magento1-module-front-commerce.git 2. Copy `app` directory and paste it in your Magento's root directory 3. Clone or download https://gitlab.blackswift.cloud/front-commerce/magento1-module-enterprise-front-commerce 4. Copy `app` directory and paste it in your Magento's root directory 5. Clone or download https://github.com/PH2M/Magento-Extra-RESTful 6. Copy `modules/Clockworkgeek_Extrarestful.xml` file and paste it in your Magento's `app/etc/modules` directory 7. Create `app/code/community/Clockworkgeek/Extrarestful` directory in your Magento's 8. Copy all directories inside `code` in your Magento's `app/code/community/Clockworkgeek/Extrarestful` directory ## Configuration :::note If the installation is successful, in Magento's administration panel, you will have a new entry "Front-Commerce" in the top menu and a new tab "Front-Commerce" in System > Configuration. ::: ### Check install Go to the `Front-Commerce > Configuration` admin menu entry. You should see an "Installer checker" that will ensure that everything is configured correctly in your shop. Right after your first installation, most of the checks should be invalid. Please refer to the next steps to validate them. ### REST roles Front-Commerce requires you to define 3 roles. Here are the main steps: - Go to admin menu entry `System > Web services > REST Roles` - You need to have 3 roles, `Guest`, `Customer`, and `Admin`. If you don't, create them. - Set all roles access to all resources (Role API Resources tab > Resource Access "All"). See the [official documentation](https://docs.magento.com/m1/ce/user_guide/system-operations/web-services-activate.html) for detailed information about how to achieve this with Magento. ### REST attributes Front-Commerce roles must have access to several data on each entities, so it can expose them in GraphQL. You must allow each role to read these information. - Go to admin menu entry `System > Web services > REST Attributes` - You can see 3 user types `Guest`, `Customer`, and `Admin`. - Set all ACL attributes rules to all resource access (ACL Attribute Rules tab > Resource Access "All"). ### REST OAuth Consumer Front-Commerce must be able to create tokens for users when they log-in. You should ensure there is a `Front-Commerce` consumer configured in your install. - Go to admin menu entry `System > Web services > REST OAuth Consumers` - Add New OAuth Consumer: - Name: `Front-Commerce` - Callback URL: `http://local.host` \<- is useless but can't be empty The Key/Secret values should be used to configure your Front-Commerce, with the `FRONT_COMMERCE_MAGENTO1_CONSUMER_KEY` and `FRONT_COMMERCE_MAGENTO1_CONSUMER_SECRET` environment variables in your `.env` file. ### Admin user Advanced features such as Embedded Payments have to use an admin user with REST access. You must ensure it exists, by doing the following steps. - Go to admin menu entry `System > Permissions > User` - Create a new user (only used for Front-Commerce's embedded payments feature, users will never interact with your shop using this account) - User Name: `Front-Commerce` - First Name: `Front-Commerce` - Last Name `Front-Commerce` - Email: enter an administrator email - Password: enter a password - REST Role: Admin ### REST admin token You must generate admin tokens to configure Front-Commerce. - Go to admin menu entry Front-Commerce > Configuration - Click on "Generate Token" link in installer checker section (This should be the same key as `FRONT_COMMERCE_MAGENTO1_ACCESS_TOKEN` and `FRONT_COMMERCE_MAGENTO1_ACCESS_TOKEN_SECRET` in your Front-Commerce `.env`) ### Magento core patch If the OAuth Zend Patch is not valid in the installer checker, please follow these steps: - Copy the [`fix-sort-params-core.patch`](https://gitlab.blackswift.cloud/front-commerce/magento1-module-front-commerce/blob/master/fix-sort-params-core.patch) file in your root directory - Past it on your root Magento directory - Apply them - With GIT: `git apply fix-sort-params-core.patch` - Without GIT: `patch -p1 < fix-sort-params-core.patch` ### URLs settings - Go to System > Configuration > Front-Commerce General > URLs Settings - Add your Front-Commerce Front URL. In development environment, it should be `http://localhost:4000/`. In production environment, it is the URL of your main store. - Go to System > Configuration > General > Web - Make sure _Add Store Code to Urls_ is disabled - Set your base URL (secure + unsecure) with your Front-Commerce front URL (`http://localhost:4000/` in development environment) for each store view value. You can keep your admin URL for the default value. ### Cache settings - Go to System > Configuration > Front-Commerce General > Cache Settings - Add random Key (This should be the same key as `FRONT_COMMERCE_CACHE_API_TOKEN` in your Front-Commerce `.env`) ### Front-Commerce secret key For more security add a random key on `frontcommerce_secret_key` in your `app/etc/local.xml` ```xml // highlight-next-line {REPLACE_ME_BY_STRING_VALUE} // ... ``` ### Reviews configuration :::info This feature requires Front-Commerce Magento 1 module version 1.6.0 or higher. ::: Front-Commerce, by default, uses only one review "rating", named `"main"`. Those are configured in Magento1's back office under `Catalog > Reviews and Ratings > Manage Ratings`. If your project uses more than one rating, or one with a different name, you will have to configure them. In order to do so, you need to override `magento1ReviewsConfigProvider`'s `ratingTypes` default value. To learn more on how to do this, follow [the override an existing configuration guide](/docs/2.x/advanced/server/configurations#override-an-existing-configuration). In this config provider, you will need to add each the name and id of each provider you want your customer to see. ### Configure facet order If you want to be able to organize your facets' order, you will need to enable it in the `magentoProvider` (see [code](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/commit/fcaf0d0ddec410caf1a4d250d197e5f382e9004b#d43f790dbbf8f5142f59364a4b017caa013c10b5_82_83)). In this provider, override `magento.attributes.useFacetSortOrder` to set it to `true`. :::tip To learn more about how to override configuration provider, see our [guide on overriding an existing provider](/docs/2.x/advanced/server/configurations#override-an-existing-configuration). ::: Once that's done, all facets will be sorted by their "position" attribute that is defined in the Magento's back office. ## Ensure it works Once this is done, all the checks should be green in your installer checker and you should be good to go. You can check that the guest permissions are correctly configured by accessing this URL: `{MAGENTO_BASE_URL}/api/rest/frontcommerce/urls/match?urls[0]=/about-magento-demo-store` ## Common issues ### URL Rewrite The entry point of the API is the file `api.php`. In order to work, you need to have the following rules: For Apache in an `.htaccess` or in the server configuration: ``` RewriteRule ^api/rest api.php?type=rest [QSA,L] ``` For nginx in server configuration: ``` location /api { rewrite ^/api/rest /api.php?type=rest last; rewrite ^/api/v2_soap /api.php?type=v2_soap last; rewrite ^/api/soap /api.php?type=soap last; } ``` ### Mistake in ACL access Double check that you have followed [REST roles](#rest-roles) and [REST attributes](#rest-attributes) sections carefully. --- # Magento 1 URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento1/index

{frontMatter.description}

## Prerequisites [Install the module on Magento.](/docs/3.x/extensions/e-commerce/magento1/how-to/setup-magento-module) ## Installation First ensure you have installed the package: ```bash $ pnpm install @front-commerce/magento1@latest ``` ## Setup Magento1 Extension Update your `front-commerce.config.ts` to include the Magento1 Extension : ```typescript title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; import magento1 from "@front-commerce/magento1"; import storesConfig from "./app/config/stores"; import cacheConfig from "./app/config/caching"; export default defineConfig({ extensions: [magento1({ storesConfig }), themeChocolatine()], stores: storesConfig, cache: cacheConfig, configuration: { providers: [], }, v2_compat: { useApolloClientQueries: true, useFormsy: true, }, pwa: { appName: "Front-Commerce", shortName: "Front-Commerce", description: "My e-commerce application", themeColor: "#fbb03b", icon: "assets/icon.png", maskableIcon: "assets/icon.png", }, }); ``` Add the following variables to your `.env` file: ```shell title=".env" FRONT_COMMERCE_MAGENTO1_ENDPOINT=https://magento1.example.com FRONT_COMMERCE_MAGENTO1_CONSUMER_KEY=xxxxxxxxx FRONT_COMMERCE_MAGENTO1_CONSUMER_SECRET=xxxxxxxxx FRONT_COMMERCE_MAGENTO1_ACCESS_TOKEN=xxxxxxxxx FRONT_COMMERCE_MAGENTO1_ACCESS_TOKEN_SECRET=xxxxxxxxx ``` ## Feature Flags
The Magento 1 extension supports the following feature flags: Click to expand. - `Cart` (default: `true`) - Enable the cart feature - `Catalog` (default: `true`) - Enable the catalog feature - `Checkout` (default: `true`) - Enable the checkout feature - `Cms` (default: `true`) - Enable the CMS feature - `CmsSearch` (default: `true`) - Enable the CMS search feature - `Configuration` (default: `true`) - Enable the Configuration feature - `Contact` (default: `true`) - Enable the contact feature - `Customer` (default: `true`) - Enable the customer feature - `DownloadableProduct` (default: `true`) - Enable the downloadable product feature - `InStockAlert` (default: `true`) - Enable the in stock alert feature - `Invoice` (default: `true`) - Enable the invoice feature - `MagentoAdmin` (default: `true`) - Enable the Magento admin feature - `Order` (default: `true`) - Enable the order feature - `Refund` (default: `true`) - Enable the refund feature - `Reviews` (default: `true`) - Enable the reviews feature - `Store` (default: `true`) - Enable the store feature - `Wishlist` (default: `true`) - Enable the wishlist feature - `Wysiwyg` (default: `true`) - Enable the WYSIWYG feature
All these features are active by default. To disable a feature you should return a falsy value for the feature flag in your extension options: ```typescript title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; import magento1 from "@front-commerce/magento1"; import storesConfig from "./app/config/stores"; import cacheConfig from "./app/config/caching"; export default defineConfig({ extensions: [ magento1({ storesConfig // add-start features: { Contact: false, // Contact feature will be disabled Refund: false, // Refund feature will be disabled Reviews: false, // Reviews feature will be disabled // all other features will be enabled by default }, // add-end }), themeChocolatine(), ], stores: storesConfig, cache: cacheConfig, configuration: { providers: [], }, }); ``` :::tip If a feature is not defined in the feature flags, it will be enabled by default. ::: ## Disable Health Checks By default, Front-Commerce enables [Health Checks](/docs/3.x/guides/maintenance-mode/automatic-detection-with-service-health-checks) to monitor the availability of the Magento1 backend. These checks run on the `FRONT_COMMERCE_MAGENTO1_ENDPOINT` URL. If you want to disable these health checks, you can pass the `disabled` option to your extension configuration. ```typescript title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; import magento1 from "@front-commerce/magento1"; export default defineConfig({ extensions: [ magento1({ // highlight-start healthChecks: { disabled: true, }, // highlight-end }), ], }); ``` --- # Axios instances URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento1/reference/axios-instances

{frontMatter.description}

## Overview The Front-Commerce's Magento1 extension exposes preconfigured HTTP client instances for interacting with Magento1's API. These instances are exposed via the [Dependency Injection](../../../../02-guides/dependency-injection.mdx) system, under the `magento1` HTTP namespace. You can access them via `services.DI.get("magento1").http.*` in your code. ### `publicRestAPI` The `publicRestAPI` instance is an `axios` instance that can be used to interact with Magento1's API. It will handle authentication and other configuration details automatically. Example: ```ts title="path/to/graphql/module/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ contextEnhancer: ({ services, makeDataLoader }) => { const magento1Client = services.DI.get("magento1").http.publicRestAPI; return { MyFeature: new MyFeatureLoader(magento1Client, makeDataLoader), }; }, }); ``` ## `makeAdminClientFromRequest` The `makeAdminClientFromRequest` function creates an `axios` instance that can be used to interact with Magento's API as an Admin. ```js makeAdminClientFromRequest(request); ``` Arguments: | Name | Type | Description | | :-------- | :------ | :--------------------------- | | `request` | Request | v2 compatible request object | Example: ```ts import { createGraphQLRuntime } from "@front-commerce/core/graphql"; import { makeAdminClientFromRequest } from "@front-commerce/magento1/axios"; export default createGraphQLRuntime({ contextEnhancer: ({ req }) => { const adminAxiosInstance = makeAdminClientFromRequest(req); // rest of logic }, }); ``` ## `makeAuthServiceFromRequest` The `makeAuthServiceFromRequest` function creates a service that expose related session information. ```js makeAuthServiceFromRequest(request); ``` Arguments: | Name | Type | Description | | :-------- | :------ | :--------------------------- | | `request` | Request | v2 compatible request object | Example: ```ts import { createGraphQLRuntime } from "@front-commerce/core/graphql"; import { makeAuthServiceFromRequest } from "@front-commerce/magento1/axios"; export default createGraphQLRuntime({ contextEnhancer: ({ req }) => { const authService = makeAuthServiceFromRequest(req); // rest of logic }, }); ``` ## `makeUserClientFromRequest` The `makeUserClientFromRequest` function creates an `axios` instance that can be used to interact with Magento's API as a customer. ```js makeUserClientFromRequest(request); ``` Arguments: | Name | Type | Description | | :-------- | :------ | :--------------------------- | | `request` | Request | v2 compatible request object | Example: ```ts import { createGraphQLRuntime } from "@front-commerce/core/graphql"; import { makeUserClientFromRequest } from "@front-commerce/magento1/axios"; export default createGraphQLRuntime({ contextEnhancer: ({ req }) => { const axiosInstance = makeUserClientFromRequest(req); // rest of logic }, }); ``` ## `makeCartUrlBuilderFromRequest` The `makeCartUrlBuilderFromRequest` function creates a `CartUrlBuilder` instance. ```js makeCartUrlBuilderFromRequest(request); ``` Arguments: | Name | Type | Description | | :-------- | :------ | :--------------------------- | | `request` | Request | v2 compatible request object | Example: ```js import { createGraphQLRuntime } from "@front-commerce/core/graphql"; import { makeCartUrlBuilderFromRequest } from "@front-commerce/magento1/axios"; export default createGraphQLRuntime({ contextEnhancer: ({ req }) => { const cartUrlBuilder = makeCartUrlBuilderFromRequest(req); // rest of logic }, }); ``` ## `makeFilterParams` The `makeFilterParams` function creates a filter for Magento API request to filter on properties. ```js makeFilterParams(filters, initialFilters); ``` Arguments: | Name | Type | Description | | :--------------- | :------- | :-------------------------------------- | | `filters` | object[] | Array of filters | | `initialFilters` | object[] | Initial filters computed by other means | Example: ```js import { makeFilterParams } from "@front-commerce/magento1/axios"; const params = makeFilterParams([{ code: "is_filterable", value: "1" }], { initialParams: { limit: 0 }, }); ``` ## `makeSkuFilters` The `makeSkuFilters` function creates a filter for Magento API request to filter on SKU. ```js makeSkuFilters(skus); ``` Arguments: | Name | Type | Description | | :----- | :------- | :----------------------------- | | `skus` | string[] | Array of SKU | | `code` | string | Filter code (default to `sku`) | Example: ```js import { makeSkuFilters } from "@front-commerce/magento1/axios"; const filter = makeSkuFilters(["sku1", "sku2", "sku3"]); ``` --- # Caching strategies URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento1/reference/caching-strategies

{frontMatter.description}

## `PerMagentoCustomerGroup` The `PerMagentoCustomerGroup` implementation is a decorator that is specific to Magento 1 modules. It will decorate the existing caching strategies so that DataLoader keys are specific to the current customer group. We highly recommend to use it on Magento stores that have price per group, so they can leverage other caching mechanisms (such as `Redis`). It is possible to provide a default group to use as scope for guest users. Here is a configuration example: ```js title="app/config/caching.js" export default { strategies: [ // The PerMagentoCustomerGroup strategy MUST be registered after a persistent cache implementation // because it has no effect in the context of the default per-request in-memory caching. { implementation: "Redis", supports: "*", config: { host: "127.0.0.1", }, }, { implementation: "PerMagentoCustomerGroup", // Will scope all data from the CatalogPrice DataLoader with the customer group // (and other relevant price data loaders) // before they are transmitted to the previous strategy (Redis). // Other dataLoaders will use Redis storage in a standard fashion. supports: [ "CatalogPrice", "CatalogProductChildrenPrice", "CatalogProductBundlePrice", ], config: { defaultGroupId: 0, }, }, ], }; ``` ## `PerMagentoCustomerTaxZone` The `PerMagentoCustomerTaxZone` implementation is a decorator that is specific to the Magento platform. It will decorate the existing caching strategies so that DataLoader keys are specific to the current customer's tax zone. We highly recommend to use it on Magento stores that have price with taxes depending on several tax zones, so they can leverage other caching mechanisms (such as `Redis`). As this strategy can be complex, the tax zone definition is left to the integrator through the `taxZoneKeyFromAddress` function Here is a configuration example: ```js title="app/config/caching.js" export default { strategies: [ // The PerMagentoCustomerTaxZone strategy MUST be registered after a persistent cache implementation // because it has no effect in the context of the default per-request in-memory caching. { implementation: "Redis", supports: "*", config: { host: "127.0.0.1", }, }, { implementation: "PerMagentoCustomerTaxZone", supports: [ "CatalogPrice", "CatalogProductChildrenPrice", "CatalogProductBundlePrice", ], config: { addressType: "shipping", // or "billing" depending on which address is used to define the taxes showed to the user defaultTaxZoneKey: "FR", // Uncomment this line to override the default tax zone partitioning // taxZoneKeyFromAddress: (address) => { // // see https://docs.magento.com/user-guide/tax/tax-zones-rates.html // return address.country_id; // }, }, }, ], }; ``` ## `PerCurrency` The `PerCurrency` implementation is a decorator that is specific to Magento 1 integrations. It will decorate the existing caching strategies so that DataLoader keys are different depending on the store's currency selected by the user. This is only useful if a store has multiple currencies ([`config/stores.js::availableCurrencies`](/docs/2.x/advanced/production-ready/multistore#multiple-currencies)). It should be used on any DataLoader that returns a price value based on the user's session. Here is a configuration example: ```js title="app/config/caching.js" export default { strategies: [ // The PerCurrency strategy MUST be registered after a persistent cache implementation // because it has no effect in the context of the default per-request in-memory caching. { implementation: "Redis", supports: "*", config: { host: "127.0.0.1", }, }, { implementation: "PerCurrency", supports: [ "CatalogPrice", "CatalogProductChildrenPrice", "CatalogProductBundlePrice", // only for Magento 2 "CatalogProductBundle", // only for Magento 1 ], }, ], }; ``` --- # Dispatch event list URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento1/reference/dispatch-event-list

{frontMatter.description}

Events are grouped by the area they relate to. See sections below for details. :::info Lookup [Openmage's documentation](https://docs.openmage.org/blog/2022/08/17/events--observer) for more information on the events. ::: ## Catalog - `frontcommerce_api_prepare_category_product_collection_after`: event after retrieve products from category - `collection`: products resource collection - `request`: API request - `store`: Current store - `clockworkgeek_api_prepare_product_after`: event after retrieve product data - `product`: product model - `request`: API request - `store`: Current store ## Customer - `frontcommerce_api_customer_create_before_save`: event before create new user - `customer_api`: Customer API class - `customer`: customer model - `store`: Current store - `frontcommerce_api_customer_create_after_save`: event after create new user - `customer_api`: Customer API class - `customer`: customer model - `store`: Current store - `frontcommerce_api_customer_login_create_quote_before_save`: event before set customer quote after login - `oauth_server`: OAuth API class - `customer`: customer model - `quote`: quote model - `store`: Current store ## Sales - `frontcommerce_api_retrieve_cart_data_before_render`: event before retrieve cart data - `cart_api`: Cart API class - `cart_data`: Varien object of cart data - `request`: API request - `store`: Current store - `frontcommerce_api_set_payment_information_before`: event before save payment information - `quote`: Quote model - `onepage`: Onepage model - `update_data`: Payment data to set - `request`: API request - `store`: Current store - `frontcommerce_api_set_shipping_information_before_save`: event before save shipping information - `onepage`: Onepage model - `shipping_data`: Shipping data to set - `request`: API request - `store`: Current store - `frontcommerce_api_create_cart_before_save`: event before save cart - `cart_api`: Cart API class - `cart`: Cart model - `quote`: Quote model - `customer`: Current customer - `store`: Current store - `frontcommerce_api_update_order_status_after_save`: event after update order status - `order`: Current order - `update_data`: Order data updated - `request`: API request - `store`: Current store ## URL - `frontcommerce_api_prepare_url_find_collection`: event before retrieve URL from URL Rewrite URL FIND - `collection`: products resource collection - `request`: API request - `store`: Current store - `frontcommerce_api_prepare_url_match_collection`: event before retrieve URL from URL Rewrite URL MATCH - `catalog_collection`: Catalog rewrite collection - `page_collection`: CMS Page collection - `filter_urls`: URL to match - `request`: API request - `store`: Current store --- # Environment variables URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento1/reference/environment-variables | Environment Variable | Description | | --------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `FRONT_COMMERCE_MAGENTO1_ENDPOINT` | **Required** The URL of the Magento instance (ex: `http://magento1.local`) | | `FRONT_COMMERCE_MAGENTO1_CONSUMER_KEY` | **Required** Integration token configured in Magento's `Front-Commerce > Configuration` | | `FRONT_COMMERCE_MAGENTO1_CONSUMER_SECRET` | **Required** Integration token configured in Magento's `Front-Commerce > Configuration` | | `FRONT_COMMERCE_MAGENTO1_ACCESS_TOKEN` | **Required** Integration token configured in Magento's `Front-Commerce > Configuration` | | `FRONT_COMMERCE_MAGENTO1_ACCESS_TOKEN_SECRET` | **Required** Integration token configured in Magento's `Front-Commerce > Configuration` | | `FRONT_COMMERCE_MAGENTO1_TIMEOUT` | The timeout in milliseconds for Magento requests (default: `60000`) | | `FRONT_COMMERCE_MAGENTO1_ADMIN_TIMEOUT` | The timeout in milliseconds for Magento requests that require admin privileges (default: `10000`) | | `FRONT_COMMERCE_MAGENTO1_ADMIN_PATH` | The path of the Magento administration interface (default: `admin`) | | `FRONT_COMMERCE_XRAY_MAGENTO1_VERSION` | The magento 1 version you are using (default: `openmage`, possible values: `openmage`, `magento1`) | --- # StoreConfigLoader URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento1/reference/store-config-loader

{frontMatter.description}

## StoreConfigLoader The `StoreConfigLoader` is used to load the store configuration. ```js StoreConfigLoader(axios, storeCode); ``` Arguments: | Name | Type | Description | | :---------- | :----- | :------------- | | `axios` | Axios | Axios instance | | `storeCode` | String | Store code | Example ```js import { createGraphQLRuntime } from "@front-commerce/core/graphql"; import { StoreConfigLoader } from "@front-commerce/magento1/config"; export default createGraphQLRuntime({ contextEnhancer: ({ req }) => { const adminAxiosInstance = makeAdminClientFromRequest(req); const storeConfig = StoreConfigLoader(adminAxiosInstance, "default"); }, }); ``` --- # Add a new attribute URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento2/how-to/add-a-new-attribute

{frontMatter.description}

Let's say we want to add a new element to display on a product's page (for instance, a product's rate). You will have to: 1. Add an attribute to your Magento 2. 2. Expose the attribute in your GraphQL schema. 3. Display it on screen. ## Add a value to your database The process to do so may vary with the backend software you are using. For Magento 2 (used in this example), you can refer to [How to Create Product Attributes in Magento 2](https://www.fastcomet.com/tutorials/magento2/product-attributes). ## Expose the attribute in your GraphQL schema :::warning This section follows the [Extend the GraphQL schema guide](../../../../02-guides/extend-the-graphql-schema.mdx). We won't go into too much detail here, so please refer to the guide if you need more information. ::: We want to display the `rate` of a product on the product page. To keep it simple, it will only consist in displaying a number, which can be manually edited by the merchant. This feature will be stored in a new extension called `ratings`. Let's create a new extension along with its folder structure. At the root of your repository, type the following command. ```shell mkdir -p extensions/ratings ``` ### Create the extension Let's create our extension : ```ts title=extensions/ratings/index.ts import { defineExtension } from "@front-commerce/core"; export default function ratings() { return defineExtension({ name: "ratings", theme: "extensions/ratings/theme", meta: import.meta, }); } ``` And add the extension we just made to Front-Commerce. Edit the `front-commerce.config.ts` file (root folder) as shown below to do so. ```ts title=front-commerce.config.ts import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; import storesConfig from "./app/config/stores"; import magento2 from "@front-commerce/magento2"; import ratings from "extensions/ratings"; export default defineConfig({ extensions: [themeChocolatine(), magento2({ storesConfig }), ratings()], // ... }); ``` ### Extend the Product definition First, we need to tell the server how to customise the GraphQL schema. To do so, we will create graphQL module and extend `Product` to add our `rate`. ```ts title="extensions/ratings/graphql/index.ts" import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "Ratings", typeDefs: /* GraphQL */ ` extend type Product { rate: Float } `, }); ``` ### Implement the resolver We must now define what Front-Commerce should fetch when `rate` is requested in a GraphQL query; in other words we must write the code that will **resolve** the queries for the rates. Create a file named `runtime.ts` and type the following code. It implements the runtime with its resolvers. ```ts title="extensions/ratings/graphql/runtime.ts" import { createGraphQLRuntime } from "@front-commerce/core/graphql"; export default createGraphQLRuntime({ resolvers: { Product: { rate: ({ rate }) => parseFloat(rate), }, }, }); ``` And load the runtime in the graphQL module : ```ts title="extensions/ratings/graphql/index.ts" import { createGraphQLModule } from "@front-commerce/core/graphql"; export default createGraphQLModule({ namespace: "Ratings", typeDefs: /* GraphQL */ ` extend type Product { rate: Float } `, // highlight-next-line loadRuntime: () => import("./runtime"), }); ``` ### Declare the module If we want our code to be taken into account when the module is loaded, we must reference it. ```ts title=extensions/ratings/index.ts import { defineExtension } from "@front-commerce/core"; export default function ratings() { return defineExtension({ name: "ratings", theme: "extensions/ratings/theme", meta: import.meta, // highlight-start graphql: { modules: [ratingsModule], }, // highlight-end }); } ``` ### Discover the playground By typing `yourhostname/graphql` (in our case `localhost:4000/graphql`) in your browser address bar, you can access a GraphQL playground with a nice GUI to test your queries. **You will need to restart the application to access the updated schema in the playground.** ```graphql title="http://localhost:4000/graphql" { product(sku: "yourSKUhere") { name rate } } ``` From this GraphQL query, you should get a JSON content that looks like this: ```json { "data": { "product": { "name": "Your product name", "rate": // the value stored in your backoffice (Magento 2 in this case) } } } ``` ## Displaying the result on screen :::warning This section follows the [Override a component guide](../../../../02-guides/override-a-component.mdx#override-a-component). We won't go into too much detail here, so please refer to the guide if you need more information. ::: Now that you have access to your attribute, you can display it however you want in your React application. In this example, we will add it on the product's page, in the right section. ### How to find which files to edit #### With React Dev Tools With the [React Dev Tools](https://react.dev/learn/react-developer-tools), you can easily find the component name : 1. Open the Dev Tools 2. Browse to the "Component" tab 3. Click on "Select an element in the page to inspect it" 4. Click on the section you want to find the name
![React Dev Tools screenshot demonstrating that when you inspect a product page description, you find the product-sythesis component in component tab](./assets/react-dev-tools.png)
Then you can search for that keyword in your `node_modules/@front-commerce` folder and you will find the right files. #### With the inspector On your web page, right click on the element you wish to edit, then click on **Inspect Element**. In the inspector, you can look for the `class` that holds the elements you want to edit. Here, the `class` is `product-synthesis`.
![Inspector screenshot demonstrating that when you inspect a product page description, you find the product-sythesis class in the DOM](./assets/inspector-screenshot.png)
Then you can search for that keyword in your `node_modules/@front-commerce` folder and you will find the right files. ### Overriding the files In this example, the files to be overridden are `ProductSynthesisFragment.gql` and `Synthesis.jsx` both located at [`theme/modules/ProductView/Synthesis`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/d72ed131a281f985b64444190a39b35443129c7e/packages/theme-chocolatine/theme/modules/ProductView/Synthesis). Copy and paste them into `extensions/ratings/theme/modules/ProductView/Synthesis`. See [Override a component](../../../../02-guides/override-a-component.mdx) for more details. Once you have created the new files, **restart your application** to ensure that these are used instead of the core ones. ### Updating the query In `extensions/ratings/theme/modules/ProductView/Synthesis/ProductSynthesisFragment.gql`, add the following query line. It means that the application will request the `rate` field as well when sending the query. (Depending on the version of Front-Commerce you are using, the content might differ slightly.) ```graphql title="extensions/ratings/theme/modules/ProductView/Synthesis/ProductSynthesisFragment.gql" #import "theme/components/organisms/Configurator/ProductConfiguratorFragment.gql" #import "theme/modules/AddToCart/ProductStockFragment.gql" #import "theme/modules/ProductView/ProductName/ProductNameFragment.gql" #import "theme/modules/AddToCart/AddToCartFragment.gql" #import "theme/modules/ProductView/GroupedItems/GroupedItemsFragment.gql" fragment ProductSynthesisFragment on Product { sku # highlight-next-line rate ...ProductNameFragment ...ProductConfiguratorFragment ...ProductStockFragment ...AddToCartFragment ...GroupedItemsFragment groupedItems { product { sku } } } ``` :::info GraphQL queries can be split in Fragments and each fragment is supposed to live next to a component in order to easily request all the data needed for your component. Thus, since we need to display some new data on the `ProductSynthesis` component, we update the `ProductSynthesisFragment`. ::: We're almost there! In the file `Synthesis.jsx`, you can add some code to display the `rate`. Knowing React.js or at least HTML syntax is necessary to perform this step without impacting other items on the page. ```jsx // Many more things here const ProductSynthesis = (props) => { const { product, // Many more things here } = props; return (
{/* Many more things here */} // highlight-next-line
Rate: {product.rate} / 5
{/* Many more things here */}
); }; export default ProductSynthesis; ```
--- # Add registration question URL: https://developers.front-commerce.com/docs/3.x/extensions/e-commerce/magento2/how-to/add-registration-question

{frontMatter.description}

![Synthesis screenshot here](./assets/question-screenshot.png)
## Add a new attribute First, you need to add a new attribute. Follow [this guide](./add-a-new-attribute.mdx) to learn how to do it. ## Add the form to the page ### Create a new extension Create and register a new extension : ```ts title="extensions/register-with-question/index.ts" import { defineRemixExtension } from "@front-commerce/remix"; import path from "node:path"; export default function registerWithQuestion() { const basePath = "extensions/register-with-question"; return defineRemixExtension({ meta: import.meta, name: "register-with-question", routes: import.meta.url, theme: path.join(basePath, "theme"), translations: path.join(basePath, "lang"), }); } ``` ```ts title="front-commerce.config.ts" import { defineConfig } from "@front-commerce/core/config"; import themeChocolatine from "@front-commerce/theme-chocolatine"; import storesConfig from "./app/config/stores"; import magento2 from "@front-commerce/magento2"; // highlight-next-line import registerWithQuestion from "extensions/register-with-question"; // ... export default defineConfig({ extensions: [ themeChocolatine(), magento2({ storesConfig }), // highlight-next-line registerWithQuestion(), ], // ... }); ``` ### Override the register form module The source code for the `Account creation` page is located at [`theme/modules/User/RegisterForm/`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/bb1a727b369cacb23d42ebe6846261ea57c83a35/packages/theme-chocolatine/theme/modules/User/RegisterForm). Copy it into your extension. (If you don't know how to locate the source code for a page, click [here](./add-a-new-attribute.mdx#how-to-find-which-files-to-edit).) ```shell mkdir -p extensions/register-with-question/theme/modules/User/ cp -r node_modules/@front-commerce/theme-chocolatine/theme/modules/User/RegisterForm/ \ extensions/register-with-question/theme/modules/User/ ``` Copy also the register route : ```shell mkdir -p extensions/register-with-question/routes/ cp -r node_modules/@front-commerce/theme-chocolatine/routes/_main.register.tsx \ extensions/register-with-question/routes/ ``` ### Modify the form Edit `RegisterForm.tsx` as shown below. This adds the actual form field seen on the page. ```tsx title="extensions/register-with-question/theme/modules/User/RegisterForm/RegisterForm.tsx" // More code here import { Text, Email, Password, PasswordStrengthHint, Hidden, Checkbox, TitleSelect, // highlight-next-line Textarea } from "theme/components/atoms/Forms/Input"; // More code here const messages = defineMessages({ // More code here titleLabel: { id: "modules.User.RegisterForm.title", defaultMessage: "Title", }, // highlight-start questionLabel: { id: "modules.User.RegisterForm.question", defaultMessage: "Question", }, // highlight-end }); // More code here // highlight-start
``` If we were to transform this with Front-Commerce components, it would instead look like this: ```jsx import { Form } from "@remix-run/react"; import FormItem from "theme/components/molecules/Form/Item"; import FormActions from "theme/components/molecules/Form/FormActions"; import Fieldset from "theme/components/atoms/Forms/Fieldset"; import { Email, Textarea } from "theme/components/atoms/Forms/Input"; import SubmitButton from "theme/components/atoms/Button/SubmitButton"; const MyForm = () => { return (