588 lines
16 KiB
Plaintext
588 lines
16 KiB
Plaintext
---
|
||
addHowToData: true
|
||
---
|
||
|
||
# Contentful Plugin
|
||
|
||
In this document, you’ll 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](../../development/backend/configurations.md#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 [Contentful’s documentation](https://www.contentful.com/help/find-space-id/) if you’re unsure where to find it.
|
||
2. `access_token`: (required) a string indicating the personal access token for content management. Refer to [Contentful’s 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 product’s `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 Medusa’s 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 Contentful’s 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 store’s `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 haven’t 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, they’ll run when the Medusa backend first starts and migrate your content models to Contentful. You can go to your space’s dashboard to confirm they’ve been created.
|
||
|
||
After that, try the sync functionality by creating or updating products in the Medusa backend. If you’ve 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.
|
||
|
||
:::
|
||
|
||
---
|
||
|
||
## What’s 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.
|