Files
medusa-store/www/apps/docs/content/plugins/cms/contentful.mdx
2024-01-22 18:38:35 +01:00

588 lines
16 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
addHowToData: true
---
# Contentful Plugin
In this document, youll learn how to install and use the Contentful plugin.
## Overview
[Contentful](https://www.contentful.com/) is a headless CMS service that allows developers to integrate rich CMS functionalities into any platform.
By integrating Contentful to Medusa, you can benefit from powerful features in your ecommerce store including detailed product CMS details, easy-to-use interface to use for static content and pages, localization, and much more. The data is also automatically synced between Contentful and your Medusa store.
---
## Prerequisites
### Medusa Components
This guide assumes you already have a Medusa backend installed. If not, you can learn how to install it [here](../../create-medusa-app.mdx).
Redis must also be configured in your Medusa backend if you want to sync data from Medusa to Contentful. Learn how to configure it [here](../../references/medusa_config/interfaces/medusa_config.ConfigModule.mdx#redis_url).
### Needed Account
- [Contentful](https://www.contentful.com/sign-up/) account with a space created. A space is created by default when you create a new account.
---
## Install Plugin
In the directory of your Medusa backend, run the following command to install the Contentful plugin:
```bash npm2yarn
npm install medusa-plugin-contentful
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
```js title="medusa-config.js"
const plugins = [
// ...
{
resolve: `medusa-plugin-contentful`,
options: {
space_id: process.env.CONTENTFUL_SPACE_ID,
access_token: process.env.CONTENTFUL_ACCESS_TOKEN,
environment: process.env.CONTENTFUL_ENV,
},
},
]
```
### Plugin Options
The plugin accepts the following options:
1. `space_id`: (required) a string indicating the ID of your Contentful space. Refer to [Contentfuls documentation](https://www.contentful.com/help/find-space-id/) if youre unsure where to find it.
2. `access_token`: (required) a string indicating the personal access token for content management. Refer to [Contentfuls documentation](https://www.contentful.com/help/personal-access-tokens/#how-to-get-a-personal-access-token-the-web-app) to learn how to create it.
3. `environment`: (required) a string indicating the [Contentful environment](https://www.contentful.com/developers/docs/concepts/multiple-environments/). Typically, its value should be `master`.
4. `ignore_threshold`: (optional) a number indicating the number of seconds to wait before re-syncing a specific record. By default, its value is `2`.
5. `custom_<TYPE>_fields`: (optional) an object that allows you to map fields in Medusa to custom field names. Learn more [here](#custom-field-mapping).
Make sure to add required values as environment variables:
```bash
CONTENTFUL_SPACE_ID=<YOUR_SPACE_ID>
CONTENTFUL_ACCESS_TOKEN=<YOUR_ACCESS_TOKEN>
CONTENTFUL_ENV=master
```
### Custom Field Mapping
When the plugin syncs data between Contentful and Medusa, it expects a set of fields to be defined in the respective content models in Contentful. If you choose to use different names to define those fields in Contentful, you have to specify them in the `custom_<TYPE>_fields` option mentioned earlier, where `<TYPE>` is the name of the content model.
For example, to change the name of the products `title` field, pass the following option to the plugin:
```js title="medusa-config.js"
const plugins = [
// ...
{
resolve: `medusa-plugin-contentful`,
options: {
space_id: process.env.CONTENTFUL_SPACE_ID,
access_token: process.env.CONTENTFUL_ACCESS_TOKEN,
environment: process.env.CONTENTFUL_ENV,
custom_product_fields: {
title: "name",
},
},
},
]
```
The rest of this section includes the field names you can customize using this option for each content model type.
<Details>
<Summary>product</Summary>
- `title`
- `subtitle`
- `description`
- `variants`
- `options`
- `medusaId`
- `type`
- `collection`
- `tags`
- `handle`
</Details>
<Details>
<Summary>variant</Summary>
- `title`
- `sku`
- `prices`
- `options`
- `medusaId`
</Details>
<Details>
<Summary>region</Summary>
- `name`
- `countries`
- `paymentProviders`
- `fulfillmentProviders`
- `medusaId`
</Details>
<Details>
<Summary>collection</Summary>
- `title`
- `medusaId`
</Details>
<Details>
<Summary>type</Summary>
- `name`
- `medusaId`
</Details>
### Migrate Content Models
In your Contentful space, you must have content models for Medusa entities such as products and regions.
You can either create the content models manually, or you can write a migration script in the Medusa backend that migrates these content models into Contentful.
This section includes migration scripts for Medusas entities that are relevant for Contentful. You can customize the scripts if necessary. You can also create scripts for custom content models, such as a Link or Navigation Item content model.
Before creating the migration scripts, run the following command in the root of your Medusa backend to install Contentfuls migration SDK:
```bash npm2yarn
npm install --save-dev contentful-migration
```
<Details>
<Summary>product Content Model</Summary>
Create the file `src/loaders/contentful-migrations/product.ts` with the following content:
```ts title="src/loaders/contentful-migrations/product.ts"
import Migration, {
MigrationContext,
} from "contentful-migration"
export function productMigration(
migration: Migration,
context?: MigrationContext
) {
const product = migration
.createContentType("product")
.name("Product")
.displayField("title")
product
.createField("title")
.name("Title")
.type("Symbol")
.required(true)
product
.createField("subtitle")
.name("Subtitle")
.type("Symbol")
product
.createField("handle")
.name("Handle")
.type("Symbol")
product
.createField("thumbnail")
.name("Thumbnail")
.type("Link")
.linkType("Asset")
product
.createField("description")
.name("Description")
.type("Text")
product
.createField("options")
.name("Options")
.type("Object")
product
.createField("tags")
.name("Tags")
.type("Object")
product
.createField("collection")
.name("Collection")
.type("Symbol")
product
.createField("type")
.name("Type")
.type("Symbol")
product
.createField("variants")
.name("Variants")
.type("Array")
.items({
type: "Link",
linkType: "Entry",
validations: [
{
linkContentType: ["productVariant"],
},
],
})
product
.createField("medusaId")
.name("Medusa ID")
.type("Symbol")
}
```
</Details>
<Details>
<Summary>productVariant Content Model</Summary>
Create the file `src/loaders/contentful-migrations/product-variant.ts` with the following content:
```ts title="src/loaders/contentful-migrations/product-variant.ts"
import Migration, {
MigrationContext,
} from "contentful-migration"
export function productVariantMigration(
migration: Migration,
context?: MigrationContext
) {
const productVariant = migration
.createContentType("productVariant")
.name("Product Variant")
.displayField("title")
productVariant
.createField("title")
.name("Title")
.type("Symbol")
.required(true)
productVariant
.createField("sku")
.name("SKU")
.type("Symbol")
productVariant
.createField("options")
.name("Options")
.type("Object")
productVariant
.createField("prices")
.name("Prices")
.type("Object")
productVariant
.createField("medusaId")
.name("Medusa ID")
.type("Symbol")
}
```
</Details>
<Details>
<Summary>collection Content Model</Summary>
Create the file `src/loaders/contentful-migrations/product-collection.ts` with the following content:
```ts title="src/loaders/contentful-migrations/product-collection.ts"
import Migration, {
MigrationContext,
} from "contentful-migration"
export function productCollectionMigration(
migration: Migration,
context?: MigrationContext
) {
const collection = migration
.createContentType("collection")
.name("Product Collection")
.displayField("title")
collection
.createField("title")
.name("Title")
.type("Symbol")
.required(true)
collection
.createField("medusaId")
.name("Medusa ID")
.type("Symbol")
}
```
</Details>
<Details>
<Summary>productType Content Model</Summary>
Create the file `src/loaders/contentful-migrations/product-type.ts` with the following content:
```ts title="src/loaders/contentful-migrations/product-type.ts"
import Migration, {
MigrationContext,
} from "contentful-migration"
export function productTypeMigration(
migration: Migration,
context?: MigrationContext
) {
const collection = migration
.createContentType("productType")
.name("Product Type")
.displayField("title")
collection
.createField("title")
.name("Title")
.type("Symbol")
.required(true)
collection
.createField("medusaId")
.name("Medusa ID")
.type("Symbol")
}
```
</Details>
<Details>
<Summary>region Content Model</Summary>
Create the file `src/loaders/contentful-migrations/region.ts` with the following content:
```ts title="src/loaders/contentful-migrations/region.ts"
import Migration, {
MigrationContext,
} from "contentful-migration"
export function regionMigration(
migration: Migration,
context?: MigrationContext
) {
const region = migration
.createContentType("region")
.name("Region")
.displayField("name")
region
.createField("name")
.name("Name")
.type("Symbol")
.required(true)
region
.createField("countries")
.name("Options")
.type("Object")
region
.createField("paymentProviders")
.name("Payment Providers")
.type("Object")
region
.createField("fulfillmentProviders")
.name("Fulfillment Providers")
.type("Object")
region
.createField("currencyCode")
.name("Currency Code")
.type("Symbol")
region
.createField("medusaId")
.name("Medusa ID")
.type("Symbol")
}
```
</Details>
Finally, create a [loader](../../development/loaders/overview.mdx) at `src/loaders/index.ts` with the following content:
```ts title="src/loaders/index.ts"
import {
ConfigModule,
StoreService,
MedusaContainer,
} from "@medusajs/medusa"
import { runMigration } from "contentful-migration"
import {
productMigration,
} from "./contentful-migrations/product"
import {
productVariantMigration,
} from "./contentful-migrations/product-variant"
import {
productCollectionMigration,
} from "./contentful-migrations/product-collection"
import {
productTypeMigration,
} from "./contentful-migrations/product-type"
import {
regionMigration,
} from "./contentful-migrations/region"
type ContentfulPluginType = {
resolve: string
options: {
space_id: string
access_token: string
environment: string
}
}
export default async (
container: MedusaContainer,
config: ConfigModule
): Promise<void> => {
// ensure that migration only runs once
const storeService = container.resolve<StoreService>(
"storeService"
)
const store = await storeService.retrieve()
if (store.metadata?.ran_contentful_migrations) {
return
}
console.info("Running contentful migrations...")
// load Contentful options
const contentfulPlugin = config.plugins
.find((plugin) =>
typeof plugin === "object" &&
plugin.resolve === "medusa-plugin-contentful"
) as ContentfulPluginType
if (!contentfulPlugin) {
console.log(
"Didn't find Contentful plugin. Aborting migration..."
)
return
}
const options = {
spaceId: contentfulPlugin.options.space_id,
accessToken: contentfulPlugin.options.access_token,
environment: contentfulPlugin.options.environment,
yes: true,
}
const migrationFunctions = [
{
name: "Product",
function: productMigration,
},
{
name: "Product Variant",
function: productVariantMigration,
},
{
name: "Product Collection",
function: productCollectionMigration,
},
{
name: "Product Type",
function: productTypeMigration,
},
{
name: "Region",
function: regionMigration,
},
]
await Promise.all(
migrationFunctions.map(async (migrationFunction) => {
console.info(`Migrating ${
migrationFunction.name
} component...`)
try {
await runMigration({
...options,
migrationFunction: migrationFunction.function,
})
console.info(`Finished migrating ${
migrationFunction.name
} component`)
} catch (e) {
if (
typeof e === "object" && "errors" in e &&
Array.isArray(e.errors) &&
e.errors.length > 0 &&
e.errors[0].type === "Invalid Action" &&
e.errors[0].message.includes("already exists")
) {
console.info(`${
migrationFunction.name
} already exists. Skipping its migration.`)
} else {
throw new Error(e)
}
}
})
)
await storeService.update({
metadata: {
ran_contentful_migrations: true,
},
})
console.info("Finished contentful migrations")
}
```
Notice that in the script you store a flag in the default stores `metadata` attribute to ensure these migrations only run once.
### Setup Webhooks
As mentioned in the introduction, this plugin supports two-way sync. A [subscriber](../../development/events/create-subscriber.md) listens to changes in the data, such as adding a new product, and syncs the data with Contentful.
To update the Medusa backend when changes occur in Contentful, you must configure webhooks settings in Contentful.
:::note
For webhooks to work, your backend must be deployed and accessible publicly. If you havent deployed your backend, refer to [these deployment guides](../../deployments/server/index.mdx).
:::
To do that:
1. On your Contentful Space Dashboard, click on Settings from the navigation bar, then choose Webhooks.
2. Click on the Add Webhook button.
3. In the form, enter a name for the webhook.
4. In the URL field, choose the method `POST` and in the input next to it enter the URL `<BACKEND_URL>/hooks/contentful` where `<BACKEND_URL>` is the URL of your deployed Medusa backend.
5. Scroll down to find the Content Type select field. Choose `application/json` as its value.
6. You can leave the rest of the fields the same and click on the Save button.
---
## Test the Plugin
Run the following command to start your Medusa backend and test the plugin:
```bash
npx medusa develop
```
If you created migration scripts, theyll run when the Medusa backend first starts and migrate your content models to Contentful. You can go to your spaces dashboard to confirm theyve been created.
After that, try the sync functionality by creating or updating products in the Medusa backend. If youve also setup webhooks, you can test out the sync from Contentful to Medusa.
:::note
As mentioned in the [Prerequisites section](#prerequisites), you must configure Redis for the Medusa to Contentful sync to work.
:::
---
## Whats Next?
After installing the plugin, you can either customize the [Next.js storefront](../../starters/nextjs-medusa-starter.mdx) to fetch data from Contentful, or [build a storefront](../../storefront/roadmap.mdx) that connects to both Medusa and Contentful.