私と家とiPhoneと

私とGatsbyとChakra UI

gatsby_x_chakura

はじめに

こんにちは私です。 今日は以前から書いていたこのブログのデザインをテンプレートのものから刷新してChakra UIを使ったオリジナルのものに変えてみましたのでその方法について紹介できればと思います。

合わせてせっかくTypescriptを使っているのでGraphQLの型サポートもちゃんと得られるようにしたのですが、それはまた後日気力が残っていれば記事にできればと思います。

GatsbyでChakra UIを使う

gatsbyコマンドでテンプレートからプロジェクトを作る

まずはgatsbyコマンドを使ってChakra UI導入済みのプロジェクトを作成します。

gatsby new my-chakra-ui-starter https://github.com/chakra-ui/gatsby-starter-chakra-ui-ts

これだけでChakraUIを使う準備が整います。 初期のファイル構成としてはnode_modulesを除くとこのようになっています。 なんとrenovateまで標準装備しています。(私は自前で作っている設定があるのでそちらを使っています)

❯ tree -I node_modules
.
├── LICENSE
├── README.md
├── gatsby-config.ts
├── package-lock.json
├── package.json
├── renovate.json
├── src
│   └── pages
│       ├── 404.tsx
│       └── index.tsx
└── tsconfig.json

gatsby-config.tsを除いてみると以下のようになっています。

import type { GatsbyConfig } from "gatsby";

const config: GatsbyConfig = {
  siteMetadata: {
    siteUrl: `https://www.yourdomain.tld`,
  },
  // More easily incorporate content into your pages through automatic TypeScript type generation and better GraphQL IntelliSense.
  // If you use VSCode you can also use the GraphQL plugin
  // Learn more at: https://gatsby.dev/graphql-typegen
  graphqlTypegen: true,
  plugins: [
    {
      // https://chakra-ui.com/getting-started/gatsby-guide
      resolve: "@chakra-ui/gatsby-plugin",
      options: {},
    },
  ],
};

export default config;

このままではcontentfulと連携したページ生成ができないため以下のような設定に書き換えます。 npm installコマンドは省略していますので必要に応じて入れましょう。

import type { GatsbyConfig } from 'gatsby'

require('dotenv').config({
  path: `.env.${process.env.NODE_ENV}`,
})

const siteUrl = process.env.SITE_URL ?? `https://yourdomain.example.com`
const siteTitle = process.env.SITE_TITLE ?? `Your blog title`
const siteDescription = process.env.SITE_DESCRIPTION ?? `Your site description`

const config: GatsbyConfig = {
  siteMetadata: {
    title: siteTitle,
    description: siteDescription,
    siteUrl,
  },
  // More easily incorporate content into your pages through automatic TypeScript type generation and better GraphQL IntelliSense.
  // If you use VSCode you can also use the GraphQL plugin
  // Learn more at: https://gatsby.dev/graphql-typegen
  graphqlTypegen: true,
  plugins: [
    {
      resolve: 'gatsby-plugin-google-gtag',
      options: {
        trackingIds: [process.env.GOOGLE_ANALYTICS_TRACKING_ID],
        pluginConfig: {
          head: true,
        },
      },
    },
    {
      resolve: 'gatsby-source-contentful',
      options: {
        accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
        spaceId: process.env.CONTENTFUL_SPACE_ID,
        enableTags: true,
      },
    },
    {
      resolve: 'gatsby-transformer-remark',
    },
    {
      resolve: 'gatsby-plugin-image',
    },
    {
      resolve: 'gatsby-plugin-sitemap',
      options: {
        output: '/',
      },
    },
    {
      // https://chakra-ui.com/getting-started/gatsby-guide
      resolve: '@chakra-ui/gatsby-plugin',
      options: {
        /**
         * @property {boolean} [resetCSS=true]
         * if false, this plugin will not use `<CSSReset />
         */
        resetCSS: true,
        /**
         * @property {number} [portalZIndex=undefined]
         * The z-index to apply to all portal nodes. This is useful
         * if your app uses a lot z-index to position elements.
         */
        portalZIndex: undefined,
      },
    },
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        name: 'images',
        path: './src/images/',
      },
      __key: 'images',
    },
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        name: 'pages',
        path: './src/pages/',
      },
      __key: 'pages',
    },
    {
      resolve: 'gatsby-plugin-manifest',
      options: {
        icon: 'src/images/icon.png',
      },
    },
  ],
}

export default config

設定を入れたら今度は記事ページの生成に必要な以下のファイルを作成していきます。

  • gatsby-node.js
  • templates/blog-post.tsx

参考までにファイル構成は以下のようになっています。

.
├── LICENSE
├── README.md
├── gatsby-config.ts
├── gatsby-node.js
├── package-lock.json
├── package.json
├── renovate.json
├── src
│   ├── @chakra-ui
│   │   └── gatsby-plugin
│   │       └── theme.ts
│   ├── components
│   │   ├── Footer.tsx
│   │   ├── Header.tsx
│   │   ├── MarkdownTemplate.tsx
│   │   └── Seo.tsx
│   ├── gatsby-types.d.ts
│   ├── images
│   │   └── icon.png
│   ├── layout
│   │   └── Layout.tsx
│   ├── pages
│   │   ├── 404.tsx
│   │   └── index.tsx
│   └── templates
│       └── blog-post.tsx
└── tsconfig.json

各ファイルの中身は以下のようになっていて、ContentfulからGraphQLで取得した記事結果のページ情報に ./src/templates/blog-post.tsx の内容をテンプレートとしてページ生成をするようになっています。 またその際のページのURLは /blog/${post.slug}/ としています。

const path = require('path')

exports.createPages = async ({ graphql, actions, reporter }) => {
  const { createPage } = actions

  // Define a template for blog post
  const blogPost = path.resolve('./src/templates/blog-post.tsx')

  const result = await graphql(
    `
      query CreatePage {
        allContentfulBlogPost {
          nodes {
            title
            slug
          }
        }
      }
    `
  )

  if (result.errors) {
    reporter.panicOnBuild(
      `There was an error loading your Contentful posts`,
      result.errors
    )
    return
  }

  const posts = result.data.allContentfulBlogPost.nodes

  // Create blog posts pages
  // But only if there's at least one blog post found in Contentful
  // `context` is available in the template as a prop and as a variable in GraphQL

  if (posts.length > 0) {
    posts.forEach((post, index) => {
      const previousPostSlug = index === 0 ? null : posts[index - 1].slug
      const nextPostSlug =
        index === posts.length - 1 ? null : posts[index + 1].slug

      createPage({
        path: `/blog/${post.slug}/`,
        component: blogPost,
        context: {
          slug: post.slug,
          previousPostSlug,
          nextPostSlug,
        },
      })
    })
  }
}

記事ページを生成するテンプレートページは以下のようになっており、GraphQLで取得した各ページの詳細情報をChakra UIで作ったコンポーネントでレイアウトしていってます。 ここで重要なのはContentfulからMarkdown形式で取得したコンテンツのChakraUIへのタグへの変換となり MarkdownTemplate.tsx というコンポーネントを作って、その中で変換を行っています。

import React from 'react'
import { PageProps, graphql } from 'gatsby'
import { Center, Container, Heading, Image, Stack } from '@chakra-ui/react'
import { MarkdownTemplate } from '../components/MarkdownTemplate'
import Layout from '../layout/Layout'
import Seo from '../components/Seo'

const BlogPostTemplate: React.FC<PageProps<Queries.BlogPostQuery>> = ({
  data,
}) => {
  const post = data.contentfulBlogPost
  return (
    <Layout>
      <Seo title={post?.title ?? ''} />
      <Container as="main" maxW="container.lg" marginTop="4" marginBottom="16">
        <Stack spacing="8">
          <Heading as="h1">{post?.title}</Heading>
          <Center>
            {post?.heroImage?.resize && (
              <Image
                src={post.heroImage.resize.src ?? undefined}
                alt={post.heroImage.title ?? ''}
              />
            )}
          </Center>
          <MarkdownTemplate
            source={post?.body?.childMarkdownRemark?.html ?? ''}
          />
        </Stack>
      </Container>
    </Layout>
  )
}

export default BlogPostTemplate

export const pageQuery = graphql`
  query BlogPost(
    $slug: String!
    $previousPostSlug: String
    $nextPostSlug: String
  ) {
    contentfulBlogPost(slug: { eq: $slug }) {
      slug
      title
      author {
        name
      }
      publishDate(formatString: "YYYY/MM/DD")
      rawDate: publishDate
      heroImage {
        gatsbyImageData(layout: FULL_WIDTH, placeholder: BLURRED, width: 1280)
        resize(height: 630, width: 1200) {
          src
        }
        title
      }
      body {
        childMarkdownRemark {
          html
          timeToRead
        }
      }
      description {
        childMarkdownRemark {
          excerpt
        }
      }
      metadata {
        tags {
          id
          name
        }
      }
    }
    previous: contentfulBlogPost(slug: { eq: $previousPostSlug }) {
      slug
      title
    }
    next: contentfulBlogPost(slug: { eq: $nextPostSlug }) {
      slug
      title
    }
  }
`

記事ページにおいてMarkdownコンテンツをChakra UIのコンポーネントを使って描画する

先程のコードにあった MarkdownTemplate.tsx ですが、以下のページを参考にMarkdownのコンテンツをChakra UIのコンポーネントに変換していきました。

https://qiita.com/reona396/items/95a156be6e3ad436cf47

このコンポーネントではhtml-react-parserというパッケージを使ってhtmlのdomを解釈してmarkdownでHTML化されたタグをChakra UIのコンポーネントにマッピングするようになっています。 私の場合、iframelyを使ったタグの挿入も行っていたので一部aタグの部分だけ以下のように置き換えています。

        if (
          domNode.name === 'a' &&
          // For iframely
          !('data-iframely-url' in domNode.attribs)
        ) {
          return (
            <Link {...a.props} href={domNode.attribs.href}>
              {domToReact(domNode.children, options)}
            </Link>
          )
        }

おわりに

諸々説明を端折った部分はありますが、既存のHTMLタグとCSSベースのデザインから素人でもそれっぽい見た目やDark Mode対応可能なChakra UIのページへと生まれ変わらせることができました。 Markdownの置き換えで最後どうしようか絶望しかけましたが、先人の方の情報でなんとか乗り切ることができました。

私と電気工事士と実技試験本番と