はじめに

Obsidian で書いて、Hashnode に投稿する。

このふたつの間には、ずっと手作業の隙間があった。

  • vault のノートを開く
  • 本文をコピーする
  • Hashnode のエディタに貼り付ける
  • フロントマターを手で書き直す
  • カバー画像を探してアップロードする
  • 投稿する

毎回10〜15分。大した作業ではないが、積み重なると「投稿を後回しにする」理由になる。

この隙間を埋めるパイプラインを作った。この記事はその記録だ。


なぜ手作業が発生していたか

Obsidian のノートには独自のフロントマター体系がある。Hashnode にも独自のスキーマがある。似ているようで微妙に違う。

タグの書き方が違う。タイトルのクォートが必要なときとそうでないときがある。カバー画像は別途アップロードが必要だった。vault 固有の ## 📝 下書き ヘッダーは除去しなければならない。

どれも難しい作業ではない。ただ、小さな摩擦が積み重なると、書くことへの集中が途切れる。


作ったものの全体像

04_Publish ノート(Obsidian vault)
  ↓  /hashnode-publish スキル(Claude Code)
     フロントマター変換 → slug 生成 → ファイル書き出し → git push

hashnode-blog-articles-sources(GitHub 中継リポ)
  ↓  GitHub Actions
  ├─ Unsplash カバー画像の解決
  └─ Hashnode API → 記事公開

3 つのコンポーネントがそれぞれ単一の責務を持つ。vault はきれいなまま保たれ、投稿ロジックはその外に置かれる。


コンポーネント 1:GitHub 中継リポ

最初のピースは「受け取って転送するだけ」の GitHub リポジトリだ。

GitHub Actions のワークフローは post/**/*.md を対象に、main への push をトリガーとして起動する。パスのパターンに注意が必要で、post/*.md(直下)ではマッチしない。そのため、記事ファイルはタイムスタンプ名のサブフォルダに入れる設計にした。

post/
  20260517143000/
    slug.md

タイムスタンプフォルダは投稿順序のキーにもなる。

ワークフローは変更ファイルごとに 2 つのステップを実行する。

  1. Unsplash カバー画像の解決(後述)
  2. Hashnode への投稿raunakgurud09/[email protected]

Hashnode の API キーは GitHub Secrets に置く。ローカルマシンには触れない。


コンポーネント 2:Claude Code スキル

次のピースは Claude Code のスラッシュコマンド /hashnode-publish だ。

実行すると、現在開いている vault ノートを読み、フロントマターをパースし、Hashnode のフォーマットに変換する。ファイル名から slug を生成し、タイムスタンプフォルダを作り、変換後のファイルを書き出して中継リポに push する。

書く側から見ると、フローはこうなる。

1. Obsidian でノートを書く
2. フロントマターを埋める(title, description, tags など)
3. /hashnode-publish と入力する
4. 完了

変換、ファイル生成、コミット、プッシュまで、10 秒かからない。


コンポーネント 3:Unsplash カバー画像の自動取得

3 つ目のピースがカバー画像問題を解決する。

vault のフロントマターに splash_keyword というフィールドを設けた。cover_image が空でこのフィールドが設定されている場合、スキルはキーワードをそのまま中継リポのファイルに書き出す。GitHub Actions がそれを検出し、Unsplash API を呼び出して画像 URL を取得、cover_image として注入する。

# vault のフロントマター
splash_keyword: "developer workspace"
cover_image: ""
GitHub Actions:
  splash_keyword を検出 → Unsplash API で画像取得
  → cover_image: https://images.unsplash.com/photo-...
  → splash_keyword をフロントマターから除去

Unsplash の API キーも GitHub Secrets に置く。ローカルには持たない。

Unsplash の API 利用規約はクレジット表示を推奨している。画像を取得した場合、ワークフローは記事末尾に自動でクレジット行を追記する。

*cover image: [unsplash](https://unsplash.com/)*

なぜ中継リポを経由するか

より単純な設計として、スキルから直接 Hashnode API を呼ぶ方法もあった。中継リポも GitHub Actions も不要になる。

中継リポ経由にした理由はひとつ:API キーを GitHub Secrets に置けるからだ。

Claude Code スキルはローカル環境にアクセスできる。API キーをローカル環境変数に置くより、GitHub Secrets に置いてCI/CD システムの管理下に入れるほうが自然な境界になる。

副次的な効果もある。中継リポは投稿の記録になる。過去のすべての記事がタイムスタンプ付きのコミットとして残る。


フロントマターの設計

投稿準備が整った vault ノートのフロントマターはこうなる。

---
title: "記事タイトル"
subtitle: "サブタイトル"
tags: ["tag1", "tag2"]
description: "記事の説明文(必須)"
cover_image: ""
splash_keyword: "検索キーワード"
publish: true
enableTableOfContent: true
isNewsletterActivated: true
---

カバー画像の決定ロジックは 3 パターンだ。

cover_imagesplash_keyword結果
設定済みURL をそのまま使用
ありUnsplash から自動取得
なしカバー画像なしで投稿

教訓

「書き終わった」から「公開した」までの摩擦は、書くことの問題ではなく投稿することの問題だ。

書く環境を整えても、最後の一マイルが手作業のままでは意味がない。パイプラインは別途作る必要があった。

3 つのコンポーネントに共通して現れたパターンがある。

シークレットは、それを使うシステムのそばに置く。ローカルの環境変数ではなく、GitHub Secrets に。

SSH エージェントの記事で出てきた「環境変数はプロセスの継承関係の結果である」という話と同じ構造だ。スコープを意識する、ということ。


おわりに

/hashnode-publish と入力して、中継リポにコミットが現れるのを見るのは、思ったより気持ちいい。

書くことに集中できる環境は、書いた後の工程も含めて作られる。