BlueskyのAPIを利用した際、テキスト投稿まではスムーズに実装できても、画像投稿で躓くケースが非常に多く見られます。その最大の理由は、「投稿APIの中に画像を組み込む」のではなく、「事前に画像をアップロードして識別子(blob)を取得する」という2段階のプロセスが必要だからです。
本記事では、PHPとcURLを用いて、画像アップロードから投稿完了までの全工程を論理的に解説します。
1. 全体フローの理解
Bluesky(AT Protocol)において、画像投稿は以下の3ステップで構成されます。
-
認証 (createSession):ハンドル名とアプリパスワードでログインし、
accessJwt(トークン)を取得。 -
Blobアップロード (uploadBlob):画像のバイナリデータをサーバーへ送信。サーバーから返ってくる「受領証(blobオブジェクト)」を保持する。
-
レコード作成 (createRecord):投稿本文と一緒に、ステップ2で取得した
blobをembedフィールドに含めて送信する。
2. 実装コード(フルスクラッチ)
以下のコードは、単体で動作するように設計された完全なサンプルです。
<?php /** * Bluesky Image Post Implementation (PHP) * * @author Your Name / Blog Name * @license MIT */ // --- 0. 設定項目 --- $pds_url = 'https://bsky.social/xrpc/'; $handle = 'your-handle.bsky.social'; // 自分のハンドル名 $app_pass = 'xxxx-xxxx-xxxx-xxxx'; // アプリパスワード(設定から生成したもの) $img_path = './target_image.jpg'; // 投稿する画像ファイル // --- 1. セッションの作成(認証) --- $session_url = $pds_url . 'com.atproto.server.createSession'; $auth_payload = json_encode([ 'identifier' => $handle,
'password' => $app_pass
]);
$ch = curl_init($session_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $auth_payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
$auth_res = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!isset($auth_res['accessJwt'])) {
die("認証失敗: " . ($auth_res['message'] ?? '不明なエラー'));
}
$access_token = $auth_res['accessJwt'];
$did = $auth_res['did'];
// --- 2. 画像(Blob)のアップロード ---
// Blueskyの画像投稿における最大の難所。
// multipart/form-dataではなく、リクエストボディにバイナリを直接乗せる。
if (!file_exists($img_path)) {
die("画像ファイルが見つかりません。");
}
$image_binary = file_get_contents($img_path);
$mime_type = mime_content_type($img_path);
$upload_url = $pds_url . 'com.atproto.repo.uploadBlob';
$ch = curl_init($upload_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $image_binary); // バイナリデータを直接セット
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $access_token",
"Content-Type: $mime_type" // 画像のMIMEタイプ(image/jpeg等)を明示
]);
$upload_res = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!isset($upload_res['blob'])) {
die("Blobアップロード失敗: " . ($upload_res['message'] ?? '不明なエラー'));
}
$blob = $upload_res['blob']; // これが画像投稿に必要な「受領証」となる
// --- 3. 投稿(Create Record)の実行 ---
$post_url = $pds_url . 'com.atproto.repo.createRecord';
// 投稿データ(Record)の構築
$post_payload = [
'repo' => $did,
'collection' => 'app.bsky.feed.post',
'record' => [
'text' => "PHPによる画像投稿テスト\n" . date('Y-m-d H:i:s'),
'createdAt' => date('c'),
'embed' => [
'$type' => 'app.bsky.embed.images',
'images' => [
[
'alt' => '投稿された画像の説明文', // アクセシビリティ用の代替テキスト
'image' => $blob // STEP2で取得したblobをセット
]
]
]
]
];
$ch = curl_init($post_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $access_token",
"Content-Type: application/json"
]);
$final_res = json_decode(curl_exec($ch), true);
curl_close($ch);
// --- 結果確認 ---
if (isset($final_res['uri'])) {
echo "投稿成功! URI: " . $final_res['uri'];
} else {
echo "投稿失敗。";
print_r($final_res);
}
3. 実装上の重要な技術的注意点
① uploadBlob 時のデータ形式
一般的なWebフォームのような $_FILES 経由での送信とは仕組みが異なります。cURLの CURLOPT_POSTFIELDS に、file_get_contents() で読み込んだ生のバイナリデータをそのまま渡してください。この際、Content-Type ヘッダーに image/jpeg や image/png を指定しないとサーバー側で正しく処理されません。
② 画像サイズと制限
Bluesky PDS(個人データサーバー)には画像サイズ制限があります。2026年現在、1枚あたり約1MB(1,000,000バイト)を超える画像はエラーとなる可能性が高いため、事前にPHPの GD ライブラリや Imagick を使用してリサイズ・圧縮を行うのが実用的です。
③ embed フィールドの $type
画像投稿を行う際は、embed 内の $type に必ず app.bsky.embed.images を指定する必要があります。ここを間違えると、画像が反映されなかったり、APIがエラーを返したりします。
4. 複数枚投稿への応用
上記のサンプルでは1枚の投稿ですが、images は配列になっているため、ステップ2を繰り返し行い、複数の blob を取得して配列に詰め込むことで、最大4枚までの画像投稿が可能です。


コメント