当ブログは以前の記事りゅーそうブログを支える技術でも書いたように、Next.js × microCMSJamstack 構成で作成されています。

今回はNext.jsとmicroCMSを使って、Markdownで書く技術ブログの始め方を解説していこうと思います。

前提知識

React/Next.jsを触ったことがある。以前に書いた記事をお勧めします。当記事もTypeScriptを使用します。またNext.jsTutorialはやりがいもあって面白いので触れたことがない方にお勧めしておきます。

また当記事に使用するライブラリは以下の通りです。

  • next 9.2.1
  • react 16.12.0
  • react-dom 16.12.0
  • react-highlight 0.12.0
  • axios 0.19.2
  • dotenv 8.2.0
  • marked 0.8.0
  • typescript 3.7.5


環境構築

microCMSでコンテンツ作成

では初めていきましょう。
はじめにmicroCMSでコンテンツを作成していきます。
以下のようなスキーマのAPIを作成してください。microCMSのアカウント登録・スキーマの作成方法についてはmicroCMS Blogをご覧ください。
タグ

タグのAPIスキーマ


ブログ
コンテンツのAPIスキーマ

contentはテキストエリアを選択してください。リッチエディタは文字の装飾・画像の装飾などとても便利ですが今回はMarkdownを用いて書いて行くのでテキストエリアを選択します。
以上のようなスキーマを作成できたら、いくつかコンテンツを作成しておきましょう。

ライブラリのインストール

yarnを使います。npmを使用している方は、適宜npmに書き換えてお読みください。

yarn add  next react react-dom react-high-right axios dotenv marked


type定義のファイルなどをインストール

yarn add -D @types/node @types/react @types/react-dom @types/react-highlight @types/marked typescript


ブログ一覧・個別ページの作成

コードの解説は上記の記事をご覧ください。

ブログ一覧ページ

[src/pages/index.tsx]
import * as React from 'react';
import { NextPage } from 'next';
import Link from 'next/link';

import { axiosInstance } from '../../lib/api';
import { Post } from '../../types';


type Props = {
  posts: Post[];
};

const BlogsPage: NextPage<Props> = ({ posts }) => {
  return (
    <>
      <h2>BLOG 一覧</h2>
      <div>
        {posts.map(post => (
          <React.Fragment key={post.id}>
            <Link href={`posts/${post.id}`}>
              <a>
                <h2>{post.title}</h2>
              </a>
            </Link>
            {post.tags.map(tag => (
              <React.Fragment key={tag.id}>
                <span>{tag.name}</span>
              </React.Fragment>
          ))}
          </React.Fragment>
        ))}
      </div>
    </>
  );
};


PostsPage.getInitialProps = async () => {
  const res = await axiosInstance.get(
    `https://ryusou-mtkh.microcms.io/api/v1/posts/`,
  );
  const data: Post[] = await res.data.contents;
  return { posts: data };
};

export default PostPage;


ブログ個別ページ

[src/pages/[id].tsx]

import * as React from 'react';
import { NextPage } from 'next';

import { axiosInstance } from '../../lib/api';
import { IPost } from '../../interfaces';


type Props = {
  post: IPost;
};

const PostContent: NextPage<Props> = ({ post }) => {
  return (
    <>
      <h1>{post.title}</h1>
      <div>
        {post.tags.map(tag => (
          <React.Fragment key={tag.id}>
            <span>{tag.name}</span>
          </React.Fragment>
        ))}
      </div>
      <img src={post.image.url} />
      <div dangerouslySetInnerHTML={{ __html: `${post.content}` }}></div>
    </>
  );
};


PostContent.getInitialProps = async context => {
  const { id } = context.query;
  const res = await axiosInstance.get(
    `https://ryusou-mtkh.microcms.io/api/v1/posts/${id}`,
  );
  const post: Post = await res.data;
  return { post };
};

export default PostContent;


makedjsでMarkdownをparseする


ここからが本題です。markedjsというライブラリを使って、MarkdownをHTMLに変換して出力することで技術ブログを作成していきます。

そもそもマークダウンとは?

こちらのサイトがお勧めです。
Markdown書き方マニュアル
Qiitaの文章の書き方をイメージすると馴染みやすいですね。

markedjsとは

MarkdownをHTMLに変換するライブラリ。ドキュメントは以下の通りです。
Marked.js Documentation
使い方は簡単です。
以下のように、

import marked from 'marked'

const content = `# Marked!!  content!!`

const markedContent = marked(content);
console.log(merkedContent);
// <h1>Merked!!</h1>
//<p>content!!</p>


変換したい要素をmarkedで囲ってあげるだけなので簡単ですね。
また、markedには設定をすることもできます。

marked.setOptions({
  gfm: true,
  breaks: true,
  silent: false,
});


gfmとはGitHubが拡張したMarkdownのルールです。breaksを設定することによって改行を簡単にすることができます。設定可能なOptionについては以下をご覧ください。
Options
gfmとは

Highlight.jsとは

Markdownは以下のように記述することによって、コード例を示すことができます。

```javascript
const hoge = hogehoge;
console.log(hoge);
```

(※正しく```は半角で入力します。説明のため全角で表示しています。)
Highlight.jsはMarkdownのこのコード部分を読み取って、シンタックスハイライトを付与することができるライブラリです。
highlight.js
これを使って、marked.jsに設定を書くこともできるのですが、
Reactのライブラリ、react-hightが便利なので、今回はこれを用いて作成したいと思います。

ブログのコンテンツに適用する。

では、先ほど作成した個別記事のページにこのmarked.jsの設定とhighlight.jsを適用させてみましょう。

ブログ個別ページ


[src/pages/[id].tsx]

import * as React from 'react';
import { NextPage } from 'next';

import { axiosInstance } from '../../lib/api';
import { IPost } from '../../interfaces';
//ライブラリのインポート
import marked from 'marked'
import Highlight from 'react-highlight';

type Props = {
  post: IPost;
};

//markedのoptionを設定
marked.setOptions({
  gfm: true,
  breaks: true,
  silent: false,
});

const PostContent: NextPage<Props> = ({ post }) => {
  return (
    <>
      <h1>{post.title}</h1>
      <div>
        {post.tags.map(tag => (
          <React.Fragment key={tag.id}>
            <span>{tag.name}</span>
          </React.Fragment>
        ))}
      </div>
      <img src={post.image.url} />
      //react-highlightとmarked.jsで変換したいcontentに設定を行う
      <Highlight innerHTML={true}>{marked(post.content)}</Highlight>
    </>
  );
};


PostContent.getInitialProps = async context => {
  const { id } = context.query;
  const res = await axiosInstance.get(
    `https://ryusou-mtkh.microcms.io/api/v1/posts/${id}`,
  );
  const post: Post = await res.data;
  return { post };
};

export default PostContent;


react-highlightはinnerHTML={true}を設定することによって、dangerouslySetInnerHTMLのようにHTMLを埋め込むことができます。これでMarkdownで作成したコンテンツをHTMLに変換することができました!

react-highlightのテーマを設定

Next.jsは_app.jsを作成することによって、全ページにデフォルトの設定を行い、ルーティングもされない特別なページを作成することができます。

[src/pages/_app.tsx]

import React from 'react';
import Document, {
  Head,
  Main,
  NextScript,
  DocumentContext,
} from 'next/document';

export default class MyDocument extends Document {
  static getInitialProps(ctx: DocumentContext) {
    return Document.getInitialProps(ctx);
  }
  render() {
    return (
      <html>
        <Head lang="ja">
          <meta charSet="utf-8" />
          <meta
            name="viewport"
            content="initial-scale=1.0, width=device-width"
          />
          // hightlight.jsのテーマを設定する
          <link
            href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.10.0/styles/atom-one-light.min.css"
            rel="stylesheet"
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </html>
    );
  }
}


上記のようにテーマをHeadに設定して適用させましょう。例では、CDNでインストールしていますが、node_modules/highlight.js内にテーマがあるのでそのファイルを指定することによって好きなテーマを適用させることができるようになります。

以上でMarkdownをHTMLに変換し、コードにハイライトをつけてみやすくすることができました!技術ブログの完成ですね!

marked.renderer

以上で最低限のMarkdownで作成する技術ブログの完成です。ここからはOptionになりますが、Markdownから生成したHTMLにクラスを付与してレイアウトを整えたいといった場合があると思います。
当ブログでもReactのUIライブラリであるChakraUIを使用し、ResetCSSを適用させているためHTMLを生成しただけではタグのfontSizeが拡大されないなどの不具合が出てきてしまいます。
marked.jsではこのような場合、redererを使って拡張することができます。やり方を解説します。
libにmarkedの処理をまとめます。
先ほどのOptionもこのファイルにまとめました。

[src/lib/marked.ts]
```javascript
import marked from 'marked';

export const markedOption = marked.setOptions({
  gfm: true,
  breaks: true,
  silent: false,
});

//rendererを行う関数を実装する
export const markedRender = function() {
//rendererの初期化
  const renderer = new marked.Renderer();

//renderer.headingでh1,h2,h3...要素を取得し、クラスを付与する。
  renderer.heading = function(text, level) {
    const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');
    return `
    <h${level} class="author" href="#${escapedText}">
    ${text}
    </h${level}>
    `;
  };
//linkや他の要素も同様
  renderer.link = function(href, title, text) {
    return `
    <a class="contentLink" href=${href} title=${title}>${text}</a>
    `;
  };
  renderer.table = function(header, body) {
    return `
    <table class="contentTable">
    <thead class="contentThead">${header}</thead>
    <tbody class="contentTbody">${body}</tbody>
    </table>
    `;
  };
  renderer.paragraph = function(text) {
    return `
    <p class="paragraph">${text}</p>
    `;
  };
//最後にまとめてrendererを返す
  return renderer;
};


上記のようにrenderer.headingのようにすることによってMarkdownで出力される各要素を取得し、処理を拡張することができます。今回は単純に要素にクラスを付与する処理を行いました。
取得できる要素については以下のDocumentをご覧ください。
Extending Marked

このexportされた関数をComponentに実際に適用させます。

[src/pages/[id].tsx]

import * as React from 'react';
import { NextPage } from 'next';

import { axiosInstance } from '../../lib/api';
import { IPost } from '../../interfaces';
//インポートする
import { markedOption, markedRender } from '../../lib/marked';
import Highlight from 'react-highlight';

type Props = {
  post: IPost;
};

const PostContent: NextPage<Props> = ({ post }) => {
  return (
    <>
      <h1>{post.title}</h1>
      <div>
        {post.tags.map(tag => (
          <React.Fragment key={tag.id}>
            <span>{tag.name}</span>
          </React.Fragment>
        ))}
      </div>
      <img src={post.image.url} />
     //インポートしたmarkedの設定を利用して拡張する
      <Highlight innerHTML={true}>{markedOption(post.content, {renderer: markedRender())}</Highlight>
    </>
  );
};


PostContent.getInitialProps = async context => {
  const { id } = context.query;
  const res = await axiosInstance.get(
    `https://ryusou-mtkh.microcms.io/api/v1/posts/${id}`,
  );
  const post: Post = await res.data;
  return { post };
};

export default PostContent;


あとはよしなに、CSSを書くなりしてスタイルを整えたり、自由にカスタマイズしてください!
以上になります。

まとめ

microCMSを使って、Markdownで書く技術ブログを作成できました!拡張性がとても高いのがJamstackの魅力だと思うので、ぜひ色々拡張してみて、自身のブログを作成してみてください!

今回のコードをみたい方は以下のリポジトリをご覧ください!
GitHub

課題

  • innerHTMLできれば使いたくない。
  • 上記の実装の通り、react-highlightはHeadにテーマを組み込んで使用されるので、毎回読み込むことになってしまいパフォーマンスが悪くなる。

解決策として、

  • lazy-loading-demoのtutorialをやる。Dynamic import と条件分岐で必要ないときはhighlight.jsを読み込まない設定をする。
  • 今回はうまく実装できなかったので断念したが、このライブラリを試したい。
  • react-syntax-highlighter
  • モジュールに直接組み込めそうなので、良さそうだけどうまいことmicroCMSと組み合わせることができなかった。