Prefetch artwork prev/next navigation off the critical path.
Move neighbor lookup into a dedicated service and let the viewer load adjacent artworks after first paint instead of blocking the detail request.
This commit is contained in:
@@ -5,11 +5,15 @@ namespace App\Http\Controllers\Api;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ArtworkResource;
|
||||
use App\Models\Artwork;
|
||||
use App\Services\ArtworkNavigationService;
|
||||
use App\Services\ThumbnailPresenter;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ArtworkNavigationController extends Controller
|
||||
{
|
||||
public function __construct(private readonly ArtworkNavigationService $navigation) {}
|
||||
|
||||
/**
|
||||
* GET /api/artworks/navigation/{id}
|
||||
*
|
||||
@@ -26,35 +30,11 @@ class ArtworkNavigationController extends Controller
|
||||
'prev_id' => null, 'next_id' => null,
|
||||
'prev_url' => null, 'next_url' => null,
|
||||
'prev_slug' => null, 'next_slug' => null,
|
||||
'prev_preview' => null, 'next_preview' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$scope = Artwork::published()
|
||||
->select(['id', 'title', 'slug'])
|
||||
->where('user_id', $artwork->user_id);
|
||||
|
||||
$prev = (clone $scope)->where('id', '<', $id)->orderByDesc('id')->first();
|
||||
$next = (clone $scope)->where('id', '>', $id)->orderBy('id')->first();
|
||||
|
||||
// Infinite loop: wrap around when reaching the first or last artwork
|
||||
if (! $prev) {
|
||||
$prev = (clone $scope)->where('id', '!=', $id)->orderByDesc('id')->first();
|
||||
}
|
||||
if (! $next) {
|
||||
$next = (clone $scope)->where('id', '!=', $id)->orderBy('id')->first();
|
||||
}
|
||||
|
||||
$prevSlug = $prev ? (Str::slug($prev->slug ?: $prev->title) ?: (string) $prev->id) : null;
|
||||
$nextSlug = $next ? (Str::slug($next->slug ?: $next->title) ?: (string) $next->id) : null;
|
||||
|
||||
return response()->json([
|
||||
'prev_id' => $prev?->id,
|
||||
'next_id' => $next?->id,
|
||||
'prev_url' => $prev ? url('/art/' . $prev->id . '/' . $prevSlug) : null,
|
||||
'next_url' => $next ? url('/art/' . $next->id . '/' . $nextSlug) : null,
|
||||
'prev_slug' => $prevSlug,
|
||||
'next_slug' => $nextSlug,
|
||||
]);
|
||||
return response()->json($this->navigation->navigationFor($artwork));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,8 +42,12 @@ class ArtworkNavigationController extends Controller
|
||||
*
|
||||
* Returns full artwork resource by numeric ID for client-side (no-reload) navigation.
|
||||
*/
|
||||
public function pageData(int $id): JsonResponse
|
||||
public function pageData(Request $request, int $id): JsonResponse
|
||||
{
|
||||
if ($request->boolean('prefetch')) {
|
||||
return $this->prefetchData($id);
|
||||
}
|
||||
|
||||
$artwork = Artwork::with(['user.profile', 'categories.contentType', 'categories.parent.contentType', 'tags', 'stats'])
|
||||
->published()
|
||||
->find($id);
|
||||
@@ -73,7 +57,30 @@ class ArtworkNavigationController extends Controller
|
||||
}
|
||||
|
||||
$resource = (new ArtworkResource($artwork))->toArray(request());
|
||||
$resource['navigation'] = $this->navigation->navigationFor($artwork);
|
||||
|
||||
return response()->json($resource);
|
||||
}
|
||||
|
||||
private function prefetchData(int $id): JsonResponse
|
||||
{
|
||||
$artwork = Artwork::query()
|
||||
->published()
|
||||
->select(['id', 'title', 'slug', 'hash', 'file_path', 'file_name'])
|
||||
->find($id);
|
||||
|
||||
if (! $artwork) {
|
||||
return response()->json(['error' => 'Not found'], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'id' => (int) $artwork->id,
|
||||
'title' => (string) $artwork->title,
|
||||
'slug' => (string) $artwork->slug,
|
||||
'thumbs' => [
|
||||
'md' => ThumbnailPresenter::present($artwork, 'md'),
|
||||
'lg' => ThumbnailPresenter::present($artwork, 'lg'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
final class ArtworkNavigationService
|
||||
{
|
||||
/**
|
||||
* Build the public previous/next navigation payload for an already loaded
|
||||
* current artwork. No current-artwork lookup is performed here.
|
||||
*/
|
||||
public function navigationFor(Artwork $artwork): array
|
||||
{
|
||||
$scope = Artwork::published()
|
||||
->select(['id', 'title', 'slug', 'hash', 'file_path', 'file_name'])
|
||||
->where('user_id', $artwork->user_id);
|
||||
|
||||
$prev = (clone $scope)->where('id', '<', $artwork->id)->orderByDesc('id')->first();
|
||||
$next = (clone $scope)->where('id', '>', $artwork->id)->orderBy('id')->first();
|
||||
|
||||
if (! $prev) {
|
||||
$prev = (clone $scope)->where('id', '!=', $artwork->id)->orderByDesc('id')->first();
|
||||
}
|
||||
if (! $next) {
|
||||
$next = (clone $scope)->where('id', '!=', $artwork->id)->orderBy('id')->first();
|
||||
}
|
||||
|
||||
$prevSlug = $this->canonicalSlug($prev);
|
||||
$nextSlug = $this->canonicalSlug($next);
|
||||
|
||||
return [
|
||||
'prev_id' => $prev?->id,
|
||||
'next_id' => $next?->id,
|
||||
'prev_url' => $prev ? url('/art/'.$prev->id.'/'.$prevSlug) : null,
|
||||
'next_url' => $next ? url('/art/'.$next->id.'/'.$nextSlug) : null,
|
||||
'prev_slug' => $prevSlug,
|
||||
'next_slug' => $nextSlug,
|
||||
'prev_preview' => $this->preview($prev),
|
||||
'next_preview' => $this->preview($next),
|
||||
];
|
||||
}
|
||||
|
||||
private function canonicalSlug(?Artwork $artwork): ?string
|
||||
{
|
||||
if (! $artwork) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Str::slug($artwork->slug ?: $artwork->title) ?: (string) $artwork->id;
|
||||
}
|
||||
|
||||
private function preview(?Artwork $artwork): ?array
|
||||
{
|
||||
if (! $artwork) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) $artwork->id,
|
||||
'title' => (string) $artwork->title,
|
||||
'slug' => (string) $artwork->slug,
|
||||
'thumbs' => [
|
||||
'md' => ThumbnailPresenter::present($artwork, 'md'),
|
||||
'lg' => ThumbnailPresenter::present($artwork, 'lg'),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user