このブログは記事はMarkdownをTMLに変換し配信しており、Next.jsで実装している。
もう少し詳細に説明すると、Markdownをremark-parseでパースし、remark-rehypeでhast(HTML AST)に変換した後に、rehype-stringifyでHTML文字列に変換している。 そして、HTML文字列をdivタグにdangeroudlySetInnerHTMLで埋め込んでいた。 実装については以下のような感じになっていた。
import rehypeHighlight from "rehype-highlight";
import remarkParse from "remark-parse";
import remarkRehype from 'remark-rehype';
import rehypeStringify from "rehype-stringify"
import { unified } from "unified";
const markdownToHtml = async (markdown: string): Promise<string> => {
const result = await unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeHighlight)
.use(rehypeStringify, { allowDangerousHtml: true })
.process(markdown);
return result.toString();
}
const Page = async () => {
// 省略
const html = await markdownToHtml(markdown);
return (
<div
dangerouslySetInnerHTML={{_html: html}}
/>
);
}
このブログで利用するMarkdownはHTMLの埋め込みを可能にしている。 Twitter PublishをHTMLとして埋め込んでツイートを表示しようとした。しかし、トップページから記事に遷移すると、埋め込んだTweetが表示されないという問題が発生した。一方で、他のページから遷移せずに直接記事のURLを開くとTweetが表示されていた。
調査するとMarkdownに埋め込んだHTMLにscriptタグが含まれている場合に今回の問題が発生する事がわかった。Twitter Publishを用いてTweetを埋め込むとscriptタグを利用して https://platform.twitter.com/widgets.js を読み込むので、今回の問題が発生してしまった。
このブログの実装ではページ遷移には next/link
を利用している。next/link
による遷移はクライアントサイドのナビゲーションが行われており、Reactは dangerouslySetInnerHTML
を処理する際に内部的に innerHTML
を利用する。しかし、HTML5ではinnerHTML
はscriptタグを実行しない。そのため上記の問題が発生してしまった。
一方で直接URLを開いた場合はサーバーサイドレンダリングが行われてからクライアントに渡されるので、scriptタグの実行が行われ、問題が発生しなかった。
調べていると、 @next/mdx
と next-mdx-remote
を利用するとやりたいことは実装できそうに見えた。ただ、今のremarkとrehypeで変換する方法は、比較的トラブルシューティングがしやすそうな構成でありできれば維持したいなぁと思った。
正直に言うと @next/mdx
の導入資料を見ると next.config.js
を変更しているが、next.config.js
の変更に対する苦手意識があり、これを変更したくなかった。
最終的には rehype-reactというライブラリを見つけ、これを利用することにした。
このライブラリはrehypeの中間表現(hast)をReact Componentに変換してくれる。
これにより dangerouslySetInnerHTML
を利用する必要がなくなった。
また、rehype-react
はコンポーネントの置き換えにも対応しており、scriptタグを next/script
に置き換えている。
実際の実装は以下のような感じになった。
import rehypeHighlight from "rehype-highlight";
import remarkParse from "remark-parse";
import remarkRehype from 'remark-rehype';
import * as prod from 'react/jsx-runtime';
import React from "react";
import rehypeReact from "rehype-react";
import { unified } from "unified";
import rehypeRaw from "rehype-raw";
import Script from "next/script";
const markdownToReact = async (markdown: string): Promise<JSX.Element> => {
const result = await unified()
.use(remarkParse) // md -> mdast
.use(remarkRehype, { allowDangerousHtml: true }) // mdast -> hast
.use(rehypeRaw) // hast -> hast
.use(rehypeHighlight) // hast -> hast
.use(rehypeReact, {
...prod,
components: {
script(props) {
return React.createElement(Script, {
...props,
strategy: "lazyOnload"
})
}
}
})
.process(markdown);
return result.result;
}
const Page = async () => {
// 省略
const rc = await markdownToReact(markdown);
return rc;
}
(stackoverflow)React: Script tag not working when inserted using dangerouslySetInnerHTML