How to build a portfolio using Gatsby - part 2
Dan Norris / 6th Sep 2020
10 min read
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 Markdown23<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"34export 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...23{4 resolve: `gatsby-source-filesystem`,5 options: {6 name: `posts`,7 path: `${__dirname}/src/content/posts`,8 },9},1011...
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/posts2touch 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 13subtitle: Blogging with MDX and Gatsby4date: 2020-08-185published: true6featured: true7cover: ""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.1011Rigging 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.1213Landlubber 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")23exports.onCreateNode = ({ node, actions, getNode }) => {4 const { createNodeField } = actions56 // only applies to mdx nodes7 if (node.internal.type === "Mdx") {8 const value = createFilePath({ node, getNode })910 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 you15 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")23exports.createPages = async ({ graphql, actions, reporter }) => {4 // Destructure the createPage function from the actions object5 const { createPage } = actions67 const result = await graphql(`8 query {9 allMdx {10 edges {11 node {12 id13 fields {14 slug15 }16 }17 }18 }19 }20 `)2122 // Create blog post pages.23 const posts = result.data.allMdx.edges2425 // you'll call `createPage` for each result26 posts.forEach(({ node }, index) => {27 createPage({28 // This is the slug you created before29 path: node.fields.slug,30 // This component will wrap our MDX content31 component: path.resolve(`./src/templates/blogPost.js`),32 // You can use the values in this context in33 // our page layout component34 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"45export default ({ data }) => {6 const { frontmatter, body } = data.mdx78 return (9 <Layout>10 <section11 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>1819 <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 id5 body6 timeToRead7 frontmatter {8 title9 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:
- Blog 1 ⇒ http://localhost:9090/blog/posts/blog-1/
- Blog 2 ⇒ http://localhost:9090/blog/posts/blog-2/
- Blog 3 ⇒ http://localhost:9090/blog/posts/blog-3/
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 id5 body6 timeToRead7 frontmatter {8 title9 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.mdx3 ...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"34const Blog = ({ data }) => {5 return (6 <Layout>7 <section8 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 draft14 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}2122export 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 slug14 }15 body16 timeToRead17 frontmatter {18 title19 date(formatString: "Do MMM")20 }21 id22 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'34const Posts = ({ content }) => {5 return (6 <section7 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>1213 {content.map((posts, key) => {14 const {15 excerpt,16 id,17 body,18 frontmatter,19 timeToRead,20 fields,21 } = posts.node2223 return (24 <Link to={fields.slug}>25 <section26 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 • {timeToRead} min read35 </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 read46 </p>47 </section>48 </Link>49 )50 })}51 </section>52 )53}5455export 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"56const Blog = ({ data }) => {7 return (8 <Layout>9 <section10 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 draft16 crow's nest strike colors bounty lad ballast.17 </p>18 </section>19 <Post content={data.posts.edges} />20 </Layout>21 )22}2324export default Blog2526export 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 slug39 }40 body41 timeToRead42 frontmatter {43 title44 date(formatString: "Do MMM")45 }46 id47 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 slug13 }14 frontmatter {15 date(formatString: "Do MMM")16 title17 }18 excerpt(pruneLength: 100)19 id20 body21 timeToRead22 }23 }24 }25...
Now, let's create a FeaturedPosts.js
component.
1import React from "react"2import { Link } from "gatsby"34const 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>89 {content.map((featured, key) => {10 const {11 excerpt,12 id,13 body,14 frontmatter,15 timeToRead,16 fields,17 } = featured.node1819 return (20 <Link to={fields.slug}>21 <section22 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 • {timeToRead} min read31 </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 read40 </p>41 </section>42 </Link>43 )44 })}45 </section>46 )47}4849export default FeaturedPosts
Let's now import the new component into blog.js
.
1...2 const Blog = ({ data }) => {3 return (4 <Layout>5 <section6 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 boat12 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...1415 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 slug26 }27 frontmatter {28 date(formatString: "Do MMM")29 title30 }31 excerpt(pruneLength: 100)32 id33 body34 timeToRead35 }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'34const 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 More11 </button>12 </Link>13 )}14 ...15 )16}1718export 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 13subtitle: Blogging with MDX and Gatsby4date: 2020-08-185published: true6featured: false7cover: ''8---910...
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"56export default ({ data }) => {7 const { frontmatter, body, timeToRead } = data.mdx89 return (10 <MDXProvider11 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 <strong20 {...props}21 className="font-bold"22 style={{ display: "inline" }}23 />24 ),25 a: props => (26 <a27 {...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 <div37 {...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 <section46 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}6465export const pageQuery = graphql`66 query BlogPostQuery($id: String) {67 mdx(id: { eq: $id }) {68 id69 body70 timeToRead71 frontmatter {72 title73 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...23import React from 'react'4import { MDXProvider } from '@mdx-js/react'5import Highlight, { defaultProps } from 'prism-react-renderer'67const components = {8 ...9}1011export 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...23import React from 'react'4import { MDXProvider } from '@mdx-js/react'5import Highlight, { defaultProps } from 'prism-react-renderer'67const 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}2627export 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>.*)/)67 return (8 <Highlight9 {...defaultProps}10 code={props.children.props.children.trim()}11 language={12 matches && matches.groups && matches.groups.lang13 ? matches.groups.lang14 : ''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"67const components = {8 pre: props => {9 const className = props.children.props.className || ""10 const matches = className.match(/language-(?<lang>.*)/)1112 return (13 <Highlight14 {...defaultProps}15 code={props.children.props.children.trim()}16 language={17 matches && matches.groups && matches.groups.lang18 ? matches.groups.lang19 : ""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}3839export 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 id5 body6 timeToRead7 frontmatter {8 title9 date(formatString: "Do MMM YYYY")10 cover {11 childImageSharp {12 fluid(traceSVG: { color: "#F56565" }) {13 ...GatsbyImageSharpFluid_tracedSVG14 }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 33subtitle: Blogging with MDX and Gatsby4date: 2020-08-315published: true6featured: true7cover: './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'34export default ({ data }) => {5 const { frontmatter, body, timeToRead } = data.mdx67 return (8 <MDXProvider9 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 <strong24 {...props}25 className="font-bold"26 style={{ display: 'inline' }}27 />28 ),2930 a: (props) => (31 <a32 {...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 <div42 {...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 <section51 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 read63 </p>64 </div>65 {frontmatter.cover && frontmatter.cover ? (66 <div className="my-8 shadow-md">67 <Img68 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}8182...
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 ID6 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.