engineering

このブログの作り方

記事ひとつがフォルダひとつ、言語ごとにマークダウンが一枚。git push すれば2分ほどで公開されます。CMS もデータベースもパイプラインもありません。

2分、コマンドひとつ

このページの最後の修正は、エディタを出てコマンドをひとつ通り、約2分後に公開されました。その間にログインを求める画面はなく、公開ボタンを押した人もいません。

そこが要点です。私たちは Instagram のクロール、クリエイターの並べ方、午前3時に壊れるものについて書きます。書く手間がその仕事より高くついてはいけません。だからブログも、毎日使っている道具の上で動かします。テキストファイル、git、そしてビルドです。

37秒

フルビルド

すべての記事とすべての言語を最初から作り直します。

3

言語

ひとつのフォルダから英語、韓国語、日本語が出ます。

0

データベース

問い合わせるものも、バックアップするものもありません。

記事はフォルダひとつ

記事に必要なものはすべてひとつのディレクトリに入っていて、リポジトリの他の部分はその存在を知りません。

Text
apps/blog/content/how-virev-blog-works/
├── en.md
├── ko.md
├── ja.md
├── components/
│   └── DeployFlow.svelte
├── topology.svg
└── style.css

ロケールのファイルが本文です。その隣には、この記事に必要なものだけを置きます。図ひとつのために書いた Svelte コンポーネント、このページだけが読み込むスタイルシート、このチャートだけが読む CSV。1 記事が終われば、そのコードは他の誰にとっても関係のないものになります。

言語は、ファイルがあってフロントマターに draft: true がないときに公開されます。この規則ひとつが一覧とサイトマップ、RSS フィード、言語リンクを決めます。だから en.md だけのフォルダは英語の記事で、他に設定するものはありません。

CMS を持たない代わりに払うもの

コンテンツ管理システムを入れると、ログインとデータベースが増え、記事の写しがもう一部でき、フィールドを最初に変える日に移行作業が来ます。2 git は誰が何をいつ変えたかをすでに記録していて、テキストファイルは差分がきれいで、プルリクエストがそのままレビューになります。だから今読んでいるこのページはファイルで、この一文の歴史はコミットです。

代価はリポジトリのチェックアウトです。ここに書く人にはそれが要るので、git を使わない書き手は書けません。
引き換えたもの

承知のうえで選んだ取引です。ここに書く人は、すでにリポジトリを開いています。

マークダウンから静的なページへ

.md ファイルは mdsvex が Svelte コンポーネントにコンパイルします。だから段落の隣にチャートを置けますし、ファイルがすぐ隣のコンポーネントを読み込めます。そのあと SvelteKit がビルド時にすべてのページを HTML にします。リクエスト時にサーバーはなく、CDN の上のファイルだけがあります。

そのビルドで読者が気づくことが二つあります。コードは Shiki が色を付けてただの HTML として届き、ブラウザではハイライターが動きません。3このページのクライアントスクリプトは、余白の注の位置を決めて目次を追うだけです。チャートのある記事はそのときだけ ECharts を読み込みます。 そしてマークダウンの脚注は右の余白の注になります。小さなプラグインが [^1] を番号付きの参照に変え、注を引用した行の隣に移します。JavaScript がなくても HTML は完成しています。スクリプトは参照がどこに来たかを測るだけです。

何を公開するかを決める規則は、全部読んでも短いままです。

TypeScript
export function isPublished(entry: Entry): boolean {
	return !entry.meta.draft;
}

/** Locales of one article whose file exists and is not a draft. */
export function publishedLocales(entries: Entry[], slug: string): Locale[] {
	const set = new Set(
		entries.filter((e) => e.slug === slug && isPublished(e)).map((e) => e.locale)
	);
	return LOCALES.filter((l) => set.has(l));
}
図: GitHub の sieun/dev ブランチが apps/blog をビルドする Vercel プロジェクトにつながり、virev.ai が /blog、/ko/blog、/ja/blog、/_blog をそこへ rewrite します。
ブログが動く場所。他のパスはすべてプロダクト側が持ちます。

デプロイの流れ

四つの手順で、そのどれもフォームではありません。

  1. 1 コミット
  2. 2 push
  3. 3 ビルド
  4. 4 公開

commit.mjs は指定したファイルだけをコミットします。

コミット。 node scripts/repo/agent/commit.mjs -m "post(<slug>): title" apps/blog/content/<slug> は指定したファイルだけをステージします。十数のエージェントがこのチェックアウトを共有しているので、全部を拾うコミットは他人の書きかけまで持っていきます。

push。 git push origin sieun/dev。push は fast-forward である必要があります。拒否されたら誰かが先に push したということなので、fetch してからもう一度 push します。誰も手でマージしません。

ビルド。 Vercel はまず ignored-build を実行し、その push で apps/blog が変わっていなければビルドを飛ばします。変わっていれば、ブログ全体を約40秒で作り直します。

公開。 新しいビルドは成功したときだけ前のものと入れ替わり、URL は変わりません。

壊れたとき

壊れた記事はビルドを失敗させ、失敗したビルドはデプロイされません。前の版がそのまま残り、ログがどのファイルかを教えます。それが安全網です。最悪の場合でも、新しい記事が出ないだけで、サイトが落ちることはありません。

公開した記事を取り消すのは git revert して push するか、Vercel の画面で一度クリックするかです。読む画面そのものや余白の注、字の大きさを変えると、同じデプロイですべての記事が作り直されます。テンプレートは一組で、記事はそこを通るデータだからです。

三つの言語、それぞれの速さで

en.mdko.mdja.md はコンポーネントとデータと図を共有し、公開の時期はそれぞれ決めます。上の言語切り替えは、この記事が実際に持っている言語だけを出し、hreflang も同じ一覧に従います。だから検索エンジンが、まだない翻訳を指すことはありません。

日本語版を書く作業は、英語のファイルを複製して本文を訳すことです。チャートはチャートのままです。