TypeScript Gatsby Boilerplate


How to Set Up Gatsby with Sanity.io for Your Personal Blog

27 March 2023

I will walk you through the steps of setting up Gatsby with Sanity.io for your personal blog. I will cover everything from installing the necessary plugins to deploying your blog to a hosting service like Netlify. So let's get started!

Initialise project

Create project with Gatsby docs - https://www.gatsbyjs.com/docs/quick-start/.

npm init gatsby

Sanity.io

Follow gatsby-source-sanity plugin - https://www.gatsbyjs.com/plugins/gatsby-source-sanity/.

Rewrite index page:

import * as React from 'react'

function IndexPage(): React.ReactElement {
  return <div>Hello world!</div>
}

export default IndexPage
export function Head(): React.ReactElement {
  return <title>Home Page</title>
}

Add basic HTML markup.

Create single post page.

import * as React from 'react'

function SinglePost(): React.ReactElement {

  return (
    <>
      <header>
          <h2>Site Title</h2>
      </header>
      <hr />
      <main>
        <article>
          <h1>Post title</h1>
          <small>12 September 1998</small>
          <p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Nemo labore sed voluptatem. Iste reprehenderit cumque autem quisquam exercitationem optio eum, asperiores porro voluptatibus velit rem delectus placeat labore sit praesentium error voluptate harum vitae earum doloremque suscipit! Omnis laboriosam pariatur repellat cupiditate autem?</p>
        </article>
      </main>
    </>
  )
}

export default SinglePost
export function Head(): React.ReactElement {
  return <title>Single Post Page</title>
}

Add links cross pages.

GraphiQL

Add graphql queries for index page.

import * as React from 'react'
import { Link, type PageProps, graphql, type HeadProps } from 'gatsby'

function NoPosts(): React.ReactElement {
  return (
    <p>
      No posts found. Please create a post in Sanity Studio and publish it to
      see it here.
    </p>
  )
}

function IndexPage({
  data: {
    allSanityPost: { nodes },
    site
  }
}: PageProps<Queries.IndexPageQuery>): React.ReactElement {
  const posts = nodes !== null && nodes.length > 0 ? nodes : []
  if (posts.length === 0) {
    return <NoPosts />
  }

  const featuredPost = {
    title: posts[0].title,
    description: posts[0].description,
    link:
      posts[0].slug?.current !== undefined && posts[0].slug.current !== null
        ? `/posts/${posts[0].slug.current}`
        : '',
    readMore:
      posts[0].readMore === null ? 'Continue reading...' : posts[0].readMore
  }
  const otherPosts = posts.slice(1).map(post => ({
    title: post.title,
    description: post.description,
    link:
      post.slug?.current !== undefined && post.slug.current !== null
        ? `/posts/${post.slug.current}`
        : '',
    readMore: post.readMore === null ? 'Continue reading...' : post.readMore
  }))
  const siteMetadata = site?.siteMetadata

  return (
    <>
      <header>
        <h1>{siteMetadata?.title}</h1>
        <p>{siteMetadata?.description}</p>
      </header>
      <hr />
      <main>
        <article>
          <h2>{featuredPost.title}</h2>
          <p>{featuredPost.description}</p>
          {featuredPost.link.length > 0 && (
            <p>
              <Link to={featuredPost.link}>{featuredPost.readMore}</Link>
            </p>
          )}
        </article>
        {otherPosts.length > 0 && (
          <ul>
            {otherPosts.map(post => (
              <li key={post.title}>
                <article>
                  <h3>{post.title}</h3>
                  <p>{post.description}</p>
                  {post.link.length > 0 && (
                    <p>
                      <Link to={post.link}>{post.readMore}</Link>
                    </p>
                  )}
                </article>
              </li>
            ))}
          </ul>
        )}
      </main>
    </>
  )
}

export default IndexPage
export function Head({
  data: { site }
}: HeadProps<Queries.IndexPageQuery>): React.ReactElement {
  const siteMetadata = site?.siteMetadata

  return (
    <>
      <title>{siteMetadata?.title}</title>
      <body className="prose lg:prose-xl font-serif mx-8 my-4" />
    </>
  )
}

export const query = graphql`
  query IndexPage {
    allSanityPost {
      nodes {
        title
        readMore
        slug {
          current
        }
        description
        publishedAt(formatString: "DD MMMM YYYY")
      }
    }
    site {
      siteMetadata {
        title
        description
      }
    }
  }
`

Dynamic Pages

import { type CreatePagesArgs, type GatsbyNode } from 'gatsby'
import path from 'path'

async function turnArticlesIntoPages({
  graphql,
  actions
}: CreatePagesArgs): Promise<void> {
  // 1. Get a template for this page
  const postTemplate = path.resolve('./src/pages/single-post.tsx')
  // 2. Query all posts

  const { data } = await graphql<Queries.AllPostsQuery>(`
    query AllPosts {
      allSanityPost(sort: { publishedAt: DESC }, limit: 100) {
        nodes {
          title
          slug {
            current
          }
        }
      }
    }
  `)

  const posts =
    data?.allSanityPost.nodes !== undefined &&
    data.allSanityPost.nodes !== null &&
    data.allSanityPost.nodes.length > 0
      ? data.allSanityPost.nodes
      : []
  // 3. Loop over each post and create a page for that post
  posts.forEach(post => {
    console.log('Creating page for Article:', post.title)

    const link =
      post.slug?.current !== undefined && post.slug.current !== null
        ? post.slug.current
        : ''
    if (link.length > 0)
      actions.createPage({
        path: `posts/${link}`,
        component: postTemplate,
        context: {
          slug: link
        }
      })
  })
}

export const createPages: GatsbyNode['createPages'] =
  async function createPages(params) {
    // Create pages dynamically
    await turnArticlesIntoPages(params)
  }

Plugins

  • Parse Portable Text - https://www.npmjs.com/package/@portabletext/react
  • Highlight Code samples - https://www.npmjs.com/package/react-refractor
  • Style site with TailwindCSS - https://tailwindcss.com/docs/guides/gatsby
    • Update gatsby-browser.ts with styles
import './src/global.css'
import 'prismjs/themes/prism.css'

GitHub

  1. Create a repository - https://github.com/new.
  2. Connect repo and local folder: git remote add origin path-to-repo.git.
  3. Start new branch: git checkout -b feature/gatsby.
  4. Commit Initial commit.

Linting

  1. Lint files with ESLint: npm init @eslint/config.
  2. Format with Prettier: npm install --save-dev --save-exact prettier.
  3. Create config file: echo {}> .prettierrc.json.
  4. Bind prettier with eslint: npm install --save-dev eslint-config-prettier.
{
  "printWidth": 80,
  "tabWidth": 2,
  "semi": false,
  "singleQuote": true,
  "trailingComma": "none",
  "bracketSpacing": true,
  "arrowParens": "avoid",
  "proseWrap": "always"
}
{
  "extends": [
    "some-other-config-you-use",
    "@sanity/eslint-config-studio",
    "prettier"
  ]
}
  1. Point TypeScript config file in parserOptions.project.
{
   "format": "prettier --write schemas/**/*.ts",
    "lint": "eslint --fix schemas/**/*.ts",
    "test": "npm run format && npm run lint"
}
  1. Commit Add linting.

Pre-commit hook

Lint files before commiting them with husky: npx husky-init && npm install.

Commit Add pre-commit hook.

Deploy

Just use Netlify.

Git issue

If the problem is "main and master are entirely different commit histories.", the following will work.

git checkout feature/gatsby

git branch main feature/gatsby -f

git checkout main

git push origin main -f