Select Gatsby Contentful Locale Data in GraphQL

Credit 👏: My teammate, Riley Rangel, worked out this solution and improved the DX for our team.

Problem: How to select the correct Contentful locale content data from GraphQL

I was working with my teammate, Riley, to connect our web app to Contentful to drive page content. Previously page was handled by React-Intl and gatsby-plugin-intl. However, text updates from the product leads meant that a we had to do a full release.

(Granted our CI/CD is pretty streamlined, but we wanted to reduce this step by allowing product leads to make text changes outside of the release cycle.)

Contentful provides a nice headless CMS platform that handles internationalization, and gatsby-source-contentful allows us to access Contentful data during build time.

Our main road-bump was how to select the specific localization content data from our GraphQL layer.

All the Contentful locale content was sourced into GraphQL via gatsby-source-contentful, but we had to map this data into a new dictionary mappedLocaleContent, where content is stored according to the locale as the key.

// Page content shape from Contentful
export type Page = {
  /** Type of page. Eg: Home Page, FAQ Page */
  pageName: string;
  /** Main H1 headline */
  h1Headline: string;
  /** Contentful Locale Value */
  node_locale: "en-US" | "en-ES";};
// Shape of the mapped content stored according to the locale as the `key`
const mappedLocaleContent = {
  "en-US": Page,
  "en-ES": Page,
};

This would allow us to select the correct locale content with a quick object key reference using the currently selected locale currentLocale from react-intl:

./src/pages/index.tsx
import React from "react";
import { PageRendererProps, graphql } from "gatsby";
import { injectIntl, InjectedIntlProps } from "gatsby-plugin-intl";

import mapContentByLocale from "~/utils/mapContentByLocale";
export const query = graphql`
  query ContentfulQuery($intlLocale: String) {
    contentfulPage {
      h1Headline
      node_locale    }
  }
`;

type Props = {
  data: {
    content: Page;
  };
} & PageRendererProps &
  InjectedIntlProps;

const Index: React.FC<Props> = ({ intl, data }) => {
  const { content } = data;
  const currentLocale = intl.locale;  const mappedLocaleContent = mapContentByLocale(data);
  return (
    <div>
      <p>{mappedLocaleContent[currentLocale].h1Headline}</p>    </div>
  );
};

export default injectIntl(Index);

Where the mapping function would be something like:

./src/utils/mapContentByLocale.tsx
type QueryData = {
  contentfulPage: {
    edges: {
      node: Page;
    }[];
  };
};

const mapContentByLocale = (data: QueryData): { [key: string]: Page } => {  return data.allContentfulPage.edges.reduce((acc, curr) => {    return {      ...acc,      [curr.node.node_locale]: curr.node,    };  }, {});};
export default mapContentByLocale;

Solution: Pass React-Intl locales as a GraphQL variable during Build Time

A more "Gatsby" solution that follows the paradigm of utilizing GraphQL's query variables allows us to query for the specific locale content needed.

In this specific case, we want to filter the Contentful locale property node_locale associated with each localized content node.

// Page content shape from Contentful
export type Page = {
  /** Type of page. Eg: Home Page, FAQ Page */
  pageName: string;
  /** Main H1 headline */
  h1Headline: string;
  /** Contentful Locale Value */
  node_locale: "en-US" | "en-ES";};

Query GraphQL by a variable

To do this, we modify our GraphQL query by creating the (\$intlLocale: String) variable definition after the query operation name ContentfulQuery, which declare $intlLocale as a variable accepted by the query.

(Similar to an argument definitions for a function in a typed language, a GraphQL variable definition lists all of the variables, prefixed by \$, followed by their type.)

Now we can query the contentfulPage by the node_locale that matches the $intlLocale variable from React-Intl.

export const query = graphql`
  query ContentfulQuery($intlLocale: String) {    contentfulPage(node_locale: { eq: $intlLocale }) {      h1Headline
    }
  }
`;

Add the GraphQL variable to the Gatsby Page Context

In order to query node_locale in GraphQL by a variable, we need to make that variable available in the Gatsby page context during build time through Gatsby's createPage API.

Here we're adding intLocale to the Gatsby page context:

./gatsby-node.js
exports.onCreatePage = ({ page, actions }) => {
  const { createPage, deletePage } = actions;
  const { context } = page;
  const { intl } = context;
  if (intl.language && !page.context.intlLocale) {
    // Map React-Intl locale values to Contentful's locale string
    const intlToCms = {
      "en-us": "en-US",
      "es-us": "es-US",
    };

    deletePage(page);
    createPage({
      ...page,
      context: {
        ...page.context,
        // Build the pages with the available react-intl locales
        // passed into the page level GraphQL context
        intlLocale: intlToCms[intl.language],      },
    });
  }
};

Final solution:

This approach utilizes the power of GraphQL, and the resulting React component is much cleaner:

./src/pages/index.tsx
import React from "react";
import { PageRendererProps, graphql } from "gatsby";

export const query = graphql`
  query ContentfulQuery($intlLocale: String) {    contentfulPage(node_locale: { eq: $intlLocale }) {      h1Headline    }  }`;

type Props = {
  data: {
    content: Page;
  };
} & PageRendererProps;

const Index: React.FC<Props> = ({ data }) => {
  const { content } = data;
  return (
    <div>
      <p>{content.h1Headline}</p>    </div>
  );
};

export default Index;