Build a coding blog from scratch with Gatsby and MDX
I have been a Gatsby user since around v0 May 2017, at that time was using a template called Lumen and it was just what I needed at the time. Since then I have have gone from using a template to creating my blog.
Over the years I have made my own Progressive Disclosure of Complexity with Gatsby to where I am now.
What does that mean?
It means that although there are an awesome amount of Gatsby starters and themes out there to get you up and running in minutes, this post is going to focus on what you need to do to build your own blog. Starting with the most basic “Hello World!” to deploying your code to production.
What you’re going to build
You’re going to build a developer blog with MDX support (for some React components in Markdown goodness), so you will be able to add your own React components into your Markdown posts.
There’ll be:
- Adding a Layout
- Basic styling with styled-components
- Code blocks with syntax highlighting
- Copy code snippet to clipboard
- Cover images for the posts
- Configuring an SEO component
- Deploying it to Netlify
Who’s this how-to for?
People that may have used Gatsby before as a template and now want to get more involved in how to make changes.
If you want to have code syntax highlighting.
If you want to use styled-components in an app.
I really want to avoid this!
Requirements
You’re going to need a basic web development setup: node, terminal (bash, zsh or fish) and a text editor.
I do like to use codesandbox.io for these sort of guides to reduce the barrier to entry but in this case I have found there are some limitations with starting out from scratch on codesandbox.io which doesn’t make this possible.
I have made a guide on getting set up for web development with Windows Web-Dev Bootstrap and covered the same process in Ubuntu as well.
Ok? Time to get started!
Hello World
Kick this off with the Gatsby ‘hello world’, you’ll need to initialise the project with:
1npm init -y2git init
I suggest that you commit this code to a git repository, so you should
start with a .gitignore
file.
1touch .gitignore2
3echo "# Project dependencies4.cache5node_modules6
7# Build directory8public9
10# Other11.DS_Store12yarn-error.log" > .gitignore
Ok now is a good time to do a git init
and if you’re using VSCode
you’ll see the changes reflected in the sidebar.
basic hello world
Ok a Gatsby hello world, get started with the bare minimum! Install the following:
1yarn add gatsby react react-dom
You’re going to need to create a pages directory and add an index file. You can do that in the terminal by typing the following:
1# -p is to create parent directories too if needed2mkdir -p src/pages3touch src/pages/index.js
Ok, now you can commence the hello word incantation! In the newly
created index.js
enter the following:
1import React from 'react'2
3export default () => {4 return <h1>Hello World!</h1>5}
Now you need to add the Gatsby develop script to the package.json
file, -p
specifies what port you want to run the project on and -o
opens a new tab on your default browser, so in this case
localhost:9988
:
1"dev": "gatsby develop -p 9988 -o"
Ok it’s time to run the code! From the terminal type the npm script command you just created:
1yarn dev
Note I’m using Yarn for installing all my dependencies and running scripts, if you prefer you can use npm just bear in mind that the content on here uses yarn, so swap out commands where needed
And with that the “Hello World” incantation is complete 🧙!
Add content
Ok, now you have the base your blog you’re going to want to add some content, first up we’re going to get the convention out of the way. For this how-to, the date format will be a logical way, the most logical way for a date format is YYYYMMDD, fight me!
So you’re going to structure your posts content in years, in each one of those you’re going to have another folder relating to the post with the (correct) date format for the beginning of the file followed by the title of the post. You could drill into this further if you like by separating out months and days depending on the volume of posts going this may be a good approach. In this case and in the examples provided the convention detailed will be used.
1# create multiple directories using curly braces2mkdir -p posts/2019/{2019-06-01-hello-world,2019-06-10-second-post,2019-06-20-third-post}3touch posts/2019/2019-06-01-hello-world/index.mdx4touch posts/2019/2019-06-10-second-post/index.mdx5touch posts/2019/2019-06-20-third-post/index.mdx
Ok that’s your posts set up now you need to add some content to them,
each file you have in here should have frontmatter. Frontmatter is a
way to assign properties to the contents, in this case a title
,
published date
and a published
flag (true
or false
).
1---2title: Hello World - from mdx!3date: 2019-06-014published: true5---6
7# h1 Heading8
9My first post!!10
11## h2 Heading12
13### h3 Heading
1---2title: Second Post!3date: 2019-06-104published: true5---6
7This is my second post!8
9#### h4 Heading10
11##### h5 Heading12
13###### h6 Heading
1---2title: Third Post!3date: 2019-06-204published: true5---6
7This is my third post!8
9> with a block quote!
Gatsby config API
Ok, now you’re going to configure Gatsby so that it can read your
super awesome content you just created. So, first up you need to
create a gatsby-config.js
file, in the terminal create the file:
1touch gatsby-config.js
Plugins
And now you can add the plugins Gatsby needs to use for sourcing and displaying the the files you just created.
Gatsby source filesystem
The gatsby-source-filesystem collects the files on the local filesystem for use in Gatsby once configured.
Gatsby plugin MDX
The gatsby-plugin-mdx is what will be allowing us to write JSX in our Markdown documents and the heart of how the content is displayed in the blog.
Now is a good time to also add in dependent packages for the Gatsby
plugin MDX which are @mdx-js/mdx
and @mdx-js/react
.
In the terminal install the dependencies:
1yarn add gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react gatsby-source-filesystem
Now its time to configure gatsby-config.js
:
1module.exports = {2 siteMetadata: {3 title: `The Localhost Blog`,4 description: `This is my coding blog where I write about my coding journey.`,5 },6 plugins: [7 {8 resolve: `gatsby-plugin-mdx`,9 options: {10 extensions: [`.mdx`, `.md`],11 },12 },13 {14 resolve: `gatsby-source-filesystem`,15 options: {16 path: `${__dirname}/posts`,17 name: `posts`,18 },19 },20 ],21}
Query data from GraphQL
Ok now you can see what the gatsby-source-filesystem and gatsby-plugin-mdx have done for us. You can now go to the Gatsby GraphQL GraphiQL explorer and check out the data:
1{2 allMdx {3 nodes {4 frontmatter {5 title6 date7 }8 }9 }10}
Site Metadata
When you want to reuse common pieces of data across the site (for
example, your site title), you can store that data in siteMetadata
,
you touched on this when defining the gatsby-config.js
, now you’re
going to separate this out from the module.exports
, why? It will be
nicer to reason about once the config is filled with plugins. At the
top of gatsby-config.js
add a new object variable for the site
metadata:
1const siteMetadata = {2 title: `The Localhost Blog`,3 description: `This is my coding blog where I write about my coding journey.`,4}
Now query the Site Metadata with GraphQL.
1{2 site {3 siteMetadata {4 title5 description6 }7 }8}
Site metadata hook
Ok, so, that’s cool n’ all but how am I meant to use it? Well do some of the code stuff and make a React hook so you can get your site data in any component you need it.
Create a folder to keep all your hooks in and create a file for our hook, in the terminal do:
1mkdir src/hooks2touch src/hooks/useSiteMetadata.js
Ok, and in your newly created file were going to use the Gatsby
useStaticQuery
hook to make your own hook:
1import { graphql, useStaticQuery } from 'gatsby'2
3export const useSiteMetadata = () => {4 const { site } = useStaticQuery(5 graphql`6 query SITE_METADATA_QUERY {7 site {8 siteMetadata {9 title10 description11 }12 }13 }14 `15 )16 return site.siteMetadata17}
Now you can use this hook anywhere in your site, so do that now in
src/pages/index.js
:
1import React from 'react'2import { useSiteMetadata } from '../hooks/useSiteMetadata'3
4export default () => {5 const { title, description } = useSiteMetadata()6 return (7 <>8 <h1>{title}</h1>9 <p>{description}</p>10 </>11 )12}
Styling
You’re going to use styled-components for styling, styled-components (for me) help with scoping styles in your components. Time to go over the basics now.
install styled-components
1yarn add gatsby-plugin-styled-components styled-components babel-plugin-styled-components
So, what was all that I just installed?
The babel plugin is for automatic naming of components to help with debugging.
The Gatsby plugin is for built-in server-side rendering support.
Configure
Ok, with that detailed explanation out of the way, configure them in
gatsby-config.js
:
1const siteMetadata = {2 title: `The Localhost Blog`,3 description: `This is my coding blog where I write about my coding journey.`,4}5
6module.exports = {7 siteMetadata: siteMetadata,8 plugins: [9 `gatsby-plugin-styled-components`,10 {11 resolve: `gatsby-plugin-mdx`,12 options: {13 extensions: [`.mdx`, `.md`],14 },15 },16 {17 resolve: `gatsby-source-filesystem`,18 options: {19 path: `${__dirname}/posts`,20 name: `posts`,21 },22 },23 ],24}
Ok, time to go over a styled component, in index.js
you’re going to
import styled from 'styled-components'
and create a StyledH1
variable.
So, you’re using the variable to wrap your {title}
that you’re
destructuring from the useSiteMetadata
hook you made previously.
For this example make it the now iconic Gatsby rebeccapurple
.
1import React from 'react'2import styled from 'styled-components'3import { useSiteMetadata } from '../hooks/useSiteMetadata'4
5const StyledH1 = styled.h1`6 color: rebeccapurple;7`8
9export default () => {10 const { title, description } = useSiteMetadata()11 return (12 <>13 <StyledH1>{title}</StyledH1>14 <p>{description}</p>15 </>16 )17}
That is styled-components on a very basic level, basically create the styling you want for your page elements you’re creating in the JSX.
Layout
Gatsby doesn’t apply any layouts by default but instead uses the way you can compose React components for the layout, meaning it’s up to you how you want to layout what your building with Gatsby. In this guide we’re going to initially create a basic layout component that you’ll add to as you go along. For more detail on layout components take a look at the Gatsby layout components page.
Ok, so now you’re going to refactor the home page
(src/pages/index.js
) a little and make some components for your blog
layout and header. In the terminal create a components directory and a
Header
and Layout
component:
1mkdir src/components2touch src/components/Header.js src/components/Layout.js
Now to move the title and description from src/pages/index.js
to the
newly created src/components/Header.js
component, destructuring
props for the siteTitle
and siteDescription
, you’ll pass these
from the Layout
component to here. You’re going to add Gatsby Link
to this so users can click on the header to go back to the home page.
1import { Link } from 'gatsby'2import React from 'react'3
4export const Header = ({ siteTitle, siteDescription }) => (5 <Link to="/">6 <h1>{siteTitle}</h1>7 <p>{siteDescription}</p>8 </Link>9)
Now to the Layout component, this is going to be a basic wrapper
component for now, you’re going to use your site metadata hook for the
title and description and pass them to the header component and return
the children of the wrapper (Layout
).
1import React from 'react'2import { useSiteMetadata } from '../hooks/useSiteMetadata'3import { Header } from './Header'4
5export const Layout = ({ children }) => {6 const { title, description } = useSiteMetadata()7 return (8 <>9 <Header siteTitle={title} siteDescription={description} />10 {children}11 </>12 )13}
Now to add the slightest of styles for some alignment for
src/components/Layout.js
, create an AppStyles
styled component and
make it the main wrapper of your Layout
.
1import React from 'react'2import styled from 'styled-components'3import { useSiteMetadata } from '../hooks/useSiteMetadata'4import { Header } from './Header'5
6const AppStyles = styled.main`7 width: 800px;8 margin: 0 auto;9`10
11export const Layout = ({ children }) => {12 const { title, description } = useSiteMetadata()13 return (14 <AppStyles>15 <Header siteTitle={title} siteDescription={description} />16 {children}17 </AppStyles>18 )19}
Ok, now refactor your homepage (src/pages/index.js
) with Layout
.
1import React from 'react'2import { Layout } from '../components/Layout'3
4export default () => {5 return (6 <>7 <Layout />8 </>9 )10}
Index page posts query
Ok, now you can take a look at getting some of the posts you’ve created add them to the index page of your blog. You’re going to do that by creating a graphql query to list out the posts by title, order by date and add an excerpt of the post.
The query will look something like this:
1{2 allMdx {3 nodes {4 id5 excerpt(pruneLength: 250)6 frontmatter {7 title8 date9 }10 }11 }12}
If you put that into the GraphiQL GUI though you’ll notice that the posts aren’t in any given order, so now add a sort to this you’ll also add in a filter for posts that are marked as published or not.
1{2 allMdx(3 sort: { fields: [frontmatter___date], order: DESC }4 filter: { frontmatter: { published: { eq: true } } }5 ) {6 nodes {7 id8 excerpt(pruneLength: 250)9 frontmatter {10 title11 date12 }13 }14 }15}
On the homepage (src/pages/index.js
) you’re going to use the query
we just put together to get a list of published posts in date order;
add the following to the index.js
file:
1import { graphql } from 'gatsby'2import React from 'react'3import { Layout } from '../components/Layout'4
5export default ({ data }) => {6 return (7 <>8 <Layout>9 {data.allMdx.nodes.map(({ excerpt, frontmatter }) => (10 <>11 <h1>{frontmatter.title}</h1>12 <p>{frontmatter.date}</p>13 <p>{excerpt}</p>14 </>15 ))}16 </Layout>17 </>18 )19}20
21export const query = graphql`22 query SITE_INDEX_QUERY {23 allMdx(24 sort: { fields: [frontmatter___date], order: DESC }25 filter: { frontmatter: { published: { eq: true } } }26 ) {27 nodes {28 id29 excerpt(pruneLength: 250)30 frontmatter {31 title32 date33 }34 }35 }36 }37`
Woah! WTF was all that yo!?
Ok, you’re looping through the data passed into the component via the
GraphQL query. Gatsby graphql
runs the query (SITE_INDEX_QUERY
) at
runtime and gives us the results as props to your component via the
data
prop.
Slugs and Paths
Gatsby source filesystem will help with the creation of slugs (URL paths for the posts you’re creating) in Gatsby node you’re going to create the slugs for your posts.
First up you’re going to need to create a gatsby-node.js
file:
1touch gatsby-node.js
This will create the file path (URL) for each of the blog posts.
You’re going to be using the Gatsby Node API onCreateNode
and
destructuring out node
, actions
and getNode
for use in creating
the file locations and associated value.
1const { createFilePath } = require(`gatsby-source-filesystem`)2
3exports.onCreateNode = ({ node, actions, getNode }) => {4 const { createNodeField } = actions5 if (node.internal.type === `Mdx`) {6 const value = createFilePath({ node, getNode })7 createNodeField({8 name: `slug`,9 node,10 value,11 })12 }13}
Now to help visualise some of the data being passed into the components you’re going to use Dump.js for debugging the data. Thanks to Wes Bos for the super handy Dump.js component.
To get the component set up, create a Dump.js
file in your
src\components
folder and copypasta the code from the linked GitHub
page.
1touch src/components/Dump.js
1import React from 'react'2
3const Dump = props => (4 <div5 style={{6 fontSize: 20,7 border: '1px solid #efefef',8 padding: 10,9 background: 'white',10 }}11 >12 {Object.entries(props).map(([key, val]) => (13 <pre key={key}>14 <strong style={{ color: 'white', background: 'red' }}>15 {key} 💩16 </strong>17 {JSON.stringify(val, '', ' ')}18 </pre>19 ))}20 </div>21)22
23export default Dump
Now you can use the Dump
component anywhere in your project. To
demonstrate, use it with the index page data
to see the output.
So in the src/pages/index.js
you’re going to import the Dump
component and pass in the data
prop and see what the output looks
like.
1import { graphql } from 'gatsby'2import React from 'react'3import Dump from '../components/Dump'4import { Layout } from '../components/Layout'5
6export default ({ data }) => {7 return (8 <>9 <Layout>10 <Dump data={data} />11 {data.allMdx.nodes.map(({ excerpt, frontmatter }) => (12 <>13 <h1>{frontmatter.title}</h1>14 <p>{frontmatter.date}</p>15 <p>{excerpt}</p>16 </>17 ))}18 </Layout>19 </>20 )21}22
23export const query = graphql`24 query SITE_INDEX_QUERY {25 allMdx(26 sort: { fields: [frontmatter___date], order: DESC }27 filter: { frontmatter: { published: { eq: true } } }28 ) {29 nodes {30 id31 excerpt(pruneLength: 250)32 frontmatter {33 title34 date35 }36 }37 }38 }39`
Link Paths
Now you’ve created the paths you can link to them with Gatsby Link.
First you’ll need to add the slug to your SITE_INDEX_QUERY
Then you
can add gatsby Link
to src/pages/index.js
.
You’re also going to create some styled-components for wrapping the list of posts and each individual post as well.
1import { graphql, Link } from 'gatsby'2import React from 'react'3import styled from 'styled-components'4import { Layout } from '../components/Layout'5
6const IndexWrapper = styled.main``7
8const PostWrapper = styled.div``9
10export default ({ data }) => {11 return (12 <Layout>13 <IndexWrapper>14 {data.allMdx.nodes.map(15 ({ id, excerpt, frontmatter, fields }) => (16 <PostWrapper key={id}>17 <Link to={fields.slug}>18 <h1>{frontmatter.title}</h1>19 <p>{frontmatter.date}</p>20 <p>{excerpt}</p>21 </Link>22 </PostWrapper>23 )24 )}25 </IndexWrapper>26 </Layout>27 )28}29
30export const query = graphql`31 query SITE_INDEX_QUERY {32 allMdx(33 sort: { fields: [frontmatter___date], order: DESC }34 filter: { frontmatter: { published: { eq: true } } }35 ) {36 nodes {37 id38 excerpt(pruneLength: 250)39 frontmatter {40 title41 date42 }43 fields {44 slug45 }46 }47 }48 }49`
Adding a Blog Post Template
Now you have the links pointing to the blog posts you currently have
no file associated with the path, so clicking a link will give you a
404 and the built in gatsby 404 will list all the pages available in
the project, currently only the /
index/homepage.
So, for each one of your blog posts you’re going to use a template
that will contain, the information you need to make up your blog post.
To start, create a templates
directory and template file for that
with:
1mkdir -p src/templates2touch src/templates/blogPostTemplate.js
For now you’re going to scaffold out a basic template, you’ll be adding data to this shortly:
1import React from 'react'2
3export default () => {4 return (5 <>6 <p>post here</p>7 </>8 )9}
To populate the template you’ll need to use Gatsby node to create your pages.
Gatsby Node has many internal APIs available to us, for this example
you’re going to be using the createPages
API.
More details on Gatsby createPages
API can be found on the Gatsby
docs, details here:
https://www.gatsbyjs.org/docs/node-apis/#createPages
In your gatsby-node.js
file you’re going to add in the following in
addition to the onCreateNode
export you did earlier.
1const { createFilePath } = require(`gatsby-source-filesystem`)2const path = require(`path`)3
4exports.createPages = ({ actions, graphql }) => {5 const { createPage } = actions6 const blogPostTemplate = path.resolve(7 'src/templates/blogPostTemplate.js'8 )9
10 return graphql(`11 {12 allMdx {13 nodes {14 fields {15 slug16 }17 frontmatter {18 title19 }20 }21 }22 }23 `).then(result => {24 if (result.errors) {25 throw result.errors26 }27
28 const posts = result.data.allMdx.nodes29
30 // create page for each mdx file31 posts.forEach(post => {32 createPage({33 path: post.fields.slug,34 component: blogPostTemplate,35 context: {36 slug: post.fields.slug,37 },38 })39 })40 })41}42
43exports.onCreateNode = ({ node, actions, getNode }) => {44 const { createNodeField } = actions45 if (node.internal.type === `Mdx`) {46 const value = createFilePath({ node, getNode })47 createNodeField({48 name: `slug`,49 node,50 value,51 })52 }53}
So the part that you need to pay particular attention to right now is
the .forEach
loop where you’re using the createPage
function we
destructured from the actions
object.
This is where you pass the data needed by blogPostTemplate
you
defined earlier. You’re going to be adding more to the context
for
post navigation soon.
1// create page for each mdx node2posts.forEach(post => {3 createPage({4 path: post.fields.slug,5 component: blogPostTemplate,6 context: {7 slug: post.fields.slug,8 },9 })10})
Build out Blog Post Template
Now you’re going to take the context information passed to the
blogPostTemplate.js
to make the blog post page.
This is similar to the index.js
homepage whereas there’s GraphQL
data used to create the page but in this instance the template uses a
variable (also known as a parameter or an identifier) so you can query
data specific to that given variable.
Now quickly dig into that with a demo. In the GraphiQL GUI, create a named query and define the variable you’re going to pass in:
1query PostBySlug($slug: String!) {2 mdx(fields: { slug: { eq: $slug } }) {3 frontmatter {4 title5 date(formatString: "YYYY MMMM Do")6 }7 }8}
Here you’re defining the variable as slug with the $
denoting that
it’s a variable, you also need to define the variable type as (in this
case) String!
the exclamation after the type means that it has to be
a string being passed into the query.
Using mdx
you’re going to filter on fields
where the slug
matches the variable being passed into the query.
Running the query now will show an error as there’s no variable being
fed into the query. If you look to the bottom of the query pane you
should notice QUERY VARIABLES
, click on that to bring up the
variables pane.
This is where you can add in one of the post paths you created
earlier, if you have your dev server up and running go to one of the
posts and take the path and paste it into the quotes ""
and try
running the query again.
1{2 "slug": "/2019/2019-06-20-third-post/"3}
Time to use that data to make the post, you’re going to add body
to
the query and have that at the bottom of your page file.
Right now you’re going to add create a simple react component that will display the data you have queried.
Destructuring the frontmatter
and body
from the GraphQL query,
you’ll get the Title and the Data from the frontmatter object and wrap
the body
in the MDXRenderer
.
1import { graphql } from 'gatsby'2import { MDXRenderer } from 'gatsby-plugin-mdx'3import React from 'react'4import { Layout } from '../components/Layout'5
6export default ({ data }) => {7 const { frontmatter, body } = data.mdx8 return (9 <Layout>10 <h1>{frontmatter.title}</h1>11 <p>{frontmatter.date}</p>12 <MDXRenderer>{body}</MDXRenderer>13 </Layout>14 )15}16
17export const query = graphql`18 query PostsBySlug($slug: String!) {19 mdx(fields: { slug: { eq: $slug } }) {20 body21 frontmatter {22 title23 date(formatString: "YYYY MMMM Do")24 }25 }26 }27`
If you haven’t done so already now would be a good time to restart your dev server.
Now you can click on one of the post links and see your blog post template in all it’s basic glory!
Previous and Next
Coolio! Now you have your basic ass blog where you can list available post and click a link to see the full post in a predefined template. Once you’re in a post you have to navigate back to the home page to pick out a new post to read. In this section you’re going to work on adding in some previous and next navigation.
Remember the .forEach
snippet you looked at earlier? That’s where
you’re going to pass some additional context to the page by selecting
out the previous and next posts.
1// create page for each mdx node2posts.forEach((post, index) => {3 const previous =4 index === posts.length - 1 ? null : posts[index + 1]5 const next = index === 0 ? null : posts[index - 1]6
7 createPage({8 path: post.fields.slug,9 component: blogPostTemplate,10 context: {11 slug: post.fields.slug,12 previous,13 next,14 },15 })16})
So this should now match up with the query you have on the homepage
(src/pages/index.js
) except you currently have no filter or sort
applied here so do that now in gatsby-node.js
and apply the same
filters as on the homepage query:
1const { createFilePath } = require(`gatsby-source-filesystem`)2const path = require(`path`)3
4exports.createPages = ({ actions, graphql }) => {5 const { createPage } = actions6 const blogPostTemplate = path.resolve(7 'src/templates/blogPostTemplate.js'8 )9
10 return graphql(`11 {12 allMdx(13 sort: { fields: [frontmatter___date], order: DESC }14 filter: { frontmatter: { published: { eq: true } } }15 ) {16 nodes {17 fields {18 slug19 }20 frontmatter {21 title22 }23 }24 }25 }26 `).then(result => {27 if (result.errors) {28 throw result.errors29 }30
31 const posts = result.data.allMdx.nodes32
33 // create page for each mdx node34 posts.forEach((post, index) => {35 const previous =36 index === posts.length - 1 ? null : posts[index + 1]37 const next = index === 0 ? null : posts[index - 1]38
39 createPage({40 path: post.fields.slug,41 component: blogPostTemplate,42 context: {43 slug: post.fields.slug,44 previous,45 next,46 },47 })48 })49 })50}51
52exports.onCreateNode = ({ node, actions, getNode }) => {53 const { createNodeField } = actions54 if (node.internal.type === `Mdx`) {55 const value = createFilePath({ node, getNode })56 createNodeField({57 name: `slug`,58 node,59 value,60 })61 }62}
Now you will be able to expose the previous
and next
objects
passed in as context from Gatsby node.
You can destructure previous
and next
from pageContext
and for
now pop them into your super handy Dump
component to take a look at
their contents.
1import { graphql } from 'gatsby'2import { MDXRenderer } from 'gatsby-plugin-mdx'3import React from 'react'4import Dump from '../components/Dump'5import { Layout } from '../components/Layout'6
7export default ({ data, pageContext }) => {8 const { frontmatter, body } = data.mdx9 const { previous, next } = pageContext10 return (11 <Layout>12 <Dump previous={previous} />13 <Dump next={next} />14 <h1>{frontmatter.title}</h1>15 <p>{frontmatter.date}</p>16 <MDXRenderer>{body}</MDXRenderer>17 </Layout>18 )19}20
21export const query = graphql`22 query PostsBySlug($slug: String!) {23 mdx(fields: { slug: { eq: $slug } }) {24 body25 frontmatter {26 title27 date(formatString: "YYYY MMMM Do")28 }29 }30 }31`
Add in previous and next navigation, this is a couple of ternary
operations, if the variable is empty then return null
else render a
Gatsby Link
component with the page slug and the frontmatter title:
1import { graphql, Link } from 'gatsby'2import { MDXRenderer } from 'gatsby-plugin-mdx'3import React from 'react'4import Dump from '../components/Dump'5import { Layout } from '../components/Layout'6
7export default ({ data, pageContext }) => {8 const { frontmatter, body } = data.mdx9 const { previous, next } = pageContext10 return (11 <Layout>12 <Dump previous={previous} />13 <Dump next={next} />14 <h1>{frontmatter.title}</h1>15 <p>{frontmatter.date}</p>16 <MDXRenderer>{body}</MDXRenderer>17 {previous === false ? null : (18 <>19 {previous && (20 <Link to={previous.fields.slug}>21 <p>{previous.frontmatter.title}</p>22 </Link>23 )}24 </>25 )}26 {next === false ? null : (27 <>28 {next && (29 <Link to={next.fields.slug}>30 <p>{next.frontmatter.title}</p>31 </Link>32 )}33 </>34 )}35 </Layout>36 )37}38
39export const query = graphql`40 query PostsBySlug($slug: String!) {41 mdx(fields: { slug: { eq: $slug } }) {42 body43 frontmatter {44 title45 date(formatString: "YYYY MMMM Do")46 }47 }48 }49`
Code Blocks
Now to add some syntax highlighting for adding code blocks to your blog pages. To do that you’re going to add dependencies for prism-react-renderer and react-live and you’ll also create the files you’re going to need to use them:
1yarn add prism-react-renderer react-live2touch root-wrapper.js gatsby-ssr.js gatsby-browser.js
You’ll come onto react-live
soon for now you’re going to get
prism-react-render
up and running for syntax highlighting for any
code you’re going to add to the blog, but before that you’re going to
go over the root wrapper concept.
So, to change the rendering of a page element, such as a heading or a
code block you’re going to need to use the MDXProvider
, the
MDXProvider
is a component you can use anywhere higher in the React
component tree than the MDX content you want to render.
Gatsby browser and a Gatsby SSR both have wrapRootElement
available
to them and that is as high up the tree as you can get so you’re going
to create the root-wrapper.js
file and add out elements you want to
override there and import it into both gatsby-browser.js
and
gatsby-ssr.js
so you’re not duplicating code.
Before you go any further I want to add that there is a top quality egghead.io playlist resource for using MDX with Gatsby by Chris Chris Biscardi there’s a ton of useful information in there on MDX in Gatsby.
Ok, first up you’re going to import the root-wrapper.js
file into
both gatsby-browser.js
and gatsby-ssr.js
, in both code modules
paste the following:
1import { wrapRootElement as wrap } from './root-wrapper'2
3export const wrapRootElement = wrap
Ok, now you can work on the code that will be used in both modules.
MDX allows you to control the rendering of page elements in your
markdown. MDXProvider
is used to give to give React components to
override the markdown page elements.
Quick demonstration, in root-wrapper.js
add the following:
1import { MDXProvider } from '@mdx-js/react'2import React from 'react'3
4const components = {5 h2: ({ children }) => (6 <h2 style={{ color: 'rebeccapurple' }}>{children}</h2>7 ),8 'p.inlineCode': props => (9 <code style={{ backgroundColor: 'lightgray' }} {...props} />10 ),11}12
13export const wrapRootElement = ({ element }) => (14 <MDXProvider components={components}>{element}</MDXProvider>15)
You’re now overriding any h2
in your rendered markdown along with
any code
blocks (that’s words wrapped in `backticks`
).
Ok, now for the syntax highlighting, create a post with a block of code in it:
1mkdir posts/2019-07-01-code-blocks2touch posts/2019-07-01-code-blocks/index.mdx
Paste in some content:
1---2title: Code Blocks3date: 2019-07-014published: true5---6
7## Yes! Some code!8
9Here is the `Dump` component!10
11```jsx12import React from 'react'13
14const Dump = props => (15 <div16 style={{17 fontSize: 20,18 border: '1px solid #efefef',19 padding: 10,20 background: 'white',21 }}22 >23 {Object.entries(props).map(([key, val]) => (24 <pre key={key}>25 <strong style={{ color: 'white', background: 'red' }}>26 {key} 💩27 </strong>28 {JSON.stringify(val, '', ' ')}29 </pre>30 ))}31 </div>32)33
34export default Dump35```
Ok, if you go to the prism-react-renderer GitHub page and copy the
example code into root-wrapper.js
for the pre
element.
You’re going to copy the provided code for highlighting to validate it works.
1import { MDXProvider } from '@mdx-js/react'2import Highlight, { defaultProps } from 'prism-react-renderer'3import React from 'react'4
5const components = {6 h2: ({ children }) => (7 <h2 style={{ color: 'rebeccapurple' }}>{children}</h2>8 ),9 'p.inlineCode': props => (10 <code style={{ backgroundColor: 'lightgray' }} {...props} />11 ),12 pre: props => (13 <Highlight14 {...defaultProps}15 code={`16 (function someDemo() {17 var test = "Hello World!";18 console.log(test);19 })();20
21 return () => <App />;22 `}23 language="jsx"24 >25 {({26 className,27 style,28 tokens,29 getLineProps,30 getTokenProps,31 }) => (32 <pre className={className} style={style}>33 {tokens.map((line, i) => (34 <div {...getLineProps({ line, key: i })}>35 {line.map((token, key) => (36 <span {...getTokenProps({ token, key })} />37 ))}38 </div>39 ))}40 </pre>41 )}42 </Highlight>43 ),44}45
46export const wrapRootElement = ({ element }) => (47 <MDXProvider components={components}>{element}</MDXProvider>48)
Cool, cool! Now you want to replace the pasted in code example with
the props of the child component of the pre component, you can do that
with props.children.props.children.trim()
🙃.
1import { MDXProvider } from '@mdx-js/react'2import Highlight, { defaultProps } from 'prism-react-renderer'3import React from 'react'4
5const components = {6 pre: props => (7 <Highlight8 {...defaultProps}9 code={props.children.props.children.trim()}10 language="jsx"11 >12 {({13 className,14 style,15 tokens,16 getLineProps,17 getTokenProps,18 }) => (19 <pre className={className} style={style}>20 {tokens.map((line, i) => (21 <div {...getLineProps({ line, key: i })}>22 {line.map((token, key) => (23 <span {...getTokenProps({ token, key })} />24 ))}25 </div>26 ))}27 </pre>28 )}29 </Highlight>30 ),31}32
33export const wrapRootElement = ({ element }) => (34 <MDXProvider components={components}>{element}</MDXProvider>35)
Then to match the language, for now you’re going to add in a matches
function to match the language class assigned to the code block.
1import { MDXProvider } from '@mdx-js/react'2import Highlight, { defaultProps } from 'prism-react-renderer'3import React from 'react'4
5const components = {6 h2: ({ children }) => (7 <h2 style={{ color: 'rebeccapurple' }}>{children}</h2>8 ),9 'p.inlineCode': props => (10 <code style={{ backgroundColor: 'lightgray' }} {...props} />11 ),12 pre: props => {13 const className = props.children.props.className || ''14 const matches = className.match(/language-(?<lang>.*)/)15 return (16 <Highlight17 {...defaultProps}18 code={props.children.props.children.trim()}19 language={20 matches && matches.groups && matches.groups.lang21 ? matches.groups.lang22 : ''23 }24 >25 {({26 className,27 style,28 tokens,29 getLineProps,30 getTokenProps,31 }) => (32 <pre className={className} style={style}>33 {tokens.map((line, i) => (34 <div {...getLineProps({ line, key: i })}>35 {line.map((token, key) => (36 <span {...getTokenProps({ token, key })} />37 ))}38 </div>39 ))}40 </pre>41 )}42 </Highlight>43 )44 },45}46
47export const wrapRootElement = ({ element }) => (48 <MDXProvider components={components}>{element}</MDXProvider>49)
prism-react-renderer comes with additional themes over the default theme which is duotoneDark you’re going to use nightOwl in this example, feel free to take a look at the other examples if you like.
Import the theme
then use it in the props of the Highlight
component.
1import { MDXProvider } from '@mdx-js/react'2import Highlight, { defaultProps } from 'prism-react-renderer'3import theme from 'prism-react-renderer/themes/nightOwl'4import React from 'react'5
6const components = {7 pre: props => {8 const className = props.children.props.className || ''9 const matches = className.match(/language-(?<lang>.*)/)10
11 return (12 <Highlight13 {...defaultProps}14 code={props.children.props.children.trim()}15 language={16 matches && matches.groups && matches.groups.lang17 ? matches.groups.lang18 : ''19 }20 theme={theme}21 >22 {({23 className,24 style,25 tokens,26 getLineProps,27 getTokenProps,28 }) => (29 <pre className={className} style={style}>30 {tokens.map((line, i) => (31 <div {...getLineProps({ line, key: i })}>32 {line.map((token, key) => (33 <span {...getTokenProps({ token, key })} />34 ))}35 </div>36 ))}37 </pre>38 )}39 </Highlight>40 )41 },42}43
44export const wrapRootElement = ({ element }) => (45 <MDXProvider components={components}>{element}</MDXProvider>46)
Ok, now time to abstract this out into it’s own component so your
root-wrapper.js
isn’t so crowded.
Make a Code.js
component, move the code from root-wrapper.js
into
there
1touch src/components/Code.js
Remember this?
Cool, cool! Now you want to replace the pasted in code example with the props of the child component of the pre component, you can do that with
props.children.props.children.trim()
🙃.
If that ☝ makes no real amount of sense for you (I’ve had to read it many, many times myself), don’t worry, now you’re going to dig into that a bit more for the creation of the code block component.
So, for now in the components
you’re adding into the MDXProvider
,
take a look at the props
coming into the pre
element.
Comment out the code you added earlier and add in a console.log
:
1pre: props => {2 console.log('=====================')3 console.log(props)4 console.log('=====================')5 return <pre />6}
Now if you pop open the developer tools of your browser you can see the output.
1{children: {…}}2 children:3 $$typeof: Symbol(react.element)4 key: null5 props: {parentName: "pre", className: "language-jsx", originalType: "code", mdxType: "code", children: "import React from 'react'↵↵const Dump = props => (… </pre>↵ ))}↵ </div>↵)↵↵export default Dump↵"}6 ref: null7 type: ƒ (re....
If you drill into the props of that output you can see the children
of those props, if you take a look at the contents of that you will
see that it is the code string for your code block, this is what
you’re going to be passing into the Code
component you’re about to
create. Other properties to note here are the className
and
mdxType
.
So, take the code you used earlier for Highlight
, everything inside
and including the return
statement and paste it into the Code.js
module you created earlier.
Highlight
requires several props:
1<Highlight2 {...defaultProps}3 code={codeString}4 language={language}5 theme={theme}6>
The Code
module should look something like this now:
1import Highlight, { defaultProps } from 'prism-react-renderer'2import theme from 'prism-react-renderer/themes/nightOwl'3import React from 'react'4
5const Code = ({ codeString, language }) => {6 return (7 <Highlight8 {...defaultProps}9 code={codeString}10 language={language}11 theme={theme}12 >13 {({14 className,15 style,16 tokens,17 getLineProps,18 getTokenProps,19 }) => (20 <pre className={className} style={style}>21 {tokens.map((line, i) => (22 <div {...getLineProps({ line, key: i })}>23 {line.map((token, key) => (24 <span {...getTokenProps({ token, key })} />25 ))}26 </div>27 ))}28 </pre>29 )}30 </Highlight>31 )32}33
34export default Code
Back to the root-wrapper
where you’re going to pass the props
needed to the Code
component.
The first check you’re going to do is if the mdxType
is code
then
you can get the additional props you need to pass to your Code
component.
You’re going to get defaultProps
and the theme
from
prism-react-renderer
so all that’s needed is the code
and
language
.
The codeString
you can get from the props
, children
by
destructuring from the props
being passed into the pre
element.
The language
can either be the tag assigned to the meta property of
the backticks, like js
, jsx
or equally empty, so you check for
that with some JavaScript and also remove the language-
prefix, then
pass in the elements {...props}
:
1pre: ({ children: { props } }) => {2 if (props.mdxType === 'code') {3 return (4 <Code5 codeString={props.children.trim()}6 language={7 props.className && props.className.replace('language-', '')8 }9 {...props}10 />11 )12 }13}
Ok, now you’re back to where you were before abstracting out the
Highlight
component to it’s own module. Add some additional styles
with styled-components
and replace the pre
with a styled Pre
and
you can also add in some line numbers with a styled span and style
that as well.
1import Highlight, { defaultProps } from 'prism-react-renderer'2import theme from 'prism-react-renderer/themes/nightOwl'3import React from 'react'4import styled from 'styled-components'5
6export const Pre = styled.pre`7 text-align: left;8 margin: 1em 0;9 padding: 0.5em;10 overflow-x: auto;11 border-radius: 3px;12
13 & .token-line {14 line-height: 1.3em;15 height: 1.3em;16 }17 font-family: 'Courier New', Courier, monospace;18`19
20export const LineNo = styled.span`21 display: inline-block;22 width: 2em;23 user-select: none;24 opacity: 0.3;25`26
27const Code = ({ codeString, language, ...props }) => {28 return (29 <Highlight30 {...defaultProps}31 code={codeString}32 language={language}33 theme={theme}34 >35 {({36 className,37 style,38 tokens,39 getLineProps,40 getTokenProps,41 }) => (42 <Pre className={className} style={style}>43 {tokens.map((line, i) => (44 <div {...getLineProps({ line, key: i })}>45 <LineNo>{i + 1}</LineNo>46 {line.map((token, key) => (47 <span {...getTokenProps({ token, key })} />48 ))}49 </div>50 ))}51 </Pre>52 )}53 </Highlight>54 )55}56
57export default Code
Copy code to clipboard
What if you had some way of getting that props code string into the clipboard?
I had a look around and found the majority of the components available for this sort of thing expected an input until this in the Gatsby source code. Which is creating the input for you.
So, create a utils
directory and the copy-to-clipboard.js
file and
add in the code from the Gatsby sourcue code.
1mkdir src/utils2touch src/utils/copy-to-clipboard.js
1// https://github.com/gatsbyjs/gatsby/blob/master/www/src/utils/copy-to-clipboard.js2
3export const copyToClipboard = str => {4 const clipboard = window.navigator.clipboard5 /*6 * fallback to older browsers (including Safari)7 * if clipboard API not supported8 */9 if (!clipboard || typeof clipboard.writeText !== `function`) {10 const textarea = document.createElement(`textarea`)11 textarea.value = str12 textarea.setAttribute(`readonly`, true)13 textarea.setAttribute(`contenteditable`, true)14 textarea.style.position = `absolute`15 textarea.style.left = `-9999px`16 document.body.appendChild(textarea)17 textarea.select()18 const range = document.createRange()19 const sel = window.getSelection()20 sel.removeAllRanges()21 sel.addRange(range)22 textarea.setSelectionRange(0, textarea.value.length)23 document.execCommand(`copy`)24 document.body.removeChild(textarea)25
26 return Promise.resolve(true)27 }28
29 return clipboard.writeText(str)30}
Now you’re going to want a way to trigger copying the code to the clipboard.
Lets create a styled button but first add a position: relative;
to
the Pre
component which will let us position the styled button:
1const CopyCode = styled.button`2 position: absolute;3 right: 0.25rem;4 border: 0;5 border-radius: 3px;6 margin: 0.25em;7 opacity: 0.3;8 &:hover {9 opacity: 1;10 }11`
And now you need to use the copyToClipboard
function in the
onClick
of the button:
1import Highlight, { defaultProps } from 'prism-react-renderer'2import theme from 'prism-react-renderer/themes/nightOwl'3import React from 'react'4import styled from 'styled-components'5import { copyToClipboard } from '../utils/copy-to-clipboard'6
7export const Pre = styled.pre`8 text-align: left;9 margin: 1rem 0;10 padding: 0.5rem;11 overflow-x: auto;12 border-radius: 3px;13
14 & .token-line {15 line-height: 1.3rem;16 height: 1.3rem;17 }18 font-family: 'Courier New', Courier, monospace;19 position: relative;20`21
22export const LineNo = styled.span`23 display: inline-block;24 width: 2rem;25 user-select: none;26 opacity: 0.3;27`28
29const CopyCode = styled.button`30 position: absolute;31 right: 0.25rem;32 border: 0;33 border-radius: 3px;34 margin: 0.25em;35 opacity: 0.3;36 &:hover {37 opacity: 1;38 }39`40
41const Code = ({ codeString, language }) => {42 const handleClick = () => {43 copyToClipboard(codeString)44 }45
46 return (47 <Highlight48 {...defaultProps}49 code={codeString}50 language={language}51 theme={theme}52 >53 {({54 className,55 style,56 tokens,57 getLineProps,58 getTokenProps,59 }) => (60 <Pre className={className} style={style}>61 <CopyCode onClick={handleClick}>Copy</CopyCode>62 {tokens.map((line, i) => (63 <div {...getLineProps({ line, key: i })}>64 <LineNo>{i + 1}</LineNo>65 {line.map((token, key) => (66 <span {...getTokenProps({ token, key })} />67 ))}68 </div>69 ))}70 </Pre>71 )}72 </Highlight>73 )74}75
76export default Code
React live
So with React Live you need to add two snippets to your Code.js
component.
You’re going to import the components:
1import {2 LiveEditor,3 LiveError,4 LivePreview,5 LiveProvider,6} from 'react-live'
Then ypu’re going to check if react-live
has been added to the
language tag on your mdx file via the props:
1if (props['react-live']) {2 return (3 <LiveProvider code={codeString} noInline={true} theme={theme}>4 <LiveEditor />5 <LiveError />6 <LivePreview />7 </LiveProvider>8 )9}
Here’s the full component:
1import Highlight, { defaultProps } from 'prism-react-renderer'2import theme from 'prism-react-renderer/themes/nightOwl'3import React from 'react'4import {5 LiveEditor,6 LiveError,7 LivePreview,8 LiveProvider,9} from 'react-live'10import styled from 'styled-components'11import { copyToClipboard } from '../../utils/copy-to-clipboard'12
13const Pre = styled.pre`14 position: relative;15 text-align: left;16 margin: 1em 0;17 padding: 0.5em;18 overflow-x: auto;19 border-radius: 3px;20
21 & .token-lline {22 line-height: 1.3em;23 height: 1.3em;24 }25 font-family: 'Courier New', Courier, monospace;26`27
28const LineNo = styled.span`29 display: inline-block;30 width: 2em;31 user-select: none;32 opacity: 0.3;33`34
35const CopyCode = styled.button`36 position: absolute;37 right: 0.25rem;38 border: 0;39 border-radius: 3px;40 margin: 0.25em;41 opacity: 0.3;42 &:hover {43 opacity: 1;44 }45`46
47export const Code = ({ codeString, language, ...props }) => {48 if (props['react-live']) {49 return (50 <LiveProvider code={codeString} noInline={true} theme={theme}>51 <LiveEditor />52 <LiveError />53 <LivePreview />54 </LiveProvider>55 )56 }57
58 const handleClick = () => {59 copyToClipboard(codeString)60 }61
62 return (63 <Highlight64 {...defaultProps}65 code={codeString}66 language={language}67 theme={theme}68 >69 {({70 className,71 style,72 tokens,73 getLineProps,74 getTokenProps,75 }) => (76 <Pre className={className} style={style}>77 <CopyCode onClick={handleClick}>Copy</CopyCode>78 {tokens.map((line, i) => (79 <div {...getLineProps({ line, key: i })}>80 <LineNo>{i + 1}</LineNo>81 {line.map((token, key) => (82 <span {...getTokenProps({ token, key })} />83 ))}84 </div>85 ))}86 </Pre>87 )}88 </Highlight>89 )90}
To test this, add react-live
next to the language on your Dump
component, so you have added to the blog post you made:
1```jsx react-live
Now you can edit the code directly, try changing a few things like this:
1const Dump = props => (2 <div3 style={{4 fontSize: 20,5 border: '1px solid #efefef',6 padding: 10,7 background: 'white',8 }}9 >10 {Object.entries(props).map(([key, val]) => (11 <pre key={key}>12 <strong style={{ color: 'white', background: 'red' }}>13 {key} 💩14 </strong>15 {JSON.stringify(val, '', ' ')}16 </pre>17 ))}18 </div>19)20
21render(<Dump props={['One', 'Two', 'Three', 'Four']} />)
Cover Image
Now to add a cover image to go with each post, you’ll need to install a couple of packages to manage images in Gatsby.
install:
1yarn add gatsby-transformer-sharp gatsby-plugin-sharp gatsby-remark-images gatsby-image
Now you should config gatsby-config.js
to include the newly added
packages. Remember to add gatsby-remark-images
to
gatsby-plugin-mdx
as both a gatsbyRemarkPlugins
option and as a
plugins
option.
config:
1module.exports = {2 siteMetadata: siteMetadata,3 plugins: [4 `gatsby-plugin-styled-components`,5 `gatsby-transformer-sharp`,6 `gatsby-plugin-sharp`,7 {8 resolve: `gatsby-plugin-mdx`,9 options: {10 extensions: [`.mdx`, `.md`],11 gatsbyRemarkPlugins: [12 {13 resolve: `gatsby-remark-images`,14 options: {15 maxWidth: 590,16 },17 },18 ],19 plugins: [20 {21 resolve: `gatsby-remark-images`,22 options: {23 maxWidth: 590,24 },25 },26 ],27 },28 },29 {30 resolve: `gatsby-source-filesystem`,31 options: { path: `${__dirname}/posts`, name: `posts` },32 },33 ],34}
Add image to index query in src/pages.index.js
:
1cover {2 publicURL3 childImageSharp {4 sizes(5 maxWidth: 20006 traceSVG: { color: "#639" }7 ) {8 ...GatsbyImageSharpSizes_tracedSVG9 }10 }11}
Fix up the date in the query too:
1date(formatString: "YYYY MMMM Do")
This will show the date as full year, full month and the day as a ‘st’, ‘nd’, ‘rd’ and ‘th’. So if today’s date were 1970/01/01 it would read 1970 January 1st.
Add gatsby-image
use that in a styled component:
1const Image = styled(Img)`2 border-radius: 5px;3`
Add some JavaScript to determine if there’s anything to render:
1{2 !!frontmatter.cover ? (3 <Image sizes={frontmatter.cover.childImageSharp.sizes} />4 ) : null5}
Here’s what the full module should look like now:
1import { Link, graphql } from 'gatsby'2import Img from 'gatsby-image'3import React from 'react'4import styled from 'styled-components'5import { Layout } from '../components/Layout'6
7const IndexWrapper = styled.main``8
9const PostWrapper = styled.div``10
11const Image = styled(Img)`12 border-radius: 5px;13`14
15export default ({ data }) => {16 return (17 <Layout>18 <IndexWrapper>19 {/* <Dump data={data}></Dump> */}20 {data.allMdx.nodes.map(21 ({ id, excerpt, frontmatter, fields }) => (22 <PostWrapper key={id}>23 <Link to={fields.slug}>24 {!!frontmatter.cover ? (25 <Image26 sizes={frontmatter.cover.childImageSharp.sizes}27 />28 ) : null}29 <h1>{frontmatter.title}</h1>30 <p>{frontmatter.date}</p>31 <p>{excerpt}</p>32 </Link>33 </PostWrapper>34 )35 )}36 </IndexWrapper>37 </Layout>38 )39}40
41export const query = graphql`42 query SITE_INDEX_QUERY {43 allMdx(44 sort: { fields: [frontmatter___date], order: DESC }45 filter: { frontmatter: { published: { eq: true } } }46 ) {47 nodes {48 id49 excerpt(pruneLength: 250)50 frontmatter {51 title52 date(formatString: "YYYY MMMM Do")53 cover {54 publicURL55 childImageSharp {56 sizes(maxWidth: 2000, traceSVG: { color: "#639" }) {57 ...GatsbyImageSharpSizes_tracedSVG58 }59 }60 }61 }62 fields {63 slug64 }65 }66 }67 }68`
Additional resources:
this helped me for my own blog: https://juliangaramendy.dev/custom-open-graph-images-in-gatsby-blog/
and the Gatsby docs: https://www.gatsbyjs.org/docs/working-with-images/
Adding an SEO component to the site
There’s a Gatsby github PR on seo with some great notes from Andrew Welch on SEO and a link to a presentation he did back in 2017.
Crafting Modern SEO with Andrew Welch:
In the following comments of that PR, Gatsby’s LekoArts details his own implementation which I have implemented as a React component, you’re going to be configuring that now in this how-to.
First up, install and configure, gatsby-plugin-react-helmet
this is
used for server rendering data added with React Helmet.
1yarn add gatsby-plugin-react-helmet react-helmet react-seo-component
You’ll need to add the plugin to your gatsby-config.js
. If you
haven’t done so already now is a good time to also configure the
gatsby-plugin-styled-components
as well.
Configure SEO Component for Homepage
To visualise the data you’re going to need to get into the SEO
component use the Dump
component to begin with to validate the data.
The majority of the information needed for src/pages/index.js
can be
first added to the gatsby-config.js
, siteMetadata
object then
queried with the useSiteMetadata
hook. Some of the data added here
can then be used in src/templates/blogPostTemplate.js
, more on that
in the next section.
For now add the following:
1const siteMetadata = {2 title: `The Localhost Blog`,3 description: `This is my coding blog where I write about my coding journey.`,4 image: `/default-site-image.jpg`,5 siteUrl: `https://thelocalhost.io`,6 siteLanguage: `en-GB`,7 siteLocale: `en_gb`,8 twitterUsername: `@spences10`,9 authorName: `Scott Spence`,10}11
12module.exports = {13 siteMetadata: siteMetadata,14 plugins: [15 ...
You don’t have to abstract out the siteMetadata
into it’s own
component here, it’s only a suggestion on how to manage it.
The image
is going to be the default image for your site, you should
create a static
folder at the root of the project and add in an
image you want to be shown when the homepage of your site is shared on
social media.
For siteUrl
at this stage it doesn’t necessarily have to be valid,
add a dummy url for now and you can change this later.
The siteLanguage
is your language of choice for the site, take a
look at w3 language tags for more info.
Facebook OpenGraph is the only place the siteLocale
is used and it
is different from language tags.
Add your twitterUsername
and your authorName
.
Update the useSiteMetadata
hook now to reflect the newly added
properties:
1import { graphql, useStaticQuery } from 'gatsby'2
3export const useSiteMetadata = () => {4 const { site } = useStaticQuery(5 graphql`6 query SITE_METADATA_QUERY {7 site {8 siteMetadata {9 description10 title11 image12 siteUrl13 siteLanguage14 siteLocale15 twitterUsername16 authorName17 }18 }19 }20 `21 )22 return site.siteMetadata23}
Begin with importing the Dump
component in src/pages/index.js
then
plug in the props as they are detailed in the docs of the
react-seo-component
.
1import Dump from '../components/Dump'2import { useSiteMetadata } from '../hooks/useSiteMetadata'3
4export default ({ data }) => {5 const {6 description,7 title,8 image,9 siteUrl,10 siteLanguage,11 siteLocale,12 twitterUsername,13 } = useSiteMetadata()14 return (15 <Layout>16 <Dump17 title={title}18 description={description}19 image={`${siteUrl}${image}`}20 pathname={siteUrl}21 siteLanguage={siteLanguage}22 siteLocale={siteLocale}23 twitterUsername={twitterUsername}24 />25 <IndexWrapper>26 {data.allMdx.nodes.map(27 ...
Check that all the props are displaying valid values then you can swap
out the Dump
component with the SEO
component.
The complete src/pages/index.js
should look like this now:
1import { graphql, Link } from 'gatsby'2import Img from 'gatsby-image'3import React from 'react'4import SEO from 'react-seo-component'5import styled from 'styled-components'6import { Layout } from '../components/Layout'7import { useSiteMetadata } from '../hooks/useSiteMetadata'8
9const IndexWrapper = styled.main``10
11const PostWrapper = styled.div``12
13const Image = styled(Img)`14 border-radius: 5px;15`16
17export default ({ data }) => {18 const {19 description,20 title,21 image,22 siteUrl,23 siteLanguage,24 siteLocale,25 twitterUsername,26 } = useSiteMetadata()27 return (28 <Layout>29 <SEO30 title={title}31 description={description || `nothin’`}32 image={`${siteUrl}${image}`}33 pathname={siteUrl}34 siteLanguage={siteLanguage}35 siteLocale={siteLocale}36 twitterUsername={twitterUsername}37 />38 <IndexWrapper>39 {/* <Dump data={data}></Dump> */}40 {data.allMdx.nodes.map(41 ({ id, excerpt, frontmatter, fields }) => (42 <PostWrapper key={id}>43 <Link to={fields.slug}>44 {!!frontmatter.cover ? (45 <Image46 sizes={frontmatter.cover.childImageSharp.sizes}47 />48 ) : null}49 <h1>{frontmatter.title}</h1>50 <p>{frontmatter.date}</p>51 <p>{excerpt}</p>52 </Link>53 </PostWrapper>54 )55 )}56 </IndexWrapper>57 </Layout>58 )59}60
61export const query = graphql`62 query SITE_INDEX_QUERY {63 allMdx(64 sort: { fields: [frontmatter___date], order: DESC }65 filter: { frontmatter: { published: { eq: true } } }66 ) {67 nodes {68 id69 excerpt(pruneLength: 250)70 frontmatter {71 title72 date(formatString: "YYYY MMMM Do")73 cover {74 publicURL75 childImageSharp {76 sizes(maxWidth: 2000, traceSVG: { color: "#639" }) {77 ...GatsbyImageSharpSizes_tracedSVG78 }79 }80 }81 }82 fields {83 slug84 }85 }86 }87 }88`
Configure SEO Component for Blog Posts
This will be the same approach as with the homepage, import the Dump
component and validate the props before swapping out the Dump
component with the SEO
component.
1import Dump from '../components/Dump'2import { useSiteMetadata } from '../hooks/useSiteMetadata'3
4export default ({ data, pageContext }) => {5 const {6 image,7 siteUrl,8 siteLanguage,9 siteLocale,10 twitterUsername,11 authorName,12 } = useSiteMetadata()13 const { frontmatter, body, fields, excerpt } = data.mdx14 const { title, date, cover } = frontmatter15 const { previous, next } = pageContext16 return (17 <Layout>18 <Dump19 title={title}20 description={excerpt}21 image={22 cover === null23 ? `${siteUrl}${image}`24 : `${siteUrl}${cover.publicURL}`25 }26 pathname={`${siteUrl}${fields.slug}`}27 siteLanguage={siteLanguage}28 siteLocale={siteLocale}29 twitterUsername={twitterUsername}30 author={authorName}31 article={true}32 publishedDate={date}33 modifiedDate={new Date(Date.now()).toISOString()}34 />35 <h1>{frontmatter.title}</h1>36 ...
Add fields.slug
, excerpt
and cover.publicURL
to the
PostsBySlug
query and destructure them from data.mdx
and
frontmatter
respectively.
For the image you’ll need to do some logic as to weather the cover
exists and default to the default site image if it doesn’t.
The complete src/templates/blogPostTemplate.js
should look like this
now:
1import { graphql, Link } from 'gatsby'2import { MDXRenderer } from 'gatsby-plugin-mdx'3import React from 'react'4import SEO from 'react-seo-component'5import { Layout } from '../components/Layout'6import { useSiteMetadata } from '../hooks/useSiteMetadata'7
8export default ({ data, pageContext }) => {9 const {10 image,11 siteUrl,12 siteLanguage,13 siteLocale,14 twitterUsername,15 authorName,16 } = useSiteMetadata()17 const { frontmatter, body, fields, excerpt } = data.mdx18 const { title, date, cover } = frontmatter19 const { previous, next } = pageContext20 return (21 <Layout>22 <SEO23 title={title}24 description={excerpt}25 image={26 cover === null27 ? `${siteUrl}${image}`28 : `${siteUrl}${cover.publicURL}`29 }30 pathname={`${siteUrl}${fields.slug}`}31 siteLanguage={siteLanguage}32 siteLocale={siteLocale}33 twitterUsername={twitterUsername}34 author={authorName}35 article={true}36 publishedDate={date}37 modifiedDate={new Date(Date.now()).toISOString()}38 />39 <h1>{frontmatter.title}</h1>40 <p>{frontmatter.date}</p>41 <MDXRenderer>{body}</MDXRenderer>42 {previous === false ? null : (43 <>44 {previous && (45 <Link to={previous.fields.slug}>46 <p>{previous.frontmatter.title}</p>47 </Link>48 )}49 </>50 )}51 {next === false ? null : (52 <>53 {next && (54 <Link to={next.fields.slug}>55 <p>{next.frontmatter.title}</p>56 </Link>57 )}58 </>59 )}60 </Layout>61 )62}63
64export const query = graphql`65 query PostBySlug($slug: String!) {66 mdx(fields: { slug: { eq: $slug } }) {67 frontmatter {68 title69 date(formatString: "YYYY MMMM Do")70 cover {71 publicURL72 }73 }74 body75 excerpt76 fields {77 slug78 }79 }80 }81`
Build Site and Validate Meta Tags
Add in the build script to package.json
and also a script for
serving the built site locally.
1"scripts": {2 "dev": "gatsby develop -p 9988 -o",3 "build": "gatsby build",4 "serve": "gatsby serve -p 9500 -o"5},
Now it’s time to run:
1yarn build && yarn serve
This will build the site and open a browser tab so you can see the site as it will appear when it is on the internet. Validate meta tags have been added to the build by selecting “View page source” (Crtl+u in Windows and Linux) on the page and do a Ctrl+f to find them.
Adding the Project to GitHub
Add your code to GitHub by either selecting the plus (+) icon next to your avatar on GitHub or by going to directly to https://github.com/new
Name your repository and click create repository, then you will be given the instructions to link your local code to the repository you created via the command line.
Depending on how you authenticate with GitHub will depend on what the command looks like.
Some good resources for authenticating with GitHub via SSH are Kent Dodds Egghead.io video and also a how-to on CheatSheets.xyz.
Deploy to Netlify
To deploy your site to Netlify, if you haven’t done so already you’ll need to add the GitHub integration to your GitHub profile. If you got to app.netlify.com the wizard will walk you through the process.
From here you can add your built site’s public
folder, drag ‘n drop
style directly to the Netlify global CDNs.
You, however are going to load your site via the Netlify CLI! In your terminal, if you haven’t already got the CLI installed, run:
1yarn global add netlify-cli
Then once the CLI is installed:
1# authenticate via the CLI2netlify login3# initialise the site4netlify init
Enter the details for your team, the site name is optional, the build
command will be yarn build
and directory to deploy is public
.
You will be prompted to commit the changes and push them to GitHub
(with git push
), once you have done that your site will be published
and ready for all to see!
Validate Metadata with Heymeta
Last up is validating the metadata for the OpenGraph fields, to do
that you’ll need to make sure that the siteUrl
reflecting what you
have in your Netlify dashboard.
If you needed to change the url you’ll need to commit and push the changes to GitHub again.
Once your site is built with a valid url you can then test the homepage and a blog page for the correct meta tags with heymeta.com.
OpenGraph checking tools:
Additional resources:
Thanks for reading 🙏
That’s all folks! If there is anything I have missed, or if there is a better way to do something then please let me know.
Follow me on Twitter or Ask Me Anything on GitHub.
Back to Top