DEV Community

Cover image for Setup a modern Jamstack project using Gatsby, TypeScript, Styled Components, and Contentful
andrew shearer
andrew shearer

Posted on • Updated on

Setup a modern Jamstack project using Gatsby, TypeScript, Styled Components, and Contentful

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
Enter fullscreen mode Exit fullscreen mode

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 --> Your Content Delivery API access token
    • CONTENTFUL_SPACE_ID --> Your Space ID
  • Preview Variables
    • CONTENTFUL_PREVIEW_ACCESS_TOKEN --> Your Content Preview API access token
    • CONTENTFUL_HOST --> preview.contentful.com
    • CONTENTFUL_SPACE_ID --> Your Space 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>
Enter fullscreen mode Exit fullscreen mode

We'll make a very simple change, such as adding on a few exclamation points:

<h2 className={styles.title}>{post.title}!!</h2>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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} &middot; <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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

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\""
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

You'll notice here that I specifically put import type at the beginning. I do this because while this:

import { PageProps } from 'gatsby'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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) => {
  // ...
};
Enter fullscreen mode Exit fullscreen mode

Next, outside the function body, just above, create a new type called GraphQLResult:

type GraphQLResult = {};

const Home = ({ data, location }: PageProps) => {
  // ...
};
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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;
  };
};
Enter fullscreen mode Exit fullscreen mode

Back in index.tsx, adjust the GraphQLResult type we created to:

type GraphQLResult = {
  allContentfulBlogPost: {
    nodes: BlogPost[];
  };
  allContentfulPerson: {
    nodes: Person[];
  };
};
Enter fullscreen mode Exit fullscreen mode

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>) => {
  // ...
};
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Back in the blog-post.tsx, adjust the GraphQLResult type to:

type GraphQLResult = {
  contentfulBlogPost: SingleBlogPost;
  next: NextPrevious;
  previous: NextPrevious;
};
Enter fullscreen mode Exit fullscreen mode

Then pass it to PageProps like before:

const BlogPostTemplate = ({ data, location }: PageProps<GraphQLResult>) => {
  // ...
};
Enter fullscreen mode Exit fullscreen mode

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) => {
  // ...
};
Enter fullscreen mode Exit fullscreen mode

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 called import/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");
Enter fullscreen mode Exit fullscreen mode

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}`
});
Enter fullscreen mode Exit fullscreen mode

With this code:

import dotenv from 'dotenv';

dotenv.config({ path: `.env.${process.env.NODE_ENV}` });
Enter fullscreen mode Exit fullscreen mode

Also update the object with the plugins, etc., being exported at the bottom from this:

module.exports = {
  // ...
};
Enter fullscreen mode Exit fullscreen mode

To this:

export default {
  // ...
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

With this:

import { resolve } from 'path';
Enter fullscreen mode Exit fullscreen mode

Next, import the following type from the gatsby package:

import type { GatsbyNode } from 'gatsby';
Enter fullscreen mode Exit fullscreen mode

Next, update the createPages to this:

export const createPages: GatsbyNode["createPages"] = async ({ graphql, actions, reporter }) => {
  // ...
};
Enter fullscreen mode Exit fullscreen mode

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;
    }[];
  };
};
Enter fullscreen mode Exit fullscreen mode

Next, simply apply this type to the result variable and the error should go away:

const result = await graphql<GraphQLResult>(
  // ...
);
Enter fullscreen mode Exit fullscreen mode

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.');
}
Enter fullscreen mode Exit fullscreen mode

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 since styled-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
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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;
`;
Enter fullscreen mode Exit fullscreen mode

Back in index.tsx, add the following import:

import * as S from './styles';
Enter fullscreen mode Exit fullscreen mode

I'll explain why I import it this way in just a bit. In the JSX, replace this:

<ul className={styles.articleList}>
  // ...
</ul>
Enter fullscreen mode Exit fullscreen mode

With this:

<S.ArticleList>
  // ...
</S.ArticleList>
Enter fullscreen mode Exit fullscreen mode

And if we check the Elements Tab in DevTools, we should see something like:

<ul class="styles__ArticleList-bfmZnV jUEOQo">
  // ...
</ul>
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

Next add this starting code:

const GlobalStyle = createGlobalStyle``;

export default GlobalStyle;
Enter fullscreen mode Exit fullscreen mode

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;
  }
`;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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>
);

Enter fullscreen mode Exit fullscreen mode

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;
`;
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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 from styles.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'
}
Enter fullscreen mode Exit fullscreen mode

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};
Enter fullscreen mode Exit fullscreen mode

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};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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};
`;
Enter fullscreen mode Exit fullscreen mode

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};
`;
Enter fullscreen mode Exit fullscreen mode

Now we can pass in a dynamic color to our Container each time we use it:

return (
  <Container backgroundColor='blue'>
    // ...
  </Container>
)

Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

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)