Contentful migrations: Add validation to slug field and update existing values

In Contentful, when creating a new field, one of the choices is Text. Here you can choose either a short text, or long text, which again contains a couple of appearances you can choose from.

Here, we will use a short text with the slug appearance. This will enable the editors to get a slug generated from a chosen field, in this case its the entry title.

One thing I noticed about this though, is that the slug is only auto-generated before the first publish, which is fine - since you don't want to accidentally change your slug when editor other fields. When auto-generating the slug from my entry title, the value is slugified. Meaning, if I write a title with capital letters, spaces and Norwegian characters, this is converted to a appropriate slug.

What I didn't think about, was what would happen if you went back, after publishing the article and tried adding a slug with all sorts of characters? It turns out the default slug field doesn't have any validations on it, so I can update my slug to be "th!5 is æ slug" if I wanted. Maybe someone want that.. but as a web developer I do not.

In my case, the damage has already happened, where a few editors had updated some of the slugs. Luckily this was before production, but I still need to add validations to all slug fields across all content type, and update all the values that doesn't match this validations.

Contentful migrations scripts to the rescue!

Using Contentful migration scripts and the Contentful CLI we can quickly update all fields for all content types, and update the values for all fields that are necessary.

// update-validations-for-slug-type.js

module.exports = function (migration) {
    const types = [
        'myType',
        'anotherType',
        'thirdType',
        'gladImNotDoingThisManually'
    ]
    
	// Add custom validation for fields with pattern: ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$
// This regex will match a string that:
// 1) Starts and ends with a lowercase letter or a number
// 2) May contain any number of lowercase letters, numbers, or hyphens in the middle.
// 3) Is a whole string match, meaning the entire string must match this pattern.
    
    types.forEach((type) => {
        const contentType = migration.editContentType(type)
        const slug = contentType.editField('slug')
        slug.validations([
            {
                regexp: { pattern: '^[a-z0-9]([a-z0-9-]*[a-z0-9])?$', flags: '' },
                message: 'Add a custom validation message here',
            },
        ])
    })
}
Looping over an array of contentTypes I need to update, adding a custom validation

Now I can run my new script using the Contentful CLI

contentful space migration --environment-id 'update-validations-and-slugs' .\migrations\update-validations-for-slug-types.js

Now that that is done, I need to update my data for the slug fields that doesn't match the new validations.

const { getSlug } = require('./slugHelpers.js')

module.exports = function (migration) {
    const types = [
    'myType',
    'anotherType',
    'thirdType',
    'gladImNotDoingThisManually'
]

    types.forEach((type) => {
        migration.transformEntries({
            contentType: type,
            from: ['slug'],
            to: ['slug'],
            transformEntryForLocale: async (from, locale) => {
                const slug = from.slug ? from.slug[locale] : null
                if (!slug) {
                    return
                }

                const newSlug = getSlug(slug)

                if (!newSlug || slug === newSlug) {
                    return
                }

                console.log('Updating slug from: ', slug, 'To: ', newSlug)
                return {
                    slug: newSlug,
                }
            },
        })
    })
}
Looping over the same contentTypes as before, transforming the slugs into a valid value

In the code below, im first checking if the slug is a match to my validation pattern. If it matches, return null and I dont need to update the existing value. If however, the value needs to be updated I need to do a couple of steps to remove unwanted characters, removing whitespace and setting all characters to lowercase.


// return null if slug is valid, otherwise return a new slug
function getSlug(slug) {
    const match = slug.match(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/)
    if (match) return null
    const newSlug = slugify(slug)
    return newSlug
}

function slugify(inputStr) {
    // Remove diacritics
    inputStr = inputStr.normalize('NFD').replace(/[\u0300-\u036f]/g, '')

    inputStr = inputStr.toLowerCase()

    // Replace Norwegian characters
    inputStr = inputStr.replace(/ø/g, 'o')
    inputStr = inputStr.replace(/æ/g, 'ae')
    inputStr = inputStr.replace(/å/g, 'a')

    // Replace any character that is not a lowercase letter or hyphen with a hyphen
    inputStr = inputStr.replace(/[^a-z0-9-]/g, '-')

    // Replace consecutive hyphens with a single one
    inputStr = inputStr.replace(/-+/g, '-')

    // Remove leading and trailing hyphens
    inputStr = inputStr.replace(/^-+|-+$/g, '')

    // If string is empty or consists only of hyphens, return a default slug
    if (!inputStr) {
        return ''
    }

    return inputStr
}

module.exports = { getSlug }

Just as before, we can now run the same CLI command for the new script as well.

contentful space migration --environment-id 'update-validations-and-slugs' .\migrations\update-slug-values.js

Now we have validations for all our slug fields, and all values that didnt match our pattern is now updated!