0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiita CLI と Zenn CLI を完全同期させる二重投稿・記事ID相互連携パブリッシュパイプライン

0
Posted at

はじめに

技術記事を執筆して発信する際、国内の2大プラットフォームである ZennQiita の両方に同じコンテンツを展開したいというニーズは多く存在します。

しかし、両プラットフォームは記事の管理形式(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 から割り当てられた idupdated_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大エラーとその回避手法です。

  1. Cloudflare WAF 403 Forbidden:
    • 原因: 短時間に多数の API リクエストを発行した場合や、特定ユーザーエージェントが遮断される。
    • 対策: スクリプト内でリクエスト間に 1〜2秒のディレイ(sleep)を挿入し、バッチ処理化する。
  2. Title 422 Unprocessable Entity:
    • 原因: Qiita のタイトル長制限(100文字超)や、特殊文字のバックスラッシュエスケープ不全。
    • 対策: 変換スクリプト側で title.substring(0, 99) による切り詰めと文字列のサニタイズ処理を実装する。

5. まとめ

  1. マスターの一元化: 記事の執筆・修正は常に Zenn 形式の articles/*.md のみで行い、二重管理のコストをゼロにしました。
  2. 完全自動パブリッシュ: Git Push 1つで Zenn と Qiita の両方に同時展開され、Qiita 側の発行 ID もリポジトリ側へ全自動で還元・保存されます。
  3. CI/CD の堅牢性: GitHub Actions 内でメタデータの変換とエラーハンドリングを完結させることで、配信プラットフォームの仕様変更にも柔軟に対応できます。

マルチプラットフォームへの技術発信を効率化したい方は、本パイプラインをぜひ導入してみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?