string('category')->toString(); $search = $request->string('q')->toString(); $skills = Skill::approved() ->when($category, fn ($query) => $query->where('category', $category)) ->when($search, fn ($query) => $query->where('name', 'like', "%{$search}%")) ->orderByDesc('featured') ->orderByDesc('views') ->paginate(12) ->withQueryString(); return view('skills.index', [ 'skills' => $skills, 'categories' => config('skills.categories'), 'activeCategory' => $category, 'search' => $search, 'title' => 'Claude Skills Directory — '.config('app.name'), 'description' => 'Discover Claude Skills, with docs, install instructions, and example prompts.', 'canonical' => route('skills.index'), ]); } public function category(string $category): View { abort_unless(array_key_exists($category, config('skills.categories')), 404); $skills = Skill::approved() ->where('category', $category) ->orderByDesc('featured') ->orderByDesc('views') ->paginate(12); $label = config("skills.categories.{$category}"); return view('skills.category', [ 'skills' => $skills, 'categories' => config('skills.categories'), 'category' => $category, 'label' => $label, 'title' => $label.' Claude Skills — '.config('app.name'), 'description' => "Claude Skills for {$label}.", 'canonical' => route('skills.category', $category), ]); } public function show(Skill $skill): View { abort_unless($skill->approved, 404); $skill->increment('views'); $related = Skill::approved() ->where('category', $skill->category) ->whereKeyNot($skill->id) ->limit(4) ->get(); return view('skills.show', [ 'skill' => $skill, 'related' => $related, 'title' => $skill->name.' — Claude Skills — '.config('app.name'), 'description' => $this->metaDescription($skill->description), 'canonical' => route('skills.show', $skill), 'image' => $skill->logo_url, ]); } public function visit(Skill $skill): RedirectResponse { abort_unless($skill->approved, 404); $skill->increment('clicks'); return redirect()->away($skill->url); } /** * Truncate to ~155 chars without cutting a word in half. Google appends * its own ellipsis when it truncates in search results, so we don't add one. */ private function metaDescription(string $description, int $limit = 155): string { if (Str::length($description) <= $limit) { return $description; } return Str::of($description)->limit($limit, '')->beforeLast(' ')->toString(); } }