% cd ..

Front Matter CMS で Markdown ブログの執筆環境を整える

Front Matter CMS で Markdown ブログの執筆環境を整える

このブログでは、記事は Markdown ファイルで Git 管理しています。
私自身はソフトウェア・エンジニアなので、エディタで直接 Markdown ファイルを編集し、git コマンドでデプロイするのは特に問題ではありません。 ただ、このブログを構築する動機のひとつが、WordPress のような旧態依然のモノリシックな CMS からの脱却でしたので、このブログ編集用の CMS も物色してみることにしました。

近年モダンなブログ用 CMS は本当に色々とありますが、SaaS としてサービス提供しているものが多く、バックエンドにもそれぞれ固有のデータベース・システムを必要としています。 このブログのように記事を Markdown ファイルとして保存し、Git 管理する Git-based CMS(別の記事でふれた CloudCannon CMS などもそうですが)は少数派です。
理由は簡単で、エディタで Markdown フォーマットのファイルを編集し、git commit & push する、というエンジニア的には日常的な作業が、一般のただブログを書きたいだけのユーザーには敷居が高すぎるからです。 そもそも需要が圧倒的に少ないんですよね。

という訳で、無料で使える Git-based CMS 探しをはじめます。

Git-based CMS いろいろ

無料で使える Git-based CMS を探してみました。Markdown ファイルを Git リポジトリで管理する前提で、主要なものを比較します。

CMS形態特徴無料枠
Front Matter CMSVS Code 拡張機能エディタ内で完結。frontmatter の GUI 編集に特化。
コンテンツ編集は使い慣れた VS Code で
完全無料(OSS)
Keystaticローカル / GitHub AppThinkmill 製。リッチな管理 UI。
内部スキーマと出力を Markdoc/MDX など選択可能
完全無料(OSS)
Decap CMSブラウザベース旧 Netlify CMS。老舗の Git-based CMS。
GitHub / GitLab 連携。Netlify 以外でもホスト可能
完全無料(OSS)
Sveltia CMSブラウザベースDecap CMS のドロップイン代替。UI がモダンで高速。
Decap の config.yml がそのまま使える
完全無料(OSS)
Tina CMSSaaS + ローカルビジュアル編集が強力。Git-backed だが、クラウドの
GraphQL API 経由で動作。2ユーザーまで無料
無料枠あり(2 users)
CloudCannonSaaSGit 連携のビジュアル編集。Hugo/Jekyll/Astro 等に対応。
非エンジニアでも使いやすい UI
有料($49/月〜)

ちなみに CloudCannon は、このブログの検索機能で使っている Pagefind のスポンサーでもあります。良いツールを OSS で支えている会社なので気になる存在ですが、個人ブログに月 $49 はなかなか厳しい。

Git-based CMS の世界はざっくり エディタ統合型(Front Matter CMS)、ブラウザ管理画面型(Keystatic / Decap / Sveltia)、SaaS 型(Tina / CloudCannon)の3系統に分かれます。

SaaS 型の Tina や CloudCannon も Git-backed なので、既存の Markdown ファイルをそのまま読み書きできます。コンテンツの移行が必要になるわけではありません。ただ、外部サービスへの依存が増えること、一人で VS Code を使っている環境ではブラウザベースの管理 UI の恩恵が薄いこと、そして翻訳パイプラインが frontmatter を直接書き換える運用との二重管理になりやすいことから、今回は候補から外しました。

最終的に「ローカルで完結する」「Markdown ファイルを壊さない」「完全無料」の3条件で絞り、Front Matter CMS と Keystatic の2つが残りました。

Front Matter CMS とは

このブログ特有の翻訳パイプラインや AI タグ付けスクリプトが Markdown 先頭のメタデータ frontmatter を後から直接書き換えたりするので、CMS には次の点が求められます。

  • frontmatter の管理をカスタマイズ可能
    必要なフィールドの自動セット、ステータスのドロップダウン切り替え、日付の自動更新など、メタデータ管理が柔軟に設定できる
  • frontmatter データを勝手に改変しない
    外部から後付けで挿入された frontmatter フィールドやデータを削除・破壊しないこと
  • Markdown 記法を変換しない
    Markdown 記法は色々な方言があり、異なる記法に変換されると、後続のパイプラインやレンダラーの誤動作を招くため

Front Matter CMS はこれらをいい感じに満たすツールでした。

Front Matter CMS は VS Code の拡張機能として動く Git-based CMS です。
そもそも普段から VS Code をエディタとして使っているのであれば、導入障壁はないも同然ですし、ブラウザを開かずに VS Code の中だけで記事管理が完結します。 WordPress のような WYSIWYG エディタ付きの UI ではありませんが、frontmatter のフィールドを GUI で操作できるサイドパネルと、記事一覧のダッシュボードを備えています。

この記事では、導入から実運用に至るまでの一連のカスタマイズを紹介したいと思います。

初期設定

インストールと初期化

VS Code の拡張機能マーケットプレイスから「Front Matter CMS」をインストール。初回起動時にウィザードが走り、フレームワーク(Next.js)を選択すると frontmatter.json が生成されます。(フレームワークは色々選べる)

以降の設定はすべてこの frontmatter.json に集約されます。VS Code の settings.json にも書けますが、frontmatter.json の方がリポジトリに含めて管理しやすいです。

pageFolders — 記事の保存先

"frontMatter.content.pageFolders": [
  {
    "title": "Japanese Posts",
    "path": "[[workspace]]/posts/ja/{{year}}/{{month}}",
    "contentTypes": ["post"],
    "defaultContentType": "post",
    "filePrefix": ""
  }
]

ポイントは2つ。

path: "[[workspace]]/posts/ja/{{year}}/{{month}}":
新規記事を作ると posts/ja/2026/04/ のようにサブフォルダに自動振り分けされます。手動でフォルダを作る必要がありません。

filePrefix: "":
Front Matter CMS のデフォルト動作では新規記事のファイル名に日付プレフィックス(2026-04-09-)が付きます。 このブログでは slug がファイル名になる設計なので、空文字にして無効化しています。ここは null ではなく空文字列 "" にする必要がありました。

slug をどう管理するか

フィールド設計の話に入る前に、このブログで採用した重要な設計判断について、ここで説明しておきます。

ファイル名を slug とし、frontmatter に明示的なフィールドを持たない という方針です。 つまり contentTypes に slug フィールドを定義せず、生成された slug はそのまま .md ファイルのパスとして使います。

初期セットアップ時に自動生成される frontmatter.json には slug フィールドが含まれていますが、調べてみるとこれは削っても問題ないどころか、むしろ削った方が設計としてスッキリすることが分かりました。

ファイル名 = slug が SSG / Git-based CMS の主流

有名な静的サイトジェネレータ (SSG) や Git-based CMS のデフォルト挙動を見ると、ほぼ全てがファイル名ベースで slug を扱います。

システムデフォルトの slug 源frontmatter slug の位置づけ
Front Matter CMSファイル名(slugTemplate)フィールド定義は任意
Keystaticファイル名(slugFieldschema にフィールドはあるが保存時に strip
Decap CMSファイル名(テンプレート)基本書かない
Tina基本ファイル名ベース通常は書かない
Next.js (pages / app router)ディレクトリ・ファイル構造 = URLそもそも必須ではない
Hugoファイル名 / ディレクトリ例外時のオーバーライド
Astro (Content Collections)ファイル名例外時のオーバーライド
JekyllYYYY-MM-DD-title.md のパターン例外時のオーバーライド
Gatsbyソースプラグイン次第(典型的にファイル名)例外時のオーバーライド

先の候補リストから 4 つの Git-based CMS、後半 5 つの著名な SSG。 いずれも共通パターンは「ファイル名が真実の slug、frontmatter の slug はオーバーライドのための escape hatch(例外対応)」。 frontmatter に必ず slug を書く流儀はむしろ少数派です。

一方 DB 型の CMS(WordPress, Contentful, Sanity, Strapi, Payload など)では slug は DB カラムとして独立管理します。 ファイルシステムを持たないのでその必要があるわけですが、Markdown + Git ベースの世界にはこの流儀は合いません。 「frontmatter に slug 必須」と考えていたのは、DB 型 CMS の感覚が残っていたからかもしれません。

Front Matter CMS の仕様

公式ドキュメント(Slug) には次のように明記されています。

“If you do not define a slug in the content type, the slug will be derived from the frontMatter.taxonomy.slugTemplate setting, and if that setting is not defined, the page name will be used as the slug.”

つまり、slug 生成の優先順位は次のとおりです。

  1. contentTypes ごとの slugTemplate(コンテンツタイプ別テンプレート)
  2. frontMatter.taxonomy.slugTemplate(グローバル)
  3. ファイル名そのもの

このブログは元々 posts/(ja|en)/yyyy/mm/ のディレクトリ構造で記事を管理しているので、2 のルート(グローバル slugTemplate)を採用すればスッキリ定義できます。

"frontMatter.taxonomy.slugTemplate": "{{year}}/{{month}}/{{title}}"

加えて、contentTypes の fields[] には slug フィールドを定義しないことで、生成された slug 値は frontmatter に書き戻されず、ファイルパスにのみ反映されます。翻訳スクリプト、タグ付け、記事レンダラー、いずれもファイルパスから slug を導出するので、情報源が二重管理にならず 一本化されます。

フィールド設計

contentTypes で frontmatter のスキーマを定義

Front Matter CMS は、contentTypes で frontmatter のフィールドを型付きで定義します。これが CMS のサイドパネル(記事を開いたときに右側に出る GUI)に表示されるフィールドになります。

このブログの post タイプのフィールド一覧:

フィールド用途GUI 表示
titlestring記事タイトル表示
created_atdatetime作成日非表示
updated_atdatetime更新日非表示
statuschoicedraft / published / archived / dismissed表示
skip_translationboolean翻訳パイプラインのスキップフラグ表示
cover_imageimageカバー画像表示
cover_creditstring画像のコピーライト表記表示
tagstagsタグ ID の配列表示
descriptionstringmeta description(SEO / OGP)非表示

※ このほかに fmContentType: post という行が frontmatter に自動付与されますが、これは Front Matter CMS がコンテンツタイプを紐付けるために内部で書き込むフィールドで、contentTypes[].fields[] としては定義しません。

hidden: true — パイプラインが管理するフィールドを隠す

「GUI 表示」列で「非表示」になっているフィールドは、すべて hidden: true を設定しています。

{
  "title": "Description",
  "name": "description",
  "type": "string",
  "default": "",
  "description": "Auto-generated by tags.ts pipeline if empty",
  "hidden": true
}

なぜ隠すのか。このブログでは created_at / updated_at は Front Matter CMS がファイル作成・保存のタイミングで自動管理し、description は AI タグ付けスクリプトが埋めるフィールドです。GUI に出ていると誤操作で値を変えてしまうリスクがあるので、触らないものは隠す方針にしました。

title は当初 hidden にしていましたが、後述の「日本語タイトルで slug 生成が崩れる問題」の回避策として、記事作成後にタイトルを書き換える運用にしたため、サイドパネルに表示しています。

どのフィールドを誰が書くかの全体像は、後述の 「周辺ツールとの棲み分け」 セクションに表としてまとめてあります。

isPublishDate と isModifiedDate

created_atisPublishDate: trueupdated_atisModifiedDate: true を設定しています。

{
  "title": "Created at",
  "name": "created_at",
  "type": "datetime",
  "default": "{{now}}",
  "dateFormat": "yyyy-MM-dd",
  "isPublishDate": true,
  "hidden": true
}

isPublishDate を設定すると、そのフィールドの値がダッシュボードのカードに日付として表示されます。また、新規記事作成時に {{now}} で現在日付が自動セットされます。

同様に isModifiedDate を設定すると、Front Matter CMS 経由でファイルを保存するたびに updated_at が現在日時に自動更新されます。ただし、VS Code のエディタから直接保存した場合は更新されません(CMS のサイドパネルから操作した場合のみ)。

status フィールドの設計

このブログ記事のライフサイクルは下記の4つの状態(status)で管理しています。

status意味
draft下書き。執筆中・レビュー中
published公開中
archived取り下げ。一度公開したが非公開に戻した
dismissed却下。公開せずボツにした下書き

これをこのまま Front Matter CMS の choice 型フィールドとして定義します。

{
  "title": "Status",
  "name": "status",
  "type": "choice",
  "choices": ["draft", "published", "archived", "dismissed"],
  "default": "draft",
  "required": true
}

サイドパネルにはドロップダウンが表示されるので、draft → published の切り替えがワンクリックでできて快適です。

skip_translation — 翻訳パイプラインとの連携フィールド

{
  "title": "Skip Translation",
  "name": "skip_translation",
  "type": "boolean",
  "default": false,
  "description": "If true, the pipeline will not process this file (no translation, no tagging, no overwriting)"
}

このブログには GitHub Actions で動く翻訳パイプラインがあり、posts/ja/ への push をトリガーに自動で英訳を生成します。文化的文脈に依存する内容など、自動では翻訳したくない記事はこのフラグで除外できます。

CMS のフィールドが「ブログの見た目」だけでなく「バックエンドのパイプライン制御」にも使えるのは、frontmatter ベースの設計ならではですね。

日本語タイトルで slug 生成が崩れる問題

Front Matter CMS で新規記事を作るとき、入力するのはタイトルだけです。タイトルから slug が自動生成され、それが slugTemplate に渡ってファイル名が決まります。

ここで問題になるのが日本語タイトルです。「プロジェクト・ヘイル・メアリーを観てきました」と入力すると、ファイル名もそのまま プロジェクト・ヘイル・メアリーを観てきました.md になってしまいます。URL にも使う slug は英語にしたいので、これは困ります。

slug 生成ロジックと日本語

ソースコード(SlugHelper.ts)を確認したところ、slug の生成ロジックは小文字化・ハイフン変換・英語ストップワード除去を行いますが、charMap に CJK のエントリはなく、日本語はそのまま素通しします。つまり日本語タイトルからまともな英語 slug は生成できません。

回避策: 英語タイトルで作成 → 後から日本語に書き換え

最初に slug 候補となる英語(例えば Project Hail Mary)をタイトル欄に入れて記事を作成します。この時点で slugTemplate 経由で slug は 2026/04/project-hail-mary と生成され、それに基づいた .md ファイルが生成されます。
その後、frontmatter のタイトルを日本語に書き換えます。title は frontmatter だけの値なので、書き換えても slug やファイル名には影響しません。スマートではないですが確実です。

ダッシュボードのカスタマイズ

Front Matter CMS のダッシュボード(VS Code 起動時に表示される記事一覧画面)は、デフォルトだとタイトルと日付だけが表示されます。記事が増えてくると、ステータスやソート順もカスタマイズしたくなります。

draftField — ステータスをカードに表示する

デフォルトのダッシュボードでは、カード上のステータス表示は Front Matter CMS 組み込みの draft/published 判定が使われます。このブログのように独自の status フィールド(4値の choice 型)を使っている場合、frontMatter.content.draftField で紐付ける必要があります。

"frontMatter.content.draftField": {
  "name": "status",
  "type": "choice",
  "choices": ["draft", "published", "archived", "dismissed"]
}

この設定で、ダッシュボードのカードに draftpublished がバッジとして表示されるようになります。

sorting — 作成日でソート

"frontMatter.content.sorting": [
  {
    "title": "Created (newest first)",
    "name": "created_at",
    "order": "desc",
    "type": "date"
  },
  {
    "title": "Created (oldest first)",
    "name": "created_at",
    "order": "asc",
    "type": "date"
  }
],
"frontMatter.content.defaultSorting": "Created (newest first)"

デフォルトは最新記事が上に来る降順。ダッシュボードのソートドロップダウンから昇順にも切り替えられます。

その他のダッシュボード設定

"frontMatter.dashboard.openOnStart": true,
"frontMatter.dashboard.content.card.fields.description": null,
"frontMatter.panel.openOnSupportedFile": true

VS Code 起動時にダッシュボードを自動表示し、Markdown ファイルを開くとサイドパネルも自動表示。毎回手動で開く手間が省けます。

card.fields.descriptionnull にすると、カードから description の表示が消えます。description が長いとカードが巨大化するので、非表示にしておくのがおすすめです。同様に card.fields.titlecard.fields.datenull / false で非表示にできます。

周辺ツールとの棲み分け

翻訳パイプラインとの役割分担

このブログでは、frontmatter が「人間が操作するフィールド」と「パイプラインが管理するフィールド」の両方を持っています。

フィールド誰が書くFront Matter CMS での扱い
title記事作成時に決定 → 後から日本語に書き換えサイドパネルに表示
created_at / updated_atFront Matter CMS が自動管理非表示(hidden)
status人間サイドパネルに表示。ドロップダウンで操作
skip_translation人間サイドパネルに表示。チェックボックスで操作
cover_image / cover_credit人間サイドパネルに表示。画像ピッカー / テキスト入力
tagsAI タグ付けスクリプトサイドパネルに表示(確認用)。基本は触らない
descriptionAI タグ付けスクリプト非表示(hidden)

hidden: true の使い分けが、そのまま「人間の領域」と「パイプラインの領域」の境界線になっています。 CMS のフィールド設計がそのままワークフロー設計になる、という感覚は frontmatter ベースならではだと思います。

Prettier を posts/ で無効にする

VS Code に Prettier を入れていると、Markdown 保存時に自動整形が走ります。 整形といっても Markdown の場合、パイプテーブル(| 区切りのテーブル)のスペース揃え程度なので害はないのですが、メリットもほぼありません。
保存のたびに数秒待たされるのがウザいので、Markdown では無効にしてしまいました。

無効化には 2箇所 の設定が必要でした。

1. .prettierignore — CLI / CI 経由の Prettier を止める:

posts/**

2. .vscode/settings.json — エディタの保存時フォーマットを止める:

{
  "[markdown]": {
    "editor.defaultFormatter": null,
    "editor.formatOnSave": false
  }
}

最初は .prettierignore だけで済むと思っていたのですが、VS Code の Prettier 拡張機能は .prettierignore を参照せず、保存時のフォーマットが止まりませんでした。
.vscode/settings.json でエディタレベルのフォーマットを明示的に無効化する必要があります。

JS/TS/CSS 等のコードファイルは今まで通り Prettier が効きます。Markdown だけをピンポイントで除外する形です。

Snippets — 繰り返し使う記法のテンプレート

ブログでは画像にコピーライト表記を付けることが多いので、Snippet として登録しています。

"frontMatter.content.snippets": {
  "Image with copyright": {
    "description": "Insert image with copyright caption",
    "body": "![[[alt]]](<<image>>)\n*<<copyright>>*",
    "fields": [
      { "name": "alt", "title": "Alt text", "type": "string", "default": "" },
      { "name": "image", "title": "Image", "type": "image", "default": "" },
      { "name": "copyright", "title": "Copyright", "type": "string", "default": "© " }
    ]
  }
}

コマンドパレットから「Front Matter: Insert snippet」を実行すると、alt テキスト・画像パス・コピーライトの入力ダイアログが順に表示され、以下のような Markdown が挿入されます。

![映画のポスター](/images/posts/movie-poster.jpg)
*© 2026 Studio Name*

画像パスの手打ちミスが減るのと、コピーライト表記のフォーマットが統一されるので地味に役立ちそうです。

まとめ

Front Matter CMS は「VS Code で Markdown を書く人のための CMS」という立ち位置がはっきりしていて、設定の見通しがいいツールです。frontmatter.json 一枚に全設定が収まるので、Git で管理できるし、何がどう効いているか迷子になりにくい。

特にこのブログのように翻訳パイプラインや AI タグ付けが frontmatter を直接読み書きする環境では、「CMS が Markdown ファイルを壊さない」ことが最重要条件でした。Front Matter CMS は frontmatter のフィールドを GUI で操作するだけで、本文には一切手を加えません。この「触るべきでないところに触らない」姿勢が、パイプラインとの共存を可能にしています。

次の記事では、このブログでもう一つ試した CMS — Keystatic について書く予定です。