Markdownでブログを書けるようにした
Date: 2024-12-05
経緯
私、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
, /contacts
もreact-markdown
を用いて書き直すことにした。gray-matter
とreact-markdown
による、メタデータを利用したMarkdown to HTMLの導入
結局、
MDX
でブログを書くことは諦めて、gray-matter
とreact-markdown
を導入した。gray-matter
Markdownの先頭に記述された記事のタイトルや日時などのメタデータを解析するライブラリ
react-markdown
後述する
remark
やrehype
を使って、MarkdownをReactコンポーネントとしてレンダリングするライブラリremark
Markdownの構造を解析して
mdast
という抽象構文木(AST)を生成するライブラリ。このmdast
を直接HTMLに変換するremark-html
というプラグインや、このmdast
をHTMLのASTであるhast
に変換して、さらにそれを生のHTMLに変換するrehype
という別のライブラリと合わせて使ったりする当初
react-markdown
ではなく直接remark
を導入することも考えたが、remark
とremark-html
ではスタイルの当てられていない素のHTMLが生成されてしまい、remark
とrehype
を使えばスタイルは当てれるがやはり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;
};
// 略
return {
content,
metadata,
}
}
export default async function BlogPostPage({ params }: BlogPostPageProps) {
const { slug } = await params;
const { content, metadata } = getBlogData(slug);
return (
// 略
// 略
);
}
これで読んだファイルの中身を
gray-matter
に食わせてメタデータを取得し、内容の部分をreact-markdown
でレンダリングして表示している。今後やりたいこと
とにかくやれ!! (出典: とにかくやれ!! 【仕事の姿勢】- 桜井政博のゲーム作るには)
はい、(健康に無理のない範囲で)やります…
変なページを生やす
みんな
/575
とか/tanka
とかやったり、写真上げたりしてるのいいなと思ったので、俺もなんかやりたい。手を動かしたことをブログとして書く
ようやくこれでブログを書けるようになって、それだけで満足してしまいそうになるが、何はともあれ記事の本数は増やしたいよね。手を動かしつついい感じのタイミングでここにダンプするようにしたい。
Markdownの解析・レンダリング方法の改善
今回は
react-markdown
を用いて、MarkdownをReactコンポーネントにしているが、ここでは以下のようにして、HTML要素ごとにスタイルを当てている。src/components/MarkdownRenderer.tsx
(
),
h2: ({ ...props }) => (
),
p: ({ children, ...props }) => (
// 略
),
a: ({ children, ...props }) => (
// 略
),
// 略
}}
>
{content}
しかし、HTML単位でスタイルを当てるとなると、react-markdownで出力されたタグが囲われていた時にそれらすべてのスタイルが反映されることになり、不便である。
そこで、以下のようなサイトでは
remark
でmdast
に変換したら、それを直接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カードを追加
- レンダリング方法の部分に追記