Gatsby+remarkでMarkdown内で独自コンポーネントを使用する

Gatsby+remarkでMarkdown内で独自コンポーネントを使用する

2021年07月10日

GatsbyReact
Gatsby+remark

はじめに

以下の理由より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

となり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
      }
    }
    ...
`
  1. rehype-reactをインポートする。
  2. rehype-reactのオプションを指定する。
    • Fragmentを指定しているのはこの指定がないと余計なdivタグでラッピングされるのを防いでいます。
  3. rehype-reactを使用してレンダリング
  4. 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>
html2

となり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>
error

これは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ももう少し調べて比較したいなと思いました。

ホーム記事一覧プライバシーポリシーお問い合わせ
© 2021 luku.work