I figured, for further parts of the tutorial, it will make sense to implement the Blog now. So this part will show you how to create a Blog (post overview) with pagination.
I renamed all components to start with a capital letter. Checkout this commit to see the refactoring I did and what changed. Or, just dive into the tutorial first and compare later.
Table of Contents
- Add content to WordPress 💻
- Implement Blog components 💾
- Adjust Post/Blog Creation 📓
- Final Thoughts 🏁
- What's Next ➡️
Add content to WordPress 💻
1.) Create posts
Let's add some posts. Preferably more than 10, so that we can see our pagination in action later on. Make sure to have some posts with featured images.
2.) Add Menu Link
Now add a custom link to the Menu with /blog/
Implement Blog components 💾
Now, let's create the Blog page to preview all our posts.
1.) Add globals.js
First of all I added a file called globals.js
to the root of the project. It will serve as our "options" for the project. Like what we later might wanna use if we use this starter as a theme.
// globals.js
const Globals = {
blogURI: 'blog'
}
module.exports = Globals
2.) Create blog.js
Now, create blog.js inside the templates/post folder:
// src/templates/post/blog.js
import React from "react"
import Layout from "../../components/Layout"
import PostEntry from "../../components/PostEntry"
import Pagination from "../../components/Pagination"
import SEO from "../../components/SEO"
const Blog = ({ pageContext }) => {
const { nodes, pageNumber, hasNextPage, itemsPerPage, allPosts } = pageContext
return (
<Layout>
<SEO
title="Blog"
description="Blog posts"
keywords={[`blog`]}
/>
{nodes && nodes.map(post => <PostEntry key={post.postId} post={post}/>)}
<Pagination
pageNumber={pageNumber}
hasNextPage={hasNextPage}
allPosts={allPosts}
itemsPerPage={itemsPerPage}
/>
</Layout>
)
}
export default Blog
- As you can see I already made use of some components we will create in the upcoming steps.
3.) Create PostEntry.js
// src/components/PostEntry.js
import React from "react"
import { Link } from "gatsby"
import Image from "./Image"
import { blogURI } from "../../globals"
const PostEntry = ({ post }) => {
const { uri, title, featuredImage, excerpt } = post
return (
<div style={{ marginBottom: "30px" }}>
<header>
<Link to={`${blogURI}/${uri}/`}>
<h2 style={{ marginBottom: "5px" }}>{title}</h2>
<Image image={featuredImage} style={{ margin: 0 }}/>
</Link>
</header>
<div dangerouslySetInnerHTML={{ __html: excerpt }}/>
</div>
)
}
export default PostEntry
4.) Create Image.js
// src/components/Image.js
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
const Image = ({ image, withFallback = false, ...props }) => {
const data = useStaticQuery(graphql`
query {
fallBackImage: file(relativePath: { eq: "fallback.svg" }) {
publicURL
}
}
`)
/**
* Return fallback Image, if no Image is given.
*/
if (!image) {
return withFallback ? <img src={data.fallBackImage.publicURL} alt={"Fallback"} {...props}/> : null
}
return <img src={image.sourceUrl} alt={image.altText} {...props}/>
}
export default Image
The image component for now works simply with the absolute path to your WordPress installation. We will get more into image-processing in the next part of the tutotorial.
-
withFallback
gives you the option to show a fallback image if there is no image passed down. IfwithFallback
is false, like in the default, then it will simply not render any dom element. - Place the
fallback.svg
image inside your images folder. (Find my fallback image here)
5.) Create Pagination.js
Now if we want to only show a certain number of post previews on one page, we need to implement pagination.
// src/components/Pagination.js
import React from "react"
import { Link } from "gatsby"
import { blogURI } from "../../globals"
const Pagination = ({ pageNumber, hasNextPage }) => {
if (pageNumber === 1 && !hasNextPage) return null
return (
<div style={{ margin: "60px auto", textAlign: "center" }}>
<h2>Posts navigation</h2>
<div>
{
pageNumber > 1 && (
<Link
className="prev page-numbers"
style={{
padding: "4px 8px 5px 8px",
backgroundColor: "rgba(0,0,0,.05)",
borderRadius: "3px",
}}
to={pageNumber > 2 ? `${blogURI}/page/${pageNumber - 1}` : `${blogURI}/`}
>
<span>Previous page</span>
</Link>
)
}
<span aria-current="page" className="page-numbers current" style={{ padding: "5px 10px" }}>
<span className="meta-nav screen-reader-text">Page </span>
{pageNumber}
</span>
{
hasNextPage && (
<Link
style={{
padding: "4px 8px 5px 8px",
backgroundColor: "rgba(0,0,0,.05)",
borderRadius: "3px",
}}
className="next page-numbers"
to={`${blogURI}/page/${pageNumber + 1}`
}
>
<span>Next page </span>
</Link>
)
}
</div>
</div>
)
}
export default Pagination
- If
pageNumber === 1 && !hasNextPage
we can return null, as we only have one page and don't need any pagination. - If the current pageNumber is more than 1, we show a Previous Page Link/Button
- If the current page
hasNextPage
, we show a Link/Button to the Next page
Adjust Post/Blog Creation 📓
We have to adjust a couple of things in createPosts.js
. I'll introduce a new concept of GraphQL fragments, that live in data.js
files. This will help us to separate some concerns and make our code structure more structured.
1.) Add data.js
Let's add one fragment for the Post and one for the Blog-Preview.
If you are unfamiliar with the concepts of fragments in GraphQL checkout this link.
// src/templates/post/data.js
const PostTemplateFragment = `
fragment PostTemplateFragment on WPGraphQL_Post {
id
postId
title
content
link
featuredImage {
sourceUrl
}
categories {
nodes {
name
slug
id
}
}
tags {
nodes {
slug
name
id
}
}
author {
name
slug
}
}
`
const BlogPreviewFragment = `
fragment BlogPreviewFragment on WPGraphQL_Post {
id
postId
title
uri
date
slug
excerpt
content
featuredImage {
sourceUrl
}
author {
name
slug
}
}
`
module.exports.PostTemplateFragment = PostTemplateFragment
module.exports.BlogPreviewFragment = BlogPreviewFragment
- Notice how we are using only strings that get exported?
- We will use these strings inside the createPagesStatefully and not directly inside our React components. This gives us the freedom to define queries before the build starts and therefore we are able to concat multiple strings to one query string in the end.
- This wouldn't be possible inside the components, when used with page queries or static queries.
2.) Adjust createPosts.js
First part
// create/createPosts.js
const {
PostTemplateFragment,
BlogPreviewFragment,
} = require("../src/templates/post/data.js")
const { blogURI } = require("../globals")
const postTemplate = require.resolve("../src/templates/post/index.js")
const blogTemplate = require.resolve("../src/templates/post/blog.js")
const GET_POSTS = `
# Here we make use of the imported fragments which are referenced above
${PostTemplateFragment}
${BlogPreviewFragment}
query GET_POSTS($first:Int $after:String) {
wpgraphql {
posts(
first: $first
after: $after
# This will make sure to only get the parent nodes and no children
where: {
parent: null
}
) {
pageInfo {
hasNextPage
endCursor
}
nodes {
uri
# This is the fragment used for the Post Template
...PostTemplateFragment
#This is the fragment used for the blog preview on archive pages
...BlogPreviewFragment
}
}
}
}
`
// continues with second part
- We import the fragment strings and place them inside the
GET_PAGES
query string, but outside the actualquery GET_POSTS($first:Int $after:String) {...}
.- This will register the fragments, so they can be used in the upcoming query.
- Inside the query, you finally can make use of the standard
...
fragment syntax.
Second Part
// create/createPosts.js
// Make sure to add the first part above
const allPosts = []
const blogPages = [];
let pageNumber = 0;
const itemsPerPage = 10;
/**
* This is the export which Gatbsy will use to process.
*
* @param { actions, graphql }
* @returns {Promise<void>}
*/
module.exports = async ({ actions, graphql, reporter }, options) => {
/**
* This is the method from Gatsby that we're going
* to use to create posts in our static site.
*/
const { createPage } = actions
/**
* Fetch posts method. This accepts variables to alter
* the query. The variable `first` controls how many items to
* request per fetch and the `after` controls where to start in
* the dataset.
*
* @param variables
* @returns {Promise<*>}
*/
const fetchPosts = async (variables) =>
/**
* Fetch posts using the GET_POSTS query and the variables passed in.
*/
await graphql(GET_POSTS, variables).then(({ data }) => {
/**
* Extract the data from the GraphQL query results
*/
const {
wpgraphql: {
posts: {
nodes,
pageInfo: { hasNextPage, endCursor },
},
},
} = data
/**
* Define the path for the paginated blog page.
* This is the url the page will live at
* @type {string}
*/
const blogPagePath = !variables.after
? `${blogURI}/`
: `${blogURI}/page/${pageNumber + 1}`
/**
* Add config for the blogPage to the blogPage array
* for creating later
*
* @type {{
* path: string,
* component: string,
* context: {nodes: *, pageNumber: number, hasNextPage: *}
* }}
*/
blogPages[pageNumber] = {
path: blogPagePath,
component: blogTemplate,
context: {
nodes,
pageNumber: pageNumber + 1,
hasNextPage,
itemsPerPage,
allPosts,
},
}
/**
* Map over the posts for later creation
*/
nodes
&& nodes.map((posts) => {
allPosts.push(posts)
})
/**
* If there's another post, fetch more
* so we can have all the data we need.
*/
if (hasNextPage) {
pageNumber++
reporter.info(`fetch post ${pageNumber} of posts...`)
return fetchPosts({ first: itemsPerPage, after: endCursor })
}
/**
* Once we're done, return all the posts
* so we can create the necessary posts with
* all the data on hand.
*/
return allPosts
})
/**
* Kick off our `fetchPosts` method which will get us all
* the posts we need to create individual posts.
*/
await fetchPosts({ first: itemsPerPage, after: null }).then((wpPosts) => {
wpPosts && wpPosts.map((post) => {
/**
* Build post path based of theme blogURI setting.
*/
const path = `${blogURI}/${post.uri}/`
createPage({
path: path,
component: postTemplate,
context: {
post: post,
},
})
reporter.info(`post created: ${post.uri}`)
})
reporter.info(`# -----> POSTS TOTAL: ${wpPosts.length}`)
/**
* Map over the `blogPages` array to create the
* paginated blog pages
*/
blogPages
&& blogPages.map((blogPage) => {
if (blogPage.context.pageNumber === 1) {
blogPage.context.publisher = true;
blogPage.context.label = blogPage.path.replace('/', '');
}
createPage(blogPage);
reporter.info(`created blog archive page ${blogPage.context.pageNumber}`);
});
})
}
- Compared to the version from our previous parts, I did some constant name refactoring and added the
blogURI
coming from our globals file.
Final Thoughts 🏁
Noooow, I hope I didn't miss anything. This part was a bit tricky, as I had to do some refactoring. I hope this did not mess up your setup. If so, please feel free to ask questions here or just clone this part of the tutorial to have a clean base for the next part.
Find the code base here: https://github.com/henrikwirth/gatsby-starter-wordpress-advanced/tree/tutorial/part-5
What's Next ➡️
In the next part of the series, we will discover how to fetch images from WordPress in a way, that makes it possible to use the powerful gatsby-image plugin.
Top comments (4)
First off, great work with this tutorial! Thank you for all your effort.
I thought I would mention a problem I was having and what I did to resolve it in case others have the same issue.
I had been running into this error:
TypeError: Cannot destructure property
wpgraphql
of 'undefined' or 'null'I traced it back to the PostTemplateFragment and BlogPreviewFragment within the data.js file. Apparently, sourceUrl under 'FeaturedImage' as well as name and slug under 'author' need to be wrapped in 'node'. Example:
and
Perhaps this is the result of a breaking change to wpgraphql?
After adjusting those fragments the project built successfully again.
As of now (06. December) you can play around with the Blog-Pagination already here: gatsby-starter-wordpress-advanced....
This preview is already using gatsby-image processing which will be part of the next part. Stay tuned :)
Man, just wanted to say thank you so much! I've been trying this stuff out on an existing WP site and this has been so helpful.
Glad it helps 🎊. Feel free to share what you are creating with it. Always interesting to see what people are building with Gatsby.