本文へスキップ
キャスターアプリケーションズ

ブログ更新を X に自動投稿する - リンク付きのリプライを投下

hero

このブログを更新したら、その告知を X(旧 Twitter)に自動で投稿する。そんな仕組みを作りました。

きっかけは単純で、毎回手でやるのが面倒だったからです。本記事は、その実装の記録です。あわせて、作っていく中で「配信に関する情報をどこに置くか」を考えた結果、フロントマターにすべて寄せる設計になった話も書きます。

手動投稿をやめたかった

これまで、ブログ記事を公開したら X にも手で投稿していました。記事のリンクを貼って、ひとこと添えて、ポストする。作業自体は1分もかかりません。

ただ、1分の作業でも「やる」「やらない」の判断が毎回挟まると、そこそこの確率で抜けます。実際、公開したのに X に流し忘れた記事が何本かありました。書いた本人が告知を忘れる、というのは地味に間抜けです。

書くことと配信することは、別の作業です。書くことには集中したい。でも配信は、決まった手順をなぞるだけの作業で、人間がやる必然性がありません。これは機械にやらせる。そう決めて作り始めました。

3つの設計判断

仕組みを作るにあたって、最初に決めたことが3つあります。

投稿文は自分で書く

まず、X に流す投稿文を AI に生成させるか、手で書くか。手で書くことにしました。

記事本文から要約を生成するのは技術的には簡単です。でも、X の投稿文は記事の要約ではありません。「これを読んでほしい」という、その記事に対する自分の温度がそのまま出る場所です。そこを機械の生成文にすると、すべての告知が同じ顔になります。

自動化したいのは「配信の手順」であって「何を言うか」ではない。ここは分けました。投稿文だけは手書きで、あとは全部自動。

リンクはリプライに分ける

次に、記事リンクの置き方。投稿本文に URL を入れず、本文を投稿したあと、そのリプライとして URL を別ツイートで付ける形にしました。

X は、外部リンクを含むポストの表示が伸びにくい、という傾向が知られています。真偽はさておき、リンクを本文から外してリプライに逃がすのは、X 上でよく使われる定番の運用です。それに倣いました。

本文ツイートは言いたいことだけ、リンクはその下にぶら下げる。スレッドの形としても素直です。

配信設定はフロントマターに集める

3つ目。これが一番こだわったところです。

この仕組みでは、X 投稿に必要な情報を、記事の Markdown ファイル先頭にあるフロントマター(YAML のメタ情報)に書きます。投稿文も、リンクをリプライに付けるかどうかのフラグも、すべてそこです。

x:
  main: |
    ブログ更新を X に自動投稿する - リンク付きのリプライを投下
    (中略)
  reply_url: true

なぜ別ファイルや外部サービスにしなかったか。記事という1つの対象について、本文・配信設定・配信状態がバラバラの場所にあると、どれが最新か分からなくなるからです。

1記事1ファイル。本文も、X にどう流すかも、同じ index.md の中にある。これを記事に関する Single Source of Truth(信頼できる唯一の情報源)として扱う、というのが設計の軸です。

しかも投稿が完了すると、スクリプトが同じフロントマターに投稿日時とツイート ID を書き戻します。

x:
  posted_at: "2026-05-18T10:30:00Z"
  tweet_id: "1234567890"
  main: |
    (略)
  reply_url: true

投稿済みかどうかも、このファイルを見れば分かる。そしてファイルは Git で管理されているので、いつ・何が・どう配信されたかは、すべてコミット履歴に残ります。配信ログのための仕組みを別に用意しなくても、リポジトリがそのまま記録になっています。

実装

全体の流れ

仕組みはこう動きます。

flowchart LR
  author(["記事を書く人"])

  subgraph repo ["本体リポジトリ(GitHub)"]
    md["index.md(本文 + x.main)"]
  end

  subgraph ga ["GitHub Actions"]
    direction TB
    scan["記事を走査"]
    post["X に投稿"]
    back["posted_at を書き戻してコミット"]
    scan --> post --> back
  end

  x(["X"])

  author -->|push| md
  md -->|main の更新を検知| scan
  post -->|x.main / リプライ| x
  back -.->|"[skip ci] コミット"| md

記事を書いて本体リポジトリの main ブランチに push する。すると GitHub Actions が動き、記事を走査して、未投稿のものを X に投稿する。投稿が終わったら、フロントマターに投稿状態を書き足してコミットする。これだけです。

X への投稿には twitter-api-v2 という Node.js 向けのライブラリを使いました。X API を直接叩くより、認証まわりが楽です。

投稿スクリプト

投稿処理の本体は scripts/publish-to-x.mjs という1ファイルです。やっていることは、

  1. src/content/blog/ 配下の記事を全部見る
  2. x.main があって、まだ posted_at が無い記事を選ぶ
  3. x.main をそのまま本文として投稿する
  4. reply_urltrue なら、記事 URL をリプライで投稿する
  5. 投稿できたら、フロントマターに posted_attweet_id を書き戻す

コードにすると、投稿の中心はこのくらいの分量です。

const main = await client.v2.tweet(x.main);

if (x.reply_url) {
  await client.v2.tweet(`記事はこちら\n${articleUrl}`, {
    reply: { in_reply_to_tweet_id: main.data.id },
  });
}

posted_at を見て未投稿の記事だけ選ぶので、同じ記事が二重投稿されることはありません。一度投稿された記事は、次から自動的にスキップされます。

ローカルで試すとき用に --dry-run オプションも付けました。これを付けると、実際には投稿せず「何を投稿しようとしているか」だけを表示します。……付けたのに、後で自分が忘れるのですが、その話は後述します。

GitHub Actions と、Workers Builds との同居

配信のトリガーは GitHub Actions です。main ブランチへの push のうち、ブログ記事(src/content/blog/)が変わったときだけワークフローを起動します。

ここで1つ気にしたのが、このサイトのデプロイ方法との干渉です。本サイトは Cloudflare の Workers Builds でホストしていて、main に push されるたびに自動でビルド・デプロイが走ります(前回の移管記事に書いたとおりです)。

問題は、X 投稿のワークフロー自身もコミットを作る点です。投稿後にフロントマターへ posted_at を書き戻し、それをコミット & push する。するとその push がまた main への変更になり、ワークフローがもう一度起動し……と、放っておくと無限ループになりかねません。

対策は定番で、ワークフローが作るコミットのメッセージに [skip ci] を入れます。GitHub Actions はこの文字列を含む push を無視するので、自分のコミットで自分が再起動することはなくなります。

chore: update post status [skip ci]

なお [skip ci] が効くのは GitHub Actions に対してだけです。Cloudflare 側のビルドや、別途連携している Zenn への同期は、この文字列とは無関係に動きます。「CI のループだけ止めて、ほかの連携には手を出さない」――この切り分けを意識して設計しました。

ハマったところ

今回ハマったところはそんなにありませんでしたが、ひとつだけ ――

誤投稿事件

ローカルで動作確認をしていたときのことです。

前述のとおり、安全のために --dry-run オプションを用意してありました。投稿せずに内容だけ確認するためのものです。それを付け忘れて、本番の投稿コマンドをそのまま実行しました。

X に、テスト用の文言がそのまま流れました。しかもリプライ付きの設計なので、本文とリプライで2ツイート。慌てて両方削除しました。

ただ、結果として、これは一番確実な動作確認になりました。本文の投稿、リプライのぶら下げ、投稿後のフロントマターへの posted_at 書き戻し――一連の流れが本番環境で通しで動くことが、否応なく確認できたわけです。フロントマターにちゃんと tweet_id が書き戻されているのを見て、「あ、ちゃんと動いてる」と。

教訓は2つ。危険な操作には dry-run を用意すること。そして、用意しただけでは忘れる、ということです。

この記事自体が、この仕組みで配信されている

ここまで読んでいただいて、たぶんお気づきのとおりです。

この記事は、今まさに説明してきた仕組みで X に配信されています。この index.md のフロントマターには x.main が書いてあり、その文章がそのまま X に流れました。リンクは、その下のリプライに付いているはずです。

仕組みを説明する記事が、その仕組みで配信される。書いていて少し不思議な感覚でしたが、これ以上ない動作確認でもあります。もしあなたが X 経由でこの記事に来ているなら、それが証拠です。

次は、はてなブログ

X への自動投稿ができたので、次ははてなブログです。

このブログの記事を、はてなブログにも自動で展開したい。Zenn へのクロスポストはすでに動いているので、これができれば、1回 push するだけで本体サイト・Zenn・はてな・X の4箇所に配信される状態になります。

書く場所は1つ、届く場所は複数。配信は機械に任せて、人間は書くことに専念する。その形に、少しずつ近づけていきます。


当社では、こうした「手作業で回している定型業務を、仕組みで自動化する」開発をお手伝いしています。今回のような小さな自動化から、業務システムの開発・保守まで、長期運用を前提とした設計でご支援します。お問い合わせフォーム、または LINE 公式アカウントからお気軽にどうぞ。