Zennの記事をGitHub連携で管理する|公開済み記事を壊さず移す手順

「Zenn GitHub Sync」「Migrate Without Breaking It」の文字が入ったアイキャッチ。左のWeb Editorから伸びた矢印が、ハートのバッジが付いたままのArticleカードを運び、右のRepositoryのブロックへ入っていく 環境構築

はじめに

Zennの記事をブラウザのエディタではなく、手元のエディタとGitで書きたいと考えました。GitHubリポジトリと連携すると、pushするだけでZennに反映されます。

本記事のまとめです。

  • 連携はZennのダッシュボードからGitHub Appを入れるだけで終わる。リポジトリ側に設定ファイルを置く必要はない
  • 連携できるリポジトリは最大2つ。GitHub Appの画面は初期状態で全リポジトリを選んでいるので、必ず切り替える
  • 公開済みの記事は、slugを変えなければ上書きになる。URL・いいね数・公開日はそのまま残る
  • 捨て記事でデプロイ経路を確かめてから、公開済みの記事を取り込む。順番を逆にすると公開中の記事を壊しうる
  • 私がつまずいたのは2箇所で、GitHub Appの選択範囲とGitHub側だけ入ってZennが未連携のまま残ったことだった

公開中の記事1本を、URLといいね数を保ったままGit管理下へ移すまでに確かめたことを書きます。

ZennのGitHub連携画面。タブが「デプロイ履歴」と「リポジトリ設定」に変わり、「最近のデプロイ / まだデプロイはありません」と表示されている
この記事を読み終えると到達する画面。連携が終わると「リポジトリを連携する」ボタンが消え、デプロイ履歴とリポジトリ設定のタブに変わります

連携の全体像と、先に知っておく制約

作業はGitHub側・Zenn側・手元の3つに分かれます。それぞれで何をするかは次のとおりです。

  1. GitHub側: 記事を置くリポジトリを1つ作る
  2. Zenn側: ダッシュボードからGitHub Appを入れ、そのリポジトリを連携する
  3. 手元: リポジトリをcloneし、zenn-cliを入れて記事を書く

私はリポジトリをTerraformで作りましたが、この記事では触れません。GitHubの画面から作っても結果は同じです。

手を動かす前に、ZennとGitHubリポジトリの連携に書かれている制約を4つ確かめておきます。

制約影響
連携できるリポジトリは最大2つ3つ以上を選ぶと連携そのものが失敗する
articles/ と books/ はリポジトリのルートに置く既存リポジトリのサブディレクトリに同居させられない。専用リポジトリが要る
投稿の同一性はslugで決まるslugが同じなら上書き、変えると別記事が増える
投稿の削除はダッシュボードからのみGitHub側でファイルを消しても次回デプロイで復活する

slugは記事のURLの末尾に入る文字列で、12〜50字、使える文字はa-z0-9とハイフン・アンダースコアだけという決まりがあります。ブラウザのエディタで書いた記事には自動でslugが振られますが、その自動slugがこの規則を満たしているとは限りません。移行の前に確かめてください。

環境の前提も1つあります。記事を書くためのコマンドラインツールであるzenn-cliは、engines.nodeが>=22.12.0です(2026年11月に確認)。参考記事でよく見かける「Node.js 14以上」はこれより古い情報なので、気をつけてください。

「GitHubに遷移します」のダイアログ。「[Only select repositories]にチェックを入れ、連携するリポジトリを選んでください。最大2つまで連携できます」と書かれている
連携を始めるときに出る確認ダイアログ。最大2つという上限は、ここでZenn自身が警告しています

ZennとGitHubを連携する

連携の操作はブラウザだけで終わり、3分もかかりません。ダッシュボードの「GitHub連携」を開き、「リポジトリを連携する」を押すと、GitHub Appのインストール画面に移ります。

「All repositories」のまま進めない

インストール画面は、開いた時点で「All repositories」が選ばれています。そのままInstall & Authorizeを押すと全リポジトリが連携の対象になり、3つ以上あれば上限を超えて連携が失敗します。「Only select repositories」に切り替えて、記事を置くリポジトリだけを選んでください。

切り替えるとSelected 1 repository.と表示され、選んだリポジトリ名が下に出ます。要求される権限はRead access to code, metadata, and pull requestsだけです。

Zenn ConnectのInstall & Authorize画面。Only select repositoriesが選択され、Selected 1 repository.としてzenn-contentsだけが指定されている。権限はRead access to code, metadata, and pull requestsのみ
切り替えたあとの正しい状態。上の「All repositories」が初期選択なので、下の「Only select repositories」を選び直します

デプロイ対象ブランチを確かめる

連携が済むと、ダッシュボードのタブが「デプロイ履歴」と「リポジトリ設定」に変わります。リポジトリ設定を開いて、どのブランチへのpushでデプロイが走るかを確かめてください。私の環境では次のようになっていました。

項目値
連携状態1つのリポジトリと連携中
デプロイ対象ブランチmain
リポジトリへのリンク表示しない
プルリクエストで下書きデプロイ無効

私はmainへのpushで運用しています。プルリクエストで下書きをデプロイする設定もありますが、有効にしていません。

GitHub側は入ったのにZennが未連携のままになる

GitHub Appのインストールが成功しても、Zennのダッシュボードが未連携のままになることがあります。私は1回目の試行でこれを踏みました。ダッシュボードを開いても「リポジトリを連携する」ボタンが出続け、GitHub側ではアプリが入っている、という状態です。

このとき私がGitHub Appを入れたブラウザのプロファイルは、Zennにログインしていませんでした。インストール画面の最下部には、遷移先としてhttps://zenn.dev/api/github_app/install/callbackが書かれています。戻り先がZennなので、ログインしていないブラウザでは誰の連携なのかを記録できなかったのだと考えています。ただし試したのは1回だけで、ログイン状態だけを変えて確かめたわけではないので、これは推測です。

対処は、GitHubの設定からZenn Connectを一度アンインストールし、Zennにログイン済みのブラウザで最初から通し直すことです。アンインストールしてやり直す点は、Zennもダッシュボードの下部で案内しています。私はこれで解決しました。

デプロイが走らないときは、GitHubの Settings → Applications → Installed GitHub Apps を開きます。Zenn ConnectのRepository accessが、目的のリポジトリを含んでいるかを見てください。この項目は目で見るしかありません。GitHub CLIで確かめようとすると、GitHub App用のトークンではないため403が返ります。

You must authenticate with an access token authorized to a GitHub App
ZennのGitHub連携画面。「リポジトリを連携する」ボタンだけが出ており、下部にGitHubのアプリ一覧からZenn Connectの連携を解除したうえで再度連携を行うよう案内がある
この画面がインストール後も出続けたら、Zenn側に記録されていません。下部の案内どおり、アンインストールからやり直します

zenn-cliを入れて、捨て記事でデプロイ経路を確かめる

連携が済んでも、公開済みの記事をいきなりpushしてはいけません。frontmatterの書き方を1つ間違えるだけで、公開中の記事のタイトルや公開日が変わります。先に捨て記事を1本流して、デプロイが走ることだけを確かめます。

リポジトリをcloneして、zenn-cliを入れます。

git clone https://github.com/<ユーザー名>/zenn-contents.git
cd zenn-contents
npm init --yes
npm install zenn-cli
npx zenn init

npx zenn initがarticles/とbooks/をリポジトリのルートに作ります。この2つはルートに置く決まりなので、サブディレクトリへ寄せないでください。

ここでnpm auditがmoderateを5件返しました。すべてzenn-cliが間接的に依存しているパッケージなので、こちら側では直せません。npm audit fix --forceはzenn-cliを古い版へ戻す変更になるため、実行していません。

捨て記事を1本作り、ローカルで見てからpushします。

npx zenn new:article --slug zenn-github-deploy-smoke-test --title "連携確認" --type tech
npx zenn preview   # http://localhost:8000
git add . && git commit -m "Set up zenn-cli and add a deploy smoke test draft"
git push origin main

frontmatterのpublishedはfalseのままにしておきます。こうしておけばデプロイは走りますが、記事は公開されません。

---
title: "連携確認"
emoji: "🔧"
type: "tech"
topics: []
published: false
---

pushしたら、デプロイ履歴に成功が1件増え、投稿管理に下書きバッジ付きで記事が出ることを確かめます。公開されていないことまで見てください。

localhost:8000で開いたZenn Editorの画面。左のサイドバーにarticlesとbooksのフォルダがルート直下に並び、articlesの下に記事が1本入っている
npx zenn previewで開いたローカルプレビュー。左のサイドバーでarticlesとbooksがルートにあることと、記事が読めることを確認できます。この画面は移行後に撮ったものなので、捨て記事ではなく移行した記事が写っています

公開済みの記事をGit管理下に移す

ブラウザのエディタで書いた記事は、markdownで書き出してリポジトリに置き直します。書き出し口は2つあり、移行する本数で使い分けます。

  • 1本だけなら記事単位。記事の管理で対象記事の「⌄」を開き、「内容をmarkdownで出力」を選ぶ
  • 全部まとめてならアカウント設定。設定画面の最下部にある「データのエクスポート」からzipで落とす

私は1本だけだったので記事単位で取りました。「⌄」のメニューには「削除する」が隣接しているので、押し間違えないでください。

公開APIでは代用できませんでした。記事の情報はログインなしで読めますが、返るのは組版済みのHTMLだけで、markdownの原文は含まれていません。

curl -s "https://zenn.dev/api/articles?username=<ユーザー名>&order=latest"
curl -s "https://zenn.dev/api/articles/<slug>?username=<ユーザー名>"

HTMLからmarkdownへ戻すと、リンクカードやZenn独自の記法が壊れます。書き出しはダッシュボードから行ってください。ただしこのAPIは、移行の前後で記事が変わっていないかを照合するのには使えます。

書き出したファイルはarticles/<slug>.mdとして置きます。ファイル名をslugに一致させ、どちらも変えないでください。変えると上書きではなく別記事として増え、いいねが付いた元の記事がそのまま残ります。置いたらfrontmatterを開いて3点を見ます。

  • title / emoji / type / topics が公開中の記事と一致している
  • published が true になっている
  • published_at に元の公開日時が入っている

3つ目は落とすと公開日が変わります。過去日時は一度設定すると変更できないので、空のままpushしないでください。私の場合は書き出した内容に最初から入っており、手で足す必要はありませんでした。

npx zenn previewで表示が崩れていないことを見てから、pushします。

git add articles/<slug>.md
git commit -m "Import the existing Zenn article into Git management"
git push origin main

push後に公開APIで前後を照合した結果です。URL・いいね数・公開日は変わらず、記事の由来だけがGitHub側に切り替わりました。

項目push前push後
記事のパス変更なし同じ
liked_count33
published_at2025-02-16T15:32:58+09:00同じ
source_repo_updated_atnullpushした時刻
body_letters_count74337434

最後の行だけ、字数が1増えています。ローカルの表示と公開中の表示を288行すべて比べたところ、目に見えるテキストは完全に一致していました。改行などの見えない文字が1つ増えただけだと考えられますが、push前のHTMLを控えていなかったため特定できませんでした。次に記事を移すときは、pushの前に公開中のHTMLを保存してから作業します。

Zennの記事の管理に並んだ記事の行。「公開中」のバッジ、23時間前に本文更新・7434字・いいね3件、その下に articles/21e57cafe2e8d1.md と23時間前に同期と表示されている
移行後の記事の管理。公開中のまま7,434字といいね3件が残り、リポジトリのファイルと同期していることが1行で読めます

画像はリポジトリに持ち込まなくてよい

ブラウザのエディタでアップロードした画像は、markdownの中では絶対URLとして書かれています。リポジトリ側に画像ファイルを持ち込む必要はありませんし、書き出したファイルをそのまま置いてもリンクは切れません。

ただしmarkdownの原文に書かれているホストと、公開ページのHTMLに出てくるホストは違います。Zennが表示するときに書き換えているためです。移行の前後で画像を突き合わせるときは、ホスト名ではなくファイル名で照合してください。

捨て記事はGitHubとダッシュボードの両方から消す

リポジトリからファイルを消しても、Zennの投稿は残ります。私はファイルを消してpushしたあともダッシュボードに下書きが残っているのを見て、制約に書かれていたとおりだと確かめました。

完全に消すには、記事の管理から投稿を削除し、リポジトリからもファイルを消します。片方だけでは消えません。

まとめ

順番さえ守れば、公開済みの記事はURL・いいね数・公開日を保ったままGit管理下へ移せます。連携したあとは、mainへのpushだけでZennに反映されます。

気をつける点を並べておきます。

  • GitHub Appは「Only select repositories」に切り替える。初期状態は全リポジトリで、上限の2つを超えると連携が失敗する
  • インストールは、Zennにログイン済みのブラウザで通す。未ログインのブラウザで進めたときは、GitHub側だけ入ってZennが未連携のまま残った
  • 捨て記事でデプロイ経路を確かめてから、公開済みの記事を取り込む
  • slugとファイル名を変えない。変えると上書きではなく別記事が増える
  • 投稿を消すときは、ダッシュボードとリポジトリの両方から消す
  • 移行の前に、公開中のHTMLを控えておく

次の入口はZenn CLIをインストールするとZenn CLIで記事・本を管理するです。途中から連携を始める手順はZennで途中からGitHubリポジトリ連携をはじめるときの手順にまとまっており、ツールの中身はzenn-editorで読めます。

なお、ダッシュボードの画面構成、zenn-cliのバージョン要件、連携できるリポジトリ数の上限は変わります。実際に設定するときは、最新を公式ドキュメントで確認してください。