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
}
}
}
}
}
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"
}
]
}
}
]
}
}
}
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
});
}
};
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
}
}
}
}
`;
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;
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
}
}
`;
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
}
}
`;
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
}
}
}
}
}
}
`;
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
}
}
`;
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
}
}
}
}
}
}
`;
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>;
};
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
}
}
`;
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)
Creative approach, excellent article — thanks, Matt!
A little thing that needs to be fixed:
should be
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?
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.
A little tough to follow this tutorial since the very first query returns:
This is a great approach - thanks!