docs: editing and general fixes of medusa's learning resources (#7261)
* docs: editing and general fixes of medusa's learning resources * fix build script * update ui dependency * fix build * adjust next.js steps
This commit is contained in:
@@ -0,0 +1,686 @@
|
||||
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 product’s `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 Medusa’s 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 Contentful’s 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 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 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, they’ll run when the Medusa application 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 application. If you’ve also setup webhooks, you can test out the sync from Contentful to Medusa.
|
||||
Reference in New Issue
Block a user