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:
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:
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:
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:
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:
- 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. - 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).
- 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:
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:
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.