perpetua.digital
Published on

Syncing CMS images with Adobe Target Offers

Authors

Contentful

I use Contentful to host the images for this blog. It is easy to use, has lots of functionality, and the best part: it has an excellent free tier. If you need to host any kind of content like images or blog posts, I highly suggest Contentful.

I would like to have any image I upload to Contentful automatically become an offer in Adobe Target. This way, if I want to use an image on my site and in Target, I don't have to upload it in 2 places. To do this, I am going to tap into Contentful Webhooks to pass uploaded file info straight to Target to create (or update) an offer. Let's get started!

If you don't feel like reading, I don't blame you. Here's a video demo of what this post is all about:

Depending on when you're reading this, there might still be a dog picture on the about page!

Uploading to Contentful

I'm not going to go super deep into Contentful content management and publishing as there's a lot of good documentation out there and frankly, its not that hard. The only import thing to know here is that content (images in my case) are uploaded into the CMS, given metadata like titles and tags, then published. Published is the key word here as content can be uploaded without being published and I only want to create a Target offer for published images.

image uploaded but not published

This image of me and Sammy has been uploaded, but I still need to publish it

Contentful Publish Webhook

Contentful offers a webhook for just about every action that can apply to a piece of content. Here, I am going to setup a webhook to fire when an Asset (aka an image file) is published. This will apply to whether the asset is newly published or if I re-publish an existing asset.

Inside Contentful, go to Settings > Webhooks > Add Webhook. Since I only want to this webhook to fire when I publish an image, I'll set my trigger for Specific Triggering Events and Asset Publish.

WEBHOOK SETUP IMAGE

This webhook will only fire when I publish an image

The webhook setup above will send data that looks like this:

{
  "metadata": {
    "tags": []
  },
  "sys": {
    "type": "Asset",
    "id": "abc123",
    "space": {
      "sys": {
        "type": "Link",
        "linkType": "Space",
        "id": "abc123"
      }
    },
    "environment": {
      "sys": {
        "id": "master",
        "type": "Link",
        "linkType": "Environment"
      }
    },
    "createdBy": {
      "sys": {
        "type": "Link",
        "linkType": "User",
        "id": "abc123"
      }
    },
    "updatedBy": {
      "sys": {
        "type": "Link",
        "linkType": "User",
        "id": "abc123"
      }
    },
    "revision": 3,
    "createdAt": "2023-05-16T12:43:26.894Z",
    "updatedAt": "2023-05-16T12:50:26.041Z"
  },
  "fields": {
    "title": {
      "en-US": "johnandsammy"
    },
    "file": {
      "en-US": {
        "url": "//images.ctfassets.net/my/long/url/here/johnandsammy.png",
        "details": {
          "size": 60519,
          "image": {
            "width": 657,
            "height": 718
          }
        },
        "fileName": "johnandsammy.png",
        "contentType": "image/png"
      }
    }
  }
}

I will take this data, specifically the title and file url and use them to create offers in Target. It is important to note that I am creating HTML Offers in Target and not Target image offers.

As far as I can tell, you cannot upload image files as image offers to Target via the Target API. I suspect you may be able to do this in AEM, but I am not sure. Even if I could upload images with the API, I am on free developer tier of Adobe Target and don't have access to use image offers in activities anyway :)

Building an API endpoint

The final and most important part of the webhook setup is creating an endpoint for it to POST to when an image is published in my Contentful space. I'm not going to actually deploy this endpoint, but if you've read any of my other posts, you know I like to use Serverless framework to build & deploy API Gateways to AWS Lambda. If you haven't worked with Serverless before that sentence sounds a lot more complicated than this process actually is. Serverless has an OOTB Flask API template so I'll just use that and run my endpoint server locally and use Ngrok to do some proof of concept testing. The same setup will apply if I had an actually deployed API endpoint. That is, once the API is built, I need to add the endpoint URL to the webhook settings in Contentful.

My webhook endpoint needs to do 2 things:

  • Accept POST data and verify that its an image publish request. Technically I don't have to do this, but its always good to never trust, always verify. In reality I would also bake into this some security via a personal access token, but for just testing I don't need to do all that.
  • Collect the data from the Contentful payload and use it when calling the Target API to create or update an offer. For full CRUD capabilities I could also make an unpublish endpoint to delete offers from Target when images are unpublished from Contentful.

Verify POST Data

Given the JSON payload above, these 2 tasks are actually relatively simple. After verifying the request headers are of the correct type, I grab the file title and file url and create a string representing a generic HTML img element. It's this <img> element that will become the content of my Target offer.

@app.route("/contentful-asset-publish", methods=['POST'])
def contentful():
    data = request.get_json()
    headers = request.headers

    # was the request an asset publish double check
    if headers.get("X-Contentful-Topic") == "ContentManagement.Asset.publish":
        if data['sys']['type'] == "Asset":
            # get the url of the image
            file_url = data['fields']['file']['en-US']['url']
            file_title = data['fields']['title']['en-US']
            image_element = f"<img src='https://{file_url}' alt='{file_title}'>"

Talk to Target

I'll spare you the drudgery of exchanging tokens (thankfully Adobe is moving away from that) and jump right in to where things get interesting. First, I'm going to grab all the offers that exist in my Target instance. The paradigm I'm setting up here is all based on matching the file title from Contentful and the offer name from Target. If there is no match, create a new offer. If there is a match, just update the existing one with the new <img> element.

# get all offers
jwt_token = get_jwt()
access_token = get_access_token(jwt_token)
offers = requests.get(f"https://mc.adobe.io/{os.environ.get('tenant')}/target/offers", headers={
    'authorization': f"Bearer {access_token}",
    'cache-control': 'no-cache',
    'Accept': 'application/vnd.adobe.target.v2+json',
    'x-api-key': os.environ.get('client_id')
})
offers_dict = offers.json()

# find by name=title lower?
offer_matches = [item for item in offers_dict['offers']
                    if item.get('name').lower() == file_title.lower()]

# update if offer match on title
if offer_matches:
    # just grab whatever the first match is
    offer_match = offer_matches.pop()
    offer_id = offer_match.get('id')
    update_url = f"https://mc.adobe.io/{os.environ.get('tenant')}/target/offers/content/{offer_id}"
    update_headers = {
        'authorization': f"Bearer {access_token}",
        'cache-control': 'no-cache',
        'content-type': 'application/vnd.adobe.target.v2+json',
        'x-api-key': os.environ.get('client_id')
    }
    update_data = {
        'name': offer_match.get('name'),
        'content': image_element
    }
    update_offer = requests.put(
        update_url, headers=update_headers, json=update_data)
    pprint.pprint(update_offer.json())
    message = "UPDATE"
    print(message)

# create if no offer match on title
if not offer_matches:
    create_url = f"https://mc.adobe.io/{os.environ.get('tenant')}/target/offers/content"
    create_headers = {
        'authorization': f"Bearer {access_token}",
        'cache-control': 'no-cache',
        'content-type': 'application/vnd.adobe.target.v2+json',
        'x-api-key': os.environ.get('client_id')
    }
    create_data = {
        'name': file_title,
        'content': image_element
    }
    create_offer = requests.post(
        create_url, headers=create_headers, json=create_data)
    pprint.pprint(create_offer.json())
    message = 'CREATE'
    print(message)

return make_response(jsonify({"file_url": file_url, "file_title": file_title, "image_element": image_element, "message": message}), 200)

This is a sample response from my API when publishing an image and creating a new offer. I put the message in there as a sanity check for me because the Target API return is just the offer metadata but doesn't say whether it was created or updated.

{
  "file_url": "https://contentfulcdn.com/my/asset/url/here.png",
  "file_title": "my new image file",
  "image_element": "<img src='https://contentfulcdn.com/my/asset/url/here.png' alt='my new image file'>",
  "message": "CREATE"
}

api created offers

Offers created by the Contentful webhook will show my API technical account address instead of my username and show Target Classic as their creation source.

I can now use these images uploaded to Contentful in any of my Target activities!

In Action

Congrats on reaching the end. You're reward is the same video as the intro. Leave me a comment while you're down here!