Skip to main content

Contentful Integration - automatically send Content from Contentful to Nosto for Content Search

How to automatically send Content from Contentful to Nosto for Content Search

Dan Macarie avatar
Written by Dan Macarie
Updated over a week ago

Every content source (like your Contentful, Sanity, Contentstack or Shopify blog) is different but the same principles apply when adding content aside of products to an eCommerce search:

  1. The content source is accessed (e.g. “get all content items”, “notify me when a blog post is created” or “notify me when a page is updated”).

  2. The content fields are cleaned and mapped to the search provider’s data structure.

  3. The mapped data is sent to the index via an API of the search provider.

No-code and low-code tools like Zapier, Make or n8n enable non-developers to build automations within only a few minutes. You can follow this tutorial and have your first automated workflow set up within 30-60 minutes.

Prerequisites

  • Zapier, Make.com or n8n account (or any other tool)

  • Permission to create an API token for Contentful

  • Your public blog URL, e.g. https://your-blog/article/{my-blog-post} (make sure this domain is white-listed in your Nosto account settings)

  • Minimal technical understanding

  • Nosto API_APPS token

You can create an account in minutes and request a Nosto API token from the Nosto team.

Zapier Workflow Templates

If you are on a Zapier “Team” or “Enterprise” account, you can import the workflows from our tutorials using the following .json files. Please make sure to go through the workflow and update all fields accordingly.

1. Create a Contentful API token for your space

In case you need further information for authentication, please refer to the Contentful docs: https://www.contentful.com/developers/docs/references/authentication/

  • Login to Contentful and navigate to the API keys section via the settings at the top right corner of your screen.

  • Click the “+ Add API key” button, enter a name like “Zapier” or “Nosto” and click the “Add API key” button in the popup.

  • Copy your “Space ID” and “Content Delivery API - access token” from the following page - we need those later.

2. Create the “Contentful: Blog Posts initial Sync to Nosto” Workflow

The following principle can vary depending on the tool you are using and can also be used for other content sources like Sanity, Contentstack or Shopify.

If you have multiple Nosto accounts (e.g. because you are using the Shopify Markets integration), you will need to duplicate this workflow per language/Nosto account.

  • Create a new workflow (or “Zap” in Zapier terms) and call it “Contentful: Blog Posts initial Sync to Nosto Product API”.

  • Select “Webhooks by Zapier” as trigger and set the “Trigger event” to “Catch Hook”.

2.1. Get the data from Contentful

In case you need further information regarding the Contentful API, please refer to the Contentful documentation: https://www.contentful.com/developers/docs/references/content-delivery-api

2.2. Prepare the data from Contentful for Nosto

The Contentful API will return your content in multiple fields, so you need to combine all the paragraphs into a single field. Depending on your content, this step may vary a bit.

  • Create a new action with the “Code by Zapier” app and set the “Action event” to “Run Python”.

  • Add a new field “response” under “Input Data” and select the output from the previous step.

  • Copy the following code into the “Code” field (you might want to expand the window in this step with the “expand” icon)


import json
import re

# Function to generate a slug from a string
def slugify(text):
# 1. Convert to lowercase
text = text.lower()
# 2. Remove non-word characters (everything except letters, numbers, and hyphens/underscores/spaces)
text = re.sub(r'[^\w\s-]', '', text)
# 3. Replace all whitespace (spaces, tabs, newlines) with a single hyphen
text = re.sub(r'[\s]+', '-', text)
# 4. Remove leading/trailing hyphens
text = text.strip('-')
return text


# 1. Parse the entire JSON string response body
response = json.loads(input_data['response'])

# 2. Pre-process Assets for quick lookup (efficiently handles the 'includes' part)
asset_map = {}
if 'includes' in response and 'Asset' in response['includes']:
for asset in response['includes']['Asset']:
asset_id = asset['sys']['id']
# Contentful returns URLs without the scheme, so we prepend 'https:'
# We also check for required nested keys to prevent errors
try:
asset_url = "https:" + asset['fields']['file']['url']
asset_map[asset_id] = asset_url
except (KeyError, TypeError):
# Skip assets where the URL path is invalid or missing
pass

# 3. Extract the 'items' array from the parsed data
items = response.get('items', [])

output = []

for item in items:
# Get the rich text body content array
content_nodes = item['fields']['body']['content']

concatenated_body = ""

# 1. Iterate through the top-level nodes (paragraphs, headings, etc.)
for node in content_nodes:
# 2. Check if the node is a 'paragraph' type
if node.get('nodeType') == 'paragraph':
# 3. Iterate through the content *inside* the paragraph (usually text)
for text_node in node['content']:
# 4. Check if the inner node is 'text' and has a 'value'
if text_node.get('nodeType') == 'text' and 'value' in text_node:
# 5. Concatenate the text value, followed by space
concatenated_body += text_node['value'] + " "

# Clean up any leading/trailing whitespace
final_body = concatenated_body.strip()

# 2. Image URL Lookup
image_url = None
# Check if the 'image' field exists and has the necessary link structure
image_link = item['fields'].get('image', {}).get('sys', {})
if image_link.get('linkType') == 'Asset' and 'id' in image_link:
asset_id = image_link['id']
# Look up the full URL using the map created earlier
image_url = asset_map.get(asset_id)

# Create the new structured item with the concatenated body
processed_item = {
"id": item['sys']['id'],
"title": item['fields']['title'],
"concatenated_body": final_body,
"slug": slugify(item['fields']['title']),
"image_url": image_url
# You can add other fields here (e.g., 'createdAt', 'image_id')
}
output.append(processed_item)

# Zapier expects a list of dictionaries for the output and will process each item in a single run
return output
  • Click the “Continue” button and verify the data has been processed correctly (e.g. the body only contains text, the image URL is valid and the slug is correct)

2.3. Send the clean data to Nosto

You could extend the code in the previous step and send the following GraphQL request via Python if you feel comfortable adding such a call to your code.

  • Create a new action “Webhooks by Zapier”, set the “Action event” to “Custom Request” and set the “Method” to “POST”.

  • Set the “URL” to: https://api.nosto.com/v1/graphql

  • Copy the following query into the “Data” field:

mutation {
updateProducts(products: [
{
productId: ""
url: ""
imageUrl: ""
priceCurrencyCode: "USD"
name: ""
attributes: [
{
key: "contentType"
value: "article"
},
{
key: "body"
value: ""
}
]
price: 1
listPrice: 1
inventoryLevel:1
availability:"InStock"
}
]) {
result {
errors {
field
message
}
data {
productId
}
}
}
}
  • One by one, click into all the empty strings (""), click the “+” icon and select the matching field from previous steps with the popup. You will likely “map” the fields similar to this (make sure to adjust your blog URL when adding the slug):

    • productId: “Step3.ID”

    • url: https://your-blog/article/“Step3.Slug”

    • imageUrl: “Step3.Image Url”

    • name: “Step3.Title”

    • attributes.body.value: “Step3.Concatenated Body”

  • Scroll down, add a new “Headers” field “Content-Type” and set the value to “application/graphql”

  • Copy your Nosto API_APPS token, prepend “:” and encode the string with base64 (e.g. via https://www.base64encode.org/ or any other service, example: base64_encode(“:k8yvSJBge6Jw”))

  • Add another new “Headers” field “Authorization” and set the value to “Basic <base64_encoded_Nosto_API_APPs_token>”

  • Click “Publish” and Nosto will receive all newly published blog posts within a few minutes.

  • If you are using Nosto variations for fixed price multicurrency or customer group pricing, you need to amend your updateProducts mutation to include the main variation and the individual variations, for example:

mutation {
updateProducts(products: [
{
productId: ""
"""
Other fields from the mutation above
"""
priceCurrencyCode: "USD"
price: 1
listPrice: 1
inventoryLevel: 1
availability: "InStock"
variation_id: "GENERAL"
variations: [
{
variant_id: "LOYAL"
},
{
variant_id: "WHOLESALE"
}
]
}
]) {
result {
errors {
field
message
}
data {
productId
}
}
}
}

3. Additional Use Cases and Resources

The workflow above can be created within one hour. You will likely want to copy and adjust certain elements of the main workflow to keep your data in Nosto up-to-date as you will update your content/blog posts over time.

As always, you have different options depending on what your automation tool and content sources like Shopify, Contentful, Sanity or Contentstack provide:

  • Zapier provides an “Entry Published” and “Updated Entry” trigger that you can use in the same way we are using the “Shopify: Blog Post created” trigger in this tutorial. Just copy the Shopify workflow, change the trigger, update the mapping and you’re good to go within minutes.

  • Make.com provides an “event” trigger that might be useful for updating your content.

If you’re using a CMS or blog system that doesn’t provide APIs/webhooks but has “feed”-files (like RSS, Atom or another XML format) your approach of getting the data will vary a bit but the principle remains the same: Read the data regularly, clean and map it, then send it to Nosto.

Did this answer your question?