Zennに14本の技術記事がたまった。読まれてはいるけど、正直もったいない。Qiitaやnote.comにも出せば、もっと多くの人に届くはず——そう思いつつ、1本ずつ手作業で変換するのは現実的じゃない。
そこで GitHub Copilot CLI のカスタムスキル(cross-post-publisher)を作り、14本の記事を 28ファイル自動生成 → Qiita APIで14本一括投稿 まで一気にやった。この記事では、スキル設計から実行、途中で踏んだ地雷まで全部書く。
やりたかったこと
Zennに書いた記事を、Qiita と note.com にも展開したい。ただし、コピペではダメで、各プラットフォームに合わせた変換が必要になる。
| 項目 | Zenn | Qiita | note.com |
|---|---|---|---|
| 読者層 | エンジニア | エンジニア | 非エンジニア含む |
| 記法 | 独自(:::message等) | 独自(:::note等) | Markdown(制限あり) |
| コードブロック | ✅ そのまま | ✅ そのまま | ❌ 非エンジニア向けに削除 |
| テーブル | ✅ パイプ | ✅ パイプ | ❌ 非対応 → 箇条書きに |
| 投稿方法 | Git push | API or Web | Web手動 |
ポイントは、Qiita向けはフロントマターとZenn独自記法の変換だけでいいが、note.com向けはコードブロックを削除して日本語の説明に置き換える必要があること。つまり、2種類の変換ロジックが必要になる。
cross-post-publisher スキルを作る
Copilot CLIには「スキル」というカスタム指示書の仕組みがある。.github/skills/にMarkdownファイルを置くと、AIがその指示に従って作業してくれる。
今回作ったcross-post-publisherスキルの中身を簡単にまとめると、こんな構成になっている。
Qiita変換ルール
フロントマターの変換が地味にややこしい。
## Zenn(変換前)
---
title: "記事タイトル"
emoji: "🔧"
type: "tech"
topics: ["csharp", "dotnet", "blazor"]
published: true
---
## Qiita(変換後)
---
title: "記事タイトル"
tags:
- name: "csharp"
versions: []
- name: "dotnet"
versions: []
- name: "blazor"
versions: []
private: false
canonical_url: "https://zenn.dev/jodycraft/articles/{slug}"
---
ここでハマりポイントが2つある。
1つ目: tagsのversionsは空配列でも必須
Qiita APIに投稿するとき、versionsフィールドがないとバリデーションエラーになる。中身は空の[]でいいけど、キー自体が必要。スキルの指示書には CRITICAL マーカーをつけて明記した。
2つ目: canonical_urlを忘れると重複コンテンツ扱いされる
同じ内容をZennとQiitaの両方に公開するなら、canonical_urlでオリジナルの記事を指定しないとSEO的にまずい。これもスキルに組み込んだ。
note.com変換ルール
note.comは非エンジニアの読者も多い。だから技術記事をそのまま持っていっても読まれない。
スキルに書いた変換方針はこんな感じ:
- コードブロック → 削除して「何をしているか」を日本語で説明
- 技術用語 → 補足を追加(例: ClosedXML → 「Excelをプログラムから操作できる無料ツール」)
- テーブル → 箇条書きに変換(noteはパイプテーブル非対応)
- タイトル → 問いかけ型に変更(「C# ClosedXMLで〜」→「Excelの帳票作成、まだ手作業ですか?」)
- ビジネス価値を前面に出す(時間削減・コスト削減の数字を強調)
実際に変換されたnote向けタイトルの例を1つ出すと:
| Zennタイトル | noteタイトル |
|---|---|
| Buffer GraphQL APIでSNS自動投稿パイプラインを構築した話 | SNS投稿、まだ手作業でやっていますか? — 記事を書いたらInstagram・X・YouTubeに自動で投稿される仕組みを作った話 |
読者に刺さるのはどちらか、一目瞭然だと思う。
14本を一括変換する
スキルができたので、実行する。14本を1本ずつ変換していたら時間がかかるので、4つの並列エージェントに分割して処理した。
エージェント1: 4本(excel-vba, blazor, buffer×2)
エージェント2: 4本(closedxml, copilot-cli, ecc-vs, skill-creator)
エージェント3: 3本(remotion, winforms, cowork-01)
エージェント4: 3本(cowork-02, cowork-03, cowork-04)
4エージェントが並列で動いて、約10分で28ファイル(Qiita 14本 + note 14本)が生成完了。手作業なら1本あたり30分としても7時間はかかる作業が、スキル設計込みでも1時間程度で終わった。
Qiita APIで一括投稿
変換されたMarkdownをQiita APIで投稿する。エンドポイントはシンプル。
## 記事作成
POST https://qiita.com/api/v2/items
## 記事更新
PATCH https://qiita.com/api/v2/items/{id}
リクエストボディのJSONはこんな形式になる。
{
"title": "記事タイトル",
"body": "本文のMarkdown",
"tags": [
{"name": "csharp", "versions": []},
{"name": "dotnet", "versions": []}
],
"private": false,
"canonical_url": "https://zenn.dev/jodycraft/articles/slug"
}
14本をまとめてPOSTした。レート制限(1,000リクエスト/時間)があるので、1秒のインターバルを入れて投稿。結果は以下の通り。
| 区分 | 本数 | 理由 |
|---|---|---|
| 公開(public) | 6本 | Zennで公開済みの記事 |
| 限定公開(private) | 8本 | Zennで未公開の記事(先にZennで公開してから切り替える想定) |
14本すべてエラーなしで投稿完了。やったと思った。
画像URLの罠
ところが、投稿後にQiitaで記事を確認すると、画像が全部404エラーで表示されない記事があった。
原因は単純で、Zenn記事内の画像パスが /images/xxx.png という相対パスで書かれていたから。Zennではzenn-contentリポジトリのimages/フォルダに画像を置けるが、このリポジトリがプライベートだと、外部からアクセスできない。
最初は raw.githubusercontent.com のURLに置き換えてみたが、プライベートリポジトリなのでやっぱり404。
結局、Zennの公開済みページのHTMLソースから、Zenn CDNのURLを取得して置き換えた。
## Zenn CDNのURLはこういう形式
https://storage.googleapis.com/zenn-user-upload/deployed-images/{hash}.png?sha={sha}
これをQiita記事にPATCH APIで反映して、ようやく画像が表示された。
ここから得た教訓:画像を含む記事をクロスポストするなら、最初からZennの画像アップローダー経由でCDN URLを使うべき。リポジトリの/images/に置く方式は、クロスポスト時に面倒なことになる。
スキルの品質を評価する
作ったスキルがちゃんと動くか、22パターンのテストケースで評価した。
テストケースの例:
- Zenn記事のフロントマターが正しくQiita形式に変換されるか
-
:::messageが:::note infoに変換されるか - note.com向けにコードブロックが削除されて日本語説明になるか
- canonical_urlが正しく設定されるか
- tagsのversions配列が含まれるか
結果:22/22テスト合格(100%)。
スキルに CRITICAL マーカーをつけた部分(versionsの必須、canonical_urlの追加)は、マーカーなしだと漏れることがあった。AIへの指示書は「強調の仕方」で結果が変わるというのは面白い発見だった。
最終的な成果
| 項目 | 数値 |
|---|---|
| 変換元の記事数 | 14本 |
| 生成ファイル数 | 28ファイル(Qiita 14 + note 14) |
| Qiita API投稿 | 14本(公開6 + 限定公開8) |
| 並列エージェント数 | 4 |
| 変換にかかった時間 | 約10分 |
| スキル評価スコア | 100%(22/22) |
| 手作業見積もり | 7時間以上 |
この仕組みを使い回すには
同じことをやりたい人向けに、スキル設計のポイントをまとめておく。
1. プラットフォーム差分をテーブルで整理する
最初にZenn/Qiita/noteの記法差分をテーブルにまとめてからスキルを書くと、漏れが減る。私は途中で差分に気づいて何度かスキルを修正したので、最初からやっておけばよかった。
2. CRITICALマーカーを使う
AIが見落としがちな重要ルール(versionsの必須、canonical_url)には、スキル内で CRITICAL という単語をつけて強調する。これだけで遵守率が目に見えて上がった。
3. 並列実行で時間を短縮する
14本を1本ずつ処理すると30分以上かかるが、4並列にすると10分で終わる。Copilot CLIではtaskツールで複数のバックグラウンドエージェントを起動できる。
4. APIレート制限を意識する
Qiita APIは1,000リクエスト/時間の制限がある。14本程度なら問題ないが、大量投稿するなら1秒以上のインターバルを入れること。
note.comは手動投稿になる
ここまで自動化した話を書いてきたが、note.comにはAPIがない(2026年3月時点)。生成された14本のMarkdownを、Web画面から1本ずつ手動で貼り付ける必要がある。
とはいえ、コンテンツの変換自体は自動化できているので、手動作業は「貼り付けて公開ボタンを押す」だけ。これは許容範囲だと思っている。
まとめ
Copilot CLIのカスタムスキルを使えば、Zenn記事を複数プラットフォームに展開する作業を大幅に効率化できる。
今回学んだこと:
- スキル設計が8割 — 変換ルールを正確にスキルに書けば、あとはAIが勝手にやってくれる
- CRITICALマーカーの効果 — AIへの指示書は「書き方」で結果が変わる
- 画像URLは最初から外部URLを使う — プライベートリポジトリの相対パスはクロスポスト時に詰む
- 並列処理で10倍速 — 14本を4並列で処理して約10分
コンテンツを1つのプラットフォームに閉じ込めておくのは機会損失。変換の手間をAIに任せて、自分は新しい記事を書くことに時間を使う——これが今の私のスタイルになった。
📝 この記事は Zenn で最初に公開されました。