asuto.dev

Markdownでブログを書けるようにした。 - 20241205

経緯

私、asutoは、人に見せられるような個人サイトを作りたいなと思ってB2の春頃である2023年3月3日にGoogle Domainsでこのasuto.devを購入し、既に怠惰ではあるがそこから少し経った2023年6月10日には少なくとも最初のページ(個人サイト要素ゼロのcreate-next-appそのままの天然物)をVercelにデプロイした。
それからさらに放置されて2023年9月22日頃には/about/contactsが生えて、ようやく個人サイトっぽいものが出来上がった。ところがその時点で急速にやる気がなくなってしまい、細かい更新はあれども、それから一年以上手をつけていない状態だった。
そして、2024年12月4日にTLに個人サイトリプレイスしている人
が流れてきて「俺もやるかぁ〜〜」となんか急にやる気が出てきたので、やった。

やったこと

主なデザインの改修

流石にcreate-next-appを何も変えていないスタイルは恥ずかしい気持ちがあったので、最初にこれをやった。
適当にfigmaでフレームを組んでこんな感じにしようと考えてみて、結局React Componentに落とし込む時になんか色々変わったが、何も考えずやるよりかはマシだったと思う。

MDXの導入

デザインをある程度改修し終わったときに、次はブログを書きたい(書くとは言っていない)と思った。
そこで、まず@next/mdxを導入して、/about, /contactsに適用したが、ページごとにいちいちフォルダを生やしてそのなかにpage.mdxで書いていく感じだったため、今あるページを.tsxから.mdxにリプレイスする分には良いが、今後ブログをこの形で生やすことを考えると渋かった。 その後、後述のreact-markdownを用いたものと共存させたときに、結局別々の二箇所でスタイルを当ててデザインの管理を行わなければならないという不便さに気づいたので、結局@next/mdxを廃止して/about, /contactsreact-markdownを用いて書き直すことにした。

gray-matterreact-markdownによる、メタデータを利用したMarkdown to HTMLの導入

結局、MDXでブログを書くことは諦めて、gray-matterreact-markdownを導入した。

gray-matter

Markdownの先頭に記述された記事のタイトルや日時などのメタデータを解析するライブラリ

react-markdown

後述するremarkrehypeを使って、MarkdownをReactコンポーネントとしてレンダリングするライブラリ

remark

Markdownの構造を解析してmdastという抽象構文木(AST)を生成するライブラリ。このmdastを直接HTMLに変換するremark-htmlというプラグインや、このmdastをHTMLのASTであるhastに変換して、さらにそれを生のHTMLに変換するrehypeという別のライブラリと合わせて使ったりする
当初react-markdownではなく直接remarkを導入することも考えたが、remarkremark-htmlではスタイルの当てられていない素のHTMLが生成されてしまい、remarkrehypeを使えばスタイルは当てれるがやはりHTMLに変換されてしまい、 今後扱う時にReactコンポーネントでないと不便になるかもしれないと思ったため、最終的にreact-markdownを採用した。このページもこの仕組みでレンダリングされている。
以下のようにして、generateStaticParamsを用いて任意のディレクトリにあるファイル(この例ではsrc/blog/*.md)を読んで、URLを任意の文字列(この例ではファイル名から.mdを抜いたもの)にマッピングすることができた。
// src/app/blogs/[slug]/page.tsx
export function generateStaticParams() {
  const blogDir = path.join(process.cwd(), "src/blogs");
  const filenames = fs.readdirSync(blogDir).filter((file) => file.endsWith(".md"));

  return filenames.map((filename) => ({
    slug: filename.replace(/\.md$/, ""),
  }));
}

function getBlogData(slug: string): BlogPostProps {
  const filePath = path.join(process.cwd(), `src/blogs/${slug}.md`);
  const fileContent = fs.readFileSync(filePath, "utf8");

  const { content, data } = matter(fileContent) as {
    content: string;
    data: Partial<BlogMetadata>;
  };

  // 略

  return {
    content,
    metadata,
  }
}

export default async function BlogPostPage({ params }: BlogPostPageProps) {
  const { slug } = await params;
  const { content, metadata } = getBlogData(slug);

  return (
    // 略
      <MarkdownRenderer content={content} />
    // 略
  );
}
これで読んだファイルの中身をgray-matterに食わせてメタデータを取得し、内容の部分をreact-markdownでレンダリングして表示している。

今後やりたいこと

とにかくやれ!! (出典: とにかくやれ!! 【仕事の姿勢】- 桜井政博のゲーム作るには)
はい、(健康に無理のない範囲で)やります…

変なページを生やす

みんな/575とか/tankaとかやったり、写真上げたりしてるのいいなと思ったので、俺もなんかやりたい。

手を動かしたことをブログとして書く

ようやくこれでブログを書けるようになって、それだけで満足してしまいそうになるが、何はともあれ記事の本数は増やしたいよね。手を動かしつついい感じのタイミングでここにダンプするようにしたい。

Markdownの解析・レンダリング方法の改善

今回はreact-markdownを用いて、MarkdownをReactコンポーネントにしているが、ここでは以下のようにして、HTML要素ごとにスタイルを当てている。
<ReactMarkdown
  remarkPlugins={[remarkGfm]}
  rehypePlugins={[rehypeSlug, rehypeAutolinkHeadings]}
  components={{
    h1: ({ ...props }) => (
      <h1 className="text-3xl font-normal border-b pb-2 mb-8" {...props} />
    ),
    h2: ({ ...props }) => (
      <h2 className="text-2xl font-normal mt-6 border-b pb-1" {...props} />
    ),
    p: ({ children, ...props }) => (
      // 略
    ),
    a: ({ children, ...props }) => (
      // 略
    ),
    // 略
  }}
>
  {content}
</ReactMarkdown>
しかし、HTML単位でスタイルを当てるとなると、react-markdownで出力されたタグが囲われていた時にそれらすべてのスタイルが反映されることになり、不便である。
そこで、以下のようなサイトではremarkmdastに変換したら、それを直接Reactコンポーネントに変換するプラグインを作成している。
これと同じ方法を取るかはともかくとして、レンダリング方法に改善の余地があり、このような選択肢があることがわかった。

追記

後日、本ページのようなOGPを表示できるようにしていたら、上のサイトで主張されているようなHTMLのセマンティックで処理する辛さがよくわかった。
というのも、MarkdownからOGPを表示するURLを抽出したいときに、文章の途中で[適当なリンク](https://example.com)という形でリンクを貼っていたときと1行全部がURLのときはHTMLにするとどちらもaタグであり、これを区別するような処理をすべてのaタグについて挟まなければいけないという意味で冗長であり、HTMLではなくMarkdownのセマンティックで場合分けしたいという欲求が強い。
また、文章の段落中にあるリンクを処理したいときに、pの中でaが使われている場合この処理の結果pの中にdivが入ってしまってHTMLのセマンティックに違反してしまい、エラーが出る。ここでpをすべてdivに置き換えるなどすれば見た目を維持しながらエラーは解決できたが、HTMLのセマンティックを崩してまでこのようなことをするのは心が痛い。というわけでいずれmdast->React Componentなプラグインを書こうと思った。

修正履歴

2024-12-05

  • 初稿を公開。

2024-12-06

  • 修正履歴セクションと@next/mdxを廃止したことへの言及を追加。
  • 文章の調整

2024-12-07

  • OGPカードを追加
  • レンダリング方法の部分に追記