run(); return $probe->isSuccessful(); } public function render(NovaCard $card): array { if (! $this->isAvailable()) { throw new RuntimeException('Playwright rendering is disabled or Node.js is not available.'); } $format = Arr::get(config('nova_cards.formats'), $card->format, config('nova_cards.formats.square')); $width = (int) Arr::get($format, 'width', 1080); $height = (int) Arr::get($format, 'height', 1080); // Signed URL is valid for 10 minutes — enough for the queue job. $signedUrl = URL::temporarySignedRoute( 'nova-cards.render-frame', now()->addMinutes(10), ['uuid' => $card->uuid], ); $tmpDir = rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'nova-render-' . $card->uuid; $pngPath = $tmpDir . DIRECTORY_SEPARATOR . 'canvas.png'; if (! is_dir($tmpDir)) { mkdir($tmpDir, 0755, true); } $script = base_path('scripts/render-nova-card.cjs'); $process = new Process( ['node', $script, "--url={$signedUrl}", "--out={$pngPath}", "--width={$width}", "--height={$height}"], null, null, null, 60, // seconds ); $process->run(); if (! $process->isSuccessful()) { throw new RuntimeException('Playwright render failed: ' . trim($process->getErrorOutput())); } if (! file_exists($pngPath) || filesize($pngPath) < 100) { throw new RuntimeException('Playwright render produced no output or an empty file.'); } $pngBlob = (string) file_get_contents($pngPath); @unlink($pngPath); @rmdir($tmpDir); // Convert the PNG to WebP + JPEG using GD (already a project dependency). $img = @imagecreatefromstring($pngBlob); if ($img === false) { throw new RuntimeException('Could not decode Playwright PNG output with GD.'); } ob_start(); imagewebp($img, null, (int) config('nova_cards.render.preview_quality', 86)); $webpBinary = (string) ob_get_clean(); ob_start(); imagejpeg($img, null, (int) config('nova_cards.render.og_quality', 88)); $jpgBinary = (string) ob_get_clean(); imagedestroy($img); $disk = Storage::disk((string) config('nova_cards.storage.public_disk', 'public')); $basePath = trim((string) config('nova_cards.storage.preview_prefix', 'cards/previews'), '/') . '/' . $card->user_id; $previewPath = $basePath . '/' . $card->uuid . '.webp'; $ogPath = $basePath . '/' . $card->uuid . '-og.jpg'; $disk->put($previewPath, $webpBinary, 'public'); $disk->put($ogPath, $jpgBinary, 'public'); $card->forceFill([ 'preview_path' => $previewPath, 'preview_width' => $width, 'preview_height' => $height, 'last_rendered_at' => now(), ])->save(); return [ 'preview_path' => $previewPath, 'og_path' => $ogPath, 'width' => $width, 'height' => $height, ]; } }