Files
medusa-store/www/apps/resources/app/plugins/cms/contentful/page.mdx
Shahed Nasser 4fe28f5a95 chore: reorganize docs apps (#7228)
* reorganize docs apps

* add README

* fix directory

* add condition for old docs
2024-05-03 17:36:38 +03:00

687 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
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.
import { Table } from "docs-ui"
export const metadata = {
title: `Contentful Plugin`,
}
# {metadata.title}
## Features
[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 such as:
- Rich CMS details for product.
- Easy-to-use interface to manage content for static content and pages.
- Localization for product and storefront content.
- Two-way sync between Contentful and Medusa.
---
## Install the Contentful Plugin
<Note type="check">
- [Contentful account with a space](https://www.contentful.com/sign-up/).
- An Event Module installed in the Medusa application, such as the [Redis Event Module](../../../architectural-modules/event/redis/page.mdx).
</Note>
To install the Contentful plugin, run the following command in the directory of your Medusa application:
```bash npm2yarn
npm install medusa-plugin-contentful
```
Next, add the plugin into the `plugins` array in `medusa-config.js`:
export const highlights = [
["6", "space_id", "The Contentful space's ID."],
["7", "access_token", "The personal access token for content management."],
["8", "environment", "The Contentful environment."],
]
```js title="medusa-config.js" highlights={highlights}
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,
},
},
]
```
### Contentful Plugin Options
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Option</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Required</Table.HeaderCell>
<Table.HeaderCell>Default</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`space_id`
</Table.Cell>
<Table.Cell>
A string indicating the ID of your [Contentful space](https://www.contentful.com/help/find-space-id/).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`access_token`
</Table.Cell>
<Table.Cell>
A string indicating [the personal access token for content management](https://www.contentful.com/help/personal-access-tokens/#how-to-get-a-personal-access-token-the-web-app).
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`environment`
</Table.Cell>
<Table.Cell>
A string indicating the [Contentful environment](https://www.contentful.com/developers/docs/concepts/multiple-environments/). Typically, its value should be `master`.
</Table.Cell>
<Table.Cell>
Yes
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`ignore_threshold`
</Table.Cell>
<Table.Cell>
The number of seconds to wait before re-syncing a specific record.
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
`2`
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`custom_<TYPE>_fields`
</Table.Cell>
<Table.Cell>
An object that allows you to map fields in Medusa to [custom field names](#custom-field-mapping).
</Table.Cell>
<Table.Cell>
No
</Table.Cell>
<Table.Cell>
\-
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
### Environment Variables
Make sure to add the following 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 use different names to define those fields in Contentful, 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" highlights={[["9"], ["10"], ["11"]]}
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 summaryContent="product">
- `title`
- `subtitle`
- `description`
- `variants`
- `options`
- `medusaId`
- `type`
- `collection`
- `tags`
- `handle`
</Details>
<Details summaryContent="variant">
- `title`
- `sku`
- `prices`
- `options`
- `medusaId`
</Details>
<Details summaryContent="region">
- `name`
- `countries`
- `paymentProviders`
- `fulfillmentProviders`
- `medusaId`
</Details>
<Details summaryContent="collection">
- `title`
- `medusaId`
</Details>
<Details summaryContent="type">
- `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 create a loader in the Medusa application that migrates these content models into Contentful.
This section includes migration scripts for Medusas data models that are relevant for Contentful.
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 summaryContent="product Content Model">
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 summaryContent="productVariant Content Model">
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 summaryContent="collection Content Model">
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 summaryContent="productType Content Model">
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 summaryContent="region Content Model">
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 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 in the plugin listens to changes in the data, such as adding a new product, and syncs the data with Contentful.
To update the Medusa application when changes occur in Contentful, you must configure webhooks settings in Contentful.
<Note>
For webhooks to work, your Medusa application must be deployed and accessible publicly.
</Note>
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 `<MEDUSA_URL>/hooks/contentful` where `<MEDUSA_URL>` is the URL of your deployed Medusa application.
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 application and test the plugin:
```bash npm2yarn
npm run dev
```
If you created migration scripts, theyll run when the Medusa application 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 application. If youve also setup webhooks, you can test out the sync from Contentful to Medusa.