Building dynamic URL structures with Contentful and Nextjs

In a traditional CMS, we might be used to structuring our site by nesting pages underneath each other. In this scenario, the CMS often handles the creation of the URL for us. This works a bit differently in a headless CMS, where the content is usually handled as just a piece of content and isn't tied up to any specific view or structure. But what if we want to give our editors more control over the URL structure, other than placing all new articles under "/articles/:articleSlug"?

Updating the content type in Contentful

One way of solving this could be to add a parent field in your content type.

The article content type, with a parent reference field to another page.

By including a reference field to another page (which again could have another reference to another page) we can build up the structure as we'd like. For the example shown in the image, the path would be "/om-oss/partnere".

Building the slugs in Nextjs

When fetching our data, we also need to generate the correct slug on our website. By using the Contentful JS client, we can fetch the entries and dereference the parent(s) content for the current page to access the slugs. The include parameter decides how many levels we can do this.

contentfulClient
    .getEntries({
    content_type: `${contentType}`,
    include: 3,
	})
    .then(function (entry) {
    return entry.items
	})

After we get the entry or entries - we need a way to get all slugs and concatenate them, as long as the page has a parent. I solved this using a recursive method.

function buildSlugFromParent(page) {
    const parent = page.parent?.fields
    return parent ? `${buildSlugFromParent(parent)}/${page.slug}` : page.slug
}

In the method above, I'm sending in the entry's fields and checking if a parent exists. If it does, rerun the method, while building up the URL segments.

Now we can set up the static paths for Nextjs to build up our site.

export const getStaticPaths = async () => {
    var result = await getAllEntries('article')
    const slugs = result.map((x) => ({ slug: buildSlugFromParent(x.fields, x.fields.slug) }))

    const paths = slugs.map((x) => {
        return {
            params: {
                slug: x.slug.split('/'),
            },
        }
    })

    return {
        paths,
        fallback: 'blocking',
    }
}

Conclusion

Although this seems to work fine, we are fetching the whole parent objects, when we only need the slug. This might be possible using GraphQL, but I haven't had time to explore that option yet. Another thought was creating a custom component in Contentful, that would generate a read-only slug field using the parent(s), so we could fetch that directly.

This is probably just one or many ways of solving this problem, and I'll be exploring more solutions to this later on.