マークダウンから変換したhtmlのaタグをNext.jsのLinkタグに変換する
自分用のメモです。
Next.jsのチュートリアルではマークダウンファイルを自動でhtmlに変換していますが、その変換したhtmlの中でnext/linkを使いたい、と思ったのがきっかけです。
端的に言うと「rehype-reactのオプションでaを自作コンポーネントに置換する」だけです。
ただ、この方法が良いのかどうかはわかりません。
マークダウンの中でjsx記法が使えるmdxを使うのが一番良いのかな~。
コード
HtmlToReact.ts
import React from "react";
import unified from "unified";
import rehypeParse from "rehype-parse";
import rehypeReact from "rehype-react";
import MarkdownLink from "@/components/blog/MarkdownLink";
const HtmlToReact = (content: string) => {
const result = unified()
.use(rehypeParse, { fragment: true }) // head bodyを出力しない
.use(rehypeReact, {
createElement: React.createElement,
components: { a: MarkdownLink },
})
.processSync(content).result;
return result;
};
export default HtmlToReact;
やってることは単純で、htmlをReactElementに変換する際に、rehype-reactのオプションにあるcomponents: でaを自分で作ったコンポーネントMarkdownLinkに置き換えているだけです。
HtmlToReactでは、htmlの文字列を引数とします。Next.jsのチュートリアルで、remark()を使って出力するhtmlが使えると思います。
読み込んだhtml文字列はunifiedで変換します。nextjsのチュートリアルで使うremark()と、基本は同じ使い方をするプラグインです。rehype-parseで文字列を構文木にパースして
rehype-reactでReactElementに変換しています。
MarkdownLink.tsx
import React from "react";
import Link from "next/link";
type Props = {
children: React.ReactNode;
href: string;
};
const MarkdownLink = ({ children, href }: Props) => {
if (href.startsWith("http")) {
return (
<a href={ href } target="_blank" rel="noopener noreferrer">
{ children }
</a>
);
}
else {
return (
<Link href={ href }>
<a>{ children }</a>
</Link>
);
}
};
export default MarkdownLink;
こちらはaの代わりに使うリンクです。
外部リンクは普通のaタグを使っています。外部リンクと判別するための条件は「http」で始まるリンクをとしていますが、これは記事を書く際の運用で決めれば良いと思います。
httpで始まらない場合は内部リンクとみなして<Link>を使う形です。
注意点は、外部リンクでtarget="_blank"を使っているので、脆弱性対策のためにrel="noopener noreferrer"を入れているところです。
使用するプラグイン
next.jsのチュートリアルでも使われているremark-htmlなどと同類のものです。
いろんな所で使われているようです。気づかないうちにunified使用のサービスを自分も使っている、なんてことも多そうですね。
まとめと、解決できていない問題について
苦肉の策、といった感じですが、htmlにした文字列をさらにReactElementに再変換することで強引に実現しています。
マークダウンファイルを変換してブログ記事を作る際に、自動で変換できるのはとても便利ですが、個別にcssを当てたり、コンポーネントを当てたりするにはどうしたらいいの? と素人としては結構困りました。
Next.jsのチュートリアルの後、自分が思うブログを作ろうと思ったときに、この「マークダウンから変換してきた記事」の扱いでかなり躓きまして(今でも躓いて起き上がれてない部分もありますが)、そのうちの解決策の一つがrehype-reactでした。
冒頭で書きましたが、最初からmdxを使うのが良いのかな~というのが、これを作った後に色々調べて思いついた解決策です。
解決できていない問題
<a>を<Link>に変換する件に関しては、これで動いているので良いのですが、Next.jsで自分が解決できていない問題がありまして。
Next.jsで動的ルーティングをする際の<Link>とuseEffectについてです。
Next.jsでは、pageフォルダ配下で[id].tsxみたいな感じで動的ルーティングをすることができます。この[id]内で<Link>を使って別の[id]のページに飛ぶと[id]で使用するuseEffectのclean upが動作しないのです。まあ仕組み的には動作しないのが正しいのですよね、多分。useEffectのcleanupはアンマウントしたときに動作するわけなので。
router.eventsのrouteChangeCompleteとかrouteChangeStartとか使えばなんとかなるのかなと色々やってみたのですが、上手くいかず…。
なので、結局[id]内で[id]にnext/linkでリンクを貼るのは諦めました。
とは言っても[id]内でuseEffectを使っていなければ問題は無さそうなので、前の記事・次の記事のリンクなどに使えます。
Discussion