Consuming localized/translated data from GraphCMS with 11ty

So, I've been really diving deep into the Static Site Generator pool as of late. For me it's almost a nostalgic return to "the good ol' days". I know, I sound older than your grandparents. I am an Elder Millennial, after all. Back in my day (I'm gonna continue sounding like this - be forewarned) - static sites were it. No fancy server-side rendering, no drag-and-drop upload, no responsive image fetching. None of the things that we modern web developers take for granted these days.

Consuming GraphCMS data isn't the easiest thing in the world. Actually, it's not difficult per se, but it gets a bit tricky to do in 11ty when you add lozalizations. The reason for this, is that GraphCMS will give you an array of objects, where each object is the localization, and within that, you get your data. I.e. each localization needs to be it's own item. This is further complicated when you take into account all the relationships. I'm gonna supply an example to give a better idea of what I mean.

To follow along, you're gonna need the following packages installed (apart from 11ty obviously):

  • dotenv (and have /.env created, which should contain your GraphCMS endpoint (GCMS_ENDPOINT) and token (GCMS_TOKEN) )
  • graphql-request

My content model looks roughly like this. In reality it has a lot more of course, but I'm only including what's relevant to this write-up.

  • Name: Services

  • Fields:

    • Name
    • Slug
    • Eyecatcher (relationship)
    • Description
    • Content Body
    • etc.
  • Name: Eyecatcher

  • Fields:

    • Title
    • Title Support
    • etc.

Now, in order to get this data from GraphCMS, we're gonna need to create at least three files:

  • /services.11tydata.js
  • /services.njk
  • /_includes/layouts/services.njk (or wherever you place your template files)

services.11tydata.js

require("dotenv").config();
const { gql } = require("graphql-request");
const { GraphQLClient } = require("graphql-request");

const client = new GraphQLClient(process.env.GCMS_ENDPOINT);

const requestHeaders = {
authorization: "Bearer " + process.env.GCMS_TOKEN,
};

const variables = {};

const Query = gql`
query {
services(
locales: [en, sv]
) {
localizations(includeCurrent: true) {
locale
id
name
slug
description
contentBody {
html
}
eyecatcher {
localizations(includeCurrent: true) {
locale
id
title
titleSupport
}
}
}
}
}
`
;

const getServices = async () => {
try {
const { services } = await client.request(
Query,
variables,
requestHeaders
);
const result = services.map((page) => ({
// process things if necessary.
...page,
}));

return result;
} catch (e) {
throw new Error("There was a problem getting Services", e);
}
};

module.exports = async () => {
const services_raw = await getServices();

let localizations;
let services_full = new Array();

services_raw.forEach((item, index) => {
localizations = item.localizations;
localizations.forEach((subitem, index2) => {
let preSlug = '';
if (subitem.locale === 'sv') {
preSlug = 'tjanster';
} else {
preSlug = 'services';
}
localizations[index2]['pre_slug'] = preSlug;
});

services_full.push(localizations);
});

const services = services_full.reduce((acc, items) => ([...acc, ...items]), []);
return { services };
};

So what's going on here?
Well, first of all, we're fetching our data in the way we want it pulled down. Now, any data that I want localized, I need to wrap in localizations(includeCurrent: true) { ... }. If I were to console.log() services_raw (within module.exports), I'd get a response that looks something like this:

[
{ localizations: [ [Object], [Object] ] },
{ localizations: [ [Object], [Object] ] },
{ localizations: [ [Object], [Object] ] },
{ localizations: [ [Object], [Object] ] },
{ localizations: [ [Object], [Object] ] },
{ localizations: [ [Object], [Object] ] }
]

Pretty difficult to see what all that is. But, it's an array, so I create a forEach loop. If I console.log item.localizations, I get this:

[
{
locale: 'sv',
id: 'cknisezawsny90b59',
name: 'Service name SV',
slug: 'service-name-sv',
description: 'Lorem ipsum dolor sit amet',
contentBody: {
html: '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel nisl.</p>'
},
eyecatcher: { localizations: [Array] }
},
{
locale: 'en',
id: 'cknisezawsny90b59',
name: 'Service name EN',
slug: 'service-name-en',
description: 'Lorem ipsum dolor sit amet',
contentBody: {
html: '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel nisl.</p>'
},
eyecatcher: { localizations: [Array] }
}
]

Now, the above is only one service out of six.

If we left it as is, 11ty wouldn't know what to do. It can't traverse such an array/object stack and know what is a post and what isn't. In fact, services_raw would be seen as a single post by 11ty. So, we have to manipulate it. Essentially, we have to take each node within the localization object and put it in a new array so they're all at the top level of the array. 11ty will then be able to step through it in order to create each page. That's the services_full.push(localizations);.

Sidenote: Because I want the services to be in a sub-folder which is dependent on what locale it is, I've also added some code in there for that. Since this is a common thing, I've left it in to show one way of achieving this. This only works with one level of sub-folders, though.

The last step is to create the final { services } object from the services_full array. I use reduce for this in order to remove excess levels that were added when looping through the original. This is what is then returned.

A note about naming: 11ty is quite fiddly with this. If you're requesting "services", it's important that both the filename (services.11tydata.js) and the returned object has the same name.

Sorry about the back slashes in the following code. Even within code block notation, 11ty thinks it should compile the directives if I don't add something in between its start declarations. Just copy/paste the code and find/replace "\" with nothing.

/services.njk

---
pagination:
data: services
size: 1
alias: service
addAllPagesToCollections: true
permalink: "/{\{ service.locale }}/{\{ service.pre_slug }}/{\{ service.slug }}/"
tags: "services"
layout: layouts/services.njk

eleventyComputed:
title: "{\{ service.name | safe }}"
lang: "{\{ service.locale | safe }}"
---

This is the file that will actually trigger the pagination of the services global data. How this is built, is well-documented (and not too complicated) in the 11ty docs. The code is pure front matter with eleventyComputed added to it, in order to send a few useful variables into the page object upon generation. How this works is also well-documented in the 11ty docs.

Note: The "tags" directive is also important here. It needs to match the global data name for whatever you're fetching.

And lastly, you need the template file to display the paginated items. I'm just including the relevant items here because everything else will be up to how you've designed/structured your site.

With risk of repeating myself... Sorry about these back slashes too. Same remedy though.

<section class="primary-hero">
{\% for loc in service.eyecatcher.localizations %}
{\% if (loc.locale === lang) %}
<h1>{\{ loc.title | safe }}</h1>
<p class="title-support">
{\{ loc.titleSupport }}
</p>
{\% endif %}
{\% endfor %}
</section>

This is relatively self-explanatory, I think - even though it looks a little messy.

When we're loading a service (service), we're also loading its relationships. In this case, eyecatcher. Since eyecatcher is also localized, it's going to contain both localizations of this object. In order to display the correct one, we need to check against the currently loaded pages locale. That's why we added lang as a variable in eleventyComputed within the service.njk file.

So, for each localization within the eyecatcher object, we're checking if its locale (loc.locale) is the same as the lang variable. If it is, we show its fields. It's simple and works perfectly.

This can get a bit messy if we need to "dig a hole", i.e. for-loop our way down into the data tree.

Luckily, since we're generating static pages, it won't make a difference performance-wise.


Note:

I got off the ground on this using this write-up by dev.to. Very little code from that is still present, but I think it's a worthy mention.