Publish per-image alt text to Facebook photos

This commit is contained in:
Paulo Castellano 2026-07-11 09:13:05 -03:00
parent 941e517b2e
commit 2c81416bd0
2 changed files with 128 additions and 2 deletions

View file

@ -4,7 +4,9 @@
namespace App\Services\Social;
use App\DataTransferObjects\MediaItem;
use App\Enums\PostPlatform\ContentType;
use App\Enums\SocialAccount\Platform;
use App\Exceptions\Social\ErrorCategory;
use App\Exceptions\Social\FacebookPublishException;
use App\Exceptions\Social\SocialPublishException;
@ -134,6 +136,10 @@ private function publishSingleImagePost(string $pageId, string $accessToken, ?st
$payload['message'] = $content;
}
if ($alt = $this->altFor($media)) {
$payload['alt_text_custom'] = $alt;
}
$response = $this->facebookHttp()->post("{$this->baseUrl}/{$pageId}/photos", $payload);
if ($response->failed()) {
@ -163,11 +169,17 @@ private function publishMultiImagePost(string $pageId, string $accessToken, ?str
continue;
}
$uploadResponse = $this->facebookHttp()->post("{$this->baseUrl}/{$pageId}/photos", [
$uploadPayload = [
'url' => $this->cropImageForAspectRatio($media->url, $aspectRatio),
'published' => 'false',
'access_token' => $accessToken,
]);
];
if ($alt = $this->altFor($media)) {
$uploadPayload['alt_text_custom'] = $alt;
}
$uploadResponse = $this->facebookHttp()->post("{$this->baseUrl}/{$pageId}/photos", $uploadPayload);
if ($uploadResponse->failed()) {
Log::error('Facebook image upload failed', [
@ -412,6 +424,16 @@ private function handleApiError(Response $response): never
throw FacebookPublishException::fromApiResponse($response);
}
/**
* User-provided alt text for the image, capped to Facebook's accepted length.
*/
private function altFor(MediaItem $media): ?string
{
$alt = $media->altText();
return $alt === null ? null : mb_substr($alt, 0, Platform::Facebook->altTextMaxLength());
}
protected function cropFailureException(string $message): SocialPublishException
{
return new FacebookPublishException(

View file

@ -807,3 +807,107 @@ function facebookJpegBytes(int $width = 1200, int $height = 800): string
Http::assertSent(fn ($request) => str_contains($request->url(), '/page_123/photos')
&& $request['url'] === 'https://example.com/media/a.jpg');
});
test('facebook publisher sends capped alt text on single image post', function () {
$longAlt = str_repeat('a', 1500);
$this->post->update([
'media' => [
[
'id' => 'test-media-id',
'path' => 'media/2026-01/image.jpg',
'url' => 'https://example.com/media/2026-01/image.jpg',
'mime_type' => 'image/jpeg',
'original_filename' => 'image.jpg',
'meta' => ['alt_text' => $longAlt],
],
],
]);
Http::fake([
'*/page_123/photos' => Http::response(['id' => 'photo_123', 'post_id' => 'post_123'], 200),
]);
$this->publisher->publish($this->postPlatform);
$expectedAlt = mb_substr($longAlt, 0, Platform::Facebook->altTextMaxLength());
Http::assertSent(function ($request) use ($expectedAlt) {
return str_contains($request->url(), '/page_123/photos')
&& data_get($request->data(), 'alt_text_custom') === $expectedAlt
&& strlen($expectedAlt) === Platform::Facebook->altTextMaxLength();
});
});
test('facebook publisher sends capped alt text for each image in multi image post', function () {
$longAlt1 = str_repeat('b', 1200);
$longAlt2 = str_repeat('c', 1200);
$this->post->update([
'media' => [
[
'id' => 'm1',
'path' => 'media/2026-01/image1.jpg',
'url' => 'https://example.com/media/2026-01/image1.jpg',
'mime_type' => 'image/jpeg',
'original_filename' => 'image1.jpg',
'meta' => ['alt_text' => $longAlt1],
],
[
'id' => 'm2',
'path' => 'media/2026-01/image2.jpg',
'url' => 'https://example.com/media/2026-01/image2.jpg',
'mime_type' => 'image/jpeg',
'original_filename' => 'image2.jpg',
'meta' => ['alt_text' => $longAlt2],
],
],
]);
Http::fake([
'*/page_123/photos' => Http::sequence()
->push(['id' => 'photo_1'], 200)
->push(['id' => 'photo_2'], 200),
'*/page_123/feed' => Http::response(['id' => 'multi_post_alt'], 200),
]);
$this->publisher->publish($this->postPlatform);
$expectedAlt1 = mb_substr($longAlt1, 0, Platform::Facebook->altTextMaxLength());
$expectedAlt2 = mb_substr($longAlt2, 0, Platform::Facebook->altTextMaxLength());
Http::assertSent(function ($request) use ($expectedAlt1) {
return str_contains($request->url(), '/page_123/photos')
&& data_get($request->data(), 'alt_text_custom') === $expectedAlt1;
});
Http::assertSent(function ($request) use ($expectedAlt2) {
return str_contains($request->url(), '/page_123/photos')
&& data_get($request->data(), 'alt_text_custom') === $expectedAlt2;
});
});
test('facebook publisher omits alt_text_custom from photos payload when no alt text is set', function () {
$this->post->update([
'media' => [
[
'id' => 'test-media-id',
'path' => 'media/2026-01/image.jpg',
'url' => 'https://example.com/media/2026-01/image.jpg',
'mime_type' => 'image/jpeg',
'original_filename' => 'image.jpg',
],
],
]);
Http::fake([
'*/page_123/photos' => Http::response(['id' => 'photo_123', 'post_id' => 'post_123'], 200),
]);
$this->publisher->publish($this->postPlatform);
Http::assertSent(function ($request) {
return str_contains($request->url(), '/page_123/photos')
&& ! array_key_exists('alt_text_custom', $request->data());
});
});