はじめに
以下の理由よりGatsby+remarkでMarkdown内に独自コンポーネントを使用したい欲求が出てきたのでやり方をまとめておく。
- Tailwind + Twin + Emotionでスタイリングしている。
- デフォルトで変換対象のh1,h2,p..などを置き換える必要がある。
- 上記以外のタグを使用してスタイリングをしたい。
前提条件
remarkを使用してマークダウンを元に記事を作成する構成が整っていること。remarkを使用してマークダウンを表示する方法はまた別で記載したいと思う。 具体的には以下のプラグインが入っており設定されていること。今回も gatsby-starter-blog を使用します。
- gatsby-source-filesystem
- gatsby-tansformer-remark
今回使用するパッケージ
rehype-react を使用します。
npmを使用している場合
$ npm install rehype-react or npm i rehype-react
yarnを使用している場合
$ yarn add rehype-react
下準備
マークダウンファイルを追加する
なにがどう変わったのかを確認するためにマークダウンファイルを追加します。
content/blog/remark-component/index.md
---
title: remark-component
date: 2021-07-10
description: remark-component
---
# test
## test
test
表示してみる
マークダウンを表示している箇所は以下のようになっており、markdownRemark.htmlの内容をdangerouslySetInnerHTMLに渡して表示しています。
src/templates/blog-post.js
import * as React from "react"
import { Link, graphql } from "gatsby"
import Bio from "../components/bio"
import Layout from "../components/layout"
import Seo from "../components/seo"
const BlogPostTemplate = ({ data, location }) => {
...
<section
dangerouslySetInnerHTML={{ __html: post.html }}
itemProp="articleBody"
/>
...
}
export default BlogPostTemplate
export const pageQuery = graphql`
...
markdownRemark(id: { eq: $id }) {
id
excerpt(pruneLength: 160)
html
frontmatter {
title
date(formatString: "MMMM DD, YYYY")
description
}
}
...
`
gatsby-starter-blogはデフォルトでディレクトリ名がURLになるので、以下コマンドでGatsbyを起動してブラウザで http://localhost:8000/remark-component
にアクセスすると
$ gatsby develop
となりhtmlは以下のようになっており、素のh1,h2,pに変換されていることが分かります。
...
<section itemprop="articleBody">
<h1>test</h1>
<h2>test</h2>
<p>test</p>
</section>
...
rehype-reactを使用してマークダウンを表示する
src/template/blog-post.jsを以下のように修正します。
src/templates/blog-post.js
...
import * as React from "react"
import { Link, graphql } from "gatsby"
// (1)
+import RehypeReact from "rehype-react"
import Bio from "../components/bio"
import Layout from "../components/layout"
import Seo from "../components/seo"
// (2)
+const renderAst = new RehypeReact({
+ createElement: React.createElement,
+ Fragment: React.Fragment
+}).Compiler
const BlogPostTemplate = ({ data, location }) => {
...
// (3)
- <section
- dangerouslySetInnerHTML={{ __html: post.html }}
- itemProp="articleBody"
- />
+ <section itemProp="articleBody">
+ {renderAst(post.htmlAst)}
+ </section>
...
}
export default BlogPostTemplate
export const pageQuery = graphql`
...
markdownRemark(id: { eq: $id }) {
id
excerpt(pruneLength: 160)
// (4)
- html
+ htmlAst
frontmatter {
title
date(formatString: "MMMM DD, YYYY")
description
}
}
...
`
- rehype-reactをインポートする。
- rehype-reactのオプションを指定する。
- Fragmentを指定しているのはこの指定がないと余計なdivタグでラッピングされるのを防いでいます。
- rehype-reactを使用してレンダリング
- Graphqlで取得している内容をhtmlからhtmlAstに変更する。
htmlは先ほどと同じ以下のようになります。
...
<section itemprop="articleBody">
<h1>test</h1>
<h2>test</h2>
<p>test</p>
</section>
...
これで独自コンポーネントを使用する準備ができました。
変換先のタグを置き換えをしてみる
まずはh1,h2,p..などを別のタグに置き換えてみます。
src/template/blog-post.jsを以下のように修正します。
src/templates/blog-post.js
...
const renderAst = new RehypeReact({
createElement: React.createElement,
Fragment: React.Fragment,
+ components: {
+ h1: 'h2',
+ h2: 'h3',
+ p: 'div'
+ }
}).Compiler
...
するとhtmlは以下のようになります。h1,h2,pがh2,h3,divに変換されているのが分かります。
...
<section itemprop="articleBody">
<h2>test</h2>
<h3>test</h3>
<div>test</div>
</section>
...
独自コンポーネントを使用する
いよいよ独自コンポーネントを使用します。こ こまできたら独自コンポーネントを使用するのは簡単です。 先ほどh1,h2,pを置き換えたようにrehype-reactのcomponentsオプションに指定するだけで使用できるようになります。
src/templates/blog-post.js
...
const Alert = ({ children }) => <div style={{backgroundColor: "red"}}>{children}</div>;
const renderAst = new RehypeReact({
createElement: React.createElement,
Fragment: React.Fragment,
components: {
h1: 'h2',
h2: 'h3',
p: 'div',
+ alert: Alert
}
}).Compiler
...
content/blog/remark-component/index.md
...
+ <alert>test</alert>
となりhtmlは以下のようになっており、独自コンポーネントが変換されているのが分かります。
...
<section itemprop="articleBody">
<h2>test</h2>
<h3>test</h3>
<div>test</div>
<div>
<div style="background-color: red;">test</div>
</div>
</section>
h1,h2なども独自コンポーネントに変換することもできますし、独自コンポーネントを使用することでonclickなどのイベントを使用することもできます。
注意点
ValidateDOMNestingエラー
今回カスタムコンポーネントを使用する際にpタグをdivタグに変換してしまっていましたが, そこを消すとhtmlは以下のようpタグ内にdivタグが生成されエラーが出てしまいます。
src/templates/blog-post.js
...
const Alert = ({ children }) => <div style={{backgroundColor: "red"}}>{children}</div>;
const renderAst = new RehypeReact({
createElement: React.createElement,
Fragment: React.Fragment,
components: {
- h1: 'h2',
- h2: 'h3',
- p: 'div',
+ alert: Alert
}
}).Compiler
...
<section itemprop="articleBody">
<h1>test</h1>
<h2>test</h2>
<p>test</p>
<p>
<div style="background-color: red;">test</div>
</p>
</section>
これはpタグないにdivタグを使えませんよというエラーになります。
タグ名、属性名は常に小文字で指定する。
* 以下はNG
<Alert Text="test"></Alert>
* 以下のようにする
# props.textでアクセス
<alert text="test"></alert>
閉じタグをつける。
* 以下はNG
<alert />
* 以下のようにする。
<alert></alert>
属性値は常に文字列になる
* React内で文字列の2として受け取る。
<alert value=2></alert>
エラー回避策
先程のようにpタグをdivタグのようなものに変換するとエラーは出ませんがpタグが使用できなくなってしまいますので、これを解決するには gatsby-transformer-remark を使用します。
gatsby-remark-componentはカスタムコンポーネントの親要素をdivタグに変更するプラグインです。
また以下のようにすることでdivタグで囲まれなくなりますのでそちらでもエラーは発生しなくなります。ただし要素にスペースが入りました。
<div>
<alert>test</alert>
</div>
<alert>
test
</alert>
gatsby-remark-componentの使い方
gatsby-config.jsを以下のように変更します。
optionsでコンポーネントを指定することもできますがアップデートで自動検出されるようになっているので特段指定する必要はありません。
...
{
resolve:"gatsby-transformer-remark"、
options:{
plugins:["gatsby-remark-component"]
}
}
...
htmlを見てみると以下のようにpタグからdivタグに変更されているのでエラーが発生しなくなりました。
<section itemprop="articleBody">
<h1>test</h1>
<h2>test</h2>
<p>test</p>
<div>
<div style="background-color: red;">test</div>
</div>
</section>
さいごに
remarkを用いてカスタムコンポーネントを使用する方法を解説しましたが、 色々調べてみるとMDXを使用する方法も多く書かれていてMDXももう少し調べて比較したいなと思いました。