🐱

マークダウンから変換したhtmlのaタグをNext.jsのLinkタグに変換する

2021/06/29に公開

自分用のメモです。

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.eventsrouteChangeCompleteとかrouteChangeStartとか使えばなんとかなるのかなと色々やってみたのですが、上手くいかず…。

なので、結局[id]内で[id]next/linkでリンクを貼るのは諦めました。

とは言っても[id]内でuseEffectを使っていなければ問題は無さそうなので、前の記事・次の記事のリンクなどに使えます。

Discussion