DEV Community

Matt Kane
Matt Kane

Posted on • Updated on

How to use Drupal Paragraphs in your Gatsby site

If you're using Drupal as a data source for Gatsby, you really should be using the Paragraphs module. It's one of the most powerful ways of building a site. It allows you to create "Paragraph Types" that an editor can add and reorder to assemble a page. Paragraphs can be simple types such as a block of HTML or a list of images, or more complex types with many different fields. Most of the documentation is related to using it within Drupal themes, but it also maps really well to React components. You can create custom components, then define matching paragraph types to let editors add them to a page. This can give editors a lot more flexibility than they would otherwise get with a headless CMS.

It can be a bit tricky to work out how to set this up, but hopefully this post will help you build beautiful component-based sites with Drupal Paragraphs and Gatsby.

I am not going to go into detail about how to set up Drupal. I will assume that you have installed Paragraphs and enabled the paragraphs and paragraphs_demo modules. I have created a content type called "Paragraphed article" with a single field called field_paragraphs_demo which I'll be using here.

Once you have Drupal set up and populated with some content, add and enable gatsby-source-drupal in Gatsby. Start the Gatsby server, and take a look at GraphiQL. You should see your paragraph page type in the explorer. Try a simple query to see what you get:

{
    allNodeParagraphedContentDemo {
        nodes {
            relationships {
                field_paragraphs_demo {
                    __typename
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

You should see an array of pages, with paragraphs of various types. In my example I get the following:

{
    "data": {
        "allNodeParagraphedContentDemo": {
            "nodes": [
                {
                    "relationships": {
                        "field_paragraphs_demo": [
                            {
                                "__typename": "paragraph__image_text"
                            },
                            {
                                "__typename": "paragraph__images"
                            },
                            {
                                "__typename": "paragraph__text"
                            }
                        ]
                    }
                },
                {
                    "relationships": {
                        "field_paragraphs_demo": [
                            {
                                "__typename": "paragraph__image_text"
                            },
                            {
                                "__typename": "paragraph__text"
                            },
                            {
                                "__typename": "paragraph__text"
                            },
                            {
                                "__typename": "paragraph__image_text"
                            }
                        ]
                    }
                }
            ]
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

That's two pages, the first of which has three paragraphs and the second of which has four. You should note that there's more than one of each paragraph type.

In gatsby-node.js you first need to create the pages:

const path = require("path");

exports.createPages = async ({ graphql, actions }) => {
    const { createPage } = actions;
    const tpl = path.resolve(`src/templates/paragraph.js`);

    //   Adjust these field names as needed
    const result = await graphql(`
        {
            paragraphPages: allNodeParagraphedContentDemo {
                edges {
                    node {
                        fields {
                            slug
                        }
                        drupal_internal__nid
                    }
                }
            }
        }
    `);
    result.data.paragraphPages.edges.forEach(({ node }) => {
        createPage({
            path: node.fields.slug,
            component: tpl,
            context: {
                slug: node.fields.slug
            }
        });
    });
};

exports.onCreateNode = ({ node, getNode, actions }) => {
    const { createNodeField } = actions;
    // Use the type of your own paragraph page
    if (node.internal.type === `node__paragraphed_content_demo`) {
        const slug = `/pages/${node.drupal_internal__nid}/`;
        createNodeField({
            node,
            name: `slug`,
            value: slug
        });
    }
};
Enter fullscreen mode Exit fullscreen mode

So far this is similar to the using-drupal demo. The interesting bit starts in the page template. I called mine paragraph.js. You can use the page query inside that.

// paragraph.js
import React from "react";
import { graphql } from "gatsby";
import Layout from "../components/layout";

const PageTemplate = ({ data }) => {
    const paragraphs = data.page.relationships.paragraphs.map(paragraph => (
        <li>{paragraph.type}</li>
    ));

    return (
        <Layout>
            <h1>{data.page.title}</h1>
            <ul>{paragraphs}</ul>
        </Layout>
    );
};

export default PageTemplate;

export const pageQuery = graphql`
    query($slug: String!) {
        page: nodeParagraphedContentDemo(fields: { slug: { eq: $slug } }) {
            id
            title
            relationships {
                paragraphs: field_paragraphs_demo {
                    type: __typename
                }
            }
        }
    }
`;
Enter fullscreen mode Exit fullscreen mode

Before you load a page, you can create the index page to give an easy list of links:

// index.js
import React from "react";
import { Link, useStaticQuery, graphql } from "gatsby";

import Layout from "../components/layout";
import SEO from "../components/seo";

const IndexPage = () => {
    const data = useStaticQuery(graphql`
        query PageQuery {
            paragraphPages: allNodeParagraphedContentDemo {
                nodes {
                    title
                    fields {
                        slug
                    }
                }
            }
        }
    `);
    return (
        <Layout>
            <ul>
                {data.paragraphPages.nodes.map(node => (
                    <li>
                        <Link to={node.fields.slug}>{node.title}</Link>
                    </li>
                ))}
            </ul>
        </Layout>
    );
};

export default IndexPage;
Enter fullscreen mode Exit fullscreen mode

If you load the front page it should give you a nice list of page links. Try clicking on one to view the page.

Right now the page just shows a list of paragraph types. The interesting part is to map that to components.

My example has the following paragraph types:

  • paragraph__image_text
  • paragraph__images
  • paragraph__text

These match the paragraph types created in Drupal. You need to to create a component for each paragraph type. Start with the Text paragraph. Drupal includes sanitised HTML for this, so we can set that directly (but "dangerously"). You need to update the query in the page template to get the data for the paragraph. It's a lot easier to keep track of this if you keep the query alongside the component by using a GraphQL fragment.

// TextParagraph.js
import React from "react";
import { graphql } from "gatsby";

export const TextParagraph = ({ node }) => (
    <div
        style={{
            borderStyle: "solid",
            marginBottom: 2
        }}
    >
        <div dangerouslySetInnerHTML={{ __html: node.text.processed }} />
    </div>
);

export const fragment = graphql`
    fragment ParagraphText on paragraph__text {
        id
        text: field_text_demo {
            format
            processed
            value
        }
    }
`;
Enter fullscreen mode Exit fullscreen mode

You can now use this fragment in the page query in your template:

export const pageQuery = graphql`
    query($slug: String!) {
        page: nodeParagraphedContentDemo(fields: { slug: { eq: $slug } }) {
            id
            relationships {
                paragraphs: field_paragraphs_demo {
                    type: __typename
                    ...ParagraphText
                }
            }
            title
        }
    }
`;
Enter fullscreen mode Exit fullscreen mode

If you log the data in your page you should see that the Text paragraph now has an object with the text fields.

As an aside, this pattern of keeping GraphQL fragments alongside the component that uses the data can be used elsewhere too, not just in these paragraphs. For example, if you have a component that needs images of a specific size, you can include a fragment alongside it that has the ImageSharp query to return the correct image size which you can then use in the page query.

You now have the right data for the Text paragraph, as well as a component to display it. Now you can create a similar component for the Text + Image paragraph type. This time we'll need to use an ImageSharp query to get the image too.

// ImageAndTextParagraph.js
import React from "react";
import Img from "gatsby-image";
import { graphql } from "gatsby";

export const ImageAndTextParagraph = ({ node }) => (
    <figure>
        <Img fixed={node.relationships.image.localFile.childImageSharp.fixed} />
        <figcaption dangerouslySetInnerHTML={{ __html: node.text.processed }} />
    </figure>
);

export const fragment = graphql`
    fragment ParagraphImageText on paragraph__image_text {
        id
        image: field_image_demo {
            alt
        }
        text: field_text_demo {
            format
            processed
            value
        }
        relationships {
            image: field_image_demo {
                id
                localFile {
                    childImageSharp {
                        fixed(width: 400) {
                            ...GatsbyImageSharpFixed_withWebp
                        }
                    }
                }
            }
        }
    }
`;
Enter fullscreen mode Exit fullscreen mode

I've left off all the null-checking in these components for brevity's sake, but you should check that each of the fields exists (I recommend optional chaining if your environment supports it). This is particularly important for the images.

Add the ParagraphImageText fragment to the page query:

// paragraph.js
export const pageQuery = graphql`
    query($slug: String!) {
        page: nodeParagraphedContentDemo(fields: { slug: { eq: $slug } }) {
            id
            relationships {
                paragraphs: field_paragraphs_demo {
                    type: __typename
                    ...ParagraphText
                    ...ParagraphImageText
                }
            }
            title
        }
    }
`;
Enter fullscreen mode Exit fullscreen mode

The Images paragraph is similar, but has an array of several images. You could create a similar paragraph for something like a carousel.

// ImagesParagraph.js
import React from "react";
import Img from "gatsby-image";
import { graphql } from "gatsby";

export const ImagesParagraph = ({ node }) => {
    const { images } = node.relationships;
    return (
        // Carousel like it's 1999! (Seriously though, never do this)
        <marquee>
            {images.map(image => (
                <Img fixed={image.localFile.childImageSharp.fixed} />
            ))}
        </marquee>
    );
};

export const fragment = graphql`
    fragment ParagraphImages on paragraph__images {
        id
        relationships {
            images: field_images_demo {
                id
                localFile {
                    childImageSharp {
                        fixed(width: 200, height: 200) {
                            ...GatsbyImageSharpFixed
                        }
                    }
                }
            }
        }
    }
`;
Enter fullscreen mode Exit fullscreen mode

Now you have components for all of your paragraph types, you just need to work out how to display the correct one. I use a little helper function for this:

// paragraphHelpers.js
import React from "react";
import { ImageAndTextParagraph } from "./components/ImageAndTextParagraph";
import { TextParagraph } from "./components/TextParagraph";
import { ImagesParagraph } from "./components/ImagesParagraph";

const components = {
    paragraph__image_text: ImageAndTextParagraph,
    paragraph__text: TextParagraph,
    paragraph__images: ImagesParagraph
};

export const getParagraph = node => {
    if (components.hasOwnProperty(node.type)) {
        const ParagraphComponent = components[node.type];
        return <ParagraphComponent key={node.id} node={node} />;
    }
    return <p key={node.id}>Unknown type {node.__typename}</p>;
};
Enter fullscreen mode Exit fullscreen mode

The components object maps the paragraph name from Drupal to the component. In this example I'm returning a warning if there's unrecognised type, but in production you'd probably just want to log something.

Now you can use this function to convert your data into components:

import React from "react";
import { graphql } from "gatsby";
import Layout from "../components/layout";

import { getParagraph } from "../paragraphHelpers";

export const PageTemplate = ({ data }) => {
    const paragraphs = data.page.relationships.paragraphs.map(getParagraph);

    return (
        <Layout>
            <h1>{data.page.title}</h1>
            {paragraphs}
        </Layout>
    );
};

export default PageTemplate;

export const pageQuery = graphql`
    query($slug: String!) {
        page: nodeParagraphedContentDemo(fields: { slug: { eq: $slug } }) {
            id
            relationships {
                paragraphs: field_paragraphs_demo {
                    type: __typename
                    ...ParagraphText
                    ...ParagraphImageText
                    ...ParagraphImages
                }
            }
            title
        }
    }
`;
Enter fullscreen mode Exit fullscreen mode

This will give you all of your paragraphs with the correct component, in the correct order.

These examples aren't anything special, because we're using the basic default paragraph types, which you could easily add to a normal page. This is most powerful when you create custom paragraph types to match specific custom components that you would like your editors to be able to add to a page. How about a map component, with a custom paragraph to set the bounds? Or a newsletter signup, with a custom paragraph to select the mailing list?

What we're not doing here is handling nested paragraphs, or using the paragraphs library module. These get a bit more complicated, but can still be modelled using the same system: you just need to go a level deeper to get at the actual data.

Top comments (5)

Collapse
 
vacilando profile image
Tomáš Fülöpp

Creative approach, excellent article — thanks, Matt!

A little thing that needs to be fixed:

import { getParagraph } from "../paragraphHandler";

should be

import { getParagraph } from "../paragraphHelpers";
Collapse
 
jhony0311 profile image
Jhony🇵🇦

Hey, thanks so much for this post, I've tried the steps of the guide but I'm not able to see the fields inside of the relationship node. That is getting skipped. Any guidance or something to look at, probably on the Drupal side?

Collapse
 
dmerckx profile image
David Merckx • Edited

Hey Matt,

Thanks for sharing! I'm following your guide but I'm not seeing any of the fields I defined in my paragraphs pop up in the graphql explorer. Which drupal library did you use exactly to expose the graphql to gatsby?

Edit:
I found out my problem was that I did not have any content defined yet. Only paragraph fields for which there is actual data in the drupal system show up in the graphql explorer.

Collapse
 
jasloe profile image
Jason Loeffler

A little tough to follow this tutorial since the very first query returns:

"message": "Cannot query field \"field_paragraphs_demo\" on type \"node__paragraphed_content_demoRelationships\".",
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ibt profile image
Isaac Bigsby Trogdon

This is a great approach - thanks!