今まで、ブログを Markdown で書いて、スクリプトで HTML に変換して Google Blogger に投稿していました。
しかし、自分の PC のスペック不足で、WSL と VS Code と Claude Code を開くと激重なんです。
Gemini に相談してみた
Web で Markdown 書けるやつと言えば Gist かなーと思って、Gist から HTML 変換を挟んで Blogger に投稿できないかと相談してみました。
返答を要約すると「Gist は Git リポジトリなので、クローンできるよ。GitHub Actions で変換と投稿させれば?」とのこと。
ということで、Claude Code に作ってもらいました。
システム設計
アーキテクチャ
以下の 2 つの投稿パターンに対応する設計とした。
パターン A: Gist 経由(GitHub Actions)
Gistで記事執筆
↓
GitHub Actions手動実行(Gist URL入力)
↓
Gist取得 → HTML変換 → Blogger投稿
↓
articles/にMarkdown + meta.json保存
パターン B: ローカル編集
ローカルでMarkdown作成(Claude Code使用可)
↓
npm run publish-from-local 実行
↓
HTML変換 → Blogger投稿
↓
meta.json保存
技術選定
Markdown→HTML 変換
要件として「数式が正しく表示される」ことが必須だったため、以下を採用した。
- markdown-it: Markdown パーサー
- KaTeX: 数式レンダリング(サーバーサイドで完結)
- Prism.js: コードハイライト
KaTeX を選んだ理由は、サーバーサイドで HTML に変換できる点だ。MathJax と異なり、Blogger 投稿時に数式が既に HTML 化されているため、ブラウザ側で JavaScript 実行が不要になる。
メタデータ管理
Gist 経由の記事とローカル作成の記事を統一的に管理するため、以下の形式でメタデータを保存する。
ディレクトリ構成:
articles/
20251010_article.md # 記事本体
20251010_article.meta.json # メタデータ
meta.json の内容:
{
"gistId": "a1b2c3d4e5f6g7h8i9j0",
"gistUrl": "https://gist.github.com/username/...",
"bloggerPostId": "1234567890123456789",
"publishedAt": "2025-10-10T12:00:00Z",
"lastUpdatedAt": "2025-10-10T15:30:00Z"
}
これにより、同じ Gist URL で再実行した場合に既存の Blogger 記事を更新できる。
実装
スクリプト構成
以下のモジュール構成で実装した。
scripts/
fetch-gist.js # Gist取得(GitHub API)
parse-frontmatter.js # Frontmatter解析
markdown-to-html.js # Markdown→HTML変換
publish-blogger.js # Blogger API投稿・更新
manage-metadata.js # meta.json管理
publish-article.js # メインスクリプト(統合)
Gist 取得スクリプト
GitHub API を使用して Gist を取得する。
function fetchGistData(gistId) {
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.github.com',
path: `/gists/${gistId}`,
method: 'GET',
headers: {
'User-Agent': 'neko-engineering-blog',
Accept: 'application/vnd.github.v3+json',
},
};
https
.get(options, (res) => {
const chunks = [];
res.on('data', (chunk) => {
chunks.push(chunk);
});
res.on('end', () => {
const data = Buffer.concat(chunks).toString('utf8');
if (res.statusCode === 200) {
resolve(JSON.parse(data));
} else {
reject(new Error(`Gist取得失敗: ${res.statusCode}`));
}
});
})
.on('error', reject);
});
}
認証なしで Public/Secret Gist の両方を取得できる。
Frontmatter 解析
YAML ライブラリを使わず、正規表現で解析した。
function parseFrontmatter(markdown) {
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
const match = markdown.match(frontmatterRegex);
if (!match) {
throw new Error('Frontmatterが見つかりません');
}
const frontmatterText = match[1];
const content = match[2].trim();
// titleとtagsを抽出
const titleMatch = frontmatterText.match(/title:\s*["'](.+?)["']/);
const tagsMatch = frontmatterText.match(/tags:\s*\[(.*?)\]/);
return {
title: titleMatch[1],
tags: tagsMatch
? tagsMatch[1].split(',').map((t) => t.trim().replace(/["']/g, ''))
: [],
content,
};
}
依存関係を減らすため、シンプルな実装とした。
Markdown→HTML 変換
KaTeX と Prism.js を統合した変換処理。
const MarkdownIt = require('markdown-it');
const mk = require('markdown-it-katex');
const Prism = require('prismjs');
function markdownToHtml(markdown) {
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: false,
highlight: function (str, lang) {
if (lang && Prism.languages[lang]) {
return `<pre class="language-${lang}"><code class="language-${lang}">
${Prism.highlight(str, Prism.languages[lang], lang)}
</code></pre>`;
}
return `<pre><code>${md.utils.escapeHtml(str)}</code></pre>`;
},
});
md.use(mk); // KaTeX有効化
return md.render(markdown);
}
KaTeX の CSS は CDN 経由で読み込む前提とし、HTML 内に埋め込んだ。
Blogger API 投稿
既存記事の更新と新規投稿を統一的に処理する。
async function publishOrUpdate({ title, content, labels = [], postId = null }) {
const config = await loadConfig();
const auth = await createAuthClient(config);
const blogger = google.blogger({ version: 'v3', auth });
const post = { kind: 'blogger#post', title, content, labels };
if (postId) {
// 更新
const response = await blogger.posts.update({
blogId: config.blogger.blogId,
postId,
resource: post,
});
return response.data.id;
} else {
// 新規投稿(下書き)
const response = await blogger.posts.insert({
blogId: config.blogger.blogId,
resource: post,
isDraft: true,
});
return response.data.id;
}
}
メインスクリプト
Gist 経由とローカル実行の両方に対応する。
async function publishFromGist(gistUrl) {
// 1. Gist取得
const { gistId, filename, content } = await fetchGist(gistUrl);
// 2. 既存記事を検索(meta.jsonからgistIdで検索)
const existingArticlePath = await findArticleByGistId(gistId);
const existingMetadata = existingArticlePath
? await loadMetadata(existingArticlePath)
: null;
// 3. Frontmatter解析
const { title, tags, content: markdownContent } = parseFrontmatter(content);
// 4. HTML変換
const html = markdownToHtml(markdownContent);
// 5. Blogger投稿または更新
const postId = existingMetadata?.bloggerPostId || null;
const bloggerPostId = await publishOrUpdate({
title,
content: html,
labels: tags,
postId,
});
// 6. 記事ファイル保存
// 7. meta.json保存
}
GitHub Actions
手動トリガーで Gist URL を入力する形式とした。
name: Gist to Blogger
on:
workflow_dispatch:
inputs:
gist_url:
description: 'Gist URL'
required: true
type: string
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- env:
BLOGGER_CLIENT_ID: ${{ secrets.BLOGGER_CLIENT_ID }}
BLOGGER_CLIENT_SECRET: ${{ secrets.BLOGGER_CLIENT_SECRET }}
BLOGGER_BLOG_ID: ${{ secrets.BLOGGER_BLOG_ID }}
BLOGGER_TOKEN: ${{ secrets.BLOGGER_TOKEN }}
run: |
echo '${{ secrets.BLOGGER_TOKEN }}' > blogger-token.json
node scripts/publish-article.js --gist-url "${{ inputs.gist_url }}"
- run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add articles/
git diff --quiet && git diff --staged --quiet || (git commit -m "記事投稿: Gist経由 [skip ci]" && git push)
GitHub Secrets に以下を設定する必要がある:
BLOGGER_CLIENT_IDBLOGGER_CLIENT_SECRETBLOGGER_BLOG_IDBLOGGER_TOKEN(blogger-token.json の内容を JSON 文字列として)
使い方
ローカル実行
npm run publish-from-local articles/20251010_test-article.md
Gist 経由(GitHub Actions)
- Gist で記事作成(Frontmatter 形式)
- GitHub Actions → "Gist to Blogger" → "Run workflow"
- Gist URL を入力して実行
まとめ
- WSL 起動不要: Gist 経由ならブラウザだけで記事投稿が完結
- 柔軟な執筆環境: ローカル(Claude Code 使用)と Gist の両方に対応
- 既存記事の更新: meta.json で記事を管理し、再実行で更新
- 数式・コード対応: KaTeX + Prism.js でリッチな記事を作成
ということで、だいぶラクできる環境を作れたかなと思います。
0 件のコメント:
コメントを投稿