Building a Website With Gatsby and a Headless CMS - Part 2

This is a two part series blog post, here is the first part.

Writing the Gatsby Plugin

Open the /gatsby-source-dotcms/gatsby-node.js file and replace the code with the following:

const dotCMSApi = require('./dotcms-api');

exports.sourceNodes = ({ actions, createNodeId }, configOptions) => {
    const { createNode } = actions;

    // Gatsby adds a configOption that's not needed for this plugin, delete it
    delete configOptions.plugins;

    // Helper function that processes a contentlet to match Gatsby's node structure
    const processContentlet = (contentlet) => {
        const nodeId = createNodeId('dotcms-${contentlet.contentType}-${contentlet.inode}');
        const nodeContent = JSON.stringify(contentlet);
        const nodeData = {
            ...contentlet,
            id: nodeId,
            parent: null,
            children: [],
            internal: {
                type: 'DotCMS${contentlet.contentType}',
                content: nodeContent,
                contentDigest: JSON.stringify(contentlet)
            }
        };
        return nodeData;
    };

    // Gatsby expects sourceNodes to return a promise
    return dotCMSApi
        .getContentlets(configOptions)
        .then((contentlets) => {
            // Process the response data into a node
            contentlets.forEach((contentlet) => {
                // Process each contentlet data to match the structure of a Gatsby node
                const nodeData = processContentlet(contentlet);
                // Use Gatsby's createNode helper to create a node from the node data
                createNode(nodeData);
            });
        });
};

Let's go over the new code.

const dotCMSApi = require('./dotcms-api');

First of all, you imported the dotCMS API you created.

const processContentlet = (contentlet) => {
    const nodeId = createNodeId('dotcms-${contentlet.contentType}-${contentlet.inode}');
    const nodeContent = JSON.stringify(contentlet);
    const nodeData = {
        ...contentlet,
        id: nodeId,
        parent: null,
        children: [],
        internal: {
            type: `DotCMS${contentlet.contentType}`,
            content: nodeContent,
            contentDigest: JSON.stringify(contentlet)
        }
    };
    return nodeData;
};

The job of this function is to receive a dotCMS contentlet and return a Gatsby Node by extending it with a spread operator. The basic node structure looks like:

interface BasicNode {
    id: String;
    children: Array[String];
    parent: String;
    // Reserved for plugins who wish to extend other nodes.
    fields: Object;
    internal: {
        contentDigest: String;
        // Optional media type (https://en.wikipedia.org/wiki/Media_type) to indicate
        // to transformer plugins this node has data they can further process.
        mediaType: String;
        // A globally unique node type chosen by the plugin owner.
        type: String;
        // The plugin which created this node.
        owner: String;
        // Stores which plugins created which fields.
        fieldOwners: Object;
        // Optional field exposing the raw content for this node
        // that transformer plugins can take and further process.
        content: String;
        //...other fields specific to this type of node
    };
}

Gatsby expects sourceNodes to return a promise

return dotCMSApi
    .getContentlets(configOptions)
    .then((contentlets) => {
        // Process the response data into a node
        contentlets.forEach((contentlet) => {
            // Process each contentlet data to match the structure of a Gatsby node
            const nodeData = processContentlet(contentlet);
            // Use Gatsby's createNode helper to create a node from the node data
            createNode(nodeData);
        });
    });

The final piece of the puzzle is to return a Promise of processed Gatsby nodes. You used the dotCMSApi library to getContentlets and convert each one of those contentlets you got into a node.

Trying Our Fancy New Plugin

Edit your gatsby-config.js and update the entry for gatsby-source-dotcms:

{
    resolve: 'gatsby-source-dotcms',
    options: {
        host: {
            protocol: 'http',
            url: 'localhost: 8080',
            identifier: '48190c8c-42c4-46af-8d1a-0cd5db894797'
        },
        credentials: {
            email: 'admin@dotcms.com',
            password: 'admin'
        }
    }
};

NOTE: This configuration is set for a dotCMS instance running in localhost:8080, make sure you point this values to an actual running dotCMS instance.

Now, from the root of your project, run the following in your terminal:

$ gatsby develop

If everything went well you should see:

Gatsby Done Alert

Querying DotCMS Data With GraphQL

Every time you run $ gatsby develop you get this:

If you go to http://localhost:8000/__graphql in your browser, you will get a web app that allows you to run GraphQL queries and make sure all DotCMS content is there. It will look like this:

GraphiQL

How to Generate Pages

Note: As I mentioned before, Gatsby use React.js to build pages and components. Is this tutorial, I am assuming you are familiar with this library.

So you have all our dotCMS content ready to use. Let's create some pages.

You can create pages in Gatsby explicitly by defining React components in src/pages/, or programmatically by using the createPages API.

Create a News Listing Page

By default, Gatsby creates a src/pages/page-2.js file. Let's rename that file to src/pages/news.js and then replace the code with:

import React from 'react';
import { Link } from 'gatsby';
import Layout from '../components/layout';

const NewsPage = () => (
    <Layout>
        <h1>News list</h1>
        <p>Here we will show a news list</p>
        <Link to="/">Go back to the homepage</Link>
    </Layout>
);

export default NewsPage;

This is very simple React.js code you just created a component that will print out plain HTML.

To see this page, go to your terminal and run:

$ gatsby develop

And then open your browser to http://localhost:8000/news and you should see:

Gatsby Default Starter

Bring the dotCMS Contentlets

Okay, now is a good time to use the source plugin you built before. You're going to query all the contentlets of the content type "News" and put them in the page.

Once again, edit src/pages/news.js and add the following code:

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

const NewsPage = ({ data }) => (
    <Layout>
        <h1>News list</h1>
        <ul class="news">
            {data.allDotCmsNews.edges.map(({ node }, index) => (
                <li key={index}>
                    <h4>{node.title}</h4>

                    <p>{node.lead}</p>
                </li>
            ))}
        </ul>
    </Layout>
);

export const query = graphql`
    query {
        allDotCmsNews {
            edges {
                node {
                    lead
                    title
                    urlTitle
                }
            }
        }
    }
`;

export default NewsPage;

Let's go over this new code.

import { graphql } from 'gatsby'

You need to import GraphQL so you can use it to query the contentlets. Gatsby's GraphQL tag enables page components to retrieve data via a GraphQL query.

const NewsPage = ({ data }) => (
    <Layout>
        <h1>News list</h1>

        <ul class="news">
            {data.allDotCmsNews.edges.map(({ node }, index) => (
                <li key={index}>
                    <h4>{node.title}</h4>

                    <p>{node.lead}</p>
                </li>
            ))}
        </ul>
    </Layout>
);

Finally, you just print out the data you get from the GraphQL query.

export const query = graphql`
    query {
        allDotCmsNews {
            edges {
                node {
                    lead
                    title
                    urlTitle
                }
            }
        }
    }
`;

Here, the "magic" of getting the data to happen is a simple GraphQL query to get all the dotCMS News items. In this case, you asked for the field: lead, title, and urlTitle. If everything went well, you should see something like:

Gatsby Default Starter

Creating News Detail Pages

You have a list of news, now let's create a detail page for each news item in dotCMS. I know that's sound like a ton of work, but with Gatsby you can programmatically create pages from data: basically, you can tell Gatsby, here is a collection of news, create a page for each item using this template (another React component).

Edit your gatsby-node.js file and add the following code:

const path = require('path');

exports.createPages = ({ graphql, actions }) => {
    const { createPage } = actions;

    return new Promise((resolve, reject) => {
        graphql(`
            {
                allDotCmsNews {
                    edges {
                        node {
                            inode

                            lead

                            sysPublishDate

                            title

                            urlTitle
                        }
                    }
                }
            }
        `).then((result) => {
            result.data.allDotCmsNews.edges.forEach(({ node }) => {
                createPage({
                    path: 'news/${node.urlTitle}',
                    component: path.resolve('./src/templates/news-item.js'),
                    context: {
                        // Data passed to context is available
                        // in page queries as GraphQL variables.
                        slug: node.urlTitle
                    }
                });
            });

            resolve();
        });
    });
};

What is this code doing?

First, at all, this code will be running at build time, all these pages will be generated once when you build Gatsby, so if new a contentlet is added to dotCMS, you need to build and deploy your Gatsby site in order to get that content.

const path = require(`path`)

This is the implementation of the createPages API which Gatsby calls so plugins can add pages. By exporting createPages from a gatsby-node.js file, you're saying, "at this point in the bootstrapping sequence, run this code.

graphql(`
    {
        allDotCmsNews {
            edges {
                node {
                    inode
                    lead
                    sysPublishDate
                    title
                    urlTitle
                }
            }
        }
    }
`);

The same as you did with the listing page, here is another GraphQL query to get all the dotCMS news contentlets, the only difference is that you're asking for more fields.

.then((result) => {
    result.data.allDotCmsNews.edges.forEach(({ node }) => {
        createPage({
            path: `news/${node.urlTitle}`,
            component: path.resolve('./src/templates/news-item.js'),
            context: {
                // Data passed to context is available
                // in page queries as GraphQL variables.
                slug: node.urlTitle
            }
        });
    });
    resolve();
});

Once the GraphQL query resolves, you have access to all the news items, and you can iterate over and create a page for each item; but you need to pass an object to the createPage function:

  1. path: the URL where the generated page will be created. In this case, it will be news/page-url-title. You use the urlTitle field from the contentlet to create the path, but, whatever you use, needs to be unique, because this will be the name of the HTML generated file and thus the URL of the page.
  2. component: this is where you tell Gatsby which template you will be using to create each page (you will create this in the next step).
  3. context: this is an object that you are passing to the GraphQL query that will be in the template component.

Finally, you call resolve() for the Promise returning in the implementation of the createPages function.

Creating the Template

A template is just a regular React.js component, so let's create a file in src/templates/news-item.js (like you set in the path property of the createPage param) and add the following code:

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

export default ({ data }) => {
    const post = data.allDotCmsNews.edges[0].node
    return (
        <Layout>
            <h1>{post.title}</h1>
            <div dangerouslySetInnerHTML={{ __html: post.story }} />
        </Layout>
    )
}

export const query = graphql`
    query($slug: String!) {
        allDotCmsNews(filter: { urlTitle: { eq: $slug } }) {
            edges {
                node {
                    title,
                    story,
                }
            }
        }
    }
`

Let's go over this code

export default ({ data }) => {

Before you get the data to use in the page, you get it as a prop in the component.

<Layout>
 <h1>{post.title}</h1>
 <div dangerouslySetInnerHTML={{ __html: post.story }} />
</Layout>

You print out the content of the news item on the page.

export const query = graphql`
    query($slug: String!) {
        allDotCmsNews(filter: { urlTitle: { eq: $slug } }) {
            edges {
                node {
                    title,
                    story,
                }
            }
        }
    }
`

Here is in the GraphQL query. Everything is almost the same as the other queries with the difference that you're matching one news item by urlTitle and the $slug param, which is what you pass in the context object in the params of the createPage function in the gatsby-node.js

Now we can build our pages, we just need to run:

$ gatsby develop

Right now you don't have links to the recently created links, but you can use the urlTitle field from the contentlet. Go to: http://localhost:8080/news/the-gas-price-rollercoaster and you should see:

dotCMS Gatsby News Detail

Final Touches: Adding Links to Our Listing Page

Go and edit src/pages/news.js and:

Import the Link component on the top of the file:

import { Link } from 'gatsby'

Replace this line:

<h4>{node.title}</h4>

With:

<h4><Link to={'news/' + node.urlTitle}>{node.title}</Link></h4>

In the browser go to http://localhost:8080/news and you should see:

dotCMS Gatsby News Results

Now the news list items are linked to the detail page you dynamically created with Gatsby.

Static-Site Generator + Headless CMS = The Perfect Match

And there you have it! You've now used a static-site generator and a headless CMS to build a static website. This is just one example of how these two technologies can combine.

To recap, we built a Gatsby Source Plugin from scratch, created a listing page, and created a bunch of static HTML pages querying dotCMS data with GraphQL.

I recommend you take a look Gatsby Documentation, this is the very basic but you can do so much more, styled components, layouts, pagination, etc.

Copyright © 2024. Design and code by myself with Next.js. Fork it and create yours