refactor(posts): tighten content-length caps and simplify contentOverflow

- Platform::contentOverflow returns int (0 = fits) via max(), drops the
  nullable-int + ternary pattern; HasSocialHttpClient and the validation
  rule updated to compare against 0 instead of null.
- Drop the hardcoded 63206-char limit to 10000 across all entry points
  (Platform enum, MCP CreatePostTool/UpdatePostTool, FacebookRules, all
  three FormRequests). Facebook's API accepts up to 63206 but nobody
  writes 63k-char posts and emoji-heavy content risks overflowing the
  TEXT column's 65535-byte ceiling.
- Compiled i18n JSON regenerated with the content_exceeds_platform key.
This commit is contained in:
Paulo Castellano 2026-05-11 20:06:44 -03:00
parent 953be22b5b
commit b10f25f94d
13 changed files with 23 additions and 23 deletions

View file

@ -16,7 +16,7 @@ public function platform(): Platform
public function specs(): array
{
return [
'max_content_length' => 63206,
'max_content_length' => 10000,
'max_images' => 10,
'text_only_allowed' => true,
'formats' => [
@ -30,7 +30,7 @@ public function specs(): array
public function summary(): string
{
return <<<'TXT'
Facebook: caption up to 63k chars but short posts perform better. Text-only allowed.
Facebook: caption up to 10k chars but short posts perform better. Text-only allowed.
- Post: flexible aspect ratio, up to 10 images/videos.
- Reel: vertical 9:16 video only, up to 90s.
- Story: vertical 9:16, single image/video, ephemeral.

View file

@ -99,7 +99,9 @@ public function maxImages(): int
* (publisher derives title from the first line via `buildTitle`), and
* Shorts UX only shows ~100 chars before "more" capping at 100 keeps
* posts appropriate for the format.
* - Facebook text status: 63206
* - Facebook text status: 10000 (API allows 63206; we cap below
* that 63k-char posts are unrealistic and emoji-heavy content
* risks overflowing the TEXT column's 65535-byte ceiling)
* - Instagram feed caption: 2200
* - Threads: 500
* - Pinterest pin description: 800 (title is 100, not modeled here)
@ -113,7 +115,7 @@ public function maxContentLength(): int
self::X => 280,
self::TikTok => 2200,
self::YouTube => 100,
self::Facebook => 63206,
self::Facebook => 10000,
self::Instagram, self::InstagramFacebook => 2200,
self::Threads => 500,
self::Pinterest => 800,
@ -123,16 +125,14 @@ public function maxContentLength(): int
}
/**
* Number of characters the given content exceeds this platform's hard cap,
* or null when it fits. Single source of truth for content-length checks
* used both at schedule/publish-validation time and at publish time itself
* so the two paths can never drift apart.
* Number of characters by which the given content exceeds this platform's
* hard cap. Returns 0 when it fits. Single source of truth for content-
* length checks used both at schedule-validation time and at publish
* time itself so the two paths can never drift apart.
*/
public function contentOverflow(string $content): ?int
public function contentOverflow(string $content): int
{
$over = mb_strlen($content) - $this->maxContentLength();
return $over > 0 ? $over : null;
return max(0, mb_strlen($content) - $this->maxContentLength());
}
/**

View file

@ -28,7 +28,7 @@ public function rules(): array
'content' => [
'nullable',
'string',
'max:63206',
'max:10000',
Rule::when(
$this->filled('scheduled_at'),
[new ContentFitsPlatformLimits($this->resolveSelectedPlatforms($workspaceId))]

View file

@ -35,7 +35,7 @@ public function rules(): array
'content' => [
'nullable',
'string',
'max:63206',
'max:10000',
Rule::when(
$enforcesPlatformLimits,
[new ContentFitsPlatformLimits($this->resolveSelectedPlatforms())]

View file

@ -34,7 +34,7 @@ public function rules(): array
'content' => [
'nullable',
'string',
'max:63206',
'max:10000',
Rule::when(
$enforcesMediaCompatibility,
[new ContentFitsPlatformLimits($this->resolveSelectedPlatforms())]

View file

@ -24,7 +24,7 @@ public function handle(Request $request): ResponseFactory
$workspace = $request->user()->currentWorkspace;
$validated = $request->validate([
'content' => ['nullable', 'string', 'max:63206'],
'content' => ['nullable', 'string', 'max:10000'],
'scheduled_at' => ['nullable', 'date', 'after:now'],
'label_ids' => ['sometimes', 'array'],
'label_ids.*' => ['uuid', Rule::exists('workspace_labels', 'id')->where('workspace_id', $workspace->id)],

View file

@ -35,7 +35,7 @@ public function handle(Request $request): Response|ResponseFactory
$validated = $request->validate([
'post_id' => ['required', 'uuid'],
'content' => ['nullable', 'string', 'max:63206'],
'content' => ['nullable', 'string', 'max:10000'],
'scheduled_at' => ['nullable', 'date', 'after:now'],
'status' => ['sometimes', 'string', Rule::in([Status::Draft->value, Status::Scheduled->value])],
'label_ids' => ['sometimes', 'array'],

View file

@ -41,7 +41,7 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
}
$over = $platform->contentOverflow($content);
if ($over === null) {
if ($over === 0) {
continue;
}

View file

@ -16,7 +16,7 @@ protected function validateContentLength(PostPlatform $postPlatform): void
{
$content = $postPlatform->post->content ?? '';
if ($postPlatform->platform->contentOverflow($content) === null) {
if ($postPlatform->platform->contentOverflow($content) === 0) {
return;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -60,7 +60,7 @@
expect(Platform::X->maxContentLength())->toBe(280);
expect(Platform::TikTok->maxContentLength())->toBe(2200);
expect(Platform::YouTube->maxContentLength())->toBe(100);
expect(Platform::Facebook->maxContentLength())->toBe(63206);
expect(Platform::Facebook->maxContentLength())->toBe(10000);
expect(Platform::Instagram->maxContentLength())->toBe(2200);
expect(Platform::Threads->maxContentLength())->toBe(500);
expect(Platform::Pinterest->maxContentLength())->toBe(800);