Markdownの中に、Reactコンポーネントを使用できるようにする
目次
unifiedとは、Markdownで書いた文字列をHTMLに変換したり
さまざまなプラグインを使い、処理を行うことで、さまざまな処理が行えるようなプラグイン(ライブラリ?)になっているようです。
ちょっと全体像を理解しようとすると、かなり複雑になってしまいそうなので
詳しい説明は省いて、今回やりたいことと、必要な部分のみを乗せていきます。
今回やりたいこと
- MarkdownをHTMLに変換する
- 任意のカスタムタグを、Reactのコンポーネントに変換する
- propsも渡すようにする
必要なもの
unifiedを使って処理を書く
import React from 'react'
import { unified, Plugin } from 'unified'
import remarkGfm from 'remark-gfm'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeStringify from 'rehype-stringify'
import rehypeParse from 'rehype-parse'
import rehypeReact from 'rehype-react'
import Skill from '~/components/common/Skill'
import Comment from '~/components/comment/Comment'
import CustomLink from '~/components/common/CustomLink'
import CustomImage from '~/components/common/CustomImage'
// カスタムしたタグを作っている場合にはpタグで囲われてしまう問題があったので、pタグで囲わないように
const removeCustomElementParagraph: Plugin = () => {
return (tree: any) => {
const firstChildren = tree.children[0]
if(firstChildren?.type === 'paragraph' && firstChildren?.children![0].type === 'html') {
tree.children[0].type = 'html'
tree.children[0].value = firstChildren.children.map((v: any) => v.value).join('')
}
}
}
// 既存のタグをカスタマイズしたタグに変更したり、オリジナルのタグをコンポーネントに変換する処理を行なっています
export const parseReact = (str: string) =>
unified()
.use(rehypeParse, { fragment: true })
.use(rehypeReact, {
passNode: true,
Fragment: React.Fragment,
createElement: React.createElement,
components: {
a: CustomLink,
img: CustomImage,
skill: (props: any) => <Skill {...props}></Sns>,
comment: (props: any) => {
return <Comment isRight={props.isright === ''}>{props.children}</Comment>
},
},
} as any) // tyescriptの関係でanyにしないとエラーになったため
.processSync(str).result
// markdownの記述をHTMLに変換します
export const markdownToHtml = (markdown: string) =>
unified()
.use(remarkParse)
.use(removeCustomElementParagraph)
.use(remarkGfm)
.use(remarkRehype, {
allowDangerousHtml: true // trueにしておくことで、自分でカスタマイスしたタグをそのまま吐き出してくれるようになります。
})
.use(rehypeStringify, {
allowDangerousHtml: true
})
.processSync(markdown).value as string
markdownToHtmlについて
markdown形式の文字列(string
)をHTMLにタグに変換する処理を行なっています。removeCustomElementParagraph
では自前の処理で自分で作ったカスタムタグがpで囲われないようにしています。
parseReactについて
既存のタグをカスタマイズしたタグに変更したり、オリジナルのタグをコンポーネントに変換する処理を行なっています。
a
タグやimage
タグをNextのタグに変更
a: CustomLink,
img: CustomImage,
オリジナルのタグを作る
sns: (props: any) => <Sns {...props}></Sns>,
comment: (props: any) => {
return <Comment isRight={props.isright === ''}>{props.children}</Comment>
},
microCMS側でmarkdownで下記のように書くとそれぞれのコンポーネントに変換される仕組みになっています。
<comment>test</comment>
<comment isRight>test</comment>
<skill></skill>
propsは文字列としてであれば、渡せるようです。
なのでこれをうまく使って、<Comment isRight={props.isright === ''}>{props.children}</Comment>
このように書いてあげることで、propsを渡すような処理を書くことができます。
どうやら属性として渡す形なのか、キャメルケースにはならないようです。
この点気をつけながらであれば、propsもお手軽に使うことができます。
Linkタグ(next/link)に変更する
CustomLink.tsx
import React from 'react'
import Link from 'next/link'
const CustomLink : React.FC<{href: string}> = ({
children,
href,
}) => {
const isMyPageLink = href.startsWith('/') || href === ''
return isMyPageLink ? (
<Link href={href}>
<a>{children}</a>
</Link>
) : (
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
)
}
export default CustomLink
Imageタグ(next/image)に変更する
CustomImage.tsx
import React from 'react'
import Image from 'next/image'
interface CustomImage {
src: string
alt: string
width: number
height: number
}
const CustomImage : React.FC<CustomImage> = props => {
return ~props.src.indexOf('images.microcms-assets.io') ? (
<Image src={props.src} width={props.width} height={props.height} alt={props.alt} />
) : (
<img src={props.src} width={props.width} height={props.height} alt={props.alt} />
)
}
export default CustomImage
任意のurl(images.microcms-assets.io)が含まれている場合のみImageタグを使うようにしています。import Image from 'next/image'
この部分最初next/imaeg
をnext/Image
とI
を大文字にしちゃっていたので下記のようなエラーが出てしまっていました。
Server Error
Error: Invalid src prop (https://images.microcms-assets.io/assets/8d63ea55db89496cbf00ccb8c6bf8880/25cd3bb522294516b247308f15d7c9ca/fullbloom.jpg) on `next/image`, hostname "images.microcms-assets.io" is not configured under images in your `next.config.js`
See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host
ちゃんと正しくnext.config.jsのドメインにnext/image
で使用したいドメインを記述する必要があります。
ちゃんと記述していたのにも関わらず上のエラーが出てしまっていたので悩みました。。
const config = {
images: {
domains: ['images.microcms-assets.io'],
},
}
実際に使う
<div>
{props?.body?.map((v, i) => {
const commentValue = v as Comments
// カスタムフィールドのフィールドIDだった場合は自前のコンポーネントを使います
if(v.fieldId === 'comments') {
return commentValue.comments!.map((v) => <Comment {...v} key={`${v.fieldId}${i}`} />)
}
// htmlとmarkdownをmicroCMSのフィールドに作っていますその場合はmarkdownToHtmlを使います
if(v.fieldId === 'html' || v.fieldId === 'markdown') {
return <div key={`${v.fieldId}${i}`}>{parseReact(markdownToHtml(v?.value))}</div>
}
// リッチエディタ用
return <div key={`${v.fieldId}${i}`}>{parseReact(v?.value)}</div>
})}
</div>
fieldIdで判別を行なって、それぞれの処理を行うようにします
- カスタムフィールドで独自のコンポーネントを使いたい場合
- markdownの文字列から、HTMLタグを生成しさらに、自前のコンポーネントに差し替える処理を行いたい場合
- リッチエディタでもともとHTMLになっているタグをさらに、自前のコンポーネントに差し替える処理を行いたい場合
この3つのケースに処理を分けています。
リッチエディタとmarkdown変換分けなくても良くない問題
最初処理を分けずにmarkdownToHtml
を共通で使っていたのですがpre
タグのインデントがなくなってしまう問題があったので分けることにしました。