DN

How to build a portfolio using Gatsby - part 1

Dan Norris / 24th Aug 2020

11 min read

[Live Demo]

Hey, welcome to this two part series where I'll walk you through how to build your first portfolio with Gatsby, Tailwind CSS and Framer Motion.

This is broken down into two parts; the first covers everything you need to know to get going on building your basic portfolio and projects overview; the second part takes a bit of a deeper dive into one particular way you could choose to build a blog with Gatsby using MDX.

Like with most things in tech, there is a lot of existing content out there on similar topics but on my travels I couldn't find a complete joined up tutorial covering the two or with the technology stack I wanted to use. This was especially true when I was trying to add additional functionality to my blog such as code blocks, syntax highlighting and other features.

A small caveat; I'm no expert but I have just gone through this very process building my own portfolio, which you can take a look at here, and blog and a large part of the writing process for me is improving my own understanding of a topic.

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 from scratch without the aid of a starter.

What will this cover?

We'll cover the following:

Part 1

  • Setting up
  • Configuring Tailwind CSS
  • Create site config file
  • Create layout component
  • Create header component
  • Create icon component and helper function
  • Create footer component
  • Create a hero component
  • Implement MDX into your site
  • Make your first GraphQL query
  • Set up image plugins
  • Create an about component
  • Create projects component
  • Create a contact me component
  • Making your portfolio responsive
  • Using Framer Motion to animate your components
  • Deployment using Netlify
  • Summary

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 Gatsby?

There were three main reasons for me why I ended up choosing Gatsby compared to many of the other Static Site Generators out there like Jekyll, Next.js, Hugo or even a SSG at all.

  • It's built on React

    You can leverage all the existing capability around component development React provides and bundle it with the added functionality that Gatsby provides.

  • A lot of configuration and tooling comes free

    This was a huge draw for me. I wanted a solution for my portfolio that was quick to get off the ground and once completed, I could spent as little time as possible updating it or including a new blog post. The Developer experience is rather good and you get things like hot reloading and code splitting for free so you can spend less time on configuration and more on development.

  • The Gatsby ecosystem is really mature

    There's a lot of useful information available to get you started which helps as a beginner. On top of that, the Gatsby plugin system makes common tasks like lazy-loading and image optimisation a quick and straight-forward process.

I migrated my blog from Jekyll originally and haven't looked back. If you're wondering how Gatsby compares to other JAMstack solutions available and whether you should migrate, then you can find out more here.

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.

We're going to build a basic portfolio site that looks like the one below above in the demo. We'll go through how to setup and configure your project to use Tailwind CSS, query and present MDX data sources using GraphQL, add transitions and animations using Framer and later deploy to Netlify.

Setting up

Firstly, we're going to need to install npm and initialise a repository. The -y flag automatically accepts all the prompts during the npm wizard.

1npm init -y && git init

You'll want to exclude some of the project files from being commited to git. Include these files in the .gitignore file.

1// .gitignore
2
3.cache
4node_modules
5public

Now you'll need to install the dependencies you'll need.

1npm i gatsby react react-dom

Part of Gatsby's magic is that you are provided with routing for free. Any .js file that is created within src/pages is automatically generated with it's own url path.

Lets go and create your first page. Create a src/pages/index.js file in your root directory.

Create a basic component for now.

1// index.js
2
3import React from "react";
4
5export default () => {
6 return <div>My Portfolio</div>;
7};

This isn't strictly necessary but it's a small quality of life improvement. Let's create a script in your package.json to run your project locally. The -p specifies the port and helps to avoid conflicts if you are running multiple projects simultaneously.

You can specify any port you want here or choose to omit this. I've chosen port 9090. The -o opens a new browser tab automatically for you.

1// package.json
2
3"scripts": {
4 "run": "gatsby develop -p 9090 -o"
5}

You can run your project locally on your machine now from http://localhost:8000 with hot-reloading already baked in.

1npm run-script run

ESLint, Webpack and Babel are all automatically configured and setup for you as part of Gatsby. This next part is optional but we are going to install Prettier which is a code formatter and will help to keep your code consistent with what we are doing in the tutorial, plus it's prettier. The -D flag installs the package as a developer dependency only.

1npm i -D prettier

Create a .prettierignore and prettier.config.js file in your root directory.

1// .prettierignore
2
3.cache
4package.json
5package-lock.json
6public
1// prettier.config.js
2
3module.exports = {
4 tabWidth: 4,
5 semi: false,
6 singleQuote: true,
7}

The ignore file selects which files to ignore and not format. The second config file imports an options object with settings including the width of tabs in spaces (tabWidth), whether to include semi-colons or not (semi) and whether to convert all quotes to single quotes (singleQuote).

Configuring Tailwind CSS

Let's now install and configure Tailwind. The second command initialises a configuration file which we'll talk about shortly.

1npm i -D tailwindcss && npx tailwindcss init

Now open the new tailwind.config.js file in your root directory and include the following options object.

1// tailwind.config.js
2
3module.exports = {
4 purge: ["./src/**/*.js"],
5 theme: {
6 extend: {},
7 },
8 variants: {},
9 plugins: [],
10}

The config file uses a glob and a Tailwind dependency called PurgeCSS to remove any unused CSS classes from files located in .src/**/*.js . PurgeCSS only performs this on build but will help to make your project more performant. For more info, check out the Tailwind CSS docs here.

Install the PostCSS plugin.

1npm i gatsby-plugin-postcss

Create a postcss.config.js file in root and include the following.

1touch postcss.config.js
1// postcss.config.js
2
3module.exports = () => ({
4 plugins: [require("tailwindcss")],
5})

Create a gatsby-config.js file and include the plugin. This is where all of your plugins will go including any config needed for those plugins.

1touch gatsby-config.js
1// gatsby-config.js
2
3module.exports = {
4 plugins: [`gatsby-plugin-postcss`],
5}

You need to create an index.css file to import Tailwind's directives.

1mkdir -p src/css
2touch src/css/index.css

Then import the directives and include PurgeCSS's whitelist selectors in index.css for best practice.

1/* purgecss start ignore */
2@tailwind base;
3@tailwind components;
4/* purgecss end ignore */
5
6@tailwind utilities;

Finally, create a gatsby-browser.js file in your root and import the styles.

1// gatsby-browser.js
2
3import "./src/css/index.css"

Let's check it works. Open up your index.js file and add the following styles. Now restart your development server. The div tag should have styles applied to it.

1// index.js
2
3export default () => {
4 return <div class="bg-blue-300 text-3xl p-4">My Portfolio</div>
5}

Create site config file

We're going to create a site config file. This isn't specific to Gatsby but enables us to create a single source of truth for all of the sites metadata and will help to minimise the amount of time you need to spend updating the site in the future.

1mkdir -p src/config/
2touch src/config/index.js

Now copy the object below into your file. You can substitute the data for your own.

1// config/index.js
2
3module.exports = {
4 author: "Dan Norris",
5 siteTitle: "Dan Norris - Portfolio",
6 siteShortTitle: "DN",
7 siteDescription:
8 "v2 personal portfolio. Dan is a Software Engineer and based in Bristol, UK",
9 siteLanguage: "en_UK",
10 socialMedia: [
11 {
12 name: "Twitter",
13 url: "https://twitter.com/danielpnorris",
14 },
15 {
16 name: "LinkedIn",
17 url: "https://www.linkedin.com/in/danielpnorris/",
18 },
19 {
20 name: "Medium",
21 url: "https://medium.com/@dan.norris",
22 },
23 {
24 name: "GitHub",
25 url: "https://github.com/daniel-norris",
26 },
27 {
28 name: "Dev",
29 url: "https://dev.to/danielnorris",
30 },
31 ],
32 navLinks: {
33 menu: [
34 {
35 name: "About",
36 url: "/#about",
37 },
38 {
39 name: "Projects",
40 url: "/#projects",
41 },
42 {
43 name: "Contact",
44 url: "/#contact",
45 },
46 ],
47 button: {
48 name: "Get In Touch",
49 url: "/#contact",
50 },
51 },
52}

Create layout component

We're now going to create a layout component which will act as a wrapper for any further page content to the site.

Create a new component at src/components/Layout.js and add the following:

1import React from "react"
2import PropTypes from "prop-types"
3
4const Layout = ({ children }) => {
5 return (
6 <div
7 className="min-h-full grid"
8 style={{
9 gridTemplateRows: "auto 1fr auto",
10 }}
11 >
12 <header>My Portfolio</header>
13 <main>{children}</main>
14 <footer>Footer</footer>
15 </div>
16 )
17}
18
19Layout.propTypes = {
20 children: PropTypes.any,
21}
22
23export default Layout

Tailwind provides us a utility based CSS framework which is easily extensible and you don't have to fight to override. We've created a wrapper div here that has a min height of 100% and created a grid with three rows for our header, footer and the rest of our content.

This will ensure our footer stays at the bottom of the page once we start adding content. We'll break this into smaller sub-components shortly.

Now let's import this component into our main index.js page and pass some text as a child prop to our Layout component for now.

1import React from "react"
2import Layout from "../components/Layout"
3
4export default () => {
5 return (
6 <Layout>
7 <main>This is the hero section.</main>
8 </Layout>
9 )
10}

Create a header component

Let's now create a sub component for the header at src/components/Header.js and some navigation links using our site config.

1// Header.js
2
3import React from "react"
4import { Link } from "gatsby"
5
6import { navLinks, siteShortTitle } from "../config"
7
8const Header = () => {
9 const { menu } = navLinks
10
11 return (
12 <header className="flex items-center justify-between py-6 px-12 border-t-4 border-red-500">
13 <Link to="/" aria-label="home">
14 <h1 className="text-3xl font-bold">
15 {siteShortTitle}
16 <span className="text-red-500">.</span>
17 </h1>
18 </Link>
19 <nav className="flex items-center">
20 {menu.map(({ name, url }, key) => {
21 return (
22 <Link
23 className="text-lg font-bold px-3 py-2 rounded hover:bg-red-100 "
24 key={key}
25 to={url}
26 >
27 {name}
28 </Link>
29 )
30 })}
31 </nav>
32 </header>
33 )
34}
35
36export default Header

We've used the Gatsby Link component to route internally and then iterated over our destructured config file to create our nav links and paths.

Import your new Header component into Layout.

1// Layout.js
2
3import Header from "../components/Header"

Create icon component and helper function

Before we start on the footer, we're going to create an Icon component and helper function that will enable you to use a single class that accepts a name and color prop for all your svg icons.

Create src/components/icons/index.js and src/components/icons/Github.js. We'll use a switch for our helper function.

1// index.js
2
3import React from "react"
4
5import IconGithub from "./Github"
6
7const Icon = ({ name, color }) => {
8 switch (name.toLowerCase()) {
9 case "github":
10 return <IconGithub color={color} />
11 default:
12 return null
13 }
14}
15
16export default Icon

We're using svg icons from https://simpleicons.org/. Copy the svg tag for a Github icon and include it in your Github icon sub component. Then do the same for the remaining social media accounts you set up in your site config file.

1import React from "react"
2import PropTypes from "prop-types"
3
4const Github = ({ color }) => {
5 return (
6 <svg role="img" viewBox="0 0 24 24" fill={color}>
7 <title>GitHub icon</title>
8 <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
9 </svg>
10 )
11}
12
13Github.propTypes = {
14 color: PropTypes.string,
15}
16
17Github.defaultProps = {
18 color: "#000000",
19}
20
21export default Github

Your final index.js should look something like this:

1// index.js
2
3import React from "react"
4
5import IconGithub from "./Github"
6import IconLinkedin from "./Linkedin"
7import IconMedium from "./Medium"
8import IconDev from "./Dev"
9import IconTwitter from "./Twitter"
10
11const Icon = ({ name, color }) => {
12 switch (name.toLowerCase()) {
13 case "github":
14 return <IconGithub color={color} />
15 case "linkedin":
16 return <IconLinkedin color={color} />
17 case "dev":
18 return <IconDev color={color} />
19 case "medium":
20 return <IconMedium color={color} />
21 case "twitter":
22 return <IconTwitter color={color} />
23 default:
24 return null
25 }
26}
27
28export default Icon

Create footer component

Lets now create our footer sub component. Create src/components/Footer.js and copy across:

1import React from "react"
2import { Link } from "gatsby"
3
4import { siteShortTitle } from "../config/index"
5
6const Footer = () => {
7 return (
8 <footer className="flex items-center justify-between bg-red-500 py-6 px-12">
9 <Link to="/" aria-label="home">
10 <h1 className="text-3xl font-bold text-white">{siteShortTitle}</h1>
11 </Link>
12 </footer>
13 )
14}
15
16export default Footer

Let's now iterate over our social media icons and use our new Icon component. Add the following:

1import Icon from "../components/icons/index"
2import { socialMedia, siteShortTitle } from "../config/index"
3
4...
5
6<div className="flex">
7 {socialMedia.map(({ name, url }, key) => {
8 return (
9 <a className="ml-8 w-6 h-6" href={url} key={key} alt={`${name} icon`}>
10 <Icon name={name} color="white" />
11 </a>
12 )
13 })}
14</div>
15
16...

Create a hero component

We're going to create a hero for your portfolio site now. In order to inject a bit of personality into this site, we're going to use a svg background from http://www.heropatterns.com/ called "Diagonal Lines". Feel free to choose anything you like.

Let's extend our Tailwind CSS styles and add a new class.

1.bg-pattern {
2 background-color: #fff5f5;
3 background-image: url("data:image/svg+xml,%3Csvg width='6' height='6' viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23f56565' fill-opacity='0.4' fill-rule='evenodd'%3E%3Cpath d='M5 0h1L0 6V5zM6 5v1H5z'/%3E%3C/g%3E%3C/svg%3E");
4}

Create a new Hero.js component and let's start to build out our hero section.

1import React from "react"
2import { Link } from "gatsby"
3import { navLinks } from "../config/index"
4
5const Hero = ({ content }) => {
6 const { button } = navLinks
7
8 return (
9 <div className="flex items-center bg-pattern shadow-inner min-h-screen">
10 <div className="bg-white w-full py-6 shadow-lg">
11 <section class="mx-auto container w-3/5">
12 <h1 className="uppercase font-bold text-lg text-red-500">
13 Hi, my name is
14 </h1>
15 <h2 className="font-bold text-6xl">Dan Norris</h2>
16 <p className=" text-2xl w-3/5">
17 I’m a Software Engineer based in Bristol, UK specialising in
18 building incredible websites and applications.
19 </p>
20
21 <Link to={button.url}>
22 <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">
23 {button.name}
24 </button>
25 </Link>
26 </section>
27 </div>
28 </div>
29 )
30}
31
32export default Hero

Implement MDX into your site

Thanks to Gatsby's use of GraphQL as a data management layer, you can incorporate a lot of different data sources into your site including various headless CMS's. We're going to use MDX for our portfolio.

It enables us to put all of our text content and images together into a single query, provides the ability to extend the functionality of your content with React and JSX and for that reason is a great solution for long-form content like blog posts. We're going to start by installing:

1npm install gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react gatsby-source-filesystem

We'll put all of our .mdx content into its own file.

1mkdir -p src/content/hero
2touch src/content/hero/hero.mdx

Let's add some content to the hero.mdx file.

1---
2intro: "Hi, my name is"
3title: "Dan Norris"
4---
5
6I’m a Software Engineer based in Bristol, UK specialising in building incredible websites and applications.

We'll need to configure these new plugins in our gatsby-config.js file. Add the following.

1// gatsby-config.js
2
3module.exports = {
4 plugins: [
5 `gatsby-plugin-postcss`,
6 `gatsby-plugin-mdx`,
7 {
8 resolve: `gatsby-source-filesystem`,
9 options: {
10 name: `content`,
11 path: `${__dirname}/src/content`,
12 },
13 },
14 ],
15}

Make your first GraphQL query

Now that we are able to use .mdx files, we need to create a query to access the data. Run your development server and go to http://localhost:9090/___graphql. Gatsby has a GUI that enables you to construct your data queries in the browser.

Once we've created our query, we'll pass this into a template literal which will pass the whole data object as a prop to our component. Your index.js should now look like this:

1// index.js
2
3import React from "react"
4import Layout from "../components/Layout"
5import Hero from "../components/Hero"
6import { graphql } from "gatsby"
7
8export default ({ data }) => {
9 return (
10 <Layout>
11 <Hero content={data.hero.edges} />
12 </Layout>
13 )
14}
15
16export const pageQuery = graphql`
17 {
18 hero: allMdx(filter: { fileAbsolutePath: { regex: "/hero/" } }) {
19 edges {
20 node {
21 body
22 frontmatter {
23 intro
24 title
25 }
26 }
27 }
28 }
29 }
30`

We'll need to import MDXRenderer from gatsby-plugin-mdx to render the body text from the mdx file. Your Hero.js should now look like this:

1import React from "react"
2import { Link } from "gatsby"
3import { MDXRenderer } from "gatsby-plugin-mdx"
4import { navLinks } from "../config/index"
5
6const Hero = ({ content }) => {
7 const { frontmatter, body } = content[0].node
8 const { button } = navLinks
9
10 return (
11 <div className="flex items-center bg-pattern shadow-inner min-h-screen">
12 <div className="bg-white w-full py-6 shadow-lg">
13 <section class="mx-auto container w-4/5">
14 <h1 className="uppercase font-bold text-lg text-red-500">
15 {frontmatter.intro}
16 </h1>
17 <h2 className="font-bold text-6xl">{frontmatter.title}</h2>
18 <p className="font-thin text-2xl w-3/5">
19 <MDXRenderer>{body}</MDXRenderer>
20 </p>
21
22 <Link to={button.url}>
23 <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">
24 {button.name}
25 </button>
26 </Link>
27 </section>
28 </div>
29 </div>
30 )
31}
32
33export default Hero

Set up image plugins

We're going to need to load an image for our about page, so we'll use the gatsby-image to achieve this. It provides lazy-loading, image optimisation and additional processing features like blur-up and svg outlining with minimal effort.

1npm install gatsby-transformer-sharp gatsby-plugin-sharp gatsby-image

We need to include these new plugins into our config file.

1// gatsby-config.js
2
3module.exports = {
4 plugins: [`gatsby-plugin-sharp`, `gatsby-transformer-sharp`],
5}

We should now be able to query and import images using gatsby-image that are located in the src/content/ folder that gatsby-source-filesystem is pointing at in your gatsby-config.js file. Let's try by making our about section.

Create an about component

Let's start by creating a new mdx file for our content in src/content/about/about.mdx. I've used one of my images for the demo but you can use your own or download one here from https://unsplash.com/. It needs to be placed into the same directory as your about.mdx file.

1---
2title: About Me
3image: avatar.jpeg
4caption: Avon Gorge, Bristol, UK
5---
6
7Hey, I’m Dan. I live in Bristol, UK and I’m a Software Engineer at LexisNexis, a FTSE100 tech company that helps companies make better decisions by building applications powered by big data.
8
9I have a background and over 5 years experience as a Principal Technical Recruiter and Manager. Some of my clients have included FTSE100 and S&amp;P500 organisations including Marsh, Chubb and Hiscox.
10
11After deciding that I wanted to shift away from helping companies sell their tech enabled products and services and start building them myself, I graduating from a tech accelerator called DevelopMe\_ in 2020 and requalified as a Software Engineer. I enjoy creating seamless end-to-end user experiences and applications that add value.
12
13In my free time you can find me rock climbing around local crags here in the UK and trying to tick off all the 4,000m peaks in the Alps.

Now, let's extend our GraphQL query on our index.js page to include data for our about page. You'll also need to import and use the new About component. Make these changes to your index.js file.

1// index.js
2
3import About from '../components/About'
4
5...
6
7<About content={data.about.edges} />
8
9...
10
11export const pageQuery = graphql`
12 {
13 hero: allMdx(filter: { fileAbsolutePath: { regex: "/hero/" } }) {
14 edges {
15 node {
16 body
17 frontmatter {
18 intro
19 title
20 }
21 }
22 }
23 }
24 about: allMdx(filter: { fileAbsolutePath: { regex: "/about/" } }) {
25 edges {
26 node {
27 body
28 frontmatter {
29 title
30 caption
31 image {
32 childImageSharp {
33 fluid(maxWidth: 800) {
34 ...GatsbyImageSharpFluid
35 }
36 }
37 }
38 }
39 }
40 }
41 }
42 }
43`

Let's go make our About component now. You'll need to import MDXRenderer again for the body of your mdx file. You'll also need to import an Img component from gatsby-image.

1import React from "react"
2import { MDXRenderer } from "gatsby-plugin-mdx"
3import Img from "gatsby-image"
4
5const About = ({ content }) => {
6 const { frontmatter, body } = content[0].node
7
8 return (
9 <section id="about" className="my-6 mx-auto container w-3/5">
10 <h3 className="text-3xl font-bold mb-6">{frontmatter.title}</h3>
11 <div className=" font-light text-lg flex justify-between">
12 <div className="w-1/2">
13 <MDXRenderer>{body}</MDXRenderer>
14 </div>
15 <div className="w-1/2">
16 <figure className="w-2/3 mx-auto">
17 <Img fluid={frontmatter.image.childImageSharp.fluid} />
18 <figurecaption className="text-sm">
19 {frontmatter.caption}
20 </figurecaption>
21 </figure>
22 </div>
23 </div>
24 </section>
25 )
26}
27
28export default About

You might have noticed that your body text isn't displaying properly and doesn't have any line breaks. If you used the default syntax for Markdown for things like ## Headings then the same thing would happen; no styling would occur.

Let's fix that now and import a component called MDXProvider which will allow us to define styling for markdown elements. You could choose to link this up to already defined React components but we're just going to do it inline. Your Layout.js file should now look like this.

1import React from "react"
2import PropTypes from "prop-types"
3import { MDXProvider } from "@mdx-js/react"
4import Header from "../components/Header"
5import Footer from "../components/Footer"
6
7const Layout = ({ children }) => {
8 return (
9 <MDXProvider
10 components={{
11 p: props => <p {...props} className="mt-4" />,
12 }}
13 >
14 <div
15 className="min-h-full grid"
16 style={{
17 gridTemplateRows: "auto 1fr auto",
18 }}
19 >
20 <Header />
21 <main>{children}</main>
22 <Footer />
23 </div>
24 </MDXProvider>
25 )
26}
27
28Layout.propTypes = {
29 children: PropTypes.any,
30}
31
32export default Layout

Create projects component

Alrite, alrite, alrite. We are about halfway through.

Most of the configuration is now done for the basic portfolio, so let's go ahead and create the last two sections. Let's create some example projects that we want to feature on the front page of our portfolio.

Create a new file src/content/project/<your-project>/<your-project>.mdx for instance and an accompanying image for your project. I'm calling mine "Project Uno".

1---
2title: 'Project Uno'
3category: 'Featured Project'
4screenshot: './project-uno.jpg'
5github: 'https://github.com/daniel-norris'
6external: 'https://www.danielnorris.co.uk'
7tags:
8 - React
9 - Redux
10 - Sass
11 - Jest
12visible: 'true'
13position: 0
14---
15
16Example project, designed to solve customer's X, Y and Z problems. Built with Foo and Bar in mind and achieved over 100% increase in key metric.

Now do the same for two other projects.

Once you're done, we'll need to create an additional GraphQL query for the project component. We'll want to filter out any other files in the content directory that are not associated with projects and only display projects that have a visible frontmatter attribute equal to true. Let's all sort the data by their position frontmatter value in ascending order.

Add this query to your index.js page.

1project: allMdx(
2 filter: {
3 fileAbsolutePath: { regex: "/project/" }
4 frontmatter: { visible: { eq: "true" } }
5 }
6 sort: { fields: [frontmatter___position], order: ASC }
7 ) {
8 edges {
9 node {
10 body
11 frontmatter {
12 title
13 visible
14 tags
15 position
16 github
17 external
18 category
19 screenshot {
20 childImageSharp {
21 fluid {
22 ...GatsbyImageSharpFluid
23 }
24 }
25 }
26 }
27 }
28 }
29 }

Let's now create our Project component. You'll need to iterate over the content object to display all of the projects you have just created.

1import React from "react"
2import { MDXRenderer } from "gatsby-plugin-mdx"
3import Icon from "../components/icons/index"
4import Img from "gatsby-image"
5
6const Project = ({ content }) => {
7 return (
8 <section id="projects" className="my-8 w-3/5 mx-auto">
9 {content.map((project, key) => {
10 const { body, frontmatter } = project.node
11
12 return (
13 <div className="py-8 flex" key={frontmatter.position}>
14 <div className="w-1/3">
15 <h1 className="text-xs font-bold uppercase text-red-500">
16 {frontmatter.category}
17 </h1>
18 <h2 className="text-3xl font-bold mb-6">{frontmatter.title}</h2>
19 <div className=" font-light text-lg flex justify-between">
20 <div>
21 <MDXRenderer>{body}</MDXRenderer>
22 <div className="flex text-sm font-bold text-red-500 ">
23 {frontmatter.tags.map((tag, key) => {
24 return <p className="mr-2 mt-6">{tag}</p>
25 })}
26 </div>
27 <div className="flex mt-4">
28 <a href={frontmatter.github} className="w-8 h-8 mr-4">
29 <Icon name="github" />
30 </a>
31 <a href={frontmatter.external} className="w-8 h-8">
32 <Icon name="external" />
33 </a>
34 </div>
35 </div>
36 </div>
37 </div>
38 <div className="w-full py-6">
39 <Img fluid={frontmatter.screenshot.childImageSharp.fluid} />
40 </div>
41 </div>
42 )
43 })}
44 </section>
45 )
46}
47
48export default Project

I've created an additional External.js icon component for the external project links. You can find additional svg icons at https://heroicons.dev/.

Let's now import this into our index.js file and pass it the data object as a prop.

1import Project from "../components/Project"
2
3export default ({ data }) => {
4 return (
5 <Layout>
6 ...
7 <Project content={data.project.edges} />
8 ...
9 </Layout>
10 )
11}

Create a contact me component

The final section requires us to build out a contact component. You could do this in a few ways but we're just going to include a button with a mailto link for now.

Let's start by creating a contact.mdx file.

1---
2title: Get In Touch
3callToAction: Say Hello
4---
5
6Thanks for working through this tutorial.
7
8It's always great to hear feedback on what people think of your content and or even how you may have used this tutorial to build your own portfolio using Gatsby.
9
10Ways you could show your appreciation 🙏 include: dropping me an email below and let me know what you think, leave a star ⭐ on the GitHub repository or send me a message on Twitter 🐤.

Create a new GraphQL query for the contact component.

1contact: allMdx(filter: { fileAbsolutePath: { regex: "/contact/" } }) {
2edges {
3node {
4frontmatter {
5title
6callToAction
7}
8body
9}
10}
11}

Let's now create a Contact.js component.

1import React from "react"
2import { MDXRenderer } from "gatsby-plugin-mdx"
3
4const Contact = ({ content }) => {
5 const { frontmatter, body } = content[0].node
6
7 return (
8 <section
9 id="contact"
10 className="mt-6 flex flex-col items-center justify-center w-3/5 mx-auto min-h-screen"
11 >
12 <div className="w-1/2">
13 <h3 className="text-5xl font-bold mb-6 text-center">
14 {frontmatter.title}
15 </h3>
16 <div className="text-lg font-thin">
17 <MDXRenderer>{body}</MDXRenderer>
18 </div>
19 </div>
20 <a href="mailto:dan.norris@hotmail.com">
21 <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">
22 {frontmatter.callToAction}
23 </button>
24 </a>
25 </section>
26 )
27}
28
29export default Contact

The last thing to do is import it into the index.js file.

1import Contact from "../components/Contact"
2
3export default ({ data }) => {
4 return (
5 <Layout>
6 ...
7 <Contact content={data.contact.edges} />
8 ...
9 </Layout>
10 )
11}

Making your portfolio responsive

If we inspect our site using Chrome F12 then we can see that not all the content is optimised for mobile. The biggest problems appear to be images and spacing around the main sections. Luckily with Tailwind, setting styles for particular breakpoints takes little to no time at all. Let's do that now.

If we take a look at the Header.js component we can see that the nav bar is looking a bit cluttered. Ideally what we would do here is add a hamburger menu button but we're going to keep this simple and add some breakpoints and change the padding.

Tailwind CSS has a number of default breakpoints that you can prefix before classes. They include sm (640px), md (768px), lg (1024px) and xl (1280px). It's a mobile-first framework and so if we set a base style, e.g. sm:p-8 then it will apply padding to all breakpoints over 640px.

Let's improve the header by applying some breakpoints.

1// Header.js
2
3<header className="flex items-center justify-between py-2 px-1 sm:py-6 sm:px-12 border-t-4 border-red-500">
4 ...
5</header>

Let's do the same for the hero component.

1// Hero.js
2
3<div className="flex items-center bg-pattern shadow-inner min-h-screen">
4 ...
5 <section class="mx-auto container w-4/5 sm:w-3/5">
6 ...
7 <p className="font-thin text-2xl sm:w-4/5">
8 <MDXRenderer>{body}</MDXRenderer>
9 </p>
10 ...
11 </section>
12 ...
13</div>

Your projects component will now look like this.

1import React from "react"
2import { MDXRenderer } from "gatsby-plugin-mdx"
3import Icon from "../components/icons/index"
4import Img from "gatsby-image"
5
6const Project = ({ content }) => {
7 return (
8 <section id="projects" className="my-8 w-4/5 md:w-3/5 mx-auto">
9 {content.map((project, key) => {
10 const { body, frontmatter } = project.node
11
12 return (
13 <div className="py-8 md:flex" key={frontmatter.position}>
14 <div className="md:w-1/3 mr-4">
15 <h1 className="text-xs font-bold uppercase text-red-500">
16 {frontmatter.category}
17 </h1>
18 <h2 className="text-3xl font-bold mb-6">{frontmatter.title}</h2>
19 <div className="md:hidden">
20 <Img fluid={frontmatter.screenshot.childImageSharp.fluid} />
21 </div>
22 <div className=" font-light text-lg flex justify-between">
23 <div>
24 <MDXRenderer>{body}</MDXRenderer>
25 <div className="flex text-sm font-bold text-red-500 ">
26 {frontmatter.tags.map((tag, key) => {
27 return <p className="mr-2 mt-6">{tag}</p>
28 })}
29 </div>
30 <div className="flex mt-4">
31 <a href={frontmatter.github} className="w-8 h-8 mr-4">
32 <Icon name="github" />
33 </a>
34 <a href={frontmatter.external} className="w-8 h-8">
35 <Icon name="external" />
36 </a>
37 </div>
38 </div>
39 </div>
40 </div>
41 <div className="hidden md:block w-full py-6">
42 <Img fluid={frontmatter.screenshot.childImageSharp.fluid} />
43 </div>
44 </div>
45 )
46 })}
47 </section>
48 )
49}
50
51export default Project

Finally, your contact component should look something like this.

1import React from "react"
2import { MDXRenderer } from "gatsby-plugin-mdx"
3
4const Contact = ({ content }) => {
5 const { frontmatter, body } = content[0].node
6
7 return (
8 <section
9 id="contact"
10 className="mt-6 flex flex-col items-center justify-center w-4/5 sm:w-3/5 mx-auto min-h-screen"
11 >
12 <div className="sm:w-1/2">
13 <h3 className="text-5xl font-bold mb-6 text-center">
14 {frontmatter.title}
15 </h3>
16 <div className="text-lg font-thin">
17 <MDXRenderer>{body}</MDXRenderer>
18 </div>
19 </div>
20 <a href="mailto:dan.norris@hotmail.com">
21 <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">
22 {frontmatter.callToAction}
23 </button>
24 </a>
25 </section>
26 )
27}
28
29export default Contact

Using Framer Motion to animate your components

Framer is an incredibly simple and straight forward way to animate your React projects. It's API is well documented and can be found here. Motion enables you to declaratively add animations and gestures to any html or svg element.

For simple use cases, all you need to do is import the motion component and pass it a variants object with your start and end state values. Let's do that now and stagger transition animations for the header and hero components. Add this to your Header.js component and swap our the header element for your new motion.header component.

1// Header.js
2
3import { motion } from 'framer-motion'
4
5...
6
7const headerVariants = {
8 hidden: {
9 opacity: 0,
10 y: -10,
11 },
12 display: {
13 opacity: 1,
14 y: 0,
15 },
16}
17
18...
19
20<motion.header
21 className="flex items-center justify-between py-2 px-1 sm:py-6 sm:px-12 border-t-4 border-red-500"
22 variants={headerVariants}
23 initial="hidden"
24 animate="display">
25 ...
26</motion.header>

Let's do the same with the Hero.js component. Except this time, we'll add an additional transition prop to each element with an incremental delay to make the animation stagger. Your final Hero.js component should look like this.

1import React from "react"
2import { Link } from "gatsby"
3import { MDXRenderer } from "gatsby-plugin-mdx"
4import { navLinks } from "../config/index"
5import { motion } from "framer-motion"
6
7const Hero = ({ content }) => {
8 const { frontmatter, body } = content[0].node
9 const { button } = navLinks
10
11 const variants = {
12 hidden: {
13 opacity: 0,
14 x: -10,
15 },
16 display: {
17 opacity: 1,
18 x: 0,
19 },
20 }
21
22 return (
23 <div className="flex items-center bg-pattern shadow-inner min-h-screen">
24 <div className="bg-white w-full py-6 shadow-lg">
25 <section class="mx-auto container w-4/5 sm:w-3/5">
26 <motion.h1
27 className="uppercase font-bold text-lg text-red-500"
28 variants={variants}
29 initial="hidden"
30 animate="display"
31 transition={{ delay: 0.6 }}
32 >
33 {frontmatter.intro}
34 </motion.h1>
35 <motion.h2
36 className="font-bold text-6xl"
37 variants={variants}
38 initial="hidden"
39 animate="display"
40 transition={{ delay: 0.8 }}
41 >
42 {frontmatter.title}
43 </motion.h2>
44 <motion.p
45 className="font-thin text-2xl sm:w-4/5"
46 variants={variants}
47 initial="hidden"
48 animate="display"
49 transition={{ delay: 1 }}
50 >
51 <MDXRenderer>{body}</MDXRenderer>
52 </motion.p>
53
54 <Link to={button.url}>
55 <motion.button
56 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"
57 variants={variants}
58 initial="hidden"
59 animate="display"
60 transition={{ delay: 1.2 }}
61 >
62 {button.name}
63 </motion.button>
64 </Link>
65 </section>
66 </div>
67 </div>
68 )
69}
70
71export default Hero

Deployment using Netlify

We're nearly there. All that is left to do is push your finished project to GitHub, GitLab or BitBucket and deploy it. We're going to use Netlify to deploy our site. One of the advantages of using a Static Site Generator for your portfolio is that you can use a service like Netlify to host it.

This brings a lot of benefits; not only is it extremely easy to use but it has continuous deployment setup automatically. So, if you ever make any changes to your site and push to your master branch - it will automatically update the production version for you.

If you head over to https://app.netlify.com/ and choose "New site from git" you'll be asked to choose your git provider.

The next page should be automatically populated with the correct information but just in case, it should read as:

  • Branch to deploy: "master"
  • Build command: "gatsby build"
  • Publish directory: public/

Once you've done that click deploy and voila!

Summary

Well congratulations for making it this far. You should now have made, completely from scratch, your very own portfolio site using Gatsby. You have covered all of the following functionality using Gatsby:

  • Installation and setting up
  • Configuring Tailwind CSS using PostCSS and Purge CSS
  • Building layouts
  • Creating helper functions
  • Querying with GraphQL and using the Gatsby GUI
  • Implementing MDX
  • Working with images in Gatsby
  • Making your site responsive using Tailwind CSS
  • Deploying using Netlify

You have a basic scaffold from which you can go ahead and extend as you see fit. The full repository and source code for this project is available here.

If you've found this tutorial helpful, then please let me know. You can connect with me on Twitter at @danielpnorris.