本記事のゴール

Markdownで書いた項目を、Next.jsプロジェクト内でカンタンに呼び出せるようにしたい。

作業

要件定義

以下のようなMarkdown形式の項目があり、こちらをNext.jsのページでコンテンツとして表示したいです。

# タイトル

内容がここに入ります。
## 見出しレベル2

[リンク](https://moldspoon.jp/blog)は当ブログのトップページに貼ってあります。

.mdxファイル形式を使い、並列でファイルを作っていくことでページを作っていくアプリ構成も取れる のですが、今回はもうちょっと複雑なWebサービスを開発しており、ページの一部分だけマークダウン形式としたいので ちょっと要件がずれてきます。

そのような時のために今回のコンポーネントを作成しました。

markedを使って実装

今回は下記のmarked というmarkdown => html置換ライブラリを使用します。

markedのデモページが結構すごい

markedのデモページはこちら

どのように置換するのかを、リアルタイムでプレビューして見せてくれます。

markdown1.png

個人的に便利だなと思うのうは、Optionsを変更するとそれに合わせて出力内容を変えてくれたり

微妙に出力方式を変えたい時に事前に確認できる。

微妙に出力方式を変えたい時に事前に確認できる。

出力後のhtmlソースコードが見れるところですかね。

あとで、カスタマイズをする時に役に立ちます。

あとで、カスタマイズをする時に役に立ちます。

詳しいドキュメントはこちらのようです。

下準備〜ライブラリインストール

Next.jsプロジェクトのルートディレクトリで以下のコマンドを実行

yarn add marked

これだけです。2011年からある古いライブラリのようなのですが、typescriptにもしっかり対応しているようです。(自分のユースケースでは特にtypescriptは使いませんでした。)

コンポーネントを作成

下記のように実際のコンポーネントを組んでいきます。

components/Markdownview.tsx
import { marked } from 'marked';
import React from 'react';

var renderer = new marked.Renderer();
renderer.link = function (href, title, text) {
  var link = marked.Renderer.prototype.link.call(this, href, title, text);
  return link.replace("<a", "<a target='_blank' ");
};

const MarkdownView = ({
  markdownStringBody,
}: {
  markdownStringBody: string
}) => {


  const options = {
    gfm: true,
    breaks: true,
    pedantic: true,
    smartLists: true,
    smartypants: true,
    renderer: renderer
  };

  const renderMarkdown = (text: string) => {
    return marked(text, options);
  };

  return <div dangerouslySetInnerHTML={{ __html: renderMarkdown(markdownStringBody) }} />

}

export default MarkdownView

呼び出し方は、

<MarkdownView markdownStringBody={markdownStringBody} />

のようにして、マークダウンで入力されたコンテンツをそのままMarkdownViewに引数として渡します。

以下で詳しく解説します。

markedに渡して、単純に出力する部分

クラスの先頭で下記のようにシンプルにstring形式で受け取り、

const MarkdownView = ({
  markdownStringBody,
}: {
  markdownStringBody: string
}) => {
//...
}

optionsを定義。marked()メソッドに内容とオプションを渡してmarkdownをhtmlに変換するように します。

  const options = {
    gfm: true,
    breaks: true,
    pedantic: true,
    smartLists: true,
    smartypants: true,
    // renderer: renderer <= あとで解説します。
  };

  const renderMarkdown = (text: string) => {
    return marked(text, options);
  };
const MarkdownView = ({
  markdownStringBody,
}: {
  markdownStringBody: string
}) => {
  // ...

  return <div dangerouslySetInnerHTML={{ __html: renderMarkdown(markdownStringBody) }} />

}

export default MarkdownView

最後にhtmlとしてそのまま変換後のコンテンツを出力します。dangerouslySetInnerHTMLはCGM(ユーザー投稿があるコンテンツ)ではインジェクションの被害を受ける可能性があるので、この方法は 使えないですが、マークダウン形式でユーザーが入稿することは多分ほとんどないので大きな問題はないかなと思っています。

出力後に、htmlを置換する部分

私のケースでは、aリンクを必ず外部リンクにしたかった ので、全てのマークダウンコンテンツで変換するようにします。

それを行なっているのが以下の箇所です。

import { marked } from 'marked';
// ...

var renderer = new marked.Renderer(); // markedをimportするだけで使えます
renderer.link = function (href, title, text) {
  var link = marked.Renderer.prototype.link.call(this, href, title, text); // aリンクを全て取得
  return link.replace("<a", "<a target='_blank' "); // aリンクを単純に外部呼び出しするように変換
};

const MarkdownView = ({
  markdownStringBody,
}: {
  markdownStringBody: string
}) => {


  const options = {
    gfm: true,
    breaks: true,
    pedantic: true,
    smartLists: true,
    smartypants: true,
    renderer: renderer // ここでオプションに必ず指定
  };
  ...
}

export default MarkdownView

全ての箇所で置換を行ってくれるのがポイントで、一種のパーサーになると思います。


最終的には、冒頭で挙げたマークダウン形式のファイルが無事表示できました。

markdown4.png
補足: mdxファイル形式なら、mdx-components.tsxで一斉置換できる。

なおファイル形式が.mdx形式に限り、htmlを置換する処理については、Next.jsのmdx-components.tsxをプロジェクトルートに置く形でも対応が可能です。下記の弊ブログエントリーの中で触れておりますのでよろしければご覧ください。

参考記事: Next.js製ブログにオリジナルの目次を追加する。

まとめ

今回のエントリーを書いていてあらためて、markdownっていいよね、と思いました。 htmlをコーディングするよりずっと早くコンテンツ作れますしね。

それに、太字、色付け...といった感じで」ボタンを押していくCMSよりも、キーボードから手を外さずに素早く装飾できますし、エンジニアならコンテンツ作る時にマークダウンは外せませんね!


この記事が何かのお役に立てれば幸いです。
最後までお読みいただきありがとうございました!