* feat: ship public site with SEO and visuals Publish CMS content on public routes with responsive media, accessibility checks, and deterministic visual baselines. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: make browser visual CI deterministic for media Mount host public storage into FrankenPHP so seeded fixtures are served, and replace PNG-as-JPG fixtures with real JPEGs so Chromium can render them. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: serve public media on same-origin storage paths Pest Browser hosts on 127.0.0.1:port while Storage::url used http://localhost, so screenshots captured broken images. Use relative /storage URLs for media and absolutize only OG tags via url(). Co-authored-by: Cursor <cursoragent@cursor.com> * test: refresh visual baselines from CI Ubuntu screenshots Media now loads on same-origin /storage paths, so baselines must capture the rendered fixtures. Use full-page snapshots from the CI runner to keep Pest's exact snapshot match stable across environments. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
152 lines
4.0 KiB
PHP
152 lines
4.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support;
|
|
|
|
use Illuminate\Contracts\Filesystem\Filesystem;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Intervention\Image\Drivers\Gd\Driver;
|
|
use Intervention\Image\ImageManager;
|
|
use Throwable;
|
|
|
|
final class ResponsiveImage
|
|
{
|
|
/** @var list<int> */
|
|
public const WIDTHS = [480, 960, 1440];
|
|
|
|
public static function generate(string $path, ?string $disk = null): void
|
|
{
|
|
$filesystem = self::filesystem($disk);
|
|
|
|
if (! $filesystem->exists($path)) {
|
|
return;
|
|
}
|
|
|
|
$manager = new ImageManager(new Driver);
|
|
$contents = $filesystem->get($path);
|
|
|
|
if ($contents === null) {
|
|
return;
|
|
}
|
|
|
|
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
|
|
|
foreach (self::WIDTHS as $width) {
|
|
$variantPath = self::variantPath($path, $width);
|
|
$variant = $manager->read($contents);
|
|
|
|
if ($variant->width() > $width) {
|
|
$variant->scale(width: $width);
|
|
}
|
|
|
|
$encoded = match ($extension) {
|
|
'png' => $variant->toPng(),
|
|
'webp' => $variant->toWebp(quality: 82),
|
|
default => $variant->toJpeg(quality: 82),
|
|
};
|
|
|
|
$filesystem->put($variantPath, (string) $encoded);
|
|
}
|
|
}
|
|
|
|
public static function deleteVariants(string $path, ?string $disk = null): void
|
|
{
|
|
$filesystem = self::filesystem($disk);
|
|
|
|
foreach (self::WIDTHS as $width) {
|
|
$variantPath = self::variantPath($path, $width);
|
|
|
|
if ($filesystem->exists($variantPath)) {
|
|
$filesystem->delete($variantPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static function delete(string $path, ?string $disk = null): void
|
|
{
|
|
$filesystem = self::filesystem($disk);
|
|
|
|
self::deleteVariants($path, $disk);
|
|
|
|
if ($filesystem->exists($path)) {
|
|
$filesystem->delete($path);
|
|
}
|
|
}
|
|
|
|
public static function replace(string $previousPath, string $newPath, ?string $disk = null): void
|
|
{
|
|
if ($previousPath !== '' && $previousPath !== $newPath) {
|
|
self::delete($previousPath, $disk);
|
|
}
|
|
|
|
self::generate($newPath, $disk);
|
|
}
|
|
|
|
public static function variantPath(string $path, int $width): string
|
|
{
|
|
$directory = trim(dirname($path), '.');
|
|
$filename = pathinfo($path, PATHINFO_FILENAME);
|
|
$extension = pathinfo($path, PATHINFO_EXTENSION);
|
|
$variantName = $filename.'-'.$width.($extension !== '' ? '.'.$extension : '');
|
|
|
|
return $directory === '' ? $variantName : $directory.'/'.$variantName;
|
|
}
|
|
|
|
/**
|
|
* @return list<array{path: string, width: int}>
|
|
*/
|
|
public static function availableVariants(string $path, ?string $disk = null): array
|
|
{
|
|
$filesystem = self::filesystem($disk);
|
|
$variants = [];
|
|
|
|
foreach (self::WIDTHS as $width) {
|
|
$variantPath = self::variantPath($path, $width);
|
|
|
|
if ($filesystem->exists($variantPath)) {
|
|
$variants[] = [
|
|
'path' => $variantPath,
|
|
'width' => $width,
|
|
];
|
|
}
|
|
}
|
|
|
|
return $variants;
|
|
}
|
|
|
|
/**
|
|
* @return array{width: int, height: int}|null
|
|
*/
|
|
public static function dimensions(string $path, ?string $disk = null): ?array
|
|
{
|
|
$filesystem = self::filesystem($disk);
|
|
|
|
if (! $filesystem->exists($path)) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$contents = $filesystem->get($path);
|
|
|
|
if ($contents === null) {
|
|
return null;
|
|
}
|
|
|
|
$image = (new ImageManager(new Driver))->read($contents);
|
|
|
|
return [
|
|
'width' => $image->width(),
|
|
'height' => $image->height(),
|
|
];
|
|
} catch (Throwable) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static function filesystem(?string $disk): Filesystem
|
|
{
|
|
return Storage::disk($disk ?? PublicImageUploadRules::disk());
|
|
}
|
|
}
|