Gatsbyjs Node API provides great many ways to create static pages for your application. The thing I like most about it is, the complete control it gives. Essentially to create a page, we need to
- Supply
path
orslug
of the page. - A react component template.
- Data.
NOTE: The article is written for gatsbyjs v2, and it may still work for v1.
Gatsby doesn't care how and where the data came from, it can be from a graphql
query or maybe something else too. It just creates a page at the provided path
,
passes the data to your react component and again you are in control of how to
present the data.
So I was in a situation where I needed to iterate over a multiple thing, which
was based on promise interface and I needed createPages
to wait for all the
promise to resolve. Basically I was:
- Fetching some markdown files using
graphql
query. - Fetching some directory from the file-system using Nodejs
fs
API.
This is the solution I came up with.
Promise.all
interface
Use Under gatsby-node.js
file, we usually define createPages
like this:
// Create pages for docs
exports.createPages = ({ actions, graphql }) => {
const { createPage } = actions;
const docTemplate = path.resolve('src/templates/docTemplate.js');
// Individual doc pages
return graphql(`
{
allMarkdownRemark(
filter: { fileAbsolutePath: { glob: "**/docs/**/*.md" } }
sort: { order: DESC, fields: frontmatter___order }
) {
edges {
node {
fields {
slug
}
}
}
}
}
`).then(result => {
if (result.errors) {
Promise.reject(result.errors);
}
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
createPage({
path: node.fields.slug,
component: docTemplate,
});
});
});
};
As we can see, here we are just returning one single Promise
, we get with graphql
function and gatsby is taking care of waiting till it is resolved. This is just how Promise
work, they don't resolve until we call resolve()
or all chainables
are passed.
So naturally the right way to wait for multiple queries would be to have multiple
Promise
and return a Promise.all([promiseOne, promiseTwo])
. Let's see an example
// Create pages for docs and docRoots
exports.createPages = ({ actions, graphql }) => {
const { createPage } = actions;
const docTemplate = path.resolve('src/templates/docTemplate.js');
const docRootTemplate = path.resolve('src/templates/docRootTemplate.js');
// Individual doc pages
// graphql already returns a promise
// so we can use that instead of creating our own Promise instance
const docs = graphql(`
{
allMarkdownRemark(
filter: { fileAbsolutePath: { glob: "**/docs/**/*.md" } }
sort: { order: DESC, fields: frontmatter___order }
) {
edges {
node {
fields {
slug
}
}
}
}
}
`).then(result => {
if (result.errors) {
Promise.reject(result.errors);
}
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
createPage({
path: node.fields.slug,
component: docTemplate,
});
});
});
// Doc root pages
// This is a `sync` operation, but we are wrapping
// inside a Promise, because that's what gatsby Node API
// expects.
const docRoots = new Promise((resolve, reject) => {
// First get all directories inside docs
const docTypes = dirs(path.resolve(__dirname, './docs'));
if (docTypes && docTypes.length) {
docTypes.forEach(docType => {
createPage({
path: `/${docType}/`,
component: docRootTemplate,
context: {
docType,
},
});
});
resolve();
} else {
reject(new Error(`No directories found for document roots.`));
}
});
return Promise.all([docs, docRoots]);
};
Here we are
- Querying through
graphql
our markdown files. - At the same time, we are querying directories to get child directory and creating
pages for them too. Here the operation is
synchronous
but we are still using promise interface to demonstrate the working ofasynchronous
operations. - Finally we are returning
Promise.all([...])
, which would return a promise interface, which in turn, would wait for all the individual Promises we have passed to it.
And that's how it works.
graphql
queries?
What if I need multiple In most of the cases you don't. Just use a single graphql
query and extract
data from it.
For example, if we were to extract files from src/pages/guide/**/*.md
, and
src/blog/**/*.md
, then instead of writing two queries, we can very much do
// Create pages for docs
exports.createPages = ({ actions, graphql }) => {
const { createPage } = actions;
const docTemplate = path.resolve('src/templates/docTemplate.js');
const blogTemplate = path.resolve('src/templates/blogTemplate.js');
// Individual doc and blog pages
// All in one go
return graphql(`
{
blogs: allMarkdownRemark(
filter: { fileAbsolutePath: { glob: "**/src/pages/blog/*.md" } }
sort: { order: DESC, fields: frontmatter___date }
) {
edges {
node {
fields {
slug
}
}
}
}
docs: allMarkdownRemark(
filter: {
fileAbsolutePath: { glob: "**/src/pages/project/*.md" }
}
sort: { order: DESC, fields: frontmatter___order }
) {
edges {
node {
fields {
slug
}
}
}
}
}
`).then(result => {
if (result.errors) {
Promise.reject(result.errors);
}
// Create doc pages
result.data.docs.edges.forEach(({ node }) => {
createPage({
path: node.fields.slug,
component: docTemplate,
});
});
// Create blog pages
result.data.blogs.edges.forEach(({ node }) => {
createPage({
path: node.fields.slug,
component: docTemplate,
});
});
});
};
Notice we are querying docs
and blogs
under the same graphql
{
blogs: allMarkdownRemark(
filter: { fileAbsolutePath: { glob: "**/src/pages/blog/*.md" } }
sort: { order: DESC, fields: frontmatter___date }
) {
edges {
node {
fields {
slug
}
}
}
}
docs: allMarkdownRemark(
filter: { fileAbsolutePath: { glob: "**/src/pages/project/*.md" } }
sort: { order: DESC, fields: frontmatter___order }
) {
edges {
node {
fields {
slug
}
}
}
}
}
and extracting the results using result.data.blogs
and result.data.docs
.
IMHO, this is the perfectly fine way to have multiple query dependent createPage
stuff.
But if, for some reason, you absolutely MUST have parallel running multiple queries, then again you can use the promise interface. Let's convert the same example into multiple queries.
// Create pages for docs and blogs separately using two separate
// queries. We use the `graphql` function which returns a Promise
// and ultimately resolve all of them using Promise.all(Promise[])
exports.createPages = ({ actions, graphql }) => {
const { createPage } = actions;
const docTemplate = path.resolve('src/templates/docTemplate.js');
const blogTemplate = path.resolve('src/templates/blogTemplate.js');
// Individual blogs pages
const blogs = graphql(`
{
blogs: allMarkdownRemark(
filter: { fileAbsolutePath: { glob: "**/src/pages/blog/*.md" } }
sort: { order: DESC, fields: frontmatter___date }
) {
edges {
node {
fields {
slug
}
}
}
}
}
`).then(result => {
if (result.errors) {
Promise.reject(result.errors);
}
// Create blog pages
result.data.blogs.edges.forEach(({ node }) => {
createPage({
path: node.fields.slug,
component: blogTemplate,
});
});
});
// Individual docs pages
const docs = graphql(`
{
docs: allMarkdownRemark(
filter: {
fileAbsolutePath: { glob: "**/src/pages/project/*.md" }
}
sort: { order: DESC, fields: frontmatter___order }
) {
edges {
node {
fields {
slug
}
}
}
}
}
`).then(result => {
if (result.errors) {
Promise.reject(result.errors);
}
// Create doc pages
result.data.docs.edges.forEach(({ node }) => {
createPage({
path: node.fields.slug,
component: docTemplate,
});
});
});
// Return a Promise which would wait for both the queries to resolve
return Promise.all([blogs, docs]);
};
Here we are creating multiple Promise
es blogs
and docs
. Notice we are again
just using graphql
directly, which actually returns a Promise
. We are calling
.then()
to chain a callback function, under which we are creating the pages
through createPage
API.
Returning Promise.all([blogs, docs])
ensures that our operation is successfully
completed, before gatsby moves to create page files.