Multi-locale Assets in Contentful with one upload, hooray!

dan norton
4 min readJul 19, 2021

In fall 2020, I started moving brightcove.com (our corporate marketing site) over to a new tech stack. We’re leveraging Contentful for it’s versatile content modeling, as well as its hooks not only into Netlify for builds, but into Smartling for translations.

If you’re reading this you probably already know how Contentful works — entries and assets are available on a per-locale basis. You can specify fallback locales, but the fallback behavior is for all assets and entries, meaning if I want blog posts to only be available in one language, but custom pages should just show english across all languages if other translations aren’t available, that’s not possible out of the box.

This makes for a tricky build process when it comes to imagery and media. Let’s say we have a content type where all the text content is localizable, and is returned from Contentful properly; maybe we sent it over to Smartling and got it back. Sweet. That content type also contains references to media assets — a background image or a video.

If we request that entry, specifying a locale other than our default, the media asset would return empty, since we haven’t necessarily provided images for every locale in our CMS. This means then that our content managers, when creating pages in English (our default), would also need to upload an asset in every single locale we intend to localize against.

All of the locales!

Initially, I thought having the asset field be “localizable” would allow content managers to just associate the same uploaded media asset for each language. While the reference existed, the media URLs were empty, so building for non-english locales were devoid of imagery. This, obviously, becomes a huge content lift. How could we make it easy to have assets be available across all locales, without requiring the content manager to upload the asset 5x?

Answer: webhooks. Webhooks and the content management API.

Contentful’s Webhook interface

Contentful’s webhook interface is great. You can specify what actions on what types of entries or assets they should fire, and then specify the endpoint that should accept the data for that event.

Our webhook needs to do three things:

  1. Extract the asset media data from our default locale
  2. Copy it into our target locales
  3. Re-publish the asset

I used AWS Lambda and API Gateway to create the webhook, but the core concepts would work in a NodeJS cloud function in Google Cloud or Azure.

Start by initializing your Contentful management client, setting up the request handler, and defining what locales you’ll need:

// import client
const contentful = require('contentful-management');
// initialize client
const client = contentful.createClient({
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN
});
const locales = [ 'de', 'en-US', 'es', 'fr', 'ja', 'ko' ];
exports.handler = async (event) => {
// code goes here
}

Now we can begin the process of parsing the data posted to the endpoint. Let’s set up an async function that will handle this.

const updateAsset = async (assetData)=>{
// if the object has fields with a file, proceed
if(assetData.fields && assetData.fields.file){
const fileLocales = Object.keys(assetData.fields.file);
if(fileLocales.length !== locales.length){
await client.getSpace(process.env.CONTENTFUL_SPACE_ID)
.then((space)=> space.getEnvironment('master'))
.then((env)=> env.getAsset(assetData.sys.id))
.then((assetResponse)=>{
const srcFile = assetResponse.fields.file[fileLocales[0]];
locales.forEach((locale)=>{
if(typeof assetResponse.fields.file[locale] == 'undefined'){
assetResponse.fields.file[locale] = srcFile;
}
});
return assetResponse.update()
})
.then((asset) => {
return asset.publish()
})
.then((asset)=> {
const successObj = {
status:200,
body:`Asset ${asset.sys.id} published.`
};
return successObj
})
.catch(console.error)
}else{
const full = {status:200, body:"all locales have URLs"}
return full
}

}else{
console.info(assetData);
const respObj = {
status:500,
body:{
message:"incorrect data",
content:assetData
}
}
return respObj
}
}

This looks a bit complex but it does a few things in sequence.

if(assetData.fields && assetData.fields.file){
const fileLocales = Object.keys(assetData.fields.file);
if(fileLocales.length !== locales.length){
...}
}

In the above code block, we’re checking the object that Contentful is sending us. Does the asset have a file object? If it does, and there are as many locales in the object as there are in our locale list, we don’t have to do anything, as the file is already localized.

Next is where the work actually happens. In the block below, the client is directed to get the space data using your client space id, then to retrieve the asset based on the id provided to the function.

When we get the asset back, we use the file from the first available locale as the source file URL:

const srcFile = assetResponse.fields.file[fileLocales[0]];

Then we create an attribute on the asset for each locale within the file

locales.forEach((locale)=>{
if(typeof assetResponse.fields.file[locale] == 'undefined'){
assetResponse.fields.file[locale] = srcFile;
}
});
return assetResponse.update()

The asset returned has methods for updating and publishing the asset, which is done within the chained then() methods.

await client.getSpace(process.env.CONTENTFUL_SPACE_ID)
.then((space)=> space.getEnvironment('master'))
.then((env)=> env.getAsset(assetData.sys.id))
.then((assetResponse)=>{
const srcFile = assetResponse.fields.file[fileLocales[0]];
locales.forEach((locale)=>{
if(typeof assetResponse.fields.file[locale] == 'undefined'){
assetResponse.fields.file[locale] = srcFile;
}
});
return assetResponse.update()
})
.then((asset) => {
return asset.publish()
})
.then((asset)=> {
const successObj = {
status:200,
body:`Asset ${asset.sys.id} published.`
};
return successObj
})

The final succesObj that is returned is what the API will return to the webhook.

You’ll then see the asset you just uploaded in your primary locale saved across all locales. This will also allow you to override on a per-locale basis. Sweet!

This can also be extended to include asset names and descriptions — especially helpful if your applications are looking for other pieces of the localized asset.

Hope this (rather niche) article helps someone out down the road and gets you thinking about the power of Webhooks in Contentful!

--

--

dan norton

kerning and kick drums. UX architect at Paccurate, former web dev at Brightcove.