はじめに
技術記事を執筆して発信する際、国内の2大プラットフォームである Zenn と Qiita の両方に同じコンテンツを展開したいというニーズは多く存在します。
しかし、両プラットフォームは記事の管理形式(Zenn は GitHub 直結の Markdown、Qiita は CLI や REST API / Frontmatter のフォーマット)が異なっており、手動でコピペ投稿・更新を行うと二重管理のコストや ID マッピングのずれが発生します。
本記事では、Zenn 向けリポジトリ(articles/*.md)をマスター(一次情報)とし、GitHub Actions と Node.js 変換スクリプトを用いて Qiita CLI フォーマットへの自動変換・二重投稿・記事ID双方向マッピング を実現する自動配信パイプラインのアーキテクチャを解説します。
1. パイプラインの全体アーキテクチャ
システム全体は、マスターリポジトリ(Zenn-for-me)への Git Commit & Push をトリガーとして自律的に動きます。
[ Master Source: Zenn-for-me / articles/*.md ]
│ (Zenn Frontmatter 形式で執筆)
▼
git push origin main
│
├─────────────────────────────────────────┐
▼ ▼
[ Zenn Dashboard (GitHub連携) ] [ GitHub Actions (.github/workflows/publish.yml) ]
│ │
▼ (自動デプロイ) ▼
[ Zenn.dev に公開 ] [ 1. sync-to-qiita.js 実行 ]
│ ・Zenn 形式 ──> Qiita 形式へ変換
│ ・public/*.md を更新
▼
[ 2. qiita-cli publish アクション ]
│ ・Qiita API 経由で投稿/更新
│ ・Qiita 上の article id を取得
▼
[ 3. Qiita ID 差分を git commit & push ]
2. Zenn 形式から Qiita 形式への自動変換ロジック
Zenn と Qiita では、Markdown 先頭の YAML Frontmatter プロパティが異なります。
| 項目 | Zenn 形式 (articles/*.md) |
Qiita 形式 (public/*.md) |
|---|---|---|
| タイトル | title: "タイトル" |
title: "タイトル" |
| タグ / トピック | topics: ["zenn", "python"] |
tags: ["zenn", "python"] (配列) |
| 公開状態 | published: true |
private: false, ignorePublish: false
|
| 記事ID | なし(ファイル名が slug) |
id: "123456789abcdef" (Qiita 固有) |
| 絵文字 / タイプ |
emoji: "🚀", type: "tech"
|
(Qiita には存在しないため無視) |
Node.js 変換スクリプト (sync-to-qiita.js) の実装例
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
const ARTICLES_DIR = path.join(__dirname, '../articles');
const PUBLIC_DIR = path.join(__dirname, '../public');
if (!fs.existsSync(PUBLIC_DIR)) {
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
}
fs.readdirSync(ARTICLES_DIR).forEach(file => {
if (!file.endsWith('.md')) return;
const articlePath = path.join(ARTICLES_DIR, file);
const publicPath = path.join(PUBLIC_DIR, file);
const { data: zennFrontmatter, content } = matter.read(articlePath);
// 既に Qiita 側の public/*.md が存在する場合は Qiita ID を引き継ぐ
let qiitaId = null;
if (fs.existsSync(publicPath)) {
const existingQiita = matter.read(publicPath);
qiitaId = existingQiita.data.id || null;
}
// Qiita 用 Frontmatter の構築
const qiitaFrontmatter = {
title: zennFrontmatter.title || '無題',
tags: (zennFrontmatter.topics || []).map(topic => ({ name: topic })),
private: false,
updated_at: new Date().toISOString(),
id: qiitaId,
ignorePublish: zennFrontmatter.published === false
};
const outputContent = matter.stringify(content, qiitaFrontmatter);
fs.writeFileSync(publicPath, outputContent, 'utf-8');
console.log(`[SYNC] ${file} -> Qiita 形式へ変換完了`);
});
3. GitHub Actions による完全自動デプロイと ID 還元
GitHub Actions ワークフローでは、変換処理の実行、Qiita CLI によるパブリッシュ、そして Qiita から割り当てられた id や updated_at の差分コミットまでを一括で自動化します。
ワークフロー定義 (.github/workflows/publish.yml)
name: Publish to Qiita
on:
push:
branches:
- main
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Sync Zenn articles to Qiita format
run: node scripts/sync-to-qiita.js
- name: Publish articles to Qiita
uses: increments/qiita-cli/actions/publish@v1
with:
qiita-token: ${{ secrets.QIITA_TOKEN }}
- name: Commit and push updated Qiita IDs
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add public/
if ! git diff --staged --quiet; then
git commit -m "chore: sync Qiita article IDs and metadata [skip ci]"
git push origin main
fi
4. トラブルシューティング: WAF 403 および Title 422 エラー対策
パイプライン運用時に直面しやすい 2大エラーとその回避手法です。
-
Cloudflare WAF 403 Forbidden:
- 原因: 短時間に多数の API リクエストを発行した場合や、特定ユーザーエージェントが遮断される。
- 対策: スクリプト内でリクエスト間に 1〜2秒のディレイ(
sleep)を挿入し、バッチ処理化する。
-
Title 422 Unprocessable Entity:
- 原因: Qiita のタイトル長制限(100文字超)や、特殊文字のバックスラッシュエスケープ不全。
- 対策: 変換スクリプト側で
title.substring(0, 99)による切り詰めと文字列のサニタイズ処理を実装する。
5. まとめ
-
マスターの一元化: 記事の執筆・修正は常に Zenn 形式の
articles/*.mdのみで行い、二重管理のコストをゼロにしました。 - 完全自動パブリッシュ: Git Push 1つで Zenn と Qiita の両方に同時展開され、Qiita 側の発行 ID もリポジトリ側へ全自動で還元・保存されます。
- CI/CD の堅牢性: GitHub Actions 内でメタデータの変換とエラーハンドリングを完結させることで、配信プラットフォームの仕様変更にも柔軟に対応できます。
マルチプラットフォームへの技術発信を効率化したい方は、本パイプラインをぜひ導入してみてください。