この記事は Zenn にも同じ内容を投稿しています。
やりたいこと
これから Qiita と Zenn の両方に記事を投稿していきたい。
最初からソースを GitHub で管理しておきたいけど、プラットフォームごとにリポジトリを分けるのは面倒なので、ひとつのリポジトリにまとめて git push で両方に自動公開できる構成にする。
結論:両方ともリポジトリのルートに置けば共存できる
Zenn の GitHub 連携は リポジトリのルートに articles/ と books/ がある前提 で、サブディレクトリ指定はできません。
機能リクエストは zenn-community#645 で議論されていますが、現時点では未実装です。
一方で、Qiita CLI と Zenn CLI が使うディレクトリ・設定ファイルは互いにぶつかりません。
ひとつの package.json に両方の CLI を入れて、両者をルートに同居させれば OK です。
| Zenn | Qiita | |
|---|---|---|
| 記事 | articles/ |
public/ |
| 本 | books/ |
— |
| 設定 | (なし) |
.qiita/, qiita.config.json
|
| CLI | zenn-cli |
@qiita/qiita-cli |
完成形
qiita-zenn-content/
├── .github/workflows/publish.yml # Qiita 自動投稿
├── articles/ # Zenn 記事
├── books/ # Zenn 本
├── public/ # Qiita 記事
├── qiita.config.json
└── package.json
セットアップ手順
1. リポジトリ初期化と CLI のインストール
mkdir qiita-zenn-content && cd qiita-zenn-content
git init
npm init -y
npm install --save-dev zenn-cli @qiita/qiita-cli
2. Zenn / Qiita CLI を初期化
npx zenn init # articles/ books/ README.md を生成
npx qiita init # qiita.config.json と .github/workflows/publish.yml を生成
3. GitHub にプライベートリポジトリ作成 & push
gh repo create qiita-zenn-content --private --source=. --remote=origin
git add .
git commit -m "chore: initial setup"
git push -u origin main
4. プラットフォーム連携
- Zenn: ダッシュボード → Deploy 設定 で対象リポジトリを選択
-
Qiita: GitHub リポジトリの Settings → Secrets に
QIITA_TOKENを登録(トークン発行ページ)
これで main への push のたびに、Zenn は GitHub 連携で自動デプロイ、Qiita は GitHub Actions の publish.yml で自動投稿されます。
記事を書く
npx zenn new:article # articles/<slug>.md が生成される
npx zenn preview # http://localhost:8000
npx qiita new my-first-article # public/my-first-article.md が生成される
npx qiita preview # http://localhost:8888
フロントマターの published: true で公開、false で下書き保存になります(両方共通)。
同じ内容を Qiita と Zenn の両方に出したい場合
フロントマター形式が違うので、1ファイルで両方を兼ねることはできません。
articles/<slug>.md(Zenn)と public/<slug>.md(Qiita)を別々に置いて、本文をコピーします。
Markdown の方言に注意する
似た機能でも構文が違うので、片方の独自記法を使うともう片方で崩れます。
| 機能 | Zenn | Qiita |
|---|---|---|
| メッセージ |
:::message / :::message alert
|
:::note info / :::note warn
|
| 折りたたみ | :::details タイトル |
<details><summary> |
| コードのファイル名 | ```js:filename.js |
```js:filename.js(同じ) |
| 数式 | $$ ... $$ |
$$ ... $$(同じ) |
→ 両方に出す前提なら、標準 Markdown + ファイル名付きコードブロックに留める のが安全です。
画像はリポジトリの相対パスを使わない
Zenn は /images/... の絶対パス、Qiita は外部URLが必要で、両者で互換性のある参照方法がありません。
両方に出すなら、GitHub リポジトリの raw URL や Gyazo など 外部URLを  で直接参照 するのが手っ取り早いです。
重複コンテンツの扱い
Zenn・Qiita とも canonical URL の指定はサポートされていないので、SEO 的に「正」を伝える手段は本文で示すしかありません。
慣例的には、後から出した方の冒頭にこう書いておきます。
> この記事は [Zenn にも同じ内容を投稿しています](https://zenn.dev/...).
運用フロー
- まず Zenn 用 (
articles/<slug>.md) を書いて公開、URL を確定 - 本文をコピーして
public/<slug>.mdを作成、冒頭にクロスリンクを追加 - 以降の修正は両方のファイルに反映
頻繁に両方出すようになったら、フロントマター差し替えと方言変換をする小さなスクリプトを用意するとラクになります。
ハマりポイント
gh の OAuth トークンに workflow スコープが無い
qiita init が生成する .github/workflows/publish.yml を含めて push しようとすると、こんなエラーで弾かれます。
! [remote rejected] main -> main (refusing to allow an OAuth App to create or
update workflow `.github/workflows/publish.yml` without `workflow` scope)
gh auth refresh -s workflow でスコープを追加すれば push できます。
qiita init が生成する Workflow は Zenn 更新時にも動く
.github/workflows/publish.yml のトリガーはデフォルトで push: main のみで、パス指定がありません。
Zenn 記事だけ更新した push でも GitHub Actions が起動し、Actions の無料枠を無駄に消費します(差分がなければ Qiita 側に変更は出ませんが、ジョブは走る)。
paths フィルタを足して、Qiita 関連が変わった時だけ起動するようにしておきます。
on:
push:
branches:
- main
- master
paths:
- 'public/**'
- 'qiita.config.json'
- '.github/workflows/publish.yml'
workflow_dispatch:
Zenn のサブディレクトリ対応はまだ無い
「Qiita と Zenn を別々のサブディレクトリに分けたい」という直感的な構成は、Zenn が未対応なので諦めるしかありません。
ルートに同居させる方が結果的にシンプルです。