Next.jsでMarkdown->html変換時にnext/linkを利用したい


動機

  • Next.jsでMarkdownブログを作成するとき、記述したサイト内リンクを<Link>にしたい
  • ついでに外部リンクは別タブにしたい

参考ページはあるけどこの通りにはいかないつまずきポイントがあった(App Routerではない、という違いだけでもない)

rehypeReactのつまずきポイント

事象

createElementFragmentを指定すると、

import {createElement, Fragment} from react;

unified()
  .use(rehypeParse, {fragment: true})
  .use(rehypeReact, {
    createElement,
    Fragment,
    components: {
      a: CustomLink,
    },
  })

Type errorが出る。

Type error: Argument of type '[{ createElement: { (type: "input", props?: (InputHTMLAttributes<HTMLInputElement> & ClassAttributes<HTMLInputElement>) | null | undefined, ...children: ReactNode[]): DetailedReactHTMLElement<...>; <P extends HTMLAttributes<...>, T extends HTMLElement>(type: keyof ReactHTML, props?: (ClassAttributes<...> & P) | ... ...' is not assignable to parameter of type '[boolean] | [Options]'.
  Type '[{ createElement: { (type: "input", props?: (InputHTMLAttributes<HTMLInputElement> & ClassAttributes<HTMLInputElement>) | null | undefined, ...children: ReactNode[]): DetailedReactHTMLElement<...>; <P extends HTMLAttributes<...>, T extends HTMLElement>(type: keyof ReactHTML, props?: (ClassAttributes<...> & P) | ... ...' is not assignable to type '[boolean]'.
    Type '{ createElement: { (type: "input", props?: (React.InputHTMLAttributes<HTMLInputElement> & React.ClassAttributes<HTMLInputElement>) | null | undefined, ...children: React.ReactNode[]): React.DetailedReactHTMLElement<...>; <P extends React.HTMLAttributes<...>, T extends HTMLElement>(type: keyof React.ReactHTML, props?...' is not assignable to type 'boolean'.

Issueがあって最新verでは大丈夫だよと書いてあるけど、

it works with latest @types/react and rehype-react in a sandbox. https://codesandbox.io/s/rehype-react-types-b5pe22?file=/src/App.tsx

上のcodesandboxも最新バージョン(rehype-parse: 9.0.0 rehype-react: 8.0.0)にするとエラーになっちゃう

解決方法

rehype-reactのREADME見たら@ts-expect-errorで無視してた。 https://github.com/rehypejs/rehype-react#use

import * as prod from 'react/jsx-runtime'

// @ts-expect-error: the react types are missing.
const production = {createElement: prod.createElement, Fragment: prod.Fragment, jsx: prod.jsx, jsxs: prod.jsxs, }
unified()
  .use(rehypeParse, {fragment: true})
  .use(rehypeReact, production)

このjsxも指定してあげないとExpected jsx in production optionsというエラーになる。

けっこう時間を溶かしたが、READMEをちゃんと読みましょうということー

CustomLinkコンポーネントを定義する

next/linkはPage Router時代とちょっと仕様変わっているので注意

Linkの中にaタグを入れない方が良い

import Link from 'next/link';

const CustomLink = ({
  children,
  href,
}: {
  children: string;
  href: string;
}): JSX.Element =>
  (href.startsWith('/') || href.startsWith('#') || href === '') ? (
    <Link href={href}>
      {children}
    </Link>
  ) : (
    <a href={href} target="_blank" rel="noopener noreferrer">
      {children}
    </a>
  );

export default CustomLink;