初めに
最近は社内の活動を少しでもやりやすくするための社内システム開発を龍ちゃんです。合わせて、AIと協業して開発する検証も進めています。AIを開発に入れてから、少ない人数で開発を回せているので素晴らしいですね。今回は、AIとはちょっと関係ない領域でのコンテンツですけど…
今回は、GitHubの優れた編集・レビュー機能をそのまま活用しながら、APIで簡単に自分のシステムに組み込む方法を紹介します。
なぜGitHubをCMSとして使うのか?
最大の魅力:GitHubの機能をフル活用できる
- 編集: VS Codeライクなエディタで快適に編集
- レビュー: Pull Requestで複数人でのレビュー
- 履歴管理: すべての変更履歴が自動保存
- 検索: リポジトリ内の強力な検索機能
- 権限管理: チームでの細かいアクセス制御
つまり、コンテンツの作成・編集・管理はGitHubに任せて、自分のシステムは「読み取り」に専念できるのです。
こんな使い方ができる
GitHubリポジトリ構成例:
/posts
├── 2024-01-15-github-api-guide.md
├── 2024-01-20-nestjs-tips.md
└── 2024-01-25-typescript-tricks.md
→ これをAPIで取得して、ブログや社内ドキュメントサイトに表示!
こちらのシステムと統合して使用することで、GitHub上で作成した成果物ををシステムに統合することができます。
Personal Access Token (PAT) の設定
GitHub APIを使うためには、PATの設定が必要です。
基本設定(検証で使いまわししたい場合)
- GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
- Generate new tokenをクリック
- 権限は
repo
を選択(プライベートリポジトリアクセス用)
セキュリティ重視の場合
Fine-grained PATの使用を推奨します:
- 対象リポジトリを限定可能
- 必要な権限:
Contents: Read
のみでOK - 有効期限の強制設定でより安全
レート制限が劇的に改善されるんです!
PAT認証の違いは劇的なんですよ。
認証方式 | 1時間あたりの制限 | 1分あたり換算 |
---|---|---|
認証なし | 60回 | 1回/分 |
PAT認証 | 5,000回 | 83回/分 |
*つまり、PATを使うだけで83倍のリクエストが可能になるんです!**これは使わない手はないですよね。
コピペで動く!NestJSコントローラー実装
環境変数を設定
GITHUB_OWNER=your-username
GITHUB_REPO=your-repo-name
GITHUB_TARGET_DIRECTORY=/posts
GITHUB_PAT=ghp_xxxxxxxxxxxxxxxxxxxx
コントローラーにコピペするだけ!
import { Controller, Get, Param, HttpException, HttpStatus } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
@ApiTags('GitHub CMS')
@Controller('api/github-cms')
export class GitHubCmsController {
private readonly baseUrl = 'https://api.github.com';
private readonly githubPat = '取得したPATを埋め込んでね!';
private readonly githubOwner = 'リポジトリOwnerを入れてね';
private readonly githubRepo = 'リポジトリ名を入れてね!';
private readonly githubTargetDirectory = '/posts';
private async githubRequest(path: string): Promise<any> {
const response = await fetch(`${this.baseUrl}${path}`, {
headers: {
'Authorization': `Bearer ${this.githubPat}`,
'Accept': 'application/vnd.github+json',
'User-Agent': 'github-cms-client/1.0'
}
});
if (!response.ok) {
const errorMessages = {
401: 'GitHub認証エラー: PATを確認してください',
403: 'アクセス権限がありません',
404: 'リソースが見つかりません',
429: 'レート制限に達しました'
};
throw new HttpException(
errorMessages[response.status] || `GitHub API Error: ${response.status}`,
response.status
);
}
return response.json();
}
@Get('files')
@ApiOperation({ summary: 'Markdownファイル一覧を取得' })
@ApiResponse({ status: 200, description: 'ファイル一覧' })
async getFiles() {
const path = `/repos/${this.githubOwner}/${this.githubRepo}/contents${this.githubTargetDirectory}`;
const files = await this.githubRequest(path);
const markdownFiles = files.filter((file: any) =>
file.type === 'file' && file.name.endsWith('.md')
);
return markdownFiles.map((file: any) => ({
name: file.name,
path: file.path,
sha: file.sha,
size: file.size,
downloadUrl: file.download_url
}));
}
@Get('files/:filename')
@ApiOperation({ summary: '特定のMarkdownファイルの内容を取得' })
@ApiResponse({ status: 200, description: 'ファイル内容' })
@ApiResponse({ status: 404, description: 'ファイルが見つかりません' })
async getFileContent(@Param('filename') filename: string) {
if (!filename.endsWith('.md')) {
filename += '.md';
}
const path = `/repos/${this.githubOwner}/${this.githubRepo}/contents${this.githubTargetDirectory}/${filename}`;
try {
const file = await this.githubRequest(path);
const content = Buffer.from(file.content, 'base64').toString('utf-8');
return {
name: file.name,
path: file.path,
content: content,
sha: file.sha,
size: file.size,
downloadUrl: file.download_url,
htmlUrl: file.html_url
};
} catch (error) {
if (error.status === 404) {
throw new HttpException('ファイルが見つかりません', HttpStatus.NOT_FOUND);
}
throw error;
}
}
@Get('rate-limit')
@ApiOperation({ summary: 'GitHub APIレート制限情報を取得' })
@ApiResponse({ status: 200, description: 'レート制限情報' })
async getRateLimit() {
const data = await this.githubRequest('/rate_limit');
return {
limit: data.rate.limit,
remaining: data.rate.remaining,
resetAt: new Date(data.rate.reset * 1000),
used: data.rate.limit - data.rate.remaining
};
}
}
使い方はこんな感じ!
APIエンドポイント
# ファイル一覧を取得
GET /api/github-cms/files
# 特定のファイル内容を取得
GET /api/github-cms/files/2024-01-15-github-api-guide.md
# レート制限を確認
GET /api/github-cms/rate-limit
レスポンス例
// GET /api/github-cms/files
[
{
"name": "2024-01-15-github-api-guide.md",
"path": "posts/2024-01-15-github-api-guide.md",
"sha": "abc123...",
"size": 2048,
"downloadUrl": "https://raw.githubusercontent.com/..."
},
// ...
]
// GET /api/github-cms/files/2024-01-15-github-api-guide.md
{
"name": "2024-01-15-github-api-guide.md",
"path": "posts/2024-01-15-github-api-guide.md",
"content": "# GitHub API活用ガイド\\n\\n本文...",
"sha": "abc123...",
"size": 2048,
"downloadUrl": "https://raw.githubusercontent.com/...",
"htmlUrl": "https://github.com/..."
}
フロントエンドでの使用例
// React/Next.jsでの実装
import { useEffect, useState } from 'react';
export default function BlogList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch('/api/github-cms/files')
.then(res => res.json())
.then(data => setPosts(data));
}, []);
return (
<div>
<h1>ブログ記事一覧</h1>
{posts.map(post => (
<div key={post.sha}>
<Link href={`/blog/${post.name.replace('.md', '')}`}>
{post.name.replace('.md', '')}
</Link>
</div>
))}
</div>
);
}
まとめ
GitHubをCMSとして使うメリット:
- 編集・レビュー機能が無料で使える – GitHubの優れたUIをそのまま活用
- APIアクセスが簡単 – シンプルな実装で十分実用的
- レート制限もPATで解決 – 83倍のリクエストが可能に
- バージョン管理付き – すべての変更履歴が自動保存
つまり、コンテンツ管理の面倒な部分はGitHubに任せて、自分はAPIで読み取るだけという理想的な構成が実現できます。
こんなプロジェクトにおすすめ
- 個人ブログ・技術ブログ
- 社内ドキュメントサイト
- 静的サイトのコンテンツ管理
- チームで編集する設定ファイル管理
GitHubの編集・レビュー機能を活用しながら、APIで簡単に自分のシステムに組み込める。これが「GitHub as a CMS」の魅力です!
参考資料
GitHub公式ドキュメント
- GitHub REST API Documentation – APIの完全なリファレンス
- Rate limits for the REST API – レート制限の詳細(認証なし60回/時 vs PAT認証5,000回/時の比較表あり)
- Contents API – ファイル取得APIの仕様
Personal Access Token (PAT) について
- Creating a personal access token – PATの作成手順
- Scopes for OAuth apps – 必要な権限スコープの説明
実装に役立つリソース
- Octokit.js – GitHub公式のJavaScript SDKもあります(今回は使わずにfetch APIで実装)
- gray-matter – Markdown Frontmatterのパース用ライブラリ