皆さん、こんにちは!Futuristic Imagination LLC 代表の佐藤です。
AIオウンドメディア18サイトを1人で自動運用し、9言語展開で年間6,570記事を生成している弊社では、Next.jsとGemini API、Vercel Cronを駆使して毎日記事を自動生成しています。そして、これらの自動生成されたMarkdown記事を、いかに効率的に管理・公開するかは常に頭を悩ませるポイントでした。
「手動でGitHubにコミット&プッシュして、デプロイが走るのを待つ…」
こんな非効率な作業、もう限界じゃないですか?私は限界でした。
この記事では、そんな手動コミット地獄から抜け出し、GitHub APIを使ってプログラムでMarkdown記事を保存・公開する方法を、TypeScriptの実装例を交えて徹底解説します。
この記事でわかること
- GitHub APIでコンテンツを操作するための基本的な流れ
- GitHub Personal Access Token (PAT) の安全な取得と設定方法
- TypeScriptでGitHub APIを叩き、ファイルを作成・更新・削除する方法
- 既存の記事を自動的にリライトし、GitHubへプッシュする実践的な応用例
- もう手動コミットから卒業!完全自動化されたコンテンツ運用フローの構築方法
はじめに:なぜGitHub APIで自動化が必要なのか?
弊社では、AIを活用して膨大な数のMarkdown記事を生成しています。これらの記事は、Next.jsで構築した弊社のオウンドメディアで公開され、Vercelによって自動的にデプロイされます。VercelのデプロイはGitHubリポジトリの変更をトリガーとして走るため、記事を公開するためには「GitHubに記事ファイルを追加(または更新)する」作業が不可欠です。
当初はAIが生成した記事を人間が確認し、手動でGitHub DesktopやVS Codeからコミット&プッシュしていました。しかし、記事数が日々増えていく中で、この作業は完全にボトルネックとなり、非効率の極みだと感じていました。
「なんかこれって自動化できないかな?」
この問いから、GitHub APIを使った完全自動化への挑戦が始まりました。目指すは「補給不要の自販機型」コンテンツ運用です。人間はクリエイティブな思考に集中し、単純なファイル操作はすべてシステムに任せる。これこそが、弊社の求める高生産性エコシステムです。
ステップ1:GitHub Personal Access Token (PAT) の取得
GitHub APIをプログラムから利用するには、認証が必要です。最も一般的な方法はPersonal Access Token (PAT) を利用することです。
1. PATの取得手順
- GitHubにログインし、右上のプロフィールアイコンをクリックし、「Settings」を選択します。
- 左側のサイドバーから「Developer settings」をクリックします。
- さらに左側のサイドバーから「Personal access tokens」→「Tokens (classic)」をクリックします。
- 「Generate new token」をクリックし、「Generate new token (classic)」を選択します。
-
Note: トークンの目的を分かりやすく記入します(例:
auto-content-deployment)。 - Expiration: 適切な有効期限を設定します。自動運用であれば「No expiration」でも良いですが、セキュリティを考慮し、定期的な見直しや短い期間に設定するのもアリです。
-
Select scopes: ここが最も重要です。以下のスコープにチェックを入れてください。
-
repo(Full control of private repositories)repo:statusrepo_deploymentpublic_reporepo:invite
-
workflow(Update GitHub Action workflows)
これらのスコープは、リポジトリ内のファイルを読み書きし、コミット履歴を作成するために必要です。
-
- 「Generate token」をクリックすると、新しいPATが発行されます。このトークンは一度しか表示されないため、必ずコピーして安全な場所に保存してください。 環境変数として設定するのが最も安全な運用方法です。
2. 環境変数への設定
発行したPATは、以下のように環境変数に設定します。
# .envファイル (開発環境)
GITHUB_ACCESS_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// Next.jsなどNode.js環境の場合
const GITHUB_ACCESS_TOKEN = process.env.GITHUB_ACCESS_TOKEN;
if (!GITHUB_ACCESS_TOKEN) {
throw new Error('GitHub Access Token is not set in environment variables.');
}
ステップ2:GitHub APIクライアントの準備 (TypeScript)
GitHub APIを叩くためのHTTPクライアントを準備します。ここでは、axiosとoctokitの2つのパターンを紹介します。どちらを使っても問題ありませんが、octokitはGitHub API専用のSDKなので、より便利に型安全に扱えます。
1. axios を使う場合
axiosを使う場合は、HTTPリクエストを手動で組み立てる形になります。
npm install axios
# or
yarn add axios
// utils/githubApi.ts
import axios from 'axios';
const GITHUB_ACCESS_TOKEN = process.env.GITHUB_ACCESS_TOKEN;
const GITHUB_OWNER = 'your-github-username'; // または組織名
const GITHUB_REPO = 'your-repository-name';
const githubAxios = axios.create({
baseURL: `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/contents/`,
headers: {
Authorization: `token ${GITHUB_ACCESS_TOKEN}`,
Accept: 'application/vnd.github.v3+json',
},
});
export const createFileAxios = async (
path: string,
content: string,
message: string,
branch: string = 'main'
) => {
try {
const res = await githubAxios.put(path, {
message,
content: Buffer.from(content).toString('base64'), // ファイル内容はBase64エンコードが必要
branch,
});
console.log(`Successfully created/updated file: ${path}`, res.data);
return res.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 422) {
// ファイルが存在しない場合に422になることがある
// または、すでにファイルが存在し、shaを指定しない場合に422になる
console.error(`File ${path} already exists or other validation error.`);
// 既存のファイルを取得して更新ロジックに切り替えるなど
} else {
console.error(`Error creating/updating file ${path}:`, error);
}
throw error;
}
};
// ... 他のAPI操作も同様に実装
2. octokit を使う場合(推奨)
octokit はGitHub公式のJavaScript SDKで、型定義も豊富で非常に使いやすいです。
npm install octokit
# or
yarn add octokit
// utils/githubApi.ts
import { Octokit } from '@octokit/rest';
const GITHUB_ACCESS_TOKEN = process.env.GITHUB_ACCESS_TOKEN;
const GITHUB_OWNER = 'your-github-username'; // または組織名
const GITHUB_REPO = 'your-repository-name';
const octokit = new Octokit({
auth: GITHUB_ACCESS_TOKEN,
});
/**
* ファイルの情報を取得する
*/
export const getFileContent = async (
path: string,
branch: string = 'main'
) => {
try {
const { data } = await octokit.rest.repos.getContent({
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
path,
ref: branch,
});
if (Array.isArray(data)) {
throw new Error('Path points to a directory, not a file.');
}
if (data.type === 'file' && data.content) {
// Base64デコードして返す
return {
content: Buffer.from(data.content, 'base64').toString('utf8'),
sha: data.sha, // 更新時に必要
};
}
return null;
} catch (error: any) {
if (error.status === 404) {
return null; // ファイルが見つからない場合
}
console.error(`Error getting file content for ${path}:`, error);
throw error;
}
};
/**
* ファイルを作成または更新する
*/
export const upsertFile = async (
path: string,
content: string,
message: string,
branch: string = 'main'
) => {
let sha: string | undefined;
// ファイルが既に存在するか確認し、存在する場合はそのSHAを取得
const existingFile = await getFileContent(path, branch);
if (existingFile) {
sha = existingFile.sha;
}
try {
const { data } = await octokit.rest.repos.createOrUpdateFileContents({
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
path,
message,
content: Buffer.from(content).toString('base64'),
branch,
sha, // 更新時には既存ファイルのSHAが必要
});
console.log(`Successfully upserted file: ${path}`, data.content?.sha);
return data;
} catch (error) {
console.error(`Error upserting file ${path}:`, error);
throw error;
}
};
/**
* ファイルを削除する
*/
export const deleteFile = async (
path: string,
message: string,
branch: string = 'main'
) => {
const existingFile = await getFileContent(path, branch);
if (!existingFile) {
console.warn(`File ${path} does not exist, skipping deletion.`);
return;
}
try {
const { data } = await octokit.rest.repos.deleteFile({
owner: GITHUB_OWNER,
repo: GITHUB_REPO,
path,
message,
sha: existingFile.sha, // 削除時もSHAが必要
branch,
});
console.log(`Successfully deleted file: ${path}`, data);
return data;
} catch (error) {
console.error(`Error deleting file ${path}:`, error);
throw error;
}
};
ステップ3:Markdown記事をプログラムで保存・公開する
上記のupsertFile関数を使って、Markdown記事をGitHubリポジトリに保存します。
具体的な利用例:新しい記事の作成
例えば、AIが新しい記事を生成し、それをarticles/new-ai-article.mdとして保存したい場合です。
// src/services/articlePublisher.ts
import { upsertFile } from '../utils/githubApi';
export const publishNewArticle = async (
title: string,
markdownContent: string,
category: string // 例: AI-Generated
) => {
const slug = title
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '') // 特殊文字を除去
.replace(/\s+/g, '-') // スペースをハイフンに
.replace(/-+/g, '-'); // 連続するハイフンを一つに
const filePath = `articles/${category}/${slug}.md`;
const commitMessage = `feat: Add new article - ${title}`;
try {
await upsertFile(filePath, markdownContent, commitMessage);
console.log(`Article "${title}" published to ${filePath}`);
// VercelなどのCI/CDが自動的にデプロイを開始する
} catch (error) {
console.error(`Failed to publish article "${title}":`, error);
throw error;
}
};
// 使い方
const newArticleTitle = 'AIが生成する次世代Webサイトの構築術';
const newArticleContent = `
# ${newArticleTitle}
この記事では、AIを活用した次世代のWebサイト構築について深掘りします。
... (Gemini APIが生成したMarkdownコンテンツ) ...
`;
// publishNewArticle(newArticleTitle, newArticleContent, 'web-dev');
このように、たった数行のコードでGitHubへのコミットとプッシュが完了します。Vercelを連携させている場合、このプッシュをトリガーに自動的にデプロイが走り、生成された記事が公開される流れです。
応用例:既存記事の自動リライト
弊社では、Google Search Console APIとLLMを連携させ、低順位の記事を自動的に検出し、最新のトレンドに合わせて自動的にリライト・再インデックス送信(IndexNow)まで完結させるシステムを構築しています。
このリライトプロセスの中で、GitHub APIが非常に重要な役割を担います。
- GSC APIからデータ取得: 低順位記事のURLやキーワードを取得。
-
既存記事の取得:
getFileContentを使って、GitHubから対象Markdown記事のコンテンツとSHAを取得。 - LLMでリライト: 取得したコンテンツとGSCデータを基に、Gemini APIで記事をリライト。
-
GitHubに更新:
upsertFileを使って、リライトされたコンテンツを同じパスに、取得したSHAを付与して更新。 - IndexNow APIで再インデックス: Googleに更新を通知。
// src/services/articleRewriter.ts
import { getFileContent, upsertFile } from '../utils/githubApi';
import { rewriteArticleWithAI } from './aiRewriter'; // 仮のAIリライト関数
export const autoRewriteAndPublish = async (filePath: string) => {
try {
const existing = await getFileContent(filePath);
if (!existing) {
console.warn(`File ${filePath} not found for rewriting.`);
return;
}
const originalContent = existing.content;
const originalSha = existing.sha;
console.log(`Rewriting article: ${filePath}`);
const rewrittenContent = await rewriteArticleWithAI(originalContent); // AIでリライト
if (rewrittenContent === originalContent) {
console.log(`No changes detected for ${filePath}, skipping update.`);
return;
}
const commitMessage = `refactor(auto): Rework content based on SEO data for ${filePath}`;
await upsertFile(filePath, rewrittenContent, commitMessage);
console.log(`Article ${filePath} successfully rewritten and updated.`);
// 必要であれば、ここでIndexNow APIを叩く処理を追加
// await sendToIndexNow(filePath);
} catch (error) {
console.error(`Error during auto-rewrite for ${filePath}:`, error);
throw error;
}
};
// 使い方 (Vercel Cronや独自のスケジューラから呼び出す)
// const targetFilePath = 'articles/seo/low-ranking-article.md';
// autoRewriteAndPublish(targetFilePath);
「〇〇って切り替えたから、この作業はもう不要かな?」
まさにこの思考で、データ分析からリライト、自動デプロイまでをシームレスにつなぐ仕組みを自ら構築しています。人間が「コアな創造的思考」に集中できる環境を体現するために、無駄な作業は徹底的に排除するのが私の哲学です。
セキュリティと注意点
- PATの管理: GitHub Personal Access Tokenは、パスワードと同様に非常に機密性の高い情報です。絶対に公開リポジトリにコミットしたり、クライアントサイドで直接使用したりしないでください。サーバーサイドの環境変数として厳重に管理し、漏洩がないように注意しましょう。
-
必要なスコープのみ付与: PATには、その操作に必要な最小限のスコープのみを付与するようにしてください。上記では
repoスコープを付与しましたが、もし特定の操作(例: ファイル読み取りのみ)しか行わないのであれば、より限定的なスコープを選択することでリスクを低減できます。 - コミットメッセージの明確化: 自動化されたコミットであっても、その内容がわかるように意味のあるコミットメッセージを付けることが重要です。後から変更履歴を追う際に役立ちます。
- レートリミット: GitHub APIにはレートリミットがあります。短時間に大量のリクエストを送信すると制限がかかる可能性があるため、注意が必要です。大規模な運用を行う場合は、リトライ機構やバッチ処理を考慮しましょう。
- エラーハンドリング: API呼び出しは常に失敗する可能性があります。ネットワークエラー、認証エラー、ファイルパスの間違いなど、あらゆるケースを想定して適切なエラーハンドリングを実装することが重要です。
まとめ
GitHub APIをプログラムから操作することで、Markdown記事の保存・公開プロセスを劇的に自動化できることをご紹介しました。手動でのファイル操作やコミットから解放されることで、エンジニアはより創造的で価値の高い業務に集中できるようになります。
弊社自身がAIオウンドメディアの運用で日々実践しているように、「弊社自身が使っている状態を作らないと、顧客に刺さらない」という哲学は、このような自動化システムにも当てはまります。実際に効果を実感しているからこそ、自信を持って皆さんにおすすめできる自動化手法です。
今回紹介したようなGitHub APIを活用したコンテンツ自動化システムや、Next.jsとGemini APIを組み合わせたAIコンテンツ生成パイプラインの構築代行も行っています。もしご興味があれば、ぜひお気軽にお問い合わせください。
➡️ Futuristic Imagination LLC サービスページ: https://www.futuristicimagination.co.jp/service/
また、日々の学びや実践から得た知識を、転職・副業・キャリアに関するShorts動画として毎日配信しています。こちらもぜひチェックしてみてください。
➡️ YouTubeチャンネル: https://www.youtube.com/channel/UCFobIbWz1KDKaIdDqXpTPAA
それでは、また次の記事でお会いしましょう!LGTMとコメントもお待ちしております!