Gatsby One Pager
How to build a Gatsby site? Why the guides online are so fragmented? Isn't there a one pager guide for Gatsby with a working example? Well you have found it. This one page guide would help you build a static site with:
- Markdown based blog post
- Client side search
- Pagination
- Code Highlighting
- Google Analytics
- Responsive design, well we won't really cover this but you can have a look at the Github code.
See it in action on https://www.codeallnight.com or take a peek at the git repo. Feel free to build on top of it. Empty the src/posts
folder and start write your own.
1. Prerequisite
First thing first, install gatsby-cli
and clone the repo. Cloning the repo is optional, but isn't it always nicer to have a code example at your disposal?
npm install -g gatsby-cli
git clone git@github.com:djoepramono/code-all-night.git
cd code-all-night
npm install
gatsby develop -H 0.0.0.0
Running gatsby develop
only, makes the site only avaiable on the host computer via localhost. But sometimes you want to make it accessible to your local network, so that you can test your site with your mobile phone. For this, you need the -H 0.0.0.0
.
Each section on this guide might depends on a specific npm package. These packages are already included in the repo package.json
. If you don't clone the repo and start fresh instead, make sure you install them.
2. Markdown Posts
Markdown files can be made into pages in Gatsby with the help of gatsby-transformer-remark
Put your markdown files into src/posts
. There are some examples there already. Next up, you need to put the following entry into gatsby-node.js
exports.createPages = async ({ actions, graphql, reporter }) => {
const { createPage } = actions
const blogPostTemplate = path.resolve(`src/templates/post.js`)
const result = await graphql(`
{
allMarkdownRemark(
sort: { order: DESC, fields: [frontmatter___date] }
limit: 1000
) {
edges {
node {
frontmatter {
title
date(formatString: "DD MMMM YYYY")
author
path
}
excerpt
timeToRead
}
}
}
}
`)
// Handle errors
if (result.errors) {
reporter.panicOnBuild(`Error while running GraphQL query.`)
return
}
// Create post pages
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
createPage({
path: node.frontmatter.path,
component: blogPostTemplate,
context: {},
})
})
}
The code above is utilising Gatsby's createPages
API to create a static page for each markdown posts. Each of this markdown files can be enriched with frontmatter
, a set of key value pair that exists on top of each markdown file.
Under the hood, Gatsby is using GraphQL, which you can read more here . It also provides you with graphical UI client at http://localhost:8000/__graphql. It's a pretty good tool to explore what queries are available to use.
And if you want to change the template, you can change src/templates/posts
. It's a React component, so go nuts if you are already familiar with React.
All right, by now you should know what createPages
does.
3. Client Side Search
Before we are talking about pagination, let's talk about search first. I am using js-search to power the search page. The concept is quite simple, during the post
pages creation, we also want build the context for the search page. If you want to learn more, have a look at here.
In your gatsby-node.js
's createPages
, put the following code
const posts = result.data.allMarkdownRemark.edges.map(transformRemarkEdgeToPost)
createPage({
path: "/posts/",
component: path.resolve(`./src/templates/clientSearch.js`),
context: {
search: {
posts,
options: {
indexStrategy: "Prefix match",
searchSanitizer: "Lower Case",
TitleIndex: true,
AuthorIndex: true,
SearchByTerm: true,
},
},
},
})
where transformRemarkEdgeToPost
is just simple data transformation as follows
const transformRemarkEdgeToPost = edge => ({
path: edge.node.frontmatter.path,
author: edge.node.frontmatter.author,
date: edge.node.frontmatter.date,
title: edge.node.frontmatter.title,
excerpt: edge.node.excerpt,
timeToRead: edge.node.timeToRead,
})
The search here is a client side search. Meaning it doesn't talk to the server during the search as the javascript client already know the whole context
, which is passed into the pages via createPages
. This makes the search very responsive. Try it out!
Now you hopefully you know the concept of passing data into pages via context
. As for the templates, it's using a custom React class component, as it will need to use state. It's available in the repo at src/components/clientSearch
.
4. List Page with Pagination
Next up we are going to create a list page with pagination. The default Gatsby guide is good enough, but I went slightly further.
Put the following into gatsby-node.js
's createPages
function
const postsPerPage = config.noOfPostsPerPage
const noOfPages = Math.ceil(posts.length / postsPerPage)
Array.from({ length: noOfPages }).forEach((_, i) => {
createPage(
createListPageParameter(
`/list-${i + 1}`,
"./src/templates/list.js",
posts,
postsPerPage,
i
)
)
})
Basically, it goes through all of your posts
and create pages that contains a subset of your overall posts
. Meanwhile createListPageParameter
is yet another function that transform data
const createListPageParameter = (
routePath,
templatePath,
posts,
noOfPostsPerPage,
currentPageIndex
) => ({
path: routePath,
component: path.resolve(templatePath),
context: {
limit: noOfPostsPerPage,
skip: currentPageIndex * noOfPostsPerPage,
noOfPages: Math.ceil(posts.length / noOfPostsPerPage),
currentPage: currentPageIndex + 1,
},
})
Now since we want to have the index page / landing page to be the same with the list page. We need to create it the same way in gatsby-node.js
.
createPage(
createListPageParameter(
"/",
"./src/templates/list.js",
posts,
postsPerPage,
0
)
)
So far so good, now as you can see the context
passed contains things like limit
, skip
, noOfPages
, and currentPage
. These metadata are then used in the template to invoke yet another GraphQL query as seen in the src/templates/list.js
export const listQuery = graphql`
query listQuery($skip: Int!, $limit: Int!) {
allMarkdownRemark(
sort: { fields: [frontmatter___date], order: DESC }
limit: $limit
skip: $skip
) {
...MarkdownEdgesFragment
}
}
`
This result of the call is then available in the bespoke React component's props.data.allMarkdownRemark.edges
What do learn here? It's possible after you passed some metadata to the page through context
, e.g. skip
and limit
you can use them to make another GraphQL call. This is a powerful concept which allows you to add more data into the page.
But what is ...MarkdownEdgesFragment
? It's GraphQL fragment. But it behaves slightly differently in Gatsby.
5. Fragment
For the better or worse, Gatsby is using their own version of GraphQL. That's why on the file where a GraphQL query is executed, usually there's this import
import { graphql } from "gatsby"
Gatsby handles GraphQL fragments in slightly different way than standard GraphQL. Normally GraphQL fragments are imported, interpolated at the top of the GraphQL query and then used by spreading it. In Gatsby's GraphQL, the first and second steps are not needed as Gatsby crawls through all of your files and makes all fragments available in the query automagically.
Let's look back at src/templates/list.js
export const query = graphql`
query HomePageQuery {
allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
...MarkdownEdgesFragment
}
}
`
MarkdownEdgesFragment
is not explicitly imported/interpolated anywhere and yet it can be used in the GraphQL query. It's magic.
6. Styled Components
Gatsby by default uses CSS Modules. However I prefer to use Styled Components. There's a gotcha though. From my experience, sometimes in production the produced css is just missing even though everything is fine when run via gatsby develop
. This happens most often on the first page load.
How did I fixed it? Apparently I was missing a module. So make sure that these 3 are installed.
npm install --save gatsby-plugin-styled-components \
styled-components \
babel-plugin-styled-components
and make sure gatsby-config.js
has the following
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-styled-components`,
options: {
// Add any options here
},
},
],
}
7. Code Highlighting
To highlight code in the posts, I found PrismJs seems to be popular and easy enough to use. Based on this tutorial, you can either use gatsby-remark-prismjs or set it up manually like so:
Install the dependencies from the command line
npm install --save prismjs \
babel-plugin-prismjs \
Set .babelrc
in the root folder of your project. Make sure that the languages that you want to highlight are included in the config.
{
"presets": ["babel-preset-gatsby"],
"plugins": [
["prismjs", {
"languages": ["javascript", "css", "markup", "ruby"],
"plugins": ["show-language"],
"theme": "tomorrow",
"css": true
}]
]
}
Lastly, make sure that you invoke it on your pages/templates, i.e. src/templates/post.js
useEffect(() => {
Prism.highlightAll()
})
8. Google Analytics
A website without any tracking is not complete and we are implementing Google Analytics via Gatsby Plugin GTag.
It's reasonably simple to use. Add the following to gatsby-config.js
.
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-gtag`,
options: {
trackingId: "YOUR_GOOGLE_ANALYTICS_ID",
head: true,
anonymize: true,
respectDNT: true,
pageTransitionDelay: 0,
sampleRate: 5,
siteSpeedSampleRate: 10,
cookieDomain: "codeallnight.com",
},
},
],
}
There are several important things here.
- Google Tag Assistant prefer the tracking script to be put in
<head>
, thushead:true
- The plugin must be put as the first plugin in
plugins
array. I missed this at my first attempt.
Originally I tried to follow this default guide but it did not work, as I couldn't see any traffic on Google Tag Assistant. It simply says No HTTP response detected
. Once I switch to Gatsby Plugin GTag, I can see the tracking data on Google Analytics real time. I am not 100% certain why but it's probably related to analytics.js being deprecated
9. Epilogue
And there you have it, one pager guide for Gatsby. It's quite long, but it reflects my time spent in building my personal website at https://www.codeallnight.com. Maybe it's just that I'm not experienced enough, but there are quite a number of things to implement before I'm finally happy with my site.
If you have any feedback, feel free to hit me up on Twitter and as always thanks for reading.