2021/08/15

Next.jsで技術ブログを作った話

Yuta Takahashi


世はまさに、自作技術ブログ時代! という訳で、Next.js 等を使って技術ブログを作りました。

一旦、きりの良いところまで実装が終わったので、公開しようと思います!

このブログでは、趣味で触った技術のことについて、ざっくばらんに記事を投稿していく予定です。 また、私は Web Frontend が好きなので、関連技術の実験場にも活用していきます。

ゴールデンウィークから作り始めて、休日にちまちまやっていたので、随分時間がかかってしまいましたが、いい感じに仕上がってとりあえず満足しています。

以降のセクションでは、技術スタックと、実装に一番の時間を要した Markdown ファイルから React コンポーネントに変換するまでの処理の詳細について振り返ります。

技術スタック

本ブログの開発に使用した技術は以下の通りです。(みんな使ってそうな構成)

  • Next.js:言わずと知れた React のフレームワーク
  • Chakra UI:tailwind like に React コンポーネントのスタイリングができるコンポーネントライブラリ
  • jonschlinkert/gray-matter:Markdown に yaml でメタ情報を埋め込めるやつ
  • unifiedjs/unified:構文木を使用してテキストを処理するためのインターフェース
  • shiki:コードハイライター
  • vercel/og-image:OGP 画像の生成
  • GitHub Pages:サイトのホスティング

やったこと&学び

最初のゴールとして、Markdown から記事を生成可能かつ、それを見てもらうための最低限の機能に絞って開発を進めました。 やったことを大別すると以下の 3 つになるのかなと思います。

  • Markdown ファイルからの記事ページ生成とレイアウト設定
    • MD を変換する処理を実装することで、構文木の世界を学べた
  • プライベート記事機能
    • ブログのトップページや Google 検索の結果に載せないプライベート記事(例:Playground)を公開する機能
  • メタタグの設定
    • これまで業務的な Web アプリばかりを開発してきたため、学びが多かった

Markdown ファイルの内容を React コンポーネントに変換する

1.Next.js で全 Markdown ファイルを取得して記事ページを生成する

記事ページの生成は Next.js の SSG を使います。

具体的には、pages/[slug].tsx内で、 getStaticPaths を使って Markdown のファイル名から記事ページの URL を生成し、

export const getStaticPaths: GetStaticPaths<Params> = async () => { const posts = getAllPosts('all'); return { paths: posts.map((post) => ({ params: { slug: post.slug, }, })), fallback: false, }; };

getStaticProps を使って render する 記事の情報を取得し、Page コンポーネントの Props として渡します。

export const getStaticProps: GetStaticProps<{ post: Post }, Params> = async ({ params }) => { const post = getPostBySlug(params.slug); const html = await markdownToHtml(post.content ?? ''); return { props: { post: { ...post, content: html, }, }, }; };

2.unified で Markdown を整形して HTML の文字列に変換する

上記の getPostBySlug() では、Markdown ファイルの中身(post.content)を文字列で取得します。

続くmarkdownToHtml()では、unified を使って、ファイルの中身を HTML の文字列に変換していきます。

export async function markdownToHtml(markdown: string) { const result = unified() .use(remarkParse) .use(remarkRehype, { allowDangerousHtml: true }) .use(rehypeRaw) .use(rehypeShiki, { highlighter: await getHighlighter({ theme: 'github-dark', }), }) .use(rehypeStringify) .processSync(markdown); return result.toString(); }

変換処理の概要は以下の通りです。

  • remarkParse:Markdown の文字列をmdast(Markdown Abstract Syntax Tree format)に変換する
  • remarkRehype:mdast を hast(Hypertext Abstract Syntax Tree format)に変換する,{ allowDangerousHtml: true } により、生の HTML はそのまま残す
  • rehypeRaw:hast 内の生 HTML を hast に変換する
  • rehypeShiki:コードハイライターの shiki を使って 複数行のコードブロック をハイライトするスタイルを適用する
  • rehypeStringify:hast を HTML の文字列に変換する

3.HTML の文字列を React コンポーネントに変換する

最後に markdownToHtml() で取得した HTML 文字列を remarkjs/react-markdownを使って React コンポーネントに変換します。(components/markdowns/Markdown.tsx

import ReactMarkdown from 'react-markdown'; import rehypeRaw from 'rehype-raw'; import * as components from 'src/components/markdowns/_components'; ... export const Markdown: React.VFC<Props> = ({ rawHtml }) => ( <ReactMarkdown rehypePlugins={[rehypeRaw]} children={rawHtml} components={components} /> );

このとき、components に React コンポーネントを渡すことで、任意のタグを自由に変換することができます。今回は、変換するコンポーネントをcomponents/markdowns/_componentsの中に定義しました。

変換する対象は、<table />のように、Chakra UI のコンポーネントに置き換えるタグと、<a />のように、Props に応じて、レンダリングするコンポーネントを出し分けるタグです。

スタイルだけ当てたいタグについてはここでは変換せず、 Chakra UI のテーマを設定しました。

また、今回は変換対象の文字列(children)が HTML の形式であるため、rehypePluginsrehypeRaw を指定して、hast に変換する処理を加える必要がありました。ピュアな MD 形式の文字列である場合は、rehypeRaw は不要です。

今後実装したい機能

  • タグ機能
    • 記事が増えていくにつれて探すのが大変になりそうなので、そのうち実装したいお気持ち
  • 目次機能
    • この記事みて思ったけど、やっぱり目次あったほうが見やすいですね
  • ダークモードや PWA
    • 新しい Web の機能をどんどん使っていきたいお気持ち
  • カッコいい OGP 画像の作成
    • 今のデザインが超適当なので、どこかで気合を入れて作り込みたい

おわりに

これからは、ブログをもっと良くしたり、学んだ内容を定期的にアウトプットしていきたいと思います!

最初は、「ブログなんて今更自分で作ってもな〜」とか思ってたのですが、車輪の再発明をすることで学べることも多いなと感じました。