A step-by-step tutorial on how to build a portfolio with Gatsby
You can view the final site here
- Fork & clone this repository
- Change directories
cd building-a-portfolio-with-gatsby
- Install dependencies with
yarn
- Run
git checkout step-1
- Start the development server
gatsby develop
- Your site should look like this:
If you ever need to revert to this state, you can use git checkout step-1
to check out the tag for this state.
We want to create a navigation sidebar with four links:
- Home
- Writing
- Speaking
- Podcasting
- Open
nav.js
from the components folder. - Let's add some semantic HTML for a navigation component as the return value for our component:
<nav className="nav">
<h3 className="nav__title">My Portfolio</h3>
<ul className="nav__list">
<li>Home</li>
<li>Writing</li>
<li>Speaking</li>
<li>Podcasting</li>
</ul>
</nav>
I won't be covering CSS in this tutorial, but all the CSS you need is located in the respective .css
files. To use the default styling, be sure to include the class names.
- Now we need to turn these navigation items into links. You might notice we've already imported the Link component from
gatsby
.
import { Link } from "gatsby"
It's important to replace local links in your Gatsby project with the <Link>
element. If we use the anchor tag, <a>
for internal routing, it will cause a re-render of our content. We don't want this, because we want our site to be fast! So we use <Link>
. You can read more about that in the Gatsby Documentation.
<li><Link>Home</Link></li>
<li><Link>Writing</Link></li>
<li><Link>Speaking</Link></li>
<li><Link>Podcasting</Link></li>
- Each
<Link>
element needs to know where to route to on click, and we do this with theto
attribute. Let's add this attribute to all of our links:
<li><Link to="/" className="nav__link">Home</Link></li>
<li><Link to="/writing" className="nav__link">Writing</Link></li>
<li><Link to="/speaking" className="nav__link">Speaking</Link></li>
<li><Link to="/podcasting" className="nav__link">Podcasting</Link></li>
-
If we head over to
layout.js
, we can see we have already imported the navigation component and rendered it above the main content area. Your site should look like this: -
The last thing we want to do is apply a special style to the selected navigation link. There are two ways we can approach this:
- Use inline styling (CSS-in-JS)
- Use a class name with an external stylesheet
To apply an inline style when the link is active, we can use activeStyle
.
To apply an active class name when the link is active, we can use activeClassName
.
For this tutorial we'll use the second approach of applying an active class, but either approach is valid.
Add the following attribute to each link:
activeClassName="nav__link--active"
Your nav.js
file should now look like this:
import React from "react";
import { Link } from "gatsby";
import "./nav.css";
const Nav = () => (
<nav className="nav">
<h3 className="nav__title">My Portfolio</h3>
<ul className="nav__list">
<li><Link to="/" className="nav__link" activeClassName="nav__link--active">Home</Link></li>
<li><Link to="/writing" className="nav__link" activeClassName="nav__link--active">Writing</Link></li>
<li><Link to="/speaking" className="nav__link" activeClassName="nav__link--active">Speaking</Link></li>
<li><Link to="/podcasting" className="nav__link" activeClassName="nav__link--active">Podcasting</Link></li>
</ul>
</nav>
)
export default Nav;
If you ever need to revert to this state, you can use git checkout step-2
to check out the tag for this state.
Having a blog on your portfolio is a great way to showcase your work. So let's add a blog to our portfolio!
Gatsby works with GraphQL to do all sorts of robust data fetching. We won't delve into GraphQL in this blog post, but you can check out the documentation.
When you run gatsby develop
, you might have noticed two URLs posted in the terminal:
- The development server: http://localhost:8000/
- The GraphiQL playground: http://localhost:8000/___graphql
GraphiQL allows you to play around with GraphQL queries in your browser. So let's make some queries.
- Open http://localhost:8000/___graphql in your browser.
Here is what the GraphiQL UI looks like:
I won't be delving into GraphQL or GraphiQL in this post. You can read the online documentation for more information.
- We want to create two queries:
- One query for retrieving a list of all blog posts
- One query for retrieving details and markup for individual blog posts
- First, we need to install a plug-in to work with our markdown files. We will use the gatsby-transformer-remark plug-in to accomplish this task.
yarn add gatsby-transformer-remark gatsby-source-filesystem
- Now that we've installed the plug-in as a dependency, we need to tell Gatsby to actually use it.
Open gatsby-config.js
and inside of the plugins
array, add gatsby-transformer-remark
.
plugins: [
...
`gatsby-transformer-remark`,
...
]
- We also need to configure the plug-in gatsby-source-filesystem. It's already installed as a dependency, so head over to
gatsby-config.js
.
Create a new object inside of the plugins
array and add the following:
{
resolve: 'gatsby-source-filesystem',
options: {
path: `${__dirname}/src/pages`,
name: 'pages',
},
},
This plug-in will transform our markdown files into Markdown Remark
nodes so we can query their HTML and frontmatter.
- Stop and restart your development server. Each time we alter the
gatsby-config.js
file, we have to restart our server.
Now, we're ready to play with some GraphQL queries.
We will use the AllMarkdownRemark
query to retrieve a list of all the blog posts.
-
Inside of GraphiQL, delete the boilerplate comments from the editor.
-
We can use the explorer on the left to select what we want to query for.
Using the explorer in the left-hand sidebar, select the following:
allMarkdownRemark
| edges
> node
> frontmatter
- date
- description
- title
- path
- id
-
Change the name of the query in the main text editor from
MyQuery
toAllBlogPosts
. -
Press the play button to see the query in action.
You should now see data for two blog posts:
- Head over to
writing.js
and let's use our newly created query.
Right above the default export of the writing page, add the following:
export const allBlogsQuery = graphql`
query AllBlogPosts {
allMarkdownRemark {
edges {
node {
frontmatter {
date
description
title
path
}
id
}
}
}
}
`
- You'll notice we already have a de-structured prop being passed into the writing page. This is the data that results from our GraphQL query. We can map over this data using a JSX expression.
To ensure our data is comning in as expected, add the following code underneath the <h1>
tag:
{data.allMarkdownRemark.edges.map(post => (
document.write(post.node.frontmatter.title)
))}
-
If we head over to the browser and go to the writing page, we should see names of our two blog posts:
-
Now we want to actually make this data look nice. We will create a structure for each blog post with the
blogSquare.js
component.
Inside blogSquare.js
, we want to display some data about each blog post:
- title
- date
- path -description
Let's go ahead and add these as de-structured properties on the component. We'll pass them in later.
const BlogSquare = ({ title, date, path, description }) => (
<div></div>
)
- Let's add some semantic markup to display these properties nicely:
<section className="blogSquare">
<h2 className="blogSquare__title">{title}</h2>
<p className="blogSquare__date">{date}</p>
<p>{description}</p>
<p>Read more</p>
</section >
- We want to have each blog square link to the full blog post. So using the
<Link>
component we introduced earlier, let's wrap the inner HTML in a<Link>
component.
We want the blog square to link to the post, so we'll use the path
variable for the to
attribute.:
<section className="blogSquare">
<Link to={path}>
<h2 className="blogSquare__title">{title}</h2>
<p className="blogSquare__date">{date}</p>
<p>{description}</p>
<p>Read more</p>
</Link >
</section >
- Lastly, let's add some inline styling to make this blog square look a little nicer:
<section className="blogSquare">
<Link to={path} style={{
textDecoration: 'none',
color: '#4a4a4a'
}}>
<h2 className="blogSquare__title">{title}</h2>
<p className="blogSquare__date">{date}</p>
<p>{description}</p>
<p style={{ fontSize: '.8em', textDecoration: 'underline' }}>Read more</p>
</Link >
</section >
You can also use class names, as there is a linked stylesheet, but I wanted to demonstrate the inline styling approach.
Here is the final state of blogSquare.js
:
import React from 'react';
import { Link } from "gatsby";
import './blogSquare.css'
const BlogSquare = ({ title, date, path, description }) => (
<section className="blogSquare">
<Link to={path} style={{
textDecoration: 'none',
color: '#4a4a4a'
}}>
<h2 className="blogSquare__title">{title}</h2>
<p className="blogSquare__date">{date}</p>
<p>{description}</p>
<p style={{ fontSize: '.8em', textDecoration: 'underline' }}>Read more</p>
</Link >
</section >
)
export default BlogSquare
- Now that we have a component to render our blog data, let's use it!
Inside of writing.js
, the component is already imported, so we can just jump right in.
Within our map
function, let's render a <BlogSquare>
component for each blog post. Remove the document.write
we just included to test our data, and replace it with the following:
<BlogSquare
key={post.node.id}
title={post.node.frontmatter.title}
date={post.node.frontmatter.date}
description={post.node.frontmatter.description}
path={post.node.frontmatter.path}
/>
Remember, we included the four properties inside of blogSquare.js
(title, date, description, path), so they must be passed with the data for each post.
Additionally, when mapping over an array of data and rendering JSX for each iteration, we must include a unique key
property.
Let's check out if our blog posts are being rendered:
Great! but we get a 404 error if we click the "Read more" link.
That's because we haven't created the query for our individual blog posts.
- Back in GraphiQL, let's create a new query.
Using the explorer in the left-hand sidebar, select the following:
markdownRemark
> frontmatter
- date
- description
- path
- title
- html
- id
-
Change the name of the query to
BlogPost
. -
We'll need to pass data for an individual blog post in order to retrieve its data, so let's create a
$path
argument. We'll set it to aString
data type and require it be passed in.
query BlogPost($path:String!){
markdownRemark (frontmatter: { path: { eq: $path } }){
frontmatter {
date
description
path
title
}
html
id
}
}
- Let's test if this works by adding a query variable.
At the bottom of GraphiQL there is a Query Variables text editor. Click to open it, and add the following:
{
"path": "/five-tech-skills-to-master"
}
- To test that this works, click the play button. You should see the "Five tech skills to master" blog post data populate the right side of GraphiQL.
- Now let's use our data to build individual blog pages.
First, let's create a template which will be used to dynamically render each blog when the "Read more" link is clicked.
Open the blogPost.js
file located in the templates/
directory.
- Inside of the
postQuery
declaration, paste our newly-created GraphQL query:
export const postQuery = graphql`
query BlogPost($path: String!) {
markdownRemark(frontmatter: { path: { eq: $path } }) {
html
frontmatter {
date
path
title
}
}
}
`
- Now we can pass
data
from the GraphQL query as a de-structured prop to the template and render the data.
export default function Template({ data }) {
}
We'll save data.markdownRemark
as a constant called post
to make our rendering a little nicer.
const post = data.markdownRemark
- Finally, let's add the following JSX elements:
- A
<h1>
containing the post title - An
<h4>
containing the author and date - A
<div>
containing the HTML for the post.
To render the HTML, we'll use Gatsby's dangerouslySetInnerHTML
and pass in post.html
:
<div dangerouslySetInnerHTML={{ __html: post.html }} />
We will wrap all of the blog post JSX inside of the <Layout>
component.
Here is the completed blogPost.js
template:
import React from 'react'
import { Link, graphql } from 'gatsby'
import Layout from '../components/layout'
export default function Template({ data }) {
const post = data.markdownRemark
return (
<Layout>
<Link
to="/writing"
style={{
display: 'flex',
alignItems: 'center',
fontSize: '.8em',
color: '#4A4A4A',
textDecoration: 'none',
marginTop: '50px',
}}
>
Back to blogs
</Link>
<h1
style={{
marginTop: '20px',
}}
>
{post.frontmatter.title}
</h1>
<h4
style={{
fontSize: '.8em',
color: '#9fa7a7',
fontWeight: '400',
}}
>
Posted by {post.frontmatter.author} on {post.frontmatter.date}
</h4>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</Layout>
)
}
export const postQuery = graphql`
query BlogPost($path: String!) {
markdownRemark(frontmatter: { path: { eq: $path } }) {
html
frontmatter {
date
path
title
}
}
}
`
- There's one last thing we have to add to get our blog posts to render.
We don't want to create individual pages for each blog post; that would be extremely time consuming!
Instead, we want to use GraphQL to dynamically create each page.
We can do this within gatsby-node.js
.
- Inside of
gatsby-node.js
, importpath
, a node module for working with our file directories.
const path = require('path')
- Let's use the node
exports.createPages
API to dynamically generate our pages.
I won't delve into the details of the createPages
API in this post, but you can check out the documentation linked above.
Add the follwing code underneath the path
module import:
exports.createPages = ({ boundActionCreators, graphql }) => {
const { createPage } = boundActionCreators
const postTemplate = path.resolve('src/templates/blogPost.js')
}
- Now we want to return a GraphQL query to grab all of the blog posts:
return graphql(`
{
allMarkdownRemark {
edges {
node {
html
id
frontmatter {
path
title
date
author
}
}
}
}
}
`)
- Once we receive a response back from the query, we want to reject the promise if an error occurred, and otherwise create a page for each post.
This will create a post at the designated path received from the query results, and will use the postTemplate
we declared above (our blogPost.js
template) to render each post.
...
`).then(res => {
if (res.errors) {
return Promise.reject(res.errors)
}
res.data.allMarkdownRemark.edges.forEach(({ node }) => {
createPage({
path: node.frontmatter.path,
component: postTemplate,
})
})
Here's the finalized code for gatsby-node.js
:
const path = require('path')
exports.createPages = ({ boundActionCreators, graphql }) => {
const { createPage } = boundActionCreators
const postTemplate = path.resolve('src/templates/blogPost.js')
return graphql(`
{
allMarkdownRemark {
edges {
node {
html
id
frontmatter {
path
title
date
author
}
}
}
}
}
`).then(res => {
if (res.errors) {
return Promise.reject(res.errors)
}
res.data.allMarkdownRemark.edges.forEach(({ node }) => {
createPage({
path: node.frontmatter.path,
component: postTemplate,
})
})
})
}
Now we're ready to see if it worked!
- Re-start your development server, then head over to the browser and click one of the blog post "Read more" links:
If you ever need to revert to this state, you can use git checkout step-3
to check out the tag for this state.
You may want to add some custom Google fonts to your Gatsby site. Luckily there's a plug-in for that!
- In the terminal run the following command to install the Google fonts plugin:
yarn add gatsby-plugin-google-fonts
- Inside
gatsby-config.js
, we need to configure the plug-in.
I'm choosing to use the font PT Serif, but you can select whichever font you prefer.
Add the following to the plugins
array:
plugins: [
...
{
resolve: `gatsby-plugin-google-fonts`,
options: {
fonts: [
`PT Serif`,
],
display: 'swap'
}
},
]
You can check out the full list of options for this plugin in the Gatsby docs.
-
Inside of
nav.css
update the.nav__list
selector to usefont-family: 'PT Serif', sans-serif
; -
Restart your development server to see the updated font family.
If you ever need to revert to this state, you can use git checkout step-4
to check out the tag for this state.
Gatsby makes it extremely easy to deploy your site using Netlify.
- Head over to Netlify and sign up for an account.
- Click "New site from Git".
- Select "GitHub" for continuous deployment, authenticate with your GitHub account, and select the repository.
- Set your build options (I leave the defaults).
- Click "Deploy site".
Now, every time I push to master
, my site will deploy.
Once your site deploys, head over to the URL to check it out! You can easily add custom domains with Netlify to personalize your portfolio.