## 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/)!

---
# 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 (
);
}
```
# 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".

:::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:

---
# 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.

:::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
```

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:
\*
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 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:

---
# 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:

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:

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 `
### 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
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.

### Defining the components
First, let's split the mockup in several UI components following the
[Atomic Design Principles](http://atomicdesign.bradfrost.com/).

- **`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:

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.

:::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:
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: