私とGatsbyとChakra UI
はじめに
こんにちは私です。 今日は以前から書いていたこのブログのデザインをテンプレートのものから刷新して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の置き換えで最後どうしようか絶望しかけましたが、先人の方の情報でなんとか乗り切ることができました。