Procedure
はてなブログ自動投稿(blogsync)
- インストール(Goが要る。macOSはソースビルドなのでGLIBC問題なし):
GOBIN=$HOME/.local/bin go install github.com/x-motemen/blogsync@latest - 設定
~/.config/blogsync/config.yaml(chmod 600。password=AtomPub APIキー=秘密):default: username: <はてなID> password: <AtomPub APIキー> <blog>.hatenablog.com: username: <はてなID> password: <AtomPub APIキー>- APIキー取得: はてなブログ管理画面 → 設定 → 詳細設定 → 「AtomPub」欄のAPIキー。はてなIDはエンドポイントURL内の名前。
- 秘密はチャットに出させない。
! open -e ~/.config/blogsync/config.yamlで本人に直接貼ってもらう(Stop hookで会話が知識ベースに取り込まれるため)。
- 疎通は読み取りで(書き込みなし):
blogsync -C /tmp/bs pull <blog>.hatenablog.com - 投稿: stdinに本文、タイトルは別指定。
- 下書き:
blogsync post --draft --title 'T' <blog> < body.md - 公開:
blogsync post --title 'T' <blog> < body.md(--draftを外すと公開) - 戻り 201 = 作成成功。下書きはレスポンスにPreviewURLが入る。
- 削除:
blogsync remove <localmirror.md>(テスト下書きの後始末に)
- 下書き:
- 本文1行目の
# タイトルはエントリTitleに使い、本文からは除く(H1重複回避)。
消化キュー型パイプラインとの統合(affiliate-factory型)
- Desktop等に「未投稿md数」を数えて3本に補充する方式なら、公開したmdはキューdirから外すこと(
published/へmv)。残すと翌日「足りてる」と誤認して生成が止まる。 - daily.shの末尾で
post-to-hatena.sh --publish --allを呼ぶ。config未設定なら誤爆せずスキップするガードを必ず入れる(grep -q REPLACE_ config && exit 0)。
Pitfalls
- 楽天アフィリリンクが本文に入らない最大の罠: LLMが本文に自前の検索URL
[楽天で「X」を探す](https://search.rakuten.co.jp/...)を書く。プレースホルダー置換だけだと実URLが残り報酬ゼロ。re.sub(r"\[楽天で「[^」]*」を探す\]\([^)]*\)", affiliate_link, body)で実リンクを丸ごと差し替える。 - 0件→非アフィリにフォールバックする罠: 楽天商品検索APIが0件/エラーでも、素の検索URLでなく**アフィリ計測付き検索リンク(hgc)**へ落とす:
https://hb.afl.rakuten.co.jp/hgc/<AFFILIATE_ID>/?pc=<enc検索URL>&m=<enc検索URL>(AFFILIATE_IDは5501af..xxxx.5501af..yyyy形式の35文字=そのままhgcパスに入る。リダイレクト先にscid=af_pc_etcが付けば計測OK)。 - NGKeywordを盛りすぎると0件になる。
中古 訳あり 美品程度に留め、アクセサリ除外はminPrice(本体5万円級なら30000)で行う。 - 一部ブランド語はAPIが弾く(例:
EcoFlow→keyword is not valid/HTTP400)。ポータブル電源等の一般語はOK。弾かれたらhgc検索リンクへフォールバックで報酬は維持できる。 - API叩きすぎで 429 Too Many Requests。検証は間隔を空け1回ずつ。
[:contents](目次記法)が引用ボックス内に吸い込まれ目次化されない罠: 免責は>のblockquote。直後に空行なしで[:contents]を置くと、Markdownの遅延継続(lazy continuation)で[:contents]が引用に取り込まれ、目次が出ずリテラル表示される。[title, "", disclaimer, "", contents]のように前後を空行で分離する。既存記事の一括修正は「[:contents]の直前が空行でなければ空行挿入」する冪等スクリプトで。blogsync pushを同一ファイルに多重呼び出しするとローカルmdが消える罠: pushは成功時にレスポンスの正規パスへローカルファイルを再配置する。ループ内で1ファイルを2回以上pushすると2回目で「no such file」になり、出力ステータス判定も壊れる(401/402誤検出)。1ファイル=1回だけpushし、生ログのPUT --->直後の200 <---で成否判定する。ローカルが壊れても本番は無事=blogsync pullで復元できる(entryは本番ミラー)。- 楽天Ichiba新API:
https://openapi.rakuten.co.jp/ichibams/api/IchibaItem/Search/20260401(applicationId+accessKey+affiliateId。Referer/Originヘッダを付ける)。 - launchd実行で同じ記事が毎回重複公開される最悪の罠(2026-06-23実害・3重投稿): blogsyncは
post成功(201)の後にエントリmdを「カレントディレクトリ配下」へstoreする。launchdはcwd=/(read-only)で起動するためstoreのmkdir <blog>.hatenablog.comがread-only file systemで失敗→POSTは成功しているのにblogsyncが非0終了。post_oneが失敗扱いし、ドラフトをキューに残す→次の定時実行が再投稿→ライブ上に同一記事が量産される。対症療法では気付けない(手動shellはcwdが書込可能なので再現しない)。根治=blogsync呼び出し前に書込可能な作業dirを固定:export BLOGSYNC_WORKDIR="${BLOGSYNC_WORKDIR:-$HOME/.cache/blogsync}"; mkdir -p "$BLOGSYNC_WORKDIR"(-C <dir>でも可。env名はblogsync --helpの[$BLOGSYNC_WORKDIR])。重複検出=blogsync pullしてgrep -rl '<タイトル>' | wc -l、掃除=blogsync remove <localmirror.md>(pull後にローカルmdが要る)。 - 「0分で完了」「未公開キュー残>0なのに公開0」はstore失敗のサイン。run-and-notify等のラッパで
START→ENDが同時刻なら、生成は使用量制限で空振り+公開がstoreで死んでいる複合。daily.logが更新されないのはラッパがstdoutをtempに吸うため(plistのStandardOutPathは無関係になる)。
Verification
blogsync pullが既存記事を取得できる=認証OK。- 公開後:
curl -s https://<blog>.hatenablog.com/ | grep '<記事タイトル>'でトップに出現確認。 - 公開ページを
curlしてhb.afl.rakuten.co.jp/hgc/リンクが残っている=アフィリ計測が生きている。 - Markdownの
Draft:フィールドが無ければ公開済み。