Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ AWS_USE_PATH_STYLE_ENDPOINT=false

VITE_APP_NAME="${APP_NAME}"


# --------------------------------------------------------------------------
# 📌 Laravel Model Status Configuration
# These variables define the behavior of the package.
Expand Down Expand Up @@ -101,3 +100,6 @@ TURNSTILE_SITE_KEY=""
TURNSTILE_SECRET_KEY=""
VITE_TURNSTILE_SITE_KEY="${TURNSTILE_SITE_KEY}"
VITE_TURNSTILE_SECRET_KEY="${TURNSTILE_SECRET_KEY}"
# --------------------------------------------------------------------------
CHROME_PATH=
BROWSERSHOT_USER_DATA_DIR=
3 changes: 3 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ jobs:
TURNSTILE_SECRET_KEY=${{ secrets.TURNSTILE_SECRET_KEY }}
VITE_TURNSTILE_SITE_KEY=${{ secrets.TURNSTILE_SITE_KEY }}
VITE_TURNSTILE_SECRET_KEY=${{ secrets.TURNSTILE_SECRET_KEY }}

CHROME_PATH=${{ secrets.CHROME_PATH }}
BROWSERSHOT_USER_DATA_DIR=${{ secrets.BROWSERSHOT_USER_DATA_DIR }}
EOF

deploy:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/update-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ jobs:
TURNSTILE_SECRET_KEY=${{ secrets.TURNSTILE_SECRET_KEY }}
VITE_TURNSTILE_SITE_KEY=${{ secrets.TURNSTILE_SITE_KEY }}
VITE_TURNSTILE_SECRET_KEY=${{ secrets.TURNSTILE_SECRET_KEY }}

CHROME_PATH=${{ secrets.CHROME_PATH }}
BROWSERSHOT_USER_DATA_DIR=${{ secrets.BROWSERSHOT_USER_DATA_DIR }}
EOF

- name: Refresh configurations
Expand Down
123 changes: 123 additions & 0 deletions app/Actions/GeneratePackageOgImageAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Models\Package;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Spatie\Browsershot\Browsershot;

class GeneratePackageOgImageAction
{
public function handle(Package $package): void
{
try {
$tempUrl = route('og-images.package', ['package' => $package->id]);

// Target file
$storagePath = storage_path('app/public/package-og-images');
$this->ensureDir($storagePath);

$filename = Str::slug($package->name) . '-' . Str::random(8) . '.jpg';
$fullPath = $storagePath . '/' . $filename;

// Binaries
$nodePath = $this->which('node') ?? '/usr/bin/node';
$npmPath = $this->which('npm') ?? '/usr/bin/npm';

// Writable Chrome profile/cache dir
$userDataDir = rtrim((string) env('BROWSERSHOT_USER_DATA_DIR', '/var/www/browsershot-cache'), '/');
$this->ensureDir($userDataDir);

// Ensure the env seen by the child process points to our writable dir
foreach (['HOME', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME'] as $var) {
putenv($var . '=' . $userDataDir);
$_ENV[$var] = $userDataDir;
$_SERVER[$var] = $userDataDir;
}

// Prefer explicit path via env; otherwise auto-detect
$chromePath = env('CHROME_PATH') ?: $this->detectChromePath();

// IMPORTANT: do NOT prefix with `--` here; Browsershot will add them
$chromiumArgs = [
'headless=new',
'no-sandbox',
'disable-setuid-sandbox',
'disable-dev-shm-usage',
'disable-gpu',
'no-zygote',
'single-process',
'hide-scrollbars',
"user-data-dir={$userDataDir}",
'no-first-run',
'no-default-browser-check',
'disable-crash-reporter',
"data-path={$userDataDir}/data",
"disk-cache-dir={$userDataDir}/cache",
];

$browserShot = Browsershot::url($tempUrl)
->setNodeBinary($nodePath)
->setNpmBinary($npmPath)
->windowSize(1200, 630)
->waitUntilNetworkIdle()
->setScreenshotType('jpeg', 90)
->addChromiumArguments($chromiumArgs);

if ($chromePath) {
$browserShot->setChromePath($chromePath);
}

$browserShot->save($fullPath);

if (is_file($fullPath)) {
$package->addMedia($fullPath)
->usingName('og-image')
->usingFileName($filename)
->toMediaCollection('og-images', 'package-og-images');

Log::info('OG Image Generated: ' . $package->getFirstMediaUrl('og-images'));
} else {
Log::error('OG image file missing after save', ['path' => $fullPath]);
}
} catch (\Throwable $e) {
Log::error('OG Image Generation Error: ' . $e->getMessage(), ['exception' => $e]);
throw $e;
}
}

private function ensureDir(string $dir): void
{
if (! is_dir($dir)) {
@mkdir($dir, 0755, true);
}
}

private function which(string $bin): ?string
{
$path = @exec(sprintf('command -v %s', escapeshellarg($bin)));
return $path ?: null;
}

private function detectChromePath(): ?string
{
$candidates = [
'/usr/bin/google-chrome',
'/usr/bin/google-chrome-stable',
'/usr/local/bin/google-chrome',
'/usr/bin/chromium',
'/usr/bin/chromium-browser',
];

foreach ($candidates as $path) {
if (is_file($path) && is_executable($path)) {
return $path;
}
}

return null;
}
}
2 changes: 1 addition & 1 deletion app/Filament/Resources/PackageResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ public static function getPages(): array
return [
'index' => Pages\ListPackages::route('/'),
'create' => Pages\CreatePackage::route('/create'),
// 'edit' => Pages\EditPackage::route('/{record}/edit'),
'edit' => Pages\EditPackage::route('/{record}/edit'),
];
}
}
20 changes: 20 additions & 0 deletions app/Filament/Resources/PackageResource/Pages/CreatePackage.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,30 @@

namespace App\Filament\Resources\PackageResource\Pages;

use App\Actions\GeneratePackageOgImageAction;
use App\Filament\Resources\PackageResource;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\CreateRecord;

class CreatePackage extends CreateRecord
{
protected static string $resource = PackageResource::class;

protected function afterCreate(): void
{
try {
app(GeneratePackageOgImageAction::class)->handle($this->record);

Notification::make()
->title('OG Image Generated')
->success()
->send();
} catch (\Exception $e) {
Notification::make()
->title('Failed to generate OG image')
->body($e->getMessage())
->danger()
->send();
}
}
}
28 changes: 28 additions & 0 deletions app/Filament/Resources/PackageResource/Pages/EditPackage.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

namespace App\Filament\Resources\PackageResource\Pages;

use App\Actions\GeneratePackageOgImageAction;
use App\Filament\Resources\PackageResource;
use Filament\Actions;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Facades\Log;

class EditPackage extends EditRecord
{
Expand All @@ -16,4 +19,29 @@ protected function getHeaderActions(): array
Actions\DeleteAction::make(),
];
}

protected function afterSave(): void
{
Log::info('EditPackage afterSave triggered', ['package_id' => $this->record->id]);

try {
app(GeneratePackageOgImageAction::class)->handle($this->record);

Notification::make()
->title('OG Image Regenerated')
->success()
->send();
} catch (\Exception $e) {
Log::error('OG Image Generation Error in afterSave', [
'package_id' => $this->record->id,
'error' => $e->getMessage(),
]);

Notification::make()
->title('Failed to regenerate OG image')
->body($e->getMessage())
->danger()
->send();
}
}
}
18 changes: 18 additions & 0 deletions app/Http/Controllers/OgImageController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace App\Http\Controllers;

use App\Models\Package;

class OgImageController extends Controller
{
/**
* Render the OG image for a package
*
* @return \Illuminate\Contracts\View\View
*/
public function package(Package $package)
{
return view('og-images.package', compact('package'));
}
}
18 changes: 17 additions & 1 deletion app/Http/Controllers/PackageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

namespace App\Http\Controllers;

use App\Actions\GeneratePackageOgImageAction;
use App\Http\Resources\Admin\CategoryResource;
use App\Http\Resources\PackageResource;
use App\Models\Category;
use App\Models\Package;
use App\Queries\PackageQuery;
use App\Services\GitHubService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Inertia\Inertia;

class PackageController extends Controller
Expand Down Expand Up @@ -41,11 +43,25 @@ public function index(Request $request)
public function show(string $slug)
{
$package = Package::query()
->with('media')
->select('id', 'index_id', 'name', 'slug', 'description', 'repository_url', 'meta_title', 'meta_description', 'language', 'stars', 'owner', 'owner_avatar', 'created_at')
->with(['categories', 'indexes'])
->with(['categories', 'indexes', 'media'])
->where('slug', $slug)
->firstOrFail();

if (! $package->hasMedia('og-images')) {
try {
app(GeneratePackageOgImageAction::class)->handle($package);

$package->load('media');
} catch (\Exception $e) {
Log::error('Failed to generate OG image in controller', [
'package_id' => $package->id,
'error' => $e->getMessage(),
]);
}
}

return Inertia::render('Package', [
'package' => new PackageResource($package),
'readme' => Inertia::defer(fn () => GitHubService::fetchReadmeContent($package->repository_url)),
Expand Down
1 change: 1 addition & 0 deletions app/Http/Resources/PackageResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public function toArray(Request $request): array
'stars' => $this->stars,
'owner' => $this->owner,
'owner_avatar' => $this->owner_avatar,
'og_image' => $this->getFirstMediaUrl('og-images'),
'status' => $this->status,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
Expand Down
5 changes: 4 additions & 1 deletion app/Models/Package.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@
use Illuminate\Validation\Rule;
use Laravel\Scout\Attributes\SearchUsingFullText;
use Laravel\Scout\Searchable;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Thefeqy\ModelStatus\Casts\StatusCast;
use Thefeqy\ModelStatus\Traits\HasActiveScope;

class Package extends Model
class Package extends Model implements HasMedia
{
use Filterable;
use HasActiveScope;
use HasSlug;
use HasStatus;
use InteractsWithMedia;
use Searchable;
use SoftDeletes;

Expand Down
14 changes: 14 additions & 0 deletions config/filesystems.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,20 @@
'root' => 'Indices',
],

'package-og-images' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'report' => false,
'visibility' => 'public',
'root' => 'package-og-images',
],
],

/*
Expand Down
Loading