Next.jsとChakra UIとmicroCMSでMarkdownで書けるページを作る

# 概要
![サンプル記事](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67311/f97e6089-bcc4-18cf-1b61-9df0d05afefc.png)

こういう感じのMarkdownで書けるページをNext.jsとChakra UIとmicroCMSで実装する。

# 実装

## プロジェクトの準備

https://blog.microcms.io/microcms-next-jamstack-blog/

上記を参考にプロジェクトの準備をする。
TypeScriptとChakra UIも入れておく。

```:ターミナル
$ npx create-next-app microcms-next-jamstack-blog

$ cd microcms-next-jamstack-blog

$ yarn add -D typescript @types/react @types/node

$ touch tsconfig.json

# ここで_app.jsとindex.jsの拡張子を.tsxに書き換える

$ yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion

$ yarn dev
```

## microCMSの準備
アカウント登録、ログイン、サービスの作成、APIの作成などこちらを参考に進める。
API テンプレートからブログを選択しておく。

https://blog.microcms.io/microcms-next-jamstack-blog/

「ブログ」と「カテゴリ」のAPIが作られるのでブログを選択する。

![microCMS](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67311/83cc0ac4-fee1-9ac5-b81d-2e4403a3b6cb.png)

「ブログ」のAPI設定からAPIスキーマを選択し、`content`が「リッチエディタ」になっていることを確認する。
microCMSではこのリッチエディタがMarkdownに対応しているエディタである。

![content](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67311/763fe0ce-bbb4-612c-4993-892340f92ecc.png)

「ブログ」の中に入っているサンプル記事をリッチエディタで編集する。
`#`で見出し、`*`で箇条書きなど基本的なMarkdown記法が使える。
リンクも`[リンクテキスト](リンクURL)`の書き方で問題ない。
リッチエディタ上ではスタイルとして変換後の状態で表示される。

![Markdown](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67311/716279e0-ca96-daff-3414-2d4bed39c15c.png)

書き終わったら右上の「APIプレビュー」から「取得」を選択し、
見出しが`h1`などhtmlに変換されているか確認する。

## プロジェクトとAPIの接続
APIキーの設定を下記を参考に進める。
APIキーは先ほど開いた「APIプレビュー」の画面に書かれている。

https://blog.microcms.io/microcms-next-jamstack-blog/

同じく上記記事を参考に「microcms-js-sdk」を導入する。

```:ターミナル
$ yarn add microcms-js-sdk
```

`libs`ディレクトリを作成し`client.js`ファイルを作成する。
`serviceDomain`は`XXXX.microcms.io`のXXXXの文字列を入力する。

```js:client.js
import { createClient } from 'microcms-js-sdk';

export const client = createClient({
  serviceDomain: 'service-domain',
  apiKey: process.env.API_KEY,
});
```

## 表示のための整備

`_app.tsx`をChakra UIの書き方に合わせて修正する。

```tsx:_app.tsx
import "../styles/globals.css";

import { ChakraProvider } from "@chakra-ui/react";

function MyApp({ Component, pageProps }) {
  return (
    <ChakraProvider>
      <Component {...pageProps} />
    </ChakraProvider>
  );
}

export default MyApp;
```

`index.tsx`も修正する。

```tsx:index.tsx
import { Box, Heading } from "@chakra-ui/react";
import React from "react";
import { MarkdownTemplate } from "../components/MarkdownTemplate";
import { client } from "../libs/client";
import type { Blog } from "../types/blog";

type Props = {
  blog: Array<Blog>;
};

export default function Home({ blog }: Props) {
  const title = blog[0].title;
  const content = blog[0].content;

  return (
    <>
      <Box p="10">
        <Heading
          as="h1"
          fontWeight={"200"}
          lineHeight={1.25}
          mb={".67em"}
          pb={".3em"}
          size="4xl"
        >
          {title}
        </Heading>
        <MarkdownTemplate source={content} />
      </Box>
    </>
  );
}

export const getStaticProps = async () => {
  const data = await client.get({ endpoint: "blog" });

  return {
    props: {
      blog: data.contents,
    },
  };
};
```

`types`ディレクトリを作成し、`blog.ts`ファイルを作成する。

```ts:blog.ts
export type Blog = {
  id: string
  createdAt: string
  updatedAt: string
  publishedAt: string
  revisedAt: string
  title: string
  content: string
}
```

ここで「html-react-parser」を導入する。
[remarkablemark/html\-react\-parser: HTML to React parser\.](https://github.com/remarkablemark/html-react-parser)

```:ターミナル
$ yarn add html-react-parser
```

`components`ディレクトリを作成して`MarkdownTemplate.tsx`を作成する。
Chakra UIでは`a`タグを`Link`と表記するなど、タグの書き方もそのままでは使えないのでこのファイルで変換している。
また、見た目はGitHubのスタイルを参考にしている。

```tsx:MarkdownTemplate.tsx
import {
  BoxProps,
  Box,
  Text,
  UnorderedList,
  ListItem,
  Link,
} from "@chakra-ui/react";
import parse, { domToReact, HTMLReactParserOptions } from "html-react-parser";

type MarkdownTemplateProps = {
  source: string;
} & BoxProps;

const h1 = {
  component: Text,
  props: {
    mt: "24px",
    mb: "16px",
    lineHeight: 1.25,

    color: "#202020",
    fontWeight: "600",
    pb: ".3em",
    fontSize: "2em",
    borderBottom: "1px solid #E7ECF2",
  },
};

const h2 = {
  component: Text,
  props: {
    mt: "24px",
    mb: "16px",
    lineHeight: 1.25,

    color: "#202020",
    fontWeight: "600",
    pb: ".3em",
    fontSize: "1.5em",
    borderBottom: "1px solid #E7ECF2",
  },
};

const h3 = {
  component: Text,
  props: {
    mt: "24px",
    mb: "16px",
    lineHeight: 1.25,

    color: "#202020",
    fontWeight: "600",
    fontSize: "1.25em",
  },
};

const p = {
  component: Text,
  props: {
    lineHeight: "1.5",
    mb: "10px",
    fontSize: "16px",
    color: "##000",
  },
};

const ul = {
  component: UnorderedList,
  props: {
    color: "#000",
    mt: 0,
    mb: 0,
    pl: "2em",
    lineHeight: "1.6",
  },
};

const li = {
  component: ListItem,
};

const a = {
  component: Link,
  props: {
    isExternal: true,
    textDecoration: "none",
    color: "#0058B3",
    _hover: {
      textDecoration: "none",
      color: "#4593e6",
    },
  },
};

const options: HTMLReactParserOptions = {
  replace: (domNode: any) => {
    if (domNode.type === "tag") {
      if (domNode.name === "h1") {
        return (
          <Text as="h1" {...h1.props}>
            {domToReact(domNode.children, options)}
          </Text>
        );
      }
      if (domNode.name === "h2") {
        return (
          <Text as="h2" {...h2.props}>
            {domToReact(domNode.children, options)}
          </Text>
        );
      }
      if (domNode.name === "h3") {
        return (
          <Text as="h3" {...h3.props}>
            {domToReact(domNode.children, options)}
          </Text>
        );
      }
      if (domNode.name === "ul") {
        return (
          <UnorderedList {...ul.props}>
            {domToReact(domNode.children, options)}
          </UnorderedList>
        );
      }
      if (domNode.name === "li") {
        return <ListItem>{domToReact(domNode.children, options)}</ListItem>;
      }
      if (domNode.name === "a") {
        return (
          <Link {...a.props} href={domNode.attribs.href}>
            {domToReact(domNode.children, options)}
          </Link>
        );
      }
      if (domNode.name === "p") {
        return (
          <Text {...p.props}>{domToReact(domNode.children, options)}</Text>
        );
      }
    }
  },
};

export const MarkdownTemplate = (props: MarkdownTemplateProps) => {
  return <Box {...props}>{parse(props.source, options)}</Box>;
};
```

# 反映の確認

```:ターミナル
$ yarn dev
```

すると下記のような見た目になるはず。
Markdownで編集・更新できるページの完成🎉

![完成した見た目](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67311/0dce4ae4-881d-85d8-17ac-e4085d5b443c.png)

# 参考資料
* [NextとchakraUIとmicroCMSでJAMStackなブログを作った \- purpleeeee\.com](https://purpleeeee.com/posts/jfp9s5fsalf)
* [Next\.js\(TypeScript\)、TailwindCSS、microCMSでブログを作成しよう！](https://zenn.dev/elletech/articles/nextjs_microcms)
* [microCMSを使ってマークダウンで入力するブログを作る \| microCMSブログ](https://blog.microcms.io/create_markdown_blog/)
* [sindresorhus/github\-markdown\-css: The minimal amount of CSS to replicate the GitHub Markdown style](https://github.com/sindresorhus/github-markdown-css)