where('id', $id)->first(); if (! $raw) { // Artwork never existed → contextual 404 $suggestions = app(ErrorSuggestionService::class); return response(view('errors.contextual.artwork-not-found', [ 'trendingArtworks' => $this->safeSuggestions(fn () => $suggestions->trendingArtworks()), ]), 404); } if ($raw->trashed()) { // Artwork permanently deleted → 410 Gone return response(view('errors.410'), 410); } if (! $raw->is_public || ! $raw->is_approved) { // Artwork exists but is private/unapproved → 403 Forbidden. // Show other public artworks by the same creator as recovery suggestions. $suggestions = app(ErrorSuggestionService::class); $creatorArtworks = collect(); $creatorUsername = null; if ($raw->user_id) { $raw->loadMissing('user'); $creatorUsername = $raw->user?->username; $creatorArtworks = $this->safeSuggestions(function () use ($raw) { return Artwork::query() ->with('user') ->where('user_id', $raw->user_id) ->where('id', '!=', $raw->id) ->public() ->published() ->limit(6) ->get() ->map(function (Artwork $a) { $slug = \Illuminate\Support\Str::slug((string) ($a->slug ?: $a->title)) ?: (string) $a->id; $md = \App\Services\ThumbnailPresenter::present($a, 'md'); return [ 'id' => $a->id, 'title' => html_entity_decode((string) $a->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'author' => html_entity_decode((string) ($a->user?->name ?: $a->user?->username ?: 'Artist'), ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'url' => route('art.show', ['id' => $a->id, 'slug' => $slug]), 'thumb' => $md['url'] ?? null, ]; }); }); } return response(view('errors.contextual.artwork-not-found', [ 'message' => 'This artwork is not publicly available.', 'isForbidden' => true, 'creatorArtworks' => $creatorArtworks, 'creatorUsername' => $creatorUsername, 'trendingArtworks' => $this->safeSuggestions(fn () => $suggestions->trendingArtworks()), ]), 403); } // ── Step 2: full load with all relations ─────────────────────────── $artwork = Artwork::with(['user.profile', 'categories.contentType', 'categories.parent.contentType', 'tags', 'stats', 'awardStat']) ->where('id', $id) ->public() ->published() ->firstOrFail(); $canonicalSlug = Str::slug((string) ($artwork->slug ?: $artwork->title)); if ($canonicalSlug === '') { $canonicalSlug = (string) $artwork->id; } if ((string) $slug !== $canonicalSlug) { return redirect()->route('art.show', [ 'id' => $artwork->id, 'slug' => $canonicalSlug, ], 301); } $thumbMd = ThumbnailPresenter::present($artwork, 'md'); $thumbLg = ThumbnailPresenter::present($artwork, 'lg'); $thumbXl = ThumbnailPresenter::present($artwork, 'xl'); $thumbSq = ThumbnailPresenter::present($artwork, 'sq'); $artworkData = (new ArtworkResource($artwork))->toArray($request); $canonical = route('art.show', ['id' => $artwork->id, 'slug' => $canonicalSlug]); $authorName = $artwork->user?->name ?: $artwork->user?->username ?: 'Artist'; $description = Str::limit(trim(strip_tags(html_entity_decode((string) ($artwork->description ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8'))), 160, '…'); $meta = [ 'title' => sprintf('%s by %s | Skinbase', html_entity_decode((string) $artwork->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'), html_entity_decode((string) $authorName, ENT_QUOTES | ENT_HTML5, 'UTF-8')), 'description' => $description !== '' ? $description : html_entity_decode((string) $artwork->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'canonical' => $canonical, 'og_image' => $thumbXl['url'] ?? $thumbLg['url'] ?? null, 'og_width' => $thumbXl['width'] ?? $thumbLg['width'] ?? null, 'og_height' => $thumbXl['height'] ?? $thumbLg['height'] ?? null, ]; $categoryIds = $artwork->categories->pluck('id')->filter()->values(); $tagIds = $artwork->tags->pluck('id')->filter()->values(); $related = Artwork::query() ->with(['user', 'categories.contentType']) ->whereKeyNot($artwork->id) ->public() ->published() ->where(function ($query) use ($artwork, $categoryIds, $tagIds): void { $query->where('user_id', $artwork->user_id); if ($categoryIds->isNotEmpty()) { $query->orWhereHas('categories', function ($categoryQuery) use ($categoryIds): void { $categoryQuery->whereIn('categories.id', $categoryIds->all()); }); } if ($tagIds->isNotEmpty()) { $query->orWhereHas('tags', function ($tagQuery) use ($tagIds): void { $tagQuery->whereIn('tags.id', $tagIds->all()); }); } }) ->latest('published_at') ->limit(12) ->get() ->map(function (Artwork $item): array { $itemSlug = Str::slug((string) ($item->slug ?: $item->title)); if ($itemSlug === '') { $itemSlug = (string) $item->id; } $md = ThumbnailPresenter::present($item, 'md'); $lg = ThumbnailPresenter::present($item, 'lg'); return [ 'id' => (int) $item->id, 'title' => html_entity_decode((string) $item->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'author' => html_entity_decode((string) ($item->user?->name ?: $item->user?->username ?: 'Artist'), ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'url' => route('art.show', ['id' => $item->id, 'slug' => $itemSlug]), 'thumb' => $md['url'] ?? null, 'thumb_srcset' => ($md['url'] ?? '') . ' 640w, ' . ($lg['url'] ?? '') . ' 1280w', ]; }) ->values() ->all(); // Recursive helper to format a comment and its nested replies $formatComment = null; $formatComment = function (ArtworkComment $c) use (&$formatComment): array { $replies = $c->relationLoaded('approvedReplies') ? $c->approvedReplies : collect(); $user = $c->user; $userId = (int) ($c->user_id ?? 0); $avatarHash = $user?->profile?->avatar_hash ?? null; $canPublishLinks = (int) ($user?->level ?? 1) > 1 && strtolower((string) ($user?->rank ?? 'Newbie')) !== 'newbie'; $rawContent = (string) ($c->raw_content ?? $c->content ?? ''); $renderedContent = $c->rendered_content; if (! is_string($renderedContent) || trim($renderedContent) === '') { $renderedContent = $rawContent !== '' ? ContentSanitizer::render($rawContent) : nl2br(e(strip_tags((string) ($c->content ?? '')))); } return [ 'id' => $c->id, 'parent_id' => $c->parent_id, 'content' => html_entity_decode((string) $c->content, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'raw_content' => $c->raw_content ?? $c->content, 'rendered_content' => ContentSanitizer::sanitizeRenderedHtml($renderedContent, $canPublishLinks), 'created_at' => $c->created_at?->toIso8601String(), 'time_ago' => $c->created_at ? Carbon::parse($c->created_at)->diffForHumans() : null, 'user' => [ 'id' => $userId, 'name' => $user?->name, 'username' => $user?->username, 'display' => $user?->username ?? $user?->name ?? 'User', 'profile_url' => $user?->username ? '/@' . $user->username : ($userId > 0 ? '/profile/' . $userId : null), 'avatar_url' => AvatarUrl::forUser($userId, $avatarHash, 64), 'level' => (int) ($user?->level ?? 1), 'rank' => (string) ($user?->rank ?? 'Newbie'), ], 'replies' => $replies->map($formatComment)->values()->all(), ]; }; $comments = ArtworkComment::with(['user.profile', 'approvedReplies']) ->where('artwork_id', $artwork->id) ->where('is_approved', true) ->whereNull('parent_id') ->orderBy('created_at') ->limit(500) ->get() ->map($formatComment) ->values() ->all(); return view('artworks.show', [ 'artwork' => $artwork, 'artworkData' => $artworkData, 'presentMd' => $thumbMd, 'presentLg' => $thumbLg, 'presentXl' => $thumbXl, 'presentSq' => $thumbSq, 'meta' => $meta, 'relatedItems' => $related, 'comments' => $comments, ]); } /** Silently catch suggestion query failures so error page never crashes. */ private function safeSuggestions(callable $fn): mixed { try { return $fn(); } catch (\Throwable) { return collect(); } } }