@extends('layouts.nova') @php use App\Support\ForumPostContent; use App\Support\AvatarUrl; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Str; $filesBaseUrl = rtrim((string) config('cdn.files_url', ''), '/'); $serializePost = function ($post) use ($filesBaseUrl) { $user = $post->user ?? null; return [ 'id' => $post->id, 'user_id' => $post->user_id, 'content' => $post->content, 'rendered_content' => ForumPostContent::render($post->content), 'created_at' => $post->created_at?->toIso8601String(), 'edited_at' => $post->edited_at?->toIso8601String(), 'is_edited' => (bool) $post->is_edited, 'can_edit' => auth()->check() && ( (int) $post->user_id === (int) auth()->id() || Gate::allows('moderate-forum') ), 'current_user_id' => auth()->id(), 'user' => $user ? [ 'id' => $user->id, 'name' => $user->name, 'avatar_url' => AvatarUrl::forUser((int) $user->id, $user->profile?->avatar_hash ?? null), 'role' => $user->role ?? 'member', 'level' => (int) ($user->level ?? 1), 'rank' => (string) ($user->rank ?? 'Newbie'), ] : null, 'attachments' => collect($post->attachments ?? [])->map(fn ($a) => [ 'id' => $a->id, 'mime_type' => $a->mime_type, 'url' => $filesBaseUrl !== '' ? $filesBaseUrl . '/' . ltrim($a->file_path, '/') : '/' . ltrim($a->file_path, '/'), 'file_size' => $a->file_size, 'width' => $a->width, 'height' => $a->height, ])->values()->all(), ]; }; $serializedOp = isset($opPost) && $opPost ? $serializePost($opPost) : null; $serializedPosts = collect($posts->items())->map($serializePost)->values()->all(); $threadDescription = null; $threadDescriptionSource = (string) ($serializedOp['rendered_content'] ?? $serializedOp['content'] ?? ''); if ($threadDescriptionSource !== '') { $threadDescriptionSource = html_entity_decode($threadDescriptionSource, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $threadDescriptionSource = html_entity_decode($threadDescriptionSource, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $threadDescription = trim((string) preg_replace('/\s+/u', ' ', strip_tags($threadDescriptionSource))); $threadDescription = Str::limit($threadDescription, 220); } $paginationData = [ 'current_page' => $posts->currentPage(), 'last_page' => $posts->lastPage(), 'per_page' => $posts->perPage(), 'total' => $posts->total(), ]; $makeForumAuthor = function ($user) { if (! $user) { return null; } $name = trim((string) ($user->name ?? $user->username ?? '')); if ($name === '') { return null; } $username = trim((string) ($user->username ?? '')); return array_filter([ 'name' => $name, 'url' => $username !== '' ? url('/@' . ltrim($username, '@')) : null, ]); }; // Ensure we always provide a top-level author object for structured data. $topAuthor = $makeForumAuthor($author ?? $opPost?->user ?? null); if (! $topAuthor) { $topAuthor = ['name' => (string) ($opPost?->user?->name ?? $thread->user?->name ?? 'Skinbase')]; } $forumMicrodata = [ 'kind' => 'topic', 'canonical' => route('forum.thread.show', ['thread' => $thread->id, 'slug' => $thread->slug]), 'title' => (string) $thread->title, 'text' => $threadDescription, 'date_published' => $thread->created_at?->toIso8601String(), 'date_modified' => ($thread->last_post_at ?? $thread->updated_at)?->toIso8601String(), 'comment_count' => (int) ($reply_count ?? 0), 'author' => $topAuthor, 'comments' => collect([$opPost]) ->filter() ->merge($posts->getCollection()) ->map(function ($post) use ($makeForumAuthor, $thread) { $rendered = (string) ForumPostContent::render((string) ($post->content ?? '')); $text = trim((string) preg_replace('/\s+/u', ' ', strip_tags(html_entity_decode($rendered, ENT_QUOTES | ENT_HTML5, 'UTF-8')))); // If a post has no textual content, provide a short fallback so // the Comment structured data contains one of the required // representations (text/image/video). Prefer a simple label for // attachments or a generic reply placeholder. if ($text === '') { $hasAttachments = ! empty($post->attachments ?? null) && count($post->attachments ?? []) > 0; if ($hasAttachments) { $text = 'Attachment'; } else { $text = 'Reply'; } } return array_filter([ 'url' => route('forum.thread.show', ['thread' => $post->thread_id, 'slug' => $thread->slug]) . '#post-' . $post->id, 'text' => Str::limit($text, 300), 'date_published' => $post->created_at?->toIso8601String(), 'date_modified' => ($post->edited_at ?? $post->created_at)?->toIso8601String(), 'author' => $makeForumAuthor($post->user ?? null) ?: ['name' => (string) ($post->user?->name ?? 'Skinbase')], ], fn ($value) => $value !== null && $value !== ''); }) ->values() ->all(), ]; @endphp @section('content') @include('partials.seo.forum-microdata', ['forumMicrodata' => $forumMicrodata])
@php $forumThreadProps = json_encode([ 'thread' => [ 'id' => $thread->id, 'title' => $thread->title, 'slug' => $thread->slug, 'description'=> $threadDescription, 'views' => (int) ($thread->views ?? 0), 'is_pinned' => (bool) $thread->is_pinned, 'is_locked' => (bool) $thread->is_locked, 'created_at' => $thread->created_at?->toIso8601String(), ], 'category' => ['id' => $category->id ?? null, 'name' => $category->name ?? '', 'slug' => $category->slug ?? ''], 'author' => ['name' => $author->name ?? 'Skinbase'], 'opPost' => $serializedOp, 'posts' => $serializedPosts, 'pagination' => $paginationData, 'replyCount' => (int) ($reply_count ?? 0), 'sort' => $sort ?? 'asc', 'quotedPost' => $quoted_post ? ['user' => ['name' => data_get($quoted_post, 'user.name', 'Anonymous')]] : null, 'replyPrefill' => $reply_prefill ?? '', 'isAuthenticated' => auth()->check(), 'canModerate' => Gate::allows('moderate-forum'), 'csrfToken' => csrf_token(), 'status' => session('status'), ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE); @endphp @endsection @push('scripts') @vite(['resources/js/entry-forum.jsx']) @endpush