active() ->published() ->latest('published_at') ->paginate(12) ->withQueryString(); $packs->getCollection()->transform(fn (AcademyPromptPack $pack): array => $this->access->packPayload($pack, $request->user())); $seo = app(SeoFactory::class) ->collectionListing( 'Academy Prompt Packs — Skinbase', 'Preview bundled prompt packs for Skinbase wallpapers, worlds, mascots, and editorial workflows.', route('academy.packs.index'), ) ->toArray(); $seo = SeoDataBuilder::fromArray($seo) ->breadcrumbs([ ['name' => 'Academy', 'url' => route('academy.index')], ['name' => 'Prompt Packs', 'url' => route('academy.packs.index')], ]) ->build() ->toArray(); return Inertia::render('Academy/List', [ 'pageType' => 'packs', 'title' => 'Prompt packs', 'description' => 'Curated prompt bundles with free previews and premium pack access tiers.', 'seo' => $seo, 'items' => $packs, 'filters' => [], 'categories' => [], 'pricingUrl' => route('academy.pricing'), 'analytics' => [ 'enabled' => true, 'contentType' => AcademyAnalyticsContentType::PROMPT_PACK_LIBRARY, 'contentId' => null, 'eventUrl' => route('academy.analytics.events.store'), 'pageName' => 'academy_packs_index', 'isPremium' => false, 'isGuest' => $request->user() === null, 'isSubscriber' => $request->user()?->hasAcademyCreatorAccess() || $request->user()?->hasAcademyProAccess(), ], ])->rootView('academy'); } public function show(Request $request, string $slug): Response { abort_unless((bool) config('academy.enabled', true), 404); $pack = AcademyPromptPack::query() ->with(['prompts.category']) ->active() ->published() ->where('slug', $slug) ->firstOrFail(); $payload = $this->access->packPayload($pack, $request->user(), true); $canonical = route('academy.packs.show', ['slug' => $pack->slug]); $description = Str::limit((string) ($pack->excerpt ?? $pack->description ?? ''), 160, '...'); $seo = SeoDataBuilder::fromArray( app(SeoFactory::class)->collectionPage( $pack->title . ' — Skinbase Academy', $description, $canonical, $pack->cover_image, )->toArray() ) ->breadcrumbs([ ['name' => 'Academy', 'url' => route('academy.index')], ['name' => 'Prompt Packs', 'url' => route('academy.packs.index')], ['name' => (string) $pack->title, 'url' => $canonical], ]) ->addJsonLd($this->packStructuredData($payload, $canonical, $description)) ->build() ->toArray(); $interaction = $this->interactions->getInteractionState($request->user(), AcademyAnalyticsContentType::PROMPT_PACK, (int) $pack->id); return Inertia::render('Academy/Show', [ 'pageType' => 'pack', 'item' => $payload, 'seo' => $seo, 'pricingUrl' => route('academy.pricing'), 'interaction' => $interaction, 'interactionRoutes' => [ 'like' => route('academy.interactions.like'), 'save' => route('academy.interactions.save'), ], 'loginUrl' => route('login'), 'analytics' => [ 'enabled' => true, 'contentType' => AcademyAnalyticsContentType::PROMPT_PACK, 'contentId' => (int) $pack->id, 'eventUrl' => route('academy.analytics.events.store'), 'pageName' => 'academy_pack_show', 'isPremium' => (string) ($pack->access_level ?? 'free') !== 'free', 'isGuest' => $request->user() === null, 'isSubscriber' => $request->user()?->hasAcademyCreatorAccess() || $request->user()?->hasAcademyProAccess(), 'isLocked' => (bool) ($payload['locked'] ?? false), ], ])->rootView('academy'); } /** * @param array $payload * @return array */ private function packStructuredData(array $payload, string $canonical, string $description): array { $image = trim((string) ($payload['cover_image'] ?? '')); $imageUrl = $image !== '' && preg_match('/^https?:\/\//i', $image) === 1 ? $image : ($image !== '' ? url($image) : null); $keywords = array_values((array) ($payload['tags'] ?? [])); $isFree = (string) ($payload['access_level'] ?? 'free') === 'free'; $promptEntries = collect((array) ($payload['prompts'] ?? [])) ->map(function (array $prompt): ?array { $title = trim((string) ($prompt['title'] ?? '')); $slug = trim((string) ($prompt['slug'] ?? '')); if ($title === '' || $slug === '') { return null; } return [ '@type' => 'ListItem', 'position' => null, 'item' => [ '@type' => 'CreativeWork', 'name' => $title, 'url' => route('academy.prompts.show', ['slug' => $slug]), ], ]; }) ->filter() ->values() ->map(function (array $item, int $index): array { $item['position'] = $index + 1; return $item; }) ->all(); return array_filter([ '@context' => 'https://schema.org', '@type' => ['CreativeWork', 'LearningResource'], 'name' => (string) ($payload['title'] ?? 'Skinbase Academy prompt pack'), 'description' => $description, 'url' => $canonical, 'image' => $imageUrl, 'keywords' => $keywords !== [] ? $keywords : null, 'isAccessibleForFree' => $isFree, 'hasPart' => $promptEntries !== [] ? [ '@type' => 'ItemList', 'itemListElement' => $promptEntries, ] : null, ], fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []); } }