このブログの技術構成について①markdownでの投稿

komosyu
Next.js

目次

  1. はじめに
  2. ソースコード
  3. 技術構成
  4. 実装の前に考えたこと
  5. 実装の手順
    1. 1.環境構築
    2. 2.マークアップ
    3. 3.マークダウンでの投稿
  6. 実装を終えて
  7. さいごに

はじめに

このブログが完成したので、今回はざっくりと技術的なお話をしていきたいなと思います。

ソースコード

github のリポジトリはこちらということで、中身を見てもらえたらすむ話ではあるのですが、この記事ではなぜこのような技術を選定したのかということや、実装を進めていく中なかで詰まったところ、参考にしたサイトなどを書いていきたいなと思います。

あと、一応このブログは実装だけでなくデザインも自分でやっているのですが、その辺はまた別の記事で書こうかなと思います。(全然アマチュアなので、そんなに参考にはならないと思いますが。。)

技術構成

  • フレームワーク: Next.js
  • 言語: typescript
  • スタイリング: CSS Modules
  • 投稿: markdown
  • デザインツール: figma
  • インフラ: vercel

実装の前に考えたこと

ブログを作るぞ。と決めてから考えていたことは、前提として今ハマっている Next.js を使うということ。

となると、cms は wordpress を使うわけにはいきませんから、日本製の microcms を使うか、使い慣れている contentful を使うかと迷っていたのですが、今後どの cms が主流になっていくのかなどの流行り廃りを考えるのも面倒だし、今回はお客さん相手じゃなくて自分自身が更新するものだから、わざわざ管理画面なんてなくてもいい。
それに、シンプルな技術ブログなら markdown で書くのにちょうどいいのではないかということで更新方法は markdown を採用してみました。(事前に調査したところ、ある程度情報量もあったので)

あと、迷ったのはスタイルについてで、Next.js で css を当てるとすると、CSS Modules か Tailwind CSS が多いようで、これまでに一応どちらの手法にも触れたことがあったのですが、今人気である Tailwind CSS は web サイトを作る分には細かいスタイルの調整が難しかったり、レスポンシブを考えるのも面倒だった経験があったので、今回は使い慣れている CSS Modules を採用することにしました。 CSS Modules であれば、ふだん通りに css を書く感じでできるし、コンポーネントごとに簡単な命名をすればいいので、css 設計的なことにもそこまで頭を抱えずにすみました。

しかしながら、未だに毎回スタイルは何を選定しようかなと悩む時間があるので、そろそろ Next.js でのスタイルのベストプラクティスが決定してほしいものですね。(今のところ Tailwind CSS になりそうな雰囲気)

実装の手順

1.環境構築

まず、Next.js の最新版を typescript でインストールして環境構築を行います。
実装時の Next.js のバージョン 12 でした。

npx create-next-app@latest --typescript

Next.js での環境構築の詳しい方法は公式のドキュメントに書いてあるので読んでみてください。

2.マークアップ

まず、インストール時に構成されているディレクトリ構成から特に変更することなく、ページは pages ディレクトリに作成。

about など、更新が不要なページは何も考えずデザイン通りにマークアップをするだけですが、トップページなどの投稿を持っているページではssgで投稿情報の一覧の取得をして表示します。
今回のような web サイトではビルド時にデータが取得できているので、ssr ではなく ssg を用いるのが良いでしょう。

3.マークダウンでの投稿

こ結論から言うと、ほとんどアールエフェクト さんのブログ記事を参考にさせていただきました。
私が多少工夫したところがあるとすると、typescript に対応したところくらいでしょうか。
この記事に限らずですが、アールエフェクトさんのブログは毎回詳細に書かれていて、痒いところに手が届く的な丁寧な記事で、よく助けてもらっています。

まあ、そのブログを見ていただければ済む話ではあるのですが、以下のコードで簡単に流れを説明してみます。

まずは投稿一覧ページから
・github

import fs from 'fs'
import matter from 'gray-matter'

export const getStaticProps: GetStaticProps = async () => {
  // 私の場合postsディレクトリ直下にmarkdownファイルを置いているのでreaddirSyncでファイルを取得
  const files = fs.readdirSync('posts')

  // ここで投稿の表示に必要な情報を作成
  let posts = files.map((fileName) => {
    // markdownファイルから.mdの拡張子を取り除いてurlを作成
    const slug = fileName.replace(/\.md$/, '')
    // readFileSyncでpostsディレクトリ下のファイルを指定してファイル内部の情報を取得
    const fileContent = fs.readFileSync(`posts/${fileName}`, 'utf-8')
    // matterでmarkdownファイル内に設定しておいたtitleやdescriptionといった情報を取得
    const { data } = matter(fileContent)
    return {
      frontMatter: data,
      slug,
    }
  })

  // modifiedDateもしくはpublishedDateが新しい順に並び替え
  posts = posts.sort((a, b) =>
    new Date(
      a.frontMatter.modifiedDate
        ? a.frontMatter.modifiedDate
        : a.frontMatter.publishedDate
    ) >
    new Date(
      b.frontMatter.modifiedDate
        ? b.frontMatter.modifiedDate
        : b.frontMatter.publishedDate
    )
      ? -1
      : 1
  )

  // publicというフラグで公開・下書きの状態を管理して、公開のもだけに絞る
  posts = posts.filter((post) => post.frontMatter.public)

  return {
    props: {
      posts,
    },
  }
}

あとは、return した内容をページに props として渡しておけば、無事に投稿一覧が表示できるはずです。
投稿の一覧表示はこんなものでしょう。

それでは投稿の詳細ページに移ります。
・github

import fs from 'fs'
import matter from 'gray-matter'
import { GetStaticPaths, GetStaticProps, NextPage } from 'next'
import { ParsedUrlQuery } from 'querystring'
import { unified } from 'unified'
import remarkToc from 'remark-toc'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeStringify from 'rehype-stringify'
import rehypeSlug from 'rehype-slug'
import remarkPrism from 'remark-prism'
import { PostFrontMatter } from 'type'

type ContextProps = {
  frontMatter: { [key: string]: PostFrontMatter }
  content: string
}

interface Params extends ParsedUrlQuery {
  slug: string
}

export const getStaticProps: GetStaticProps<ContextProps, Params> = async (
  params
) => {
  const context = params.params!
  // postsディレクトリ下のmarkdownファイルの内容を取得
  const file = fs.readFileSync(`posts/${context.slug}.md`, 'utf-8')
  // matterでmarkdownファイル内に設定しておいた情報(data: titleやdescriptionなどの情報, content: 投稿内容)
  const { data, content } = matter(file)

  // unifiedでmarkdownファイルをhtmlとして出力してもらう
  const result = await unified()
    .use(remarkParse)
    .use(remarkPrism, {
      plugins: ['line-numbers'],
    })
    // 目次の設定したのでここでパラメーターを設定
    .use(remarkToc, {
      heading: '目次',
      tight: true,
      ordered: true,
    })
    .use(remarkRehype, { allowDangerousHtml: true })
    .use(rehypeStringify, { allowDangerousHtml: true })
    .use(rehypeSlug)
    .process(content)

  const allContent = result.value

  return {
    props: { frontMatter: data, content, allContent },
  }
}

export const getStaticPaths: GetStaticPaths = async () => {
  const files = fs.readdirSync('posts')
  // 投稿詳細ページのurlを生成
  const paths = files.map((fileName) => ({
    params: {
      slug: fileName.replace(/\.md/, ''),
    },
  }))
  return {
    paths,
    fallback: 'blocking',
  }
}

export default Posts

細かく解説していったらキリがなさそうだったので、ざっくりとさせていただきました。
github でソースコードを公開しておきましたので、詳しくはそちらを参考にしてみてください。

このままダラダラと書き連ねていってもよかったのかもしれませんが、あまりに長くなっていってもしょうがないので、この記事では投稿一覧と詳細のみとさせてください。

詳しいことは、これからちょこまかと記事にまとめていこうと思います。

実装を終えて

投稿部分の実装に関しては markdown ファイルを読んできてゴニョゴニョしていくだけだったので、そこまで難しいことはなかった気がします。

さいごに

はじめてガッツリと技術記事のようなものを書いてみようとしましたが、やはり難しいものですね。
これまで当たり前のように読んできた技術記事も、これからはきっと感謝して読むようになると思います。
こんなつたない文章を最後まで読んでいただきありがとうございました。
これからも Next.js などのフロントエンド関連の記事を中心に書いていこうと思いますので、よろしくお願いします。