In this post I will walk you through setting up a modern Jamstack project using Gatsby, TypeScript, Styled Components, and Contentful! Contentful and Gatsby work very well together, as Contentful allows you to focus on easily creating content for your site, and Gatsby provides a super fast, static site.
Here's a quick rundown of the tech we'll be using:
Before we started, there are a few prerequisites:
- Node.js (which comes with NPM) is installed on your machine
- Text editor of your choice (I will be using VS Code)
Something else I will also mention is that I use Linux, so all the commands listed below work in a UNIX environment, such as Mac or Linux. For Windows, some of these commands may not work, so you will have to find out the equivalent.
Please check out the repo I've created with the finished files. You can use this if you get stuck and need to reference anything.
You'll also notice that I make push commits to GitHub frequently throughout the process. I like this approach because it's easier for me to see incremental progress rather than a large number of changes all at once.
Lastly, I prefer to stick with NPM. If you use Yarn, simply replace NPM commands (such as npm i
) with the corresponding Yarn command (yarn add
).
With all that out of the way, let's get started!
Contentful Setup Pt. 1
The first thing we'll do is set up a free account with Contentful. You can do that here. To keep things simple, I would recommend signing up with your GitHub account.
After you've created your account, you should see your empty space (or be prompted to create one, and please do if you are). It is important that you do NOT add anything to this space. The reason why will come up soon!
Go to Settings
, and then API Keys
. Make sure you are on the Content delivery / preview token
tab. Click Add API key
in the top right corner, and then give the keys a name, something like "Gatsby Blog" for example, and perhaps also a short description, then click Save
.
Make sure to keep this tab open as we will definitely need it later!
GitHub Setup
First, create a new GitHub repo here. Give it at least a name, and perhaps also a short description, then click Create repository
. Keep this tab open, we'll need it in just a bit!
On your local machine, open up your terminal of choice, and cd
where you store your projects. From there, create a new directory and then go into it:
mkdir PROJECT_NAME && cd PROJECT_NAME
PROJECT_NAME
here being the name of the repo.
Next, download the Gatsby Contentful starter:
npx gatsby new . https://github.com/contentful/starter-gatsby-blog
Using npx
means we don't have to install the gatsby
package globally on our machine, which I personally prefer.
After the starter is done downloading, open it in your code editor of choice. Again, I use VS Code, so I can run the command code .
and it will open the project in VS Code for me.
Next, let's remove git
from this folder so we can start from scratch:
rm -rf .git
Finally, go back to the GitHub tab in your browser and run each of the git commands listed. If you want to make things easier on yourself, here they all are below in one long command:
git init && git add . && git commit -m "project setup" && git branch -M main && git remote add origin https://github.com/GITHUB_USERNAME/PROJECT_NAME.git && git push -u origin main
Just make sure to replace GITHUB_USERNAME
with your GitHub username, and PROJECT_NAME
with the name of the repo you just created.
Contentful Setup Pt. 2
Now, typically when you finish downloading a React boilerplate/starter project such as this, you may be inclined to start up the local development server and take a look. Well, you can do that here too, but as you may have guessed by the way I said that first thing, it's not going to work. If you run the command npm run dev
to start up the local dev server, you'll see an error like this:
Error: Contentful spaceId and the access token need to be provided.
At this point, I want to give props (pun absolutely intended) to the Contentful team because with this starter, they have actually included a setup script for us! This script will generate a couple of basic content models in our space, as well as a few pieces of starting content! This is why it's important to keep the space empty, so that the setup script can populate it. It's as simple as running the command: npm run setup
.
Once you run this command, you will have to enter your API keys in the following order:
- Space ID
- Content Management API Access Token *
- Content Delivery API Access Token
Go back to your browser and go to the tab/window you had open with Contentful. You can easily copy and paste in your Space ID first, but, wait...where's the Content Management API Access Token? And why is there a * next to it above?
For this, I would recommend clicking on Settings, then clicking on API keys but this time open it in a new tab. Here, you can click on the Content management tokens
tab. Click Generate personal token
, give the token a name, and then click Generate
. Copy and paste this token into the terminal. Then go back to the other tab and copy and paste in your Content Delivery API Access Token
.
The reason we did it this way is because if you:
- Got your Space ID
- Went back, got your Content Management API Access Token
- Went back again, got your Content Delivery API Access Token
It's just a lot of back and forth in the same tab.
Also, as you would have seen when you generate your Content Management API Access Token
, this token will NO LONGER be accessible once you close the tab / move away from this page. Save it if you wish, but we actually do not need it at any other point in this process. We just needed it for the setup script.
After that is done, now you can run npm run dev
to start the local development server!
Gatsby Cloud Setup
For deployment, we'll be using Gatsby Cloud. Gatsby Cloud is set up to optimize your Gatsby site, and adding a new site is very easy to do.
First, you'll need to create a free account if you do not have one already. You can sign up here.
For any subsequent visits, you can go straight to your dashboard here.
Once you are in your dashboard, click Add a site +
. Choose to import a GitHub repository (at this point you will have to authorize Gatsby Cloud to access your GitHub repos if this is your first time using it). Find the repo you created, and click Import
.
For Basic Configuration
, you can leave the settings as-is and click Next
.
For Connect Integrations
, Gatsby Cloud should automatically detect you're using Contentful based on your gatsby-config
. Click Connect
, then click Authorize
, then click Authorize
again. Select the Space you created earlier, then click Continue
.
For Environment variables, Gatsby Cloud actually sets up a couple extra ones for us that we don't need to use. You only need the following:
- Build Variables
-
CONTENTFUL_ACCESS_TOKEN
--> YourContent Delivery API access token
-
CONTENTFUL_SPACE_ID
--> YourSpace ID
-
- Preview Variables
-
CONTENTFUL_PREVIEW_ACCESS_TOKEN
--> YourContent Preview API access token
-
CONTENTFUL_HOST
-->preview.contentful.com
-
CONTENTFUL_SPACE_ID
--> YourSpace ID
-
If you're wondering how I figured that out, I found this piece of documentation which outlines what you need.
After you've filled in all the variables, click Save
. Then click Build site
. The build can take a couple of minutes, so you will have to wait! But, it should build successfully and now our site is deployed to Gatsby Cloud for all the world to see!
Testing the Workflow
Before we continue, let's take just a moment to test / make sure our workflow can do 2 things. Whenever we either
- Push code to GitHub
- Make a change in Contentful
Gatsby Cloud should automatically rebuild the site. But, we haven't setup any webhooks? How will Gatsby Cloud know when to rebuild?
Wrong! That was actually done automatically for us when we added the site to Gatsby cloud. In fact, if you go to your Contentful space, then go Settings
, and then Webhooks
, you should see one there!
If you do not, no worries! The documentation I linked above also includes the steps for configuring webhooks. So, just follow the steps and you'll be good to go.
Simple Code Change
In VS Code, go to /src/components/article-preview.js
. Find this piece of JSX:
<h2 className={styles.title}>{post.title}</h2>
We'll make a very simple change, such as adding on a few exclamation points:
<h2 className={styles.title}>{post.title}!!</h2>
Next, commit / push the change:
git add . && git commit -m 'quick commit for testing workflow' && git push -u origin main
Go to your Gatsby Dashboard. This should've triggered a rebuild of the site (you might need to just refresh the page so that it is).
Simple Contentful Change
As mentioned previously, the setup script we ran earlier created some starter content models and content for us, so we will make a simple change to the Person content John Doe
.
Go to your Contentful Space, then go to the Content tab, and click on the John Doe
piece of content. Make a simple change, such as changing the name to your name, then click Publish Changes
.
Go to your Gatsby Dashboard. This should've triggered a rebuild of the site (you might need to just refresh the page so that it is).
The build time for this (at least in my experience) is typically VERY quick, only 3 - 5 seconds! Although, if you are changing/adding in a LOT of content, it will likely take longer.
So, at this point, we have confirmed whenever we either:
- Commit / push code to GitHub
- Make a change in Contentful
Gatsby Cloud will automatically trigger a re-build of the site, keeping it up-to-date at all times!
Starter Cleanup
As is typically the case with starters/boilerplates, there are some things we don't need to keep around.
Removing Unnecessary Files & Folders
First, let's remove some of the files & folders at the root level of the project. After some testing, here is a list of the files folders we can & cannot delete post-setup:
✓ --> CAN be removed
✕ --> CANNOT be removed
[✓] .cache
--> Can be deleted, but is regenerated each time you rebuild, and is ignored by git anyways
[✓] /bin
& related package.json
scripts --> Used for running npm run dev
to setup Contentful
[✓] /contentful
--> Used for running npm run dev
to setup Contentful
[✓] /node_modules
--> Can be deleted, but is regenerated each time you install packages, and is ignored by git anyways
[✓] /public
--> Can be deleted, but is regenerated each time you rebuild, and is ignored by git anyways
[✕] /src
--> Essential
[✕] /static
--> Used to house files like robots.txt
and favicon
[✓] _config.yml
--> Used for GitHub pages and we're using Gatsby Cloud
[✕] .babelrc
--> Babel config file
[✓] .contentful.json.sample
--> Sample Contentful data file
[✕] .gitignore
--> Used to intentionally ignore/not track specific files/folders
[✕] .npmrc
--> configuration file for NPM, defines the settings on how NPM should behave when running commands
[✕] .nvmrc
--> specify which Node version the project should use
[✓] .prettierrc
--> Config for Prettier. This is entirely subjective, so it is up to you if you wish to delete it or not. I use Prettier settings in VS Code
[✓] .travis.yml
--> Config file for Travis CI. Travis CI is a hosted continuous integration service
[✓] app.json
--> Unsure what this is used for, as it is not used anywhere in the project
[✕] gatsby-config.js
--> Essential
[✕] gatsby-node.js
--> Essential
[✕] LICENSE
--> Okay to leave
[✓] package-lock.json
--> can be deleted, but is regenerated each time you install packages
[✕] package.json
--> Essential
[✕] README.md
--> Essential
[✓] screenshot.png
--> Was used in the README, but is no longer needed
[✓] static.json
--> Unsure what this is used for, as it is not used anywhere in the project. Possibly used for Heroku
[✓] WHATS-NEXT.md
--> Simple markdown file
You can use this command to remove all the files with a ✓ next to them at once:
rm -rf bin contentful _config.yml .contentful.json.sample .prettierrc .travis.yml app.json package-lock.json screenshot.png static.json WHATS-NEXT.md
Let's commit this progress:
git add . && git commit -m 'removed unnecessary files and folders' && git push -u origin main
Updating NPM Scripts
Next, we'll quickly update our scripts in package.json
.
First, let's add the gatsby clean
script back in (I've found most starters remove it):
"clean": "gatsby clean"
Next, update the dev command to be:
"dev": "npm run clean && gatsby develop"
This is really handy as it'll delete the .cache
and public
folders each time we start up the development server, which gives us the latest changes from Contentful. If you don't want this, you can simply add on another script:
"start": "gatsby develop"
But this is not necessary, and you'll see why later.
I've also found this utility script I created for myself a while ago has really come in handy:
"troubleshoot": "rm -rf .cache node_modules public package-lock.json && npm i && npm run dev"
This is basically a hard reset of the project.
Let's commit this progress:
git add . && git commit -m 'updated package.json scripts' && git push -u origin main
At this point, I personally encountered a git error along the lines of:
Fatal unable to access, could not resolve host when trying to commit changes.
If this happens, it's likely a proxy issue. Simply run this command and it should fix the problem:
git config --global --unset http.proxy && git config --global --unset https.proxy
Components & Pages
Somewhat frustratingly, the starter uses a mix of Classes and Functions for components & pages. Let's convert all the files using classes to use the function syntax. Specifically, the function expression syntax. This makes it easier when we convert the files to TypeScript later when everything is consistent.
The files we need to adjust are:
src/components/layout.js
src/pages/blog.js
src/pages/index.js
src/templates/blog-post.js
Furthermore, all the component files use kebab-case for naming. Personally, I prefer to use PascalCase, as I am used to in other React projects. So, I will update all file names to use PascalCase instead. I understand that they are likely all kebab-case to be consistent with the naming of the pages and templates, so this just a personal preference.
As a quick reminder, when working with Gatsby, it is very important that you DO NOT rename page files to use PascalCase. Gatsby uses the file name for routing, so if you change blog.js
to Blog.js
, the route will no longer be /blog
, but /Blog
.
Lastly, I will group each component and it's CSS module file together in a folder to keep things organized. The file/folder structure will now be:
/components
/ArticlePreview
- index.js
- article-preview.module.css
/Container
- index.js
/Footer
- index.js
- footer.module.css
etc.
Again, this is just my personal approach that I've always used. Totally up to you how you want to organize things.
Later on when we set up Styled Components, we will replace each module.css
file with a styles.ts
file. This styles.ts
file will house any styled components used only by the functional component in the same folder. So, the structure then will be:
/components
/ArticlePreview
- index.tsx
- styles.ts
/Container
- index.tsx
/Footer
- index.tsx
- styles.ts
etc.
So, I will not bother renaming the CSS module files since they will be replaced anyways.
If you wish to convert these on your own, by all means please do! Below I've provided the code you will need. You can check out the repo which I linked to earlier again here if you wish, but keep in mind since they're all in TypeScript and we have converted them over yet.
layout.js:
const Layout = ({ children, location }) => {
return (
<>
<Seo />
<Navigation />
<main>{children}</main>
<Footer />
</>
);
};
export default Layout;
blog.js:
const BlogIndex = ({ data, location }) => {
const posts = data.allContentfulBlogPost.nodes;
return (
<Layout location={location}>
<Seo title='Blog' />
<Hero title='Blog' />
<ArticlePreview posts={posts} />
</Layout>
);
};
export default BlogIndex;
With Gatsby, pages access the data returned from the GraphQL query via props.data
. We can tidy up the code a bit by destructuring our props in the ( ). We will use this approach for the remaining files.
index.js:
const Home = ({ data, location }) => {
const posts = data.allContentfulBlogPost.nodes;
const [author] = data.allContentfulPerson.nodes;
return (
<Layout location={location}>
<Hero
image={author.heroImage.gatsbyImageData}
title={author.name}
content={author.shortBio.shortBio}
/>
<ArticlePreview posts={posts} />
</Layout>
);
};
export default Home;
blog-post.js:
const BlogPostTemplate = ({ data, location }) => {
const post = data.contentfulBlogPost;
const previous = data.previous;
const next = data.next;
return (
<Layout location={location}>
<Seo
title={post.title}
description={post.description.childMarkdownRemark.excerpt}
image={`http:${post.heroImage.resize.src}`}
/>
<Hero
image={post.heroImage?.gatsbyImageData}
title={post.title}
content={post.description?.childMarkdownRemark?.excerpt}
/>
<div className={styles.container}>
<span className={styles.meta}>
{post.author?.name} · <time dateTime={post.rawDate}>{post.publishDate}</time> –{' '}
{post.body?.childMarkdownRemark?.timeToRead} minute read
</span>
<div className={styles.article}>
<div className={styles.body} dangerouslySetInnerHTML={{ __html: post.body?.childMarkdownRemark?.html }} />
<Tags tags={post.tags} />
{(previous || next) && (
<nav>
<ul className={styles.articleNavigation}>
{previous && (
<li>
<Link to={`/blog/${previous.slug}`} rel='prev'>
← {previous.title}
</Link>
</li>
)}
{next && (
<li>
<Link to={`/blog/${next.slug}`} rel='next'>
{next.title} →
</Link>
</li>
)}
</ul>
</nav>
)}
</div>
</div>
</Layout>
);
};
Let's commit this progress:
git add . && git commit -m 'updated components and pages to use function syntax' && git push -u origin main
Uninstalling Some NPM Packages
At this point, we are no longer using the following packages:
contentful-import
gh-pages
lodash
netlify-cli
We can uninstall them all by running:
npm un contentful-import gh-pages lodash netlify-cli
We can also simplify our scripts
in package.json
to:
"scripts": {
"build": "gatsby build",
"clean": "gatsby clean",
"dev": "gatsby develop",
"rebuild": "rm -rf .cache public && npm run dev",
"serve": "gatsby serve",
"troubleshoot": "rm -rf .cache node_modules public package-lock.json && npm i && npm run dev"
}
Let's commit this progress:
git add . && git commit -m 'uninstalled some npm packages and updated package.json scripts' && git push -u origin main
Organizing Components Into Folders
First, go into the components folder: cd src/components/
We need to create all the necessary folders for each component:
- ArticlePreview
- Container
- Footer
- Hero
- Layout
- Navigation
- Seo
- Tags
We can create all these folders at once by running the command:
mkdir ArticlePreview Container Footer Hero Layout Navigation Seo Tags
Now, one at a time, move the corresponding files into their folders. Hopefully VS Code automatically updates the import path(s) for you. If not, you will have to manually update them yourself.
After moving everything around, you should see the following warning:
warn chunk commons [mini-css-extract-plugin]
This error/warning is caused by the Webpack plugin mini-css-extract-plugin
wanting all CSS imports to be in the same order. This is because it has confused CSS modules with plain CSS. However, since we will be using Styled Components, we can ignore this warning and continue.
Let's commit this progress:
git add . && git commit -m 'organized components into folders' && git push -u origin main
Converting to TypeScript
UPDATE: As of Gatsby v4.8, there is full TypeScript for the gatsby-browser
and gatsby-ssr files
. Also, as of Gatsby v4.9, there is full TypeScript for the gatsby-config
and gatsby-node
files! So, if you are able to use those versions, check out the 2 links on how to best setup those files!
Now comes a BIG step: converting everything to TypeScript! We will convert all components, pages, and even the Gatsby API files (gatsby-config, gatsby-node, etc.) at the root level to use TypeScript.
For this portion, I want to give a huge thanks to Progressive Dev on YouTube. His video was immensely helpful when I first wanted to work with Gatsby and TypeScript.
Gatsby claims to support TypeScript out of the box.and this is partially true. If we create a simple Copy
component Copy.tsx
:
const Copy = () => (
<p>Lorem ipsum dolor sit amet consectetur.</p>
);
And use it in ArticlePreview
above the tags, for example, it will work just fine. However, we don't get 100% proper type checking. VS Code will highlight the error, but the Gatsby CLI will not.
The other rather annoying thing to do is we have to manually convert all the .js
/.jsx
files to .ts
/.tsx
files as Gatsby does not have TypeScript versions of their starters.
Here is a summary of the steps we will take:
- Setup
tsconfig.json
- Convert all the components & pages to TypeScript
- Convert the Gatsby API files to use TypeScript
Setup
To start things off, let's install the TypeScript package:
npm i typescript
Also install the following @types packages:
npm i @types/node @types/react @types/react-dom @types/react-helmet
Next, create a tsconfig file:
tsc --init
Select everything in tsconfig.json
, and replace it with this:
{
"compilerOptions": {
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react",
"module": "commonjs",
"noEmit": true,
"pretty": true,
"skipLibCheck": true,
"strict": true,
"target": "es5"
},
"include": ["./src", "gatsby"],
"exclude": ["./node_modules", "./public", "./.cache"]
}
You can always add on more, but this will suffice for now.
Next, we need a way to run our local development server, AND get proper type checking in the CLI. For this, we can use the package concurrently
:
npm i concurrently
concurrently
will allow us to run multiple scripts, well, concurrently! Next, let's update our scripts
in package.json
to use concurrently
:
"dev-gatsby": "gatsby develop",
"dev-typescript": "tsc -w",
"dev": "npm run clean && concurrently \"npm:dev-gatsby\" \"npm:dev-typescript\""
At this point, we should run npm run dev
to start up the local dev server and make sure everything is still working fine.
Converting Pages
Now, we can convert the .js
files to .tsx
files. Let's start with the Home Page. I will walk you through the process once, and leave it up to you to repeat the process for the other pages/templates.
To start, rename the file from index.js
to index.tsx
. When you do that though, TypeScript will complain about a few things:
- It's not sure what the types of the components are that we are using. That is because they are still plain
.js
files, and we will convert them to.tsx
in a bit anyways, so no worries there - The props
data
&location
have the any type implicitly - The types for
allContentfulBlogPost
&allContentfulPerson
are also unknown - A type error for the CSS Modules. Again, since we are replacing them with Styled Components later on, no worries here either
Luckily, Gatsby has types for us, and the one we need to use for pages is PageProps
. So, import it:
import type { PageProps } from 'gatsby'
You'll notice here that I specifically put import type
at the beginning. I do this because while this:
import { PageProps } from 'gatsby'
Is perfectly fine and will work without issue, I think it's a bit misleading. When I see that, my initial reaction is that PageProps
is a component. But it is not, it is a type. This syntax for importing types also works:
import { type PageProps } from 'gatsby'
But I prefer the way I initially did it because if we import multiple types like this, for example:
import { type PageProps, type AnotherType, type YetAnotherType } from 'gatsby'
It looks kind of messy. We can simplify it by having the single type
in front of the curly braces. Also, using { type PageProps }
is a newer syntax, and might not work with older versions of React (say an old create-react-app you have, or something like that).
So, the syntax type { PageProps }
then is the better choice because:
- We only use the
type
keyword once, making the code a bit cleaner - It can be used with older and current React + TypeScript projects
Alright, back to the page! We can then set the type of our destructured props to PageProps:
const Home = ({ data, location }: PageProps) => {
// ...
};
Next, outside the function body, just above, create a new type called GraphQLResult
:
type GraphQLResult = {};
const Home = ({ data, location }: PageProps) => {
// ...
};
Inside this object, we need to set the types for the GraphQL data being returned.
Now would be a good time to create a types
folder, and inside it a file called types.ts
.
types.ts
will house our re-usable types throughout the project. I typically use just the one file, but you can certainly separate types for specific things into their own files if you wish. For example:
/types/global.ts
/types/graphql.ts
At the top, import the following:
import type { IGatsbyImageData } from 'gatsby-plugin-image';
We will use this type multiple times in this file alone, and I know from experience that we would get type errors where we use the GatsbyImage
component if we didn't.
In types.ts
, add the following:
export type BlogPost = {
title: string;
slug: string;
publishDate: string;
tags: string[];
heroImage: {
gatsbyImageData: IGatsbyImageData;
};
description: {
childMarkdownRemark: {
html: string;
};
};
};
export type Person = {
name: string;
shortBio: {
shortBio: string;
};
title: string;
heroImage: {
gatsbyImageData: IGatsbyImageData;
};
};
Back in index.tsx
, adjust the GraphQLResult
type we created to:
type GraphQLResult = {
allContentfulBlogPost: {
nodes: BlogPost[];
};
allContentfulPerson: {
nodes: Person[];
};
};
Make sure to import these types too, of course. Now, we can pass this type in as an additional argument to PageProps:
const Home = ({ data, location }: PageProps<GraphQLResult>) => {
// ...
};
And now the type errors for the Contentful data should be gone!
You should be able to repeat this process for blog.js
without issue. blog.js
, or rather blog.tsx
, will use the BlogPost
type as well.
If you are stuck, you can always take a look at the final code here.
For converting blog-post.js to blog-post.tsx, there are a couple of extra steps. After renaming it to .tsx
, you will get an error saying Module not found
.
This is because in gatsby-node.js
, there is this line:
const blogPost = path.resolve('./src/templates/blog-post.js');
Simply change it to .tsx
at the end there. Then, in types.ts
, add the following:
export type SingleBlogPost = {
author: {
name: string;
};
body: {
childMarkdownRemark: {
html: string;
timeToRead: number;
};
};
description: {
childMarkdownRemark: {
excerpt: string;
};
};
heroImage: {
gatsbyImageData: IGatsbyImageData;
resize: {
src: string;
};
};
publishDate: string;
rawDate: string;
slug: string;
tags: string[];
title: string;
};
export type NextPrevious = { slug: string; title: string } | null;
Back in the blog-post.tsx
, adjust the GraphQLResult
type to:
type GraphQLResult = {
contentfulBlogPost: SingleBlogPost;
next: NextPrevious;
previous: NextPrevious;
};
Then pass it to PageProps like before:
const BlogPostTemplate = ({ data, location }: PageProps<GraphQLResult>) => {
// ...
};
And with that, all our pages are now using TypeScript! Let's commit this progress:
git add . && git commit -m 'updated pages to use typescript' && git push -u origin main
Converting Components
Now let's update the components to .tsx
! The steps for this process are much simpler than with converting pages:
- Rename
.js
to.tsx
- Setup type for the props (if any)
For example, ArticlePreview
:
// props
type ArticlePreviewProps = {
posts: BlogPost[];
};
const ArticlePreview = ({ posts }: ArticlePreviewProps) => {
// ...
};
Again, if you are having trouble/unsure how to type the pre-existing components, you can see how I did so here.
After converting all components to TypeScript, let's commit this progress:
git add . && git commit -m 'updated components to use typescript' && git push -u origin main
Converting Gatsby API Files
Now we will convert the Gatsby API files (gatsby-config, gatsby-node, etc.) to use TypeScript. The advantage of this is if the project should grow, it will be nice to have everything type-checked. The other benefit of .ts
files is that we can use the more modern import/export
syntax instead of modules.export/require
syntax.
The issue is, though, these files MUST be in .js for the Gatsby Runner to use them. So, how do we solve this problem?
To start, at the root level of the project, create a folder called gatsby
.
Copy and paste gatsby-config.js
& gatsby-node.js
at the root level into this folder and rename them to .ts
.
Next, we'll need the following packages:
-
dotenv
--> Because we will get an ESLint error later on calledimport/no-extraneous-dependencies
-
gatsby-plugin-typescript
--> Allows Gatsby to build TypeScript and TSX files -
ts-node
--> Will allow us to recognize the TS syntax called from the JS files
Run the command:
npm i dotenv gatsby-plugin-typescript ts-node
Go to gatsby-config.js
at the root level, select everything and replace it with just these 2 lines:
require("ts-node").register();
module.exports = require("./gatsby/gatsby-config");
Now, the Gatsby runner will recognize our TypeScript files.
Note, gatsby-config.js at the root level MUST remain as .js
. We will be able to switch gatsby-node
to .ts
though.
Go gatsby-config.ts
in the gatsby
folder, and replace this code:
require('dotenv').config({
path: `.env.${process.env.NODE_ENV}`
});
With this code:
import dotenv from 'dotenv';
dotenv.config({ path: `.env.${process.env.NODE_ENV}` });
Also update the object with the plugins
, etc., being exported at the bottom from this:
module.exports = {
// ...
};
To this:
export default {
// ...
};
Make sure to gatsby-plugin-typescript
to the array of plugins!
Lastly, we need to update the contentfulConfig
object to include this: host: process.env.CONTENTFUL_HOST
. If we don't, we get an error down below in the if
check because we try to access contentfulConfig.host
, but host
doesn't exist initially in this variable. So, contentfulConfig
should look like this:
const contentfulConfig = {
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN || process.env.CONTENTFUL_DELIVERY_TOKEN,
host: process.env.CONTENTFUL_HOST,
spaceId: process.env.CONTENTFUL_SPACE_ID
};
Now to update gatsby-node
! As previously mentioned, for the gatsby-node.js
file at the root level, we can actually rename it to .ts
. Once you do, select everything and replace it with just this one line:
export * from "./gatsby/gatsby-node";
You will get an error saying something like this file is not a module
. We just need to update the file to use the import/export
syntax.
Open gatsby-node.ts
in the gatsby
folder, and replace this:
const path = require('path');
With this:
import { resolve } from 'path';
Next, import the following type from the gatsby package:
import type { GatsbyNode } from 'gatsby';
Next, update the createPages
to this:
export const createPages: GatsbyNode["createPages"] = async ({ graphql, actions, reporter }) => {
// ...
};
At this point, we should see a type error down below for const posts = result...
saying:
Property 'allContentfulBlogPost' does not exist on type 'unknown'
We need to set up the type for the result from the GraphQL query. Just outside & above the createPages
function, create a type called GraphQLResult
. It will look like this:
type GraphQLResult = {
allContentfulBlogPost: {
nodes: {
slug: string;
title: string;
}[];
};
};
Next, simply apply this type to the result
variable and the error should go away:
const result = await graphql<GraphQLResult>(
// ...
);
And now another error should appear on result.data
saying: Object is possibly 'undefined'
. Just above this line, add the following if
check and the error should go away:
if (!result.data) {
throw new Error('Failed to get posts.');
}
Whew! That was a lot! But now our entire Gatsby project is set up to use TypeScript!
Let's commit this progress:
git add . && git commit -m 'updated gatsby api files to use typescript' && git push -u origin main
ESLint Setup
Let's add ESLint to our project for some sweet-sweet linting!
To start, run the command: npx eslint --init
Answer the questions how you like, but make sure that whichever answers you choose, you make sure to pick the same ones each time you set up ESLint. This way, you can save any custom rules in a separate repo, like I've done here, and copy and paste them in. Now, your code will be consistent across all your projects.
This is how I answer the questions:
- How would you like to use ESLint? ·
style
- What type of modules does your project use? ·
esm
- Which framework does your project use? ·
react
- Does your project use TypeScript? ·
Yes
- Where does your code run? ·
browser
,node
- How would you like to define a style for your project? ·
guide
- Which style guide do you want to follow? ·
airbnb
- What format do you want your config file to be in? ·
JSON
Download any additional packages if prompted. Once done, add in your custom rules if you have any, or you can add them as you go. Then commit this progress:
git add . && git commit -m 'added eslint' && git push -u origin main
Styled Components Setup
My go-to approach for styling React projects is Styled Components. At first, I didn't really like it. I was used to Sass for styling, and the syntax was weird at first, but after having used it in a few projects, I absolutely love it, and I haven't looked back since.
We'll need the following packages:
-
react-is
--> Because if we don't, we get an error on Gatsby Cloud saying:Can't resolve 'react-is' ...
-
babel-plugin-styled-components
,gatsby-plugin-styled-components
, &styled-components
--> These are the packages recommended by Gatsby themselves in their documentation -
@types/styled-components
--> Needed sincestyled-components
don't come with types out of the box
Run the command:
npm i babel-plugin-styled-components gatsby-plugin-styled-components react-is styled-components @types/styled-components
Open gatsby-config.ts
in the gatsby
folder and add gatsby-plugin-styled-components
to our plugins array.
Simple Component Change
Let's make a simple adjustment to the ArticlePreview
component to make sure everything will work.
In the ArticlePreview
folder, create a file called: styles.ts
Import styled-components:
import styled from 'styled-components';
Open up the CSS modules file. Let's convert the .article-list
selector to a styled component. Copy and paste this into styles.ts
:
export const ArticleList = styled.ul`
display: grid;
grid-gap: 48px;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
list-style: none;
margin: 0;
padding: 0;
`;
Back in index.tsx
, add the following import:
import * as S from './styles';
I'll explain why I import it this way in just a bit. In the JSX, replace this:
<ul className={styles.articleList}>
// ...
</ul>
With this:
<S.ArticleList>
// ...
</S.ArticleList>
And if we check the Elements Tab in DevTools, we should see something like:
<ul class="styles__ArticleList-bfmZnV jUEOQo">
// ...
</ul>
Of course, the randomly generated class names will be different from what you see here.
So, the reason why I use import * as S from './styles';
, along with named exports from styles.ts
, is because it very easily allows me to differentiate styled components from functional components in the JSX. The S
is just for for Styled
/. So, you could use import * as Styled
instead if you would like.
Adding Global Styles
Now, let's add some global styles to the project. For that we will need 2 things:
-
GlobalStyle
component -
theme
object
First, let's create the GlobalStyle
component. Inside the src
folder, create a new folder called styles
. In this folder, create a file called GlobalStyle.ts
. In this file, import createGlobalStyle
:
import { createGlobalStyle } from "styled-components";
Next add this starting code:
const GlobalStyle = createGlobalStyle``;
export default GlobalStyle;
Inside the backticks is where you can place the global styles you want applied. Let's copy and paste some from global.css
into there and make the necessary adjustments:
const GlobalStyle = createGlobalStyle`
html {
scroll-behavior: smooth;
}
html * {
box-sizing: border-box;
}
body {
background: #fff;
color: #000;
font-family: 'Inter var', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
font-size: 16px;
font-weight: 400;
line-height: 1.5;
margin: 0;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
}
`;
Next, let's create the global theme object. Inside the styles
folder, create a new file called theme.ts
, and add this code to start:
const theme = {
mediaQueries: {
desktopHD: 'only screen and (max-width: 1920px)',
desktopMedium: 'only screen and (max-width: 1680px)',
desktopSmall: 'only screen and (max-width: 1440px)',
laptop: 'only screen and (max-width: 1366px)',
laptopSmall: 'only screen and (max-width: 1280px)',
tabletLandscape: 'only screen and (max-width: 1024px)',
tabletMedium: 'only screen and (max-width: 900px)',
tabletPortrait: 'only screen and (max-width: 768px)',
mobileXLarge: 'only screen and (max-width: 640px)',
mobileLarge: 'only screen and (max-width: 576px)',
mobileMedium: 'only screen and (max-width: 480px)',
mobileSmall: 'only screen and (max-width: 415px)',
mobileXSmall: 'only screen and (max-width: 375px)',
mobileTiny: 'only screen and (max-width: 325px)'
},
colors: {
red: 'red'
}
};
export default theme;
Now, let's use both of them. To do so, open the Layout
component file (src/components/Layout/index.tsx
). In there, import both of these files, along with ThemeProvider
from styled-components
:
import { ThemeProvider } from "styled-components";
import GlobalStyle from '../../styles/GlobalStyle';
import theme from '../../styles/theme';
To use GlobalStyle
, use it as a component and place it above the Seo component (at the same level). To use ThemeProvider
, replace the fragment with it. At this point, you should get a red underline. This is because the ThemeProvider
component expects a theme
prop. So, we can pass in our theme
object as the value. In the end, the JSX should look like this:
const Layout = ({ children, location }: LayoutProps) => (
<ThemeProvider theme={theme}>
<GlobalStyle />
<Seo title='Gatsby Contentful Blog w/ TypeScript' />
<Navigation />
<main className='test'>{children}</main>
<Footer />
</ThemeProvider>
);
If you've never used Styled Components before, you might be asking "What does ThemeProvider
allow us to do?"
When using Styled Components, we automatically get access to props
, as well as children
, and we can tap into our theme
by doing props.theme
. Let's see an example.
In the components
folder, create a new folder called UI
. In this folder I like to store very simple styled components that ONLY affect the UI, such as a Wrapper
component, or Copy
component like I showed in an example earlier (of course in this instance it would be purely for styling copy throughout the site), and they can be re-used throughout the project. Think of them like global UI components.
In this starter, a few elements use a container
class. So, let's create a simple styled component that we can use to wrap JSX elements with.
In the UI
folder, create a file called Container.ts
. Since this is a simple styled component, and no JSX is involved, we name it .ts
.
In the file, add this code:
import styled from 'styled-components';
export const Container = styled.div`
margin: 0 auto;
max-width: 80rem;
padding: 24px;
`;
Next, let's go to ArticlePreview/index.tsx
. We can see the starter already has a Container
component, buuuttt I think the code there is pretty janky, and it's only meant for styling anyways. So, let's replace it with our styled component.
First, let's update our imports:
import * as S from './styles';
import { Container } from '../UI/Container';
Then simply remove the functional component Container
being imported to avoid conflicts. Since the name is the same, it will work just like before.
I like to have my styled components imported and exported this way, because I have set rules for myself that:
- Styled components should be named exports
- Functional components should be default exports
- Import everything as
S
fromstyles.ts
in the component folder - Import components from the
UI
folder below it in alphabetical order
I would highly encourage you to create rules like this for yourself. You should do this because then your code will be consistent across all your projects, and when you use the same structure and self-imposed rules, it makes sharing code between your projects a LOT easier. Try new things out here and there, but once you've found what works for you, I would then encourage you to refactor all your existing projects (on your portfolio or not), to use these rules. Think of all the green squares you'll have on GitHub!! But in all seriousness, it shows you care about the quality of your code, which I think is important. And honestly having everything be consistent is just so satisfying.
Ok, now let's use our theme in the Container
. You may have noticed there is a red color:
colors: {
red: 'red'
}
This is just the default red, and it looks terrible, but at least we will know it's working! Simply add this to the styled component:
background-color: ${(props) => props.theme.colors.red};
Now the ArticlePreview
component should be wrapped in a glorious red color!
Once you start using styled components, you may notice writing props.theme
a lot is kind of annoying. Just like with functional components, we can destructure our props inline. So, we can update the background-color
to be like this:
background-color: ${({ theme }) => theme.colors.red};
This is optional, but I like doing it this way as I think it's a bit cleaner.
Similarly to functional components, we can set up our own custom props for our styled components and type them as well. For example, let's say we want to have this Container
component take in a dynamic backgroundColor
prop, and that value be a string
. How would we do that?
In Container.ts
, just above the variable, create a new type ContainerProps
, and add the following value:
type ContainerProps = {
backgroundColor: string;
}
Next, we need to update the styled component to use this type. We can do so like this:
export const Container = styled.div<ContainerProps>`
margin: 0 auto;
max-width: 80rem;
padding: 24px;
background-color: ${({ theme }) => theme.colors.red};
`;
Now, we just need to update the component to use props.backgroundColor
instead of props.theme
:
export const Container = styled.div<ContainerProps>`
margin: 0 auto;
max-width: 80rem;
padding: 24px;
background-color: ${({ backgroundColor }) => backgroundColor};
`;
Now we can pass in a dynamic color to our Container
each time we use it:
return (
<Container backgroundColor='blue'>
// ...
</Container>
)
You can take this a step further and set the backgroundColor
type to only accept certain values. For instance, the backgroundColor
should only be red
, green
, or blue
:
type ContainerProps = {
backgroundColor: 'red' | 'green' | 'blue';
}
Now you should get some sweet auto-completion in VS Code when entering in a value for this prop!
Done!
At this point, we're done all the setup! Whew! That was a lot! Now, it is up to you to build out your project. Some things you can do from here:
- Add custom fonts (Google Fonts, Adobe Typekit, etc.)
- Add any more desired plugins and/or npm packages
- Convert the remaining existing components using CSS modules to Styled components, or just delete them entirely and start from scratch
- Update GlobalStyle and Theme to your liking
Happy coding!
Top comments (0)