DN

How to build a portfolio using Gatsby - part 2

Dan Norris / 6th Sep 2020

10 min read

[Live Demo]

Welcome to the second part of this two-part series on how to build your portfolio using Gatsby. Part 2 assumes you've gone through part 1, have built your portfolio and are now interested in taking a bit of a deeper dive into one way you could choose to build a blog with Gatsby using MDX.

If not, then take a look at part 1 here.

Who is this for?

This isn't a Gatsby starter, although you are welcome to use the GitHub repository as a starter for your own use.

If you do, please star the repository. This series is aimed at people who are interested in how to build their own Gatsby portfolio and blog from scratch without the aid of a starter.

What will this cover?

We'll cover the following:

Part 2

  • Why MDX?
  • What are you going to build?
  • Create a blog page
  • Configure the Gatsby filesystem plugin
  • Create your first MDX blog articles
  • Create slugs for your MDX blog posts
  • Programmatically create your MDX pages using the createPages API
  • Create a blog post template
  • Dynamically show article read times
  • Make an index of blog posts
  • Create a featured posts section
  • Customise your MDX components
  • Add syntax highlighting for code blocks
  • Add a featured image to blog posts
  • Add Google Analytics
  • Summary

Why MDX?

One of the major features about Gatsby is your ability to source content from nearly anywhere. The combination of GraphQL and Gatsby's source plugin ecosystem means that you could pull data from a headless CMS, database, API, JSON or without GraphQL at all. All with minimal configuration needed.

MDX enables you to write JSX into your Markdown. This allows you to write long-form content and re-use your React components like charts for instance to create some really engaging content for your users. I've included a quick example below using MDX in this article.

1## This is Markdown
2
3<div style={{ padding: '20px', backgroundColor: 'skyblue' }}>
4 <h3>This is JSX</h3>
5</div>

This is Markdown

This is JSX

What are you going to build?

There are a lot of starter templates that are accessible from the Gatsby website which enable you to get off the ground running with a ready-made blog or portfolio in a couple clicks. What that doesn't do is break down how it works and how you could make one yourself. If you're more interested in getting stuff done than how it works, then I recommend taking a look at the starters here.

You will have already created a basic portfolio in part 1 similar to the demo site available above. We're now going to create a blog for our portfolio that is programmatically created from MDX using GraphQL. We'll separate our blog into components; one section to display our featured articles and another to display an index of all of our articles. Then we'll add syntax highlighting for code blocks, read times for our users, a cover image for each post and Google Analytics.

Create a blog page

Gatsby makes it incredibly easy to implement routing into your site. Any .js file found within src/pages will automatically generate its own page and the path for that page will match the file structure it's found in.

We're going to create a new blog.js page that will display a list of featured blog articles and a complete list of all of our blog articles.

1touch src/pages/blog.js

Let's now import our Layout.js component we created in part 1 and enter some placeholder content for now.

1import React from "react"
2import Layout from "../components/Layout"
3
4export default ({ data }) => {
5 return (
6 <Layout>
7 <h1>Blog</h1>
8 <p>Our blog articles will go here!</p>
9 </Layout>
10 )
11}

If you now navigate to http://localhost:9090/blog you'll be able to see your new blog page.

Configure the Gatsby filesystem plugin

We want to colocate all of our long-form content together with their own assets, e.g. images, then we want to place them into a folder like src/content/posts. This isn't the src/pages directory we used early so we'll need to do a bit of extra work in order to dynamically generate our blog pages. We'll use Gatsby's createPages API to do this shortly.

Firstly, we need to configure the gatsby-source-filesystem plugin so that Gatsby knows where to source our MDX blog articles from. You should already have the plugin installed so let's configure it now. We'll add the location to our gatsby-config.js file.

1...
2
3{
4 resolve: `gatsby-source-filesystem`,
5 options: {
6 name: `posts`,
7 path: `${__dirname}/src/content/posts`,
8 },
9},
10
11...

Your full file should look something like this:

1module.exports = {
2 plugins: [
3 `gatsby-plugin-postcss`,
4 `gatsby-plugin-sharp`,
5 `gatsby-transformer-sharp`,
6 `gatsby-plugin-mdx`,
7 {
8 resolve: `gatsby-source-filesystem`,
9 options: {
10 name: `content`,
11 path: `${__dirname}/src/content`,
12 },
13 },
14 {
15 resolve: `gatsby-source-filesystem`,
16 options: {
17 name: `posts`,
18 path: `${__dirname}/src/content/posts`,
19 },
20 },
21 ],
22}

Create your first MDX blog articles

Let's create several dummy articles for now. We'll create quite a few so that we can differentiate some of them into featured articles to display on our home page. There's a quick way to do that:

1mkdir -p src/content/posts
2touch src/content/posts/blog-{1,2,3}.mdx

We're adding a lot of additional frontmatter now which we will use at a later date. For the time being, leave the cover property empty.

Frontmatter is just metadata for your MDX. You can inject them later into your components using a GraphQL query and are just basic YAML. They need to be at the top of the file and between triple dashes.

1---
2title: Blog 1
3subtitle: Blogging with MDX and Gatsby
4date: 2020-08-18
5published: true
6featured: true
7cover: ""
8---
9Sail ho rope's end bilge rat Chain Shot tack scuppers cutlass fathom case shot bilge jolly boat quarter ahoy gangplank coffer. Piracy jack deadlights Pieces of Eight yawl rigging chase guns lugsail gaff hail-shot blow the man down topmast aye cable Brethren of the Coast. Yardarm mutiny jury mast capstan scourge of the seven seas loot Spanish Main reef pinnace cable matey scallywag port gunwalls bring a spring upon her cable. Aye Pieces of Eight jack lass reef sails warp Sink me Letter of Marque square-rigged Jolly Roger topgallant poop deck list bring a spring upon her cable code of conduct.
10
11Rigging plunder barkadeer Gold Road square-rigged hardtack aft lad Privateer carouser port quarter Nelsons folly matey cable. Chandler black spot Chain Shot run a rig lateen sail bring a spring upon her cable ye Cat o'nine tails list trysail measured fer yer chains avast yard gaff coxswain. Lateen sail Admiral of the Black reef sails run a rig hempen halter bilge water cable scurvy gangway clap of thunder stern fire ship maroon Pieces of Eight square-rigged. Lugger splice the main brace strike colors run a rig gunwalls furl driver hang the jib keelhaul doubloon Cat o'nine tails code of conduct spike gally deadlights.
12
13Landlubber or just lubber yardarm lateen sail Barbary Coast tackle pirate cog American Main galleon aft gun doubloon Nelsons folly topmast broadside. Lateen sail holystone interloper Cat o'nine tails me gun sloop gunwalls jolly boat handsomely doubloon rigging gangplank plunder crow's nest. Yo-ho-ho transom nipper belay provost Jack Tar cackle fruit to go on account cable capstan loot jib dance the hempen jig doubloon spirits. Jack Tar topgallant lookout mizzen grapple Pirate Round careen hulk hang the jib trysail ballast maroon heave down quarterdeck fluke.

Now do the same thing for the other two blog articles we've created.

Create slugs for your MDX blog posts

We now need to create slugs for each of our blog posts. We could do this manually by including a URL or path property to each of our blog posts frontmatter but we're going to set up our blog so that the paths are generated dynamically for us. We'll be using Gatsby's onCreateNode API for this.

Create a gatsby-node.js file in your root directory. This file is one of four main files that you can optionally choose to include in a Gatsby root directory that enables you to configure your site and control its behaviour. We've already used the gatsby-browser.js file to import Tailwind CSS directives and gatsby-config.js to control what plugins we are importing.

1touch gatsby-node.js

Now copy the following into your gatsby-node.js file. This uses a helper function called createFilePath from the gatsby-source-filesystem plugin to provide the value of each of your .mdx blog post's file paths. The Gatsby onCreateNode API is then used to create a new GraphQL node with the key of slug and value of blog posts path, prefixed with anything you like - in this case its /blog.

1const { createFilePath } = require("gatsby-source-filesystem")
2
3exports.onCreateNode = ({ node, actions, getNode }) => {
4 const { createNodeField } = actions
5
6 // only applies to mdx nodes
7 if (node.internal.type === "Mdx") {
8 const value = createFilePath({ node, getNode })
9
10 createNodeField({
11 // we're called the new node field 'slug'
12 name: "slug",
13 node,
14 // you don't need a trailing / after blog as createFilePath will do this for you
15 value: `/blog${value}`,
16 })
17 }
18}

If you want to find out more about the gatsby-source-filesystem plugin then take a look at this. Further information the onCreateNode API can be found here.

Programmatically create your MDX pages using the createPages API

We're going to re-use some boilerplate from the Gatsby docs now and add the following code below to what we have already included in the previous section. This gets added to all of the existing node in the gatsby-node.js file. This uses the slug we created in the earlier section and Gatsby's createPages API to create pages for all of your .mdx files and wraps it in a template.

1const path = require("path")
2
3exports.createPages = async ({ graphql, actions, reporter }) => {
4 // Destructure the createPage function from the actions object
5 const { createPage } = actions
6
7 const result = await graphql(`
8 query {
9 allMdx {
10 edges {
11 node {
12 id
13 fields {
14 slug
15 }
16 }
17 }
18 }
19 }
20 `)
21
22 // Create blog post pages.
23 const posts = result.data.allMdx.edges
24
25 // you'll call `createPage` for each result
26 posts.forEach(({ node }, index) => {
27 createPage({
28 // This is the slug you created before
29 path: node.fields.slug,
30 // This component will wrap our MDX content
31 component: path.resolve(`./src/templates/blogPost.js`),
32 // You can use the values in this context in
33 // our page layout component
34 context: { id: node.id },
35 })
36 })
37}

If you try and restart your development server, you'll receive an error to stay that your blogPost.js component doesn't exist. Let's create a template now to display all your blog posts.

Create a blog post template

Let's firstly create a new blogPost.js template file.

1touch src/templates/blogPost.js

Let's populate the template with some basic data such as title, date and body. We'll be dynamically adding read time, cover images and syntax highlighting shortly.

1import { MDXRenderer } from "gatsby-plugin-mdx"
2import React from "react"
3import Layout from "../components/layout"
4
5export default ({ data }) => {
6 const { frontmatter, body } = data.mdx
7
8 return (
9 <Layout>
10 <section
11 className="w-2/4 my-8 mx-auto container"
12 style={{ minHeight: "80vh" }}
13 >
14 <h1 className="text-3xl sm:text-5xl font-bold">{frontmatter.title}</h1>
15 <div className="flex justify-between">
16 <p className="text-base text-gray-600">{frontmatter.date}</p>
17 </div>
18
19 <div className="mt-8 text-base font-light">
20 <MDXRenderer>{body}</MDXRenderer>
21 </div>
22 </section>
23 </Layout>
24 )
25}

Now we need to create a GraphQL query to populate the fields above.

1export const pageQuery = graphql`
2 query BlogPostQuery($id: String) {
3 mdx(id: { eq: $id }) {
4 id
5 body
6 timeToRead
7 frontmatter {
8 title
9 date(formatString: "Do MMM YYYY")
10 }
11 }
12 }
13`

We're passing an argument to this GraphQL query called $id here where we have made a type declaration that it is a String. We've passed this from the context object after using the createPage API in gatsby-node.js in the earlier section. Then we have filtered our GraphQL query to only return results that equal that $id variable.

If you now navigate to the url's below, each of your blog posts should now be working:

Dynamically show article read times

Let's start to add a few more features to our blog post template. Something that you may regularly see on technical posts is the estimated time it takes to read the article. A great example of this on Dan Abramov's blog overreacted.io.

There's an incredibly easy way to add this feature to your blog using Gatsby and GraphQL and it doesn't require you to write a function to calculate the length of your blog post. Let's add it now. Go back to your blogPost.js file and update your GraphQL query to also include the timeToRead property.

1export const pageQuery = graphql`
2 query BlogPostQuery($id: String) {
3 mdx(id: { eq: $id }) {
4 id
5 body
6 timeToRead
7 frontmatter {
8 title
9 date(formatString: "Do MMM YYYY")
10 }
11 }
12 }
13`

Now pass it as a prop and include it as an expression in your blogPost.js template.

1export default ({ data }) => {
2 const { frontmatter, body, timeToRead } = data.mdx
3 ...
4 <p className="text-base text-gray-600">{timeToRead} min read</p>
5 ...
6}

If you refresh your development server, the read time for each particular blog post should now appear. Unless you included your own blog text, they should all read "1 min read" but try experimenting with longer articles and see it dynamically change.

Make an index of blog posts

Our blog page is still looking a bit bare. Let's now populate it with a full list of all our blog posts. Let's firstly create a heading.

1import React from "react"
2import Layout from "../components/Layout"
3
4const Blog = ({ data }) => {
5 return (
6 <Layout>
7 <section
8 className="w-3/5 mx-auto container mt-6 flex flex-col justify-center"
9 style={{ minHeight: "60vh" }}
10 >
11 <h1 className="text-3xl sm:text-5xl font-bold mb-6">Blog</h1>
12 <p className="font-light text-base sm:text-lg">
13 Arr aft topsail deadlights ho snow mutiny bowsprit long boat draft
14 crow's nest strike colors bounty lad ballast.
15 </p>
16 </section>
17 <p>List of blog articles goes here.</p>
18 </Layout>
19 )
20}
21
22export default Blog

Now let's create a GraphQL query that will return all .mdx files that have a file path that includes posts/ and has a frontmatter property where the published value equals true.

We then want to sort the query in descending order so that the most recent article is displayed first. We can the pass this as a prop to a Post sub component we will create shortly, similar to what we have done with the Hero, About and other sub components we made in part 1.

1export const query = graphql`
2 {
3 posts: allMdx(
4 filter: {
5 fileAbsolutePath: { regex: "/posts/" }
6 frontmatter: { published: { eq: true } }
7 }
8 sort: { order: DESC, fields: frontmatter___date }
9 ) {
10 edges {
11 node {
12 fields {
13 slug
14 }
15 body
16 timeToRead
17 frontmatter {
18 title
19 date(formatString: "Do MMM")
20 }
21 id
22 excerpt(pruneLength: 100)
23 }
24 }
25 }
26 }
27`

Let's now create a new Post.js sub component.

1touch src/components/Post.js

We can now iterate over the content prop in Post.js and create a list of all of our blog articles.

1import React from 'react'
2import { Link } from 'gatsby'
3
4const Posts = ({ content }) => {
5 return (
6 <section
7 id="blog"
8 className="mt-6 flex flex-col mx-auto container w-3/5"
9 style={{ marginBottom: '10rem' }}
10 >
11 <h3 className="text-3xl sm:text-5xl font-bold mb-6">All Posts</h3>
12
13 {content.map((posts, key) => {
14 const {
15 excerpt,
16 id,
17 body,
18 frontmatter,
19 timeToRead,
20 fields,
21 } = posts.node
22
23 return (
24 <Link to={fields.slug}>
25 <section
26 className="flex items-center justify-between mt-8"
27 key={id}
28 >
29 <div>
30 <p className="text-xs sm:text-sm font-bold text-gray-500">
31 {frontmatter.date}
32 <span className="sm:hidden">
33 {' '}
34 &bull; {timeToRead} min read
35 </span>
36 </p>
37 <h1 className="text-lg sm:text-2xl font-bold">
38 {frontmatter.title}
39 </h1>
40 <p className="text-sm sm:text-lg font-light">
41 {excerpt}
42 </p>
43 </div>
44 <p className="hidden sm:block text-sm font-bold text-gray-500">
45 {timeToRead} min read
46 </p>
47 </section>
48 </Link>
49 )
50 })}
51 </section>
52 )
53}
54
55export default Posts

Let's now go back to blog.js and replace the <p> element with the Post.js sub component and pass it the data object.

1import React from "react"
2import { graphql, Link } from "gatsby"
3import Layout from "../components/Layout"
4import Post from "../components/Post"
5
6const Blog = ({ data }) => {
7 return (
8 <Layout>
9 <section
10 className="w-3/5 mx-auto container mt-6 flex flex-col justify-center"
11 style={{ minHeight: "60vh" }}
12 >
13 <h1 className="text-3xl sm:text-5xl font-bold mb-6">Blog</h1>
14 <p className="font-light text-base sm:text-lg">
15 Arr aft topsail deadlights ho snow mutiny bowsprit long boat draft
16 crow's nest strike colors bounty lad ballast.
17 </p>
18 </section>
19 <Post content={data.posts.edges} />
20 </Layout>
21 )
22}
23
24export default Blog
25
26export const query = graphql`
27 {
28 posts: allMdx(
29 filter: {
30 fileAbsolutePath: { regex: "/posts/" }
31 frontmatter: { published: { eq: true } }
32 }
33 sort: { order: DESC, fields: frontmatter___date }
34 ) {
35 edges {
36 node {
37 fields {
38 slug
39 }
40 body
41 timeToRead
42 frontmatter {
43 title
44 date(formatString: "Do MMM")
45 }
46 id
47 excerpt(pruneLength: 100)
48 }
49 }
50 }
51 }
52`

If you navigate to http://localhost:9090/blog you should now see a list of all your available blog articles in descending order. Choosing whether you want to publicly display a blog article is as easy as changing the boolean value of published to false on that particular article's frontmatter.

Create a featured posts section

We're going to create a featured posts section. Firstly, we'll create a new GraphQL query that enables us to filter only the posts that have a truthy featured frontmatter value.

Let's create that now and add it to our blog.js file.

1...
2 featured: allMdx(
3 filter: {
4 fileAbsolutePath: { regex: "/posts/" }
5 frontmatter: { published: { eq: true }, featured: { eq: true } }
6 }
7 sort: { order: DESC, fields: frontmatter___date }
8 ) {
9 edges {
10 node {
11 fields {
12 slug
13 }
14 frontmatter {
15 date(formatString: "Do MMM")
16 title
17 }
18 excerpt(pruneLength: 100)
19 id
20 body
21 timeToRead
22 }
23 }
24 }
25...

Now, let's create a FeaturedPosts.js component.

1import React from "react"
2import { Link } from "gatsby"
3
4const FeaturedPosts = ({ content }) => {
5 return (
6 <section className="my-6 flex flex-col mx-auto container w-3/5">
7 <h3 className="text-3xl sm:text-5xl font-bold mb-6">Featured Posts</h3>
8
9 {content.map((featured, key) => {
10 const {
11 excerpt,
12 id,
13 body,
14 frontmatter,
15 timeToRead,
16 fields,
17 } = featured.node
18
19 return (
20 <Link to={fields.slug}>
21 <section
22 className="flex items-center justify-between mt-8"
23 key={id}
24 >
25 <div>
26 <p className="text-xs sm:text-sm font-bold text-gray-500">
27 {frontmatter.date}
28 <span className="sm:hidden">
29 {" "}
30 &bull; {timeToRead} min read
31 </span>
32 </p>
33 <h1 className="text-lg sm:text-2xl font-bold">
34 {frontmatter.title}
35 </h1>
36 <p className="text-sm sm:text-lg font-light">{excerpt}</p>
37 </div>
38 <p className="hidden sm:block text-sm font-bold text-gray-500">
39 {timeToRead} min read
40 </p>
41 </section>
42 </Link>
43 )
44 })}
45 </section>
46 )
47}
48
49export default FeaturedPosts

Let's now import the new component into blog.js.

1...
2 const Blog = ({ data }) => {
3 return (
4 <Layout>
5 <section
6 className="w-3/5 mx-auto container mt-6 flex flex-col justify-center"
7 style={{ minHeight: '60vh' }}
8 >
9 <h1 className="text-3xl sm:text-5xl font-bold mb-6">Blog</h1>
10 <p className="font-light text-base sm:text-lg">
11 Arr aft topsail deadlights ho snow mutiny bowsprit long boat
12 draft crow's nest strike colors bounty lad ballast.
13 </p>
14 </section>
15 <FeaturedPost cta={false} content={data.featured.edges} />
16 <Post content={data.posts.edges} />
17 </Layout>
18 )
19 }
20...

Let's now re-use the FeaturedPosts.js component in our index.js page. You'll need to use the same GraphQL query again and pass it as a prop.

1...
2 export default ({ data }) => {
3 return (
4 <Layout>
5 <Hero content={data.hero.edges} />
6 <About content={data.about.edges} />
7 <Project content={data.project.edges} />
8 <FeaturedPosts content={data.featured.edges} />
9 <Contact content={data.contact.edges} />
10 </Layout>
11 )
12 }
13...
14
15 featured: allMdx(
16 filter: {
17 fileAbsolutePath: { regex: "/posts/" }
18 frontmatter: { published: { eq: true }, featured: { eq: true } }
19 }
20 sort: { order: DESC, fields: frontmatter___date }
21 ) {
22 edges {
23 node {
24 fields {
25 slug
26 }
27 frontmatter {
28 date(formatString: "Do MMM")
29 title
30 }
31 excerpt(pruneLength: 100)
32 id
33 body
34 timeToRead
35 }
36 }
37 }
38...

Let's add a call to action button for users who want to see the rest of our blog articles. We'll include this in our FeaturedPosts.js component and pass in a boolean prop to determine if we want to display the button or not.

1import React from 'react'
2import { Link } from 'gatsby'
3
4const FeaturedPosts = ({ content, cta = true }) => {
5 return (
6 ...
7 {!cta ? null : (
8 <Link to="/blog" className="flex justify-center">
9 <button className="bg-red-500 hover:bg-red-400 text-white font-bold py-2 px-4 border-b-4 border-red-700 hover:border-red-500 rounded mt-6">
10 See More
11 </button>
12 </Link>
13 )}
14 ...
15 )
16}
17
18export default FeaturedPosts

Why don't we also double-check our GraphQL query is correctly displaying only the articles with a truthy featured frontmatter value. So, let's edit one of our blog articles, so that it does not display. Let's edit blog-1.mdx.

1---
2title: Blog 1
3subtitle: Blogging with MDX and Gatsby
4date: 2020-08-18
5published: true
6featured: false
7cover: ''
8---
9
10...

If you now navigate to http://localhost:9090/ you'll see a featured posts section with just two articles displaying. When you navigate to http://localhost:9090/blog you should now see a header, featured posts with two articles and all posts component displaying an index of all articles.

Customise your MDX components

You may have noticed that we are having the same problem we encountered in part 1 with the markdown we are writing in our .mdx files. No styling is being applied. We could fix this by introducing some markup and include inline styles or Tailwind class names but we want to minimise the amount of time we need to spend writing a blog post.

So we'll re-iterate the process we used in part 1 and use the MDXProvider component to define styling manually for each markdown component.

1import { MDXRenderer } from "gatsby-plugin-mdx"
2import { MDXProvider } from "@mdx-js/react"
3import React from "react"
4import Layout from "../components/Layout"
5
6export default ({ data }) => {
7 const { frontmatter, body, timeToRead } = data.mdx
8
9 return (
10 <MDXProvider
11 components={{
12 p: props => <p {...props} className="text-sm font-light mb-4" />,
13 h1: props => (
14 <h1 {...props} className="text-2xl font-bold mb-4 mt-10" />
15 ),
16 h2: props => <h2 {...props} className="text-xl font-bold mb-4 mt-8" />,
17 h3: props => <h3 {...props} className="text-lg font-bold mb-4 mt-8" />,
18 strong: props => (
19 <strong
20 {...props}
21 className="font-bold"
22 style={{ display: "inline" }}
23 />
24 ),
25 a: props => (
26 <a
27 {...props}
28 className="font-bold text-red-500 hover:underline cursor-pointer"
29 style={{ display: "inline" }}
30 />
31 ),
32 ul: props => (
33 <ul {...props} className="list-disc font-light ml-8 mb-4" />
34 ),
35 blockquote: props => (
36 <div
37 {...props}
38 role="alert"
39 className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 ml-4 mb-4"
40 />
41 ),
42 }}
43 >
44 <Layout>
45 <section
46 className="w-2/4 my-8 mx-auto container"
47 style={{ minHeight: "80vh" }}
48 >
49 <h1 className="text-3xl sm:text-5xl font-bold">
50 {frontmatter.title}
51 </h1>
52 <div className="flex justify-between">
53 <p className="text-base text-gray-600">{frontmatter.date}</p>
54 <p className="text-base text-gray-600">{timeToRead} min read</p>
55 </div>
56 <div className="mt-8 text-base font-light">
57 <MDXRenderer>{body}</MDXRenderer>
58 </div>
59 </section>
60 </Layout>
61 </MDXProvider>
62 )
63}
64
65export const pageQuery = graphql`
66 query BlogPostQuery($id: String) {
67 mdx(id: { eq: $id }) {
68 id
69 body
70 timeToRead
71 frontmatter {
72 title
73 date(formatString: "Do MMM YYYY")
74 }
75 }
76 }
77`

Now when you create a new blog post and write the long-form content using Markdown, the elements you've used will now display appropriately.

Add syntax highlighting for code blocks

I'm trying to regularly use my blog to write technical articles and so I found adding syntax highlighting to code blocks made reading my articles a better experience for my users.

The process is a little involved but we'll try and break it down as best as possible. Firstly, we need to use the gatsby-browser.js API file to wrap our entire site with a plugin called prism-react-renderer that will enable us to use syntax highlighting on our code blocks in MDX.

Let's install the plugin firstly.

1npm i prism-react-renderer

Now let's add in some boilerplate for the gatsby-browser.js file, for more information check out the API docs here.

1...
2
3import React from 'react'
4import { MDXProvider } from '@mdx-js/react'
5import Highlight, { defaultProps } from 'prism-react-renderer'
6
7const components = {
8 ...
9}
10
11export const wrapRootElement = ({ element }) => {
12 return <MDXProvider components={components}>{element}</MDXProvider>
13}

We've called the wrapRootElement function and returned our Gatsby site wrapped by MDXProvider. We're using the components prop and will be shortly passing a variable called components which will define a Highlight component imported form prism-react-renderer. This MDXProvider pattern is commonly known as a shortcode, you can find out more in the Gatsby docs here.

If we navigate to the GitHub repository for the plugin, we're going to copy some of the example code and then make it fit for purpose for our blog. You can find the repository here.

1...
2
3import React from 'react'
4import { MDXProvider } from '@mdx-js/react'
5import Highlight, { defaultProps } from 'prism-react-renderer'
6
7const components = {
8 pre: (props) => {
9 return (
10 <Highlight {...defaultProps} code={exampleCode} language="jsx">
11 {({ className, style, tokens, getLineProps, getTokenProps }) => (
12 <pre className={className} style={style}>
13 {tokens.map((line, i) => (
14 <div {...getLineProps({ line, key: i })}>
15 {line.map((token, key) => (
16 <span {...getTokenProps({ token, key })} />
17 ))}
18 </div>
19 ))}
20 </pre>
21 )}
22 </Highlight>,
23 )
24 }
25}
26
27export const wrapRootElement = ({ element }) => {
28 return <MDXProvider components={components}>{element}</MDXProvider>
29}

At the moment, the code block language is hard coded and we need to replace the exampleCode variable with the actual code we want to be highlighted. Let's do that now.

1...
2 const components = {
3 pre: (props) => {
4 const className = props.children.props.className || ''
5 const matches = className.match(/language-(?<lang>.*)/)
6
7 return (
8 <Highlight
9 {...defaultProps}
10 code={props.children.props.children.trim()}
11 language={
12 matches && matches.groups && matches.groups.lang
13 ? matches.groups.lang
14 : ''
15 }
16 >
17 {({
18 className,
19 style,
20 tokens,
21 getLineProps,
22 getTokenProps,
23 }) => (
24 <pre className={className} style={style}>
25 {tokens.map((line, i) => (
26 <div {...getLineProps({ line, key: i })}>
27 {line.map((token, key) => (
28 <span {...getTokenProps({ token, key })} />
29 ))}
30 </div>
31 ))}
32 </pre>
33 )}
34 </Highlight>
35 )
36 },
37 }
38...

If you now edit one of your .mdx blog posts and include a code block using Markdown syntax, it should now be highlighted using prism-react-renderer's default theme.

The padding is a little off, so let's fix that now.

1...
2 <pre className={`${className} p-4 rounded`} style={style}>
3 {tokens.map((line, i) => (
4 <div {...getLineProps({ line, key: i })}>
5 {line.map((token, key) => (
6 <span {...getTokenProps({ token, key })} />
7 ))}
8 </div>
9 ))}
10 </pre>
11...

If you want to change the default theme, you can import it from prism-react-renderer and pass it as a prop to the Highlight component. You can find more themes here. I've decided to use the vsDark theme in our example. Your final gatsby-browser.js should look something like this.

1import "./src/css/index.css"
2import React from "react"
3import { MDXProvider } from "@mdx-js/react"
4import theme from "prism-react-renderer/themes/vsDark"
5import Highlight, { defaultProps } from "prism-react-renderer"
6
7const components = {
8 pre: props => {
9 const className = props.children.props.className || ""
10 const matches = className.match(/language-(?<lang>.*)/)
11
12 return (
13 <Highlight
14 {...defaultProps}
15 code={props.children.props.children.trim()}
16 language={
17 matches && matches.groups && matches.groups.lang
18 ? matches.groups.lang
19 : ""
20 }
21 theme={theme}
22 >
23 {({ className, style, tokens, getLineProps, getTokenProps }) => (
24 <pre className={`${className} p-4 rounded`} style={style}>
25 {tokens.map((line, i) => (
26 <div {...getLineProps({ line, key: i })}>
27 {line.map((token, key) => (
28 <span {...getTokenProps({ token, key })} />
29 ))}
30 </div>
31 ))}
32 </pre>
33 )}
34 </Highlight>
35 )
36 },
37}
38
39export const wrapRootElement = ({ element }) => {
40 return <MDXProvider components={components}>{element}</MDXProvider>
41}

Add a featured image to blog posts

One of the last things we're going to do is provide the opportunity to add a featured image to each of our blog posts.

Let's firstly install a number of packages we're going to need.

1npm i gatsby-transformer-sharp gatsby-plugin-sharp gatsby-remark-images gatsby-image

Now we need to configure the plugins, let's update our gatsby-config.js file with the following:

1...
2 {
3 resolve: `gatsby-plugin-mdx`,
4 options: {
5 extensions: [`.mdx`, `.md`],
6 gatsbyRemarkPlugins: [
7 {
8 resolve: `gatsby-remark-images`,
9 },
10 ],
11 plugins: [
12 {
13 resolve: `gatsby-remark-images`,
14 },
15 ],
16 },
17 },
18...

We now need to update our GraphQL query on blogPost.js so that it returns the image we'll be including in our blog posts frontmatter shortly. We're using a query fragment here to return a traced SVG image while our image is lazy-loading. More information on query fragment's and the Gatsby image API can be found here.

1export const pageQuery = graphql`
2 query BlogPostQuery($id: String) {
3 mdx(id: { eq: $id }) {
4 id
5 body
6 timeToRead
7 frontmatter {
8 title
9 date(formatString: "Do MMM YYYY")
10 cover {
11 childImageSharp {
12 fluid(traceSVG: { color: "#F56565" }) {
13 ...GatsbyImageSharpFluid_tracedSVG
14 }
15 }
16 }
17 }
18 }
19 }
20`

Let's now add an image to our src/content/posts folder. I've included one in the GitHub repository for this project but you can access a lot of open licence images from places like https://unsplash.com/.

Include the location of the image into your blog posts frontmatter.

1---
2title: Blog 3
3subtitle: Blogging with MDX and Gatsby
4date: 2020-08-31
5published: true
6featured: true
7cover: './splash.jpg'
8---

Now let's add it to the blogPost.js template. You'll need to import the Img component from gatsby-image.

1...
2import Img from 'gatsby-image'
3
4export default ({ data }) => {
5 const { frontmatter, body, timeToRead } = data.mdx
6
7 return (
8 <MDXProvider
9 components={{
10 p: (props) => (
11 <p {...props} className="text-sm font-light mb-4" />
12 ),
13 h1: (props) => (
14 <h1 {...props} className="text-2xl font-bold mb-4 mt-10" />
15 ),
16 h2: (props) => (
17 <h2 {...props} className="text-xl font-bold mb-4 mt-8" />
18 ),
19 h3: (props) => (
20 <h3 {...props} className="text-lg font-bold mb-4 mt-8" />
21 ),
22 strong: (props) => (
23 <strong
24 {...props}
25 className="font-bold"
26 style={{ display: 'inline' }}
27 />
28 ),
29
30 a: (props) => (
31 <a
32 {...props}
33 className="font-bold text-blue-500 hover:underline cursor-pointer"
34 style={{ display: 'inline' }}
35 />
36 ),
37 ul: (props) => (
38 <ul {...props} className="list-disc font-light ml-8 mb-4" />
39 ),
40 blockquote: (props) => (
41 <div
42 {...props}
43 role="alert"
44 className="bg-blue-100 border-l-4 border-blue-500 text-blue-700 p-4 ml-4 mb-4"
45 />
46 ),
47 }}
48 >
49 <Layout>
50 <section
51 className="w-2/4 my-8 mx-auto container"
52 style={{ minHeight: '80vh' }}
53 >
54 <h1 className="text-3xl sm:text-5xl font-bold">
55 {frontmatter.title}
56 </h1>
57 <div className="flex justify-between">
58 <p className="text-base text-gray-600">
59 {frontmatter.date}
60 </p>
61 <p className="text-base text-gray-600">
62 {timeToRead} min read
63 </p>
64 </div>
65 {frontmatter.cover && frontmatter.cover ? (
66 <div className="my-8 shadow-md">
67 <Img
68 style={{ height: '30vh' }}
69 fluid={frontmatter.cover.childImageSharp.fluid}
70 />
71 </div>
72 ) : null}
73 <div className="mt-8 text-base font-light">
74 <MDXRenderer>{body}</MDXRenderer>
75 </div>
76 </section>
77 </Layout>
78 </MDXProvider>
79 )
80}
81
82...

Your blog post's should now display a cover image on every page.

Add Google Analytics

This is a great way to monitor traffic to your site and on your blog posts. It also enables to you see where your traffic is coming from. Google Analytics is free up to c. 10 million hits per month per ID. I don't know about you but I'm not expecting that kind of traffic on my site, if you are then you may want to consider looking at the pricing options to avoid your service getting suspended.

First of all you want to sign up and get a Google Analytics account. You can do that with your normal Google account here.

Once you've set up an account, you'll be prompted to create a new property which is equivalent to your new website. You'll need to include your site's name and URL at this point which means you will have had to already deployed your site in part 1 - if you haven't you can follow the steps to do that here.

Once you have created a new "property" you can access your tracking code by navigating to Admin > Tracking Info > Tracking Code. The code will be a number similar to UA-XXXXXXXXX-X.

Now that you have your tracking code, let's install the Google Analytics plugin for Gatsby.

1npm i gatsby-plugin-google-analytics

Now, all you need to do it update your gatsby-config.js file.

1...
2 {
3 resolve: `gatsby-plugin-google-analytics`,
4 options: {
5 // replace "UA-XXXXXXXXX-X" with your own Tracking ID
6 trackingId: "UA-XXXXXXXXX-X",
7 },
8 },
9...

It can occasionally take a bit of time for statistics on Google Analytics to populate but you should start to see user data shortly after following the instructions above and deploying your site.

Summary

That's it! 🎉

You should now have a fully functioning portfolio and blog that you have created from scratch using Gatsby, Tailwind CSS and Framer.

The site should be set up a way that enables you to update project work you have created, create a new blog post or update your social media links all from a single .mdx or config file. Making the time and effort required for you to now update your portfolio as minimal as possible.

If you've found this series helpful, let me know and connect with me on Twitter at @danielpnorris for more content related to technology.