get('per_page', 12), 50); $page = (int) $request->get('page', 1); $cacheKey = "stories:api:list:{$perPage}:{$page}"; $stories = Cache::remember($cacheKey, 300, fn () => Story::published() ->with('creator.profile', 'tags') ->orderByDesc('published_at') ->paginate($perPage, ['*'], 'page', $page) ); return response()->json([ 'data' => $stories->getCollection()->map(fn (Story $s) => $this->formatCard($s)), 'meta' => [ 'current_page' => $stories->currentPage(), 'last_page' => $stories->lastPage(), 'per_page' => $stories->perPage(), 'total' => $stories->total(), ], ]); } /** * Single story detail. * GET /api/stories/{slug} */ public function show(string $slug): JsonResponse { $story = Cache::remember('stories:api:' . $slug, 600, fn () => Story::published() ->with('creator.profile', 'tags') ->where('slug', $slug) ->firstOrFail() ); return response()->json($this->formatFull($story)); } /** * Featured story. * GET /api/stories/featured */ public function featured(): JsonResponse { $story = Cache::remember('stories:api:featured', 300, fn () => Story::published()->featured() ->with('creator.profile', 'tags') ->orderByDesc('published_at') ->first() ); if (! $story) { return response()->json(null); } return response()->json($this->formatFull($story)); } /** * Stories by tag. * GET /api/stories/tag/{tag}?page=1 */ public function byTag(Request $request, string $tag): JsonResponse { $storyTag = StoryTag::where('slug', $tag)->firstOrFail(); $page = (int) $request->get('page', 1); $stories = Cache::remember("stories:api:tag:{$tag}:{$page}", 300, fn () => Story::published() ->with('creator.profile', 'tags') ->whereHas('tags', fn ($q) => $q->where('story_tags.id', $storyTag->id)) ->orderByDesc('published_at') ->paginate(12, ['*'], 'page', $page) ); return response()->json([ 'tag' => ['id' => $storyTag->id, 'slug' => $storyTag->slug, 'name' => $storyTag->name], 'data' => $stories->getCollection()->map(fn (Story $s) => $this->formatCard($s)), 'meta' => [ 'current_page' => $stories->currentPage(), 'last_page' => $stories->lastPage(), 'per_page' => $stories->perPage(), 'total' => $stories->total(), ], ]); } /** * Stories by author. * GET /api/stories/author/{username}?page=1 */ public function byAuthor(Request $request, string $username): JsonResponse { $author = User::query()->whereRaw('LOWER(username) = ?', [strtolower($username)])->firstOrFail(); $page = (int) $request->get('page', 1); $stories = Cache::remember("stories:api:author:{$author->id}:{$page}", 300, fn () => Story::published() ->with('creator.profile', 'tags') ->where('creator_id', $author->id) ->orderByDesc('published_at') ->paginate(12, ['*'], 'page', $page) ); return response()->json([ 'author' => $this->formatCreator($author), 'data' => $stories->getCollection()->map(fn (Story $s) => $this->formatCard($s)), 'meta' => [ 'current_page' => $stories->currentPage(), 'last_page' => $stories->lastPage(), 'per_page' => $stories->perPage(), 'total' => $stories->total(), ], ]); } // ── Private formatters ──────────────────────────────────────────────── private function formatCard(Story $story): array { return [ 'id' => $story->id, 'slug' => $story->slug, 'url' => $story->url, 'title' => $story->title, 'excerpt' => $story->excerpt, 'cover_image' => $story->cover_url, 'author' => $story->creator ? $this->formatCreator($story->creator) : null, 'tags' => $story->tags->map(fn ($t) => ['id' => $t->id, 'slug' => $t->slug, 'name' => $t->name, 'url' => $t->url]), 'views' => $story->views, 'featured' => $story->featured, 'reading_time' => $story->reading_time, 'published_at' => $story->published_at?->toIso8601String(), ]; } private function formatFull(Story $story): array { return array_merge($this->formatCard($story), [ 'content' => $story->content, ]); } private function formatCreator(User $creator): array { $avatarHash = $creator->profile?->avatar_hash; return [ 'id' => $creator->id, 'name' => $creator->username ?? $creator->name, 'avatar_url' => $avatarHash ? \App\Support\AvatarUrl::forUser((int) $creator->id, $avatarHash, 96) : \App\Support\AvatarUrl::default(), 'bio' => $creator->profile?->about, 'profile_url' => '/@' . strtolower((string) ($creator->username ?? $creator->id)), ]; } }