肥大化したデータベースに別れを告げ、純粋な執筆体験へ
知識有料プラットフォームを構築する際、最も頭を悩ませる問題は通常「記事をどこに保存するか?どうレイアウトするか?」です。 従来の方法は、WYSIWYGエディタ(CKEditorやTinyMCEなど)を搭載したCMS(コンテンツ管理システム)を構築し、ユーザーが作成した大量のHTMLタグを含む「汚れた文字列」をリレーショナルデータベースに詰め込むことです。 この方法には3つの致命的な欠点があります:
- レイアウトが崩れやすい:特にコードブロックを貼り付けると、しばしばぐちゃぐちゃになります。
- データベースが肥大化する:数十万字のテキストと画像コンテンツがデータベースの検索速度を著しく低下させ、移行が困難になります。
- バージョン管理機能がない:記事の過去の変更を追跡するのが難しく、誤って削除すると復元できません。
これが、Vibe Tutorで従来のCMSを完全に捨て、エンジニアが最も愛するMarkdownファイルシステムを採用した理由です。
Markdownとは?
Markdownは軽量マークアップ言語で、GitHubのREADME.mdを書く際に毎日使用しています。
いくつかの簡単な記号(#は見出し、**は太字、```はコードブロックを表す)を使うだけで、構造が明確で整理されたドキュメントを作成できます。
さらに素晴らしいのは、Markdownファイルがプレーンテキストファイルであることです。これにより:
- ほとんどストレージを消費しません
- Gitを使ったバージョン管理(Version Control)が完璧に機能します
- 好みのIDE(VS CodeやCursorなど)を使ってコースを執筆でき、強力な補完とショートカットを享受できます
Vibe Tutorの動的レンダリングマジック
では、サーバーのフォルダに置かれた.mdファイルを、ユーザー画面に美しいウェブページとして表示するにはどうすればよいでしょうか?
ここでNext.jsとNode.jsエコシステムの真価が発揮されます。
コースのコアファイルsrc/app/courses/[...slug]/page.tsxを開いてください。いくつかの強力なオープンソースライブラリをインポートしているのがわかります:
1. fsとpath(Node.js組み込みモジュール)
まず、動的ルーティングから渡されるslugパラメータを使って実際のファイルパスを構築し、fsでメモリに読み込みます。
import fs from "fs";
import path from "path";
// 仮にURLが/courses/vibe-tutor-web/01-introの場合
// slug配列は['vibe-tutor-web', '01-intro']になります
const dirPath = path.join(process.cwd(), "content", "courses", ...slug);
const filePath = dirPath + ".md";
const fileContents = fs.readFileSync(filePath, "utf8");
このコードはサーバーサイドで実行されるため、サーバーのファイルシステムに直接アクセスできます!
2. gray-matter(フロントマター解析)
Markdownファイルの先頭に---で囲まれたブロックがあるのに気づきましたか?
---
title: "第4章:Markdown動的レンダリング技術"
---
これはFront-matterと呼ばれ、通常YAML形式で記述され、記事の「メタデータ(Metadata)」(タイトル、著者、公開日など)を格納するために使用されます。
gray-matterライブラリを使うと、テキストファイルを「データ部分」と「コンテンツ部分」に簡単に分割できます:
import matter from "gray-matter";
const { data, content } = matter(fileContents);
// data.titleは"第4章:Markdown動的レンダリング技術"になります
// contentはMarkdown本文です
3. remarkとremark-html(MarkdownからHTMLへの変換エンジン)
純粋なMarkdown本文を取得したら、最後のステップはそれをブラウザが理解できるHTMLに変換することです。
Unifiedエコシステムで最も有名なMarkdownプロセッサであるremarkを使用しています。
import { remark } from "remark";
import html from "remark-html";
const processedContent = await remark()
.use(html, { sanitize: false })
.process(content);
const contentHtml = processedContent.toString();
これで、contentHtmlは標準的なHTML文字列になりました!
4. dangerouslySetInnerHTMLと@tailwindcss/typography
最後に、これをReactコンポーネントに挿入します。Reactで生のHTML文字列を挿入するには、特別なプロパティであるdangerouslySetInnerHTMLを使用する必要があります。
<article className="prose prose-invert prose-blue max-w-none">
<div dangerouslySetInnerHTML={{ __html: contentHtml }} />
</article>
ここで疑問に思うかもしれません:これらの変換されたHTML(<h1>、<p>、<code>など)にはTailwindのclassが適用されていないので、スタイルが全くない状態ではないか?
心配ありません!これが@tailwindcss/typographyプラグインの威力です!
classNameにproseを追加すると、このプラグインが自動的にすべてのHTML要素に最適な読みやすいスタイル(行間、フォントサイズ、リストのインデントなど)を適用します。
prose-invertはダークモードであることを伝え、テキストを反転処理します。prose-blueはすべてのリンクと引用符にブルーカラースキームを使用するように設定します。
動的目次生成システム
単一記事のレンダリングに加えて、Vibe Tutorには「自動目次生成」という魔法のような機能もあります。
URLに/courses/vibe-tutor-webとだけ入力すると、システムはこれがMarkdownファイルではなくディレクトリであることを認識します。
この時、私たちのプログラムは別のロジックをトリガーします:
fs.readdirSyncを使ってディレクトリ内のすべての.mdファイルをスキャンします- ファイルをアルファベット順に並べ替えます(これがファイル名を
01-、02-で始める理由です) - 各ファイルの
gray-matterからtitleを取得します - 画面上に章番号付きの目次リンクリストを動的に生成します
これは、管理者であるあなたが新しいMarkdownファイルをディレクトリに追加するだけで、目次が自動更新され、記事が自動的に公開されることを意味します! これがMarkdownシステムがもたらす究極の効率性です。
次章では、このシステムの最も重要なビジネスロジック「Freemium権限制御メカニズム」について探求します。最初の数章を無料で公開して人を集め、後ろのコンテンツを有料でロックする方法とは?お楽しみに!