はじめに
個人ブログやポートフォリオサイトを作るとき、「コンテンツの管理どうしよう」という問題に必ずぶつかります。
ContentfulやNotionをCMSとして使う方法も定番ですが、
- 無料プランの制限が気になる
- 専用の管理画面にログインするのが面倒
- どうせMarkdownで書くなら、Gitで管理したい
という思いがありました。
一方、GitHubはすでにMarkdownファイルのホスティング場所として使っているわけで、「GitHub自体をCMSとして使えればそれで十分では?」と考えたのが、このライブラリを作ったきっかけです。
作ったもの
structcms — GitHub リポジトリをヘッドレスCMSとして使うための TypeScript ライブラリです。
npm install @asahi-fj/structcms
コンセプトはシンプルで、「1つの関数を呼ぶだけでコンテンツが取れる」です。
使い方
1. コンテンツリポジトリの構造
GitHub上に(プライベートでもOK)リポジトリを作り、以下のような構造でコンテンツを置きます。
your-content-repo/
└── contents/
├── hello-world/
│ ├── content.md
│ └── meta.json
└── getting-started/
├── content.md
└── meta.json
meta.json にタイトルや日付などのメタデータを書き、content.md に本文を書くだけです。
// contents/hello-world/meta.json
{
"title": "Hello World",
"description": "My first post",
"publishedAt": "2026-04-11",
"draft": false,
"tags": ["intro"]
}
<!-- contents/hello-world/content.md -->
# Hello World
Welcome to my first post. This content is stored on GitHub.
2. GitHub Token の発行
GitHub → Settings → Developer settings → Personal access tokens から、対象リポジトリへの Contents: Read 権限を持つトークンを発行します。
3. コードを書く
import StructCms from "@asahi-fj/structcms";
const cms = StructCms({
token: process.env.GITHUB_TOKEN!,
owner: "your-org",
repo: "your-content-repo",
});
const posts = await cms.getAll();
// [{ slug: "hello-world", content: "# Hello World\n...", meta: { title: "Hello World", ... } }]
StructCms() に設定を渡すだけで、あとは cms.getAll() を呼ぶだけ。
返ってくるデータの型
type Meta = {
title: string;
description: string;
publishedAt?: string;
draft?: boolean;
tags?: string[];
};
type ContentEntry = {
slug: string; // contents/ 以下のディレクトリ名
content: string; // Markdownの生文字列
meta: Meta;
};
よくある操作のパターン
公開済み記事だけ取得
const posts = await cms.getAll();
const published = posts.filter((p) => !p.meta.draft);
タグで絞り込み
const typescriptPosts = posts.filter((p) =>
p.meta.tags?.includes("typescript")
);
新しい順にソート
const sorted = posts.sort((a, b) =>
(b.meta.publishedAt ?? "").localeCompare(a.meta.publishedAt ?? "")
);
Next.js との組み合わせ
App Router のブログページへの組み込み例です。
// lib/cms.ts
import StructCms from "@asahi-fj/structcms";
export const cms = StructCms({
token: process.env.GITHUB_TOKEN!,
owner: process.env.GITHUB_OWNER!,
repo: process.env.GITHUB_REPO!,
});
// app/blog/page.tsx
import { cms } from "@/lib/cms";
export default async function BlogPage() {
const posts = await cms.getAll();
const published = posts.filter((p) => !p.meta.draft);
return (
<ul>
{published.map((post) => (
<li key={post.slug}>
<a href={`/blog/${post.slug}`}>{post.meta.title}</a>
</li>
))}
</ul>
);
}
サーバーコンポーネントで await cms.getAll() を呼ぶだけで、コンテンツ一覧が取れます。
環境変数
.env に以下を設定します。
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx
GITHUB_OWNER=your-org
GITHUB_REPO=your-content-repo
なぜ既存のCMSを使わなかったのか
| structcms | Contentful / Notion | |
|---|---|---|
| 管理UI | GitHub(使い慣れたもの) | 専用の管理画面 |
| コスト | GitHub無料枠で十分 | プランによって制限あり |
| バージョン管理 | Gitで完結 | 独自の履歴管理 |
| Markdown | そのまま書ける | エディタ依存 |
| セットアップ | 関数1つ | SDK + 環境設定 |
「すでにGitHubを使っているなら、それ以上のツールを増やさなくていい」という発想です。
今後やりたいこと
-
cms.getById(slug)の実装(coming soon) - キャッシュ対応
- 画像ファイルのサポート
- Zodによるメタデータのバリデーション
おわりに
「GitHubをCMSにする」というアイデア自体は目新しくはないかもしれませんが、ライブラリとして1関数で使えるようにパッケージ化したところが今回のポイントです。
個人ブログや小規模なドキュメントサイトであれば、ContentfulやNotionを導入するより手軽に使えると思います。ぜひ試してみてください!
npm install @asahi-fj/structcms