Files
gamelog/docs/superpowers/plans/2026-05-30-follow-feed-notifications-sitemap-search-cache.md
T

13 KiB

GameLog Feature Batch Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add follow system, activity feed, notifications, sitemap, Scout search, and caching to the GameLog Laravel 11 app.

Architecture: All features hook into existing User/Game/UserGame/Review models. Migrations for follows/activities already ran. The nav (layouts/app.blade.php) handles all global UI additions. Scout uses the database driver for dev (SQLite).

Tech Stack: Laravel 13 + Tailwind + Alpine.js, SQLite dev, spatie/laravel-sitemap, laravel/scout


Task 1: FollowController + Routes

Files:

  • Create: app/Http/Controllers/FollowController.php

  • Modify: routes/web.php

  • Create FollowController with store/destroy returning JSON

<?php
namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\JsonResponse;

class FollowController extends Controller
{
    public function store(User $user): JsonResponse
    {
        $me = auth()->user();
        if ($me->id === $user->id) {
            return response()->json(['error' => 'Cannot follow yourself'], 422);
        }
        if (!$me->following()->where('following_id', $user->id)->exists()) {
            $me->following()->attach($user->id);
            $user->notifications()->create([
                'id'   => \Illuminate\Support\Str::uuid(),
                'type' => 'new_follower',
                'data' => json_encode([
                    'follower_id'       => $me->id,
                    'follower_username' => $me->username,
                    'follower_name'     => $me->name,
                ]),
            ]);
        }
        return response()->json([
            'following' => true,
            'count'     => $user->followers()->count(),
        ]);
    }

    public function destroy(User $user): JsonResponse
    {
        auth()->user()->following()->detach($user->id);
        return response()->json([
            'following' => false,
            'count'     => $user->followers()->count(),
        ]);
    }
}
  • Add routes to web.php (auth group)
Route::middleware('auth')->group(function () {
    Route::post('/users/{user:username}/follow', [\App\Http\Controllers\FollowController::class, 'store'])->name('users.follow');
    Route::delete('/users/{user:username}/follow', [\App\Http\Controllers\FollowController::class, 'destroy'])->name('users.unfollow');
});
  • Commit: git commit -m "feat: add FollowController and routes"

Task 2: Notifications Migration + Model

Files:

  • Create: database/migrations/2026_05_30_000001_create_notifications_table.php

  • Create: app/Models/Notification.php

  • Create notifications migration

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void {
        Schema::create('notifications', function (Blueprint $table) {
            $table->uuid('id')->primary();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->string('type', 100);
            $table->json('data');
            $table->timestamp('read_at')->nullable();
            $table->timestamp('created_at')->nullable();
        });
    }
    public function down(): void { Schema::dropIfExists('notifications'); }
};
  • Create Notification model
<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Notification extends Model
{
    public $incrementing = false;
    protected $keyType = 'string';
    public $timestamps = false;
    protected $fillable = ['id', 'user_id', 'type', 'data', 'read_at'];
    protected $casts = ['data' => 'array', 'read_at' => 'datetime', 'created_at' => 'datetime'];

    public function user(): BelongsTo { return $this->belongsTo(User::class); }
}
  • Run migration: php artisan migrate

  • Commit: git commit -m "feat: add notifications table and model"


Task 3: Follow Button on users/show.blade.php

Files:

  • Modify: resources/views/users/show.blade.php

  • Add Alpine.js follow button block after <div class="flex gap-2"> (replaces the lone "View Library" button container)

Replace the <div class="flex gap-2"> block with:

<div class="flex items-center gap-4">
    {{-- Follower / Following counts --}}
    <div class="flex gap-4 text-center">
        <div>
            <div class="text-lg font-bold text-white">{{ $user->followers()->count() }}</div>
            <div class="text-xs text-slate-500 uppercase tracking-wide">Followers</div>
        </div>
        <div>
            <div class="text-lg font-bold text-white">{{ $user->following()->count() }}</div>
            <div class="text-xs text-slate-500 uppercase tracking-wide">Following</div>
        </div>
    </div>

    <div class="flex gap-2">
        @auth
        @if(auth()->id() !== $user->id)
        <div x-data="{
            following: {{ auth()->user()->isFollowing($user) ? 'true' : 'false' }},
            count: {{ $user->followers()->count() }},
            async toggle() {
                const method = this.following ? 'DELETE' : 'POST';
                const res = await fetch('{{ route('users.follow', $user->username) }}', {
                    method,
                    headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content, 'Accept': 'application/json' }
                });
                if (res.ok) {
                    const d = await res.json();
                    this.following = d.following;
                    this.count = d.count;
                }
            }
        }">
            <button @click="toggle()"
                :class="following ? 'bg-white/10 text-slate-300 hover:bg-red-900/30 hover:text-red-400 border border-white/10' : 'bg-purple-600 hover:bg-purple-700 text-white'"
                class="px-4 py-2 text-sm font-medium rounded-lg transition-colors">
                <span x-text="following ? 'Following' : 'Follow'"></span>
            </button>
        </div>
        @endif
        @endauth
        <a href="{{ route('users.library', $user->username) }}"
           class="px-4 py-2 border border-white/10 hover:border-white/20 text-slate-300 hover:text-white text-sm font-medium rounded-lg transition-colors">
            View Library
        </a>
    </div>
</div>
  • Commit: git commit -m "feat: add follow button and follower counts to user profile"

Task 4: ActivityService + Hook into Models

Files:

  • Create: app/Services/ActivityService.php

  • Modify: app/Models/UserGame.php

  • Modify: app/Models/Review.php

  • Create ActivityService (thin wrapper around Activity::log)

<?php
namespace App\Services;

use App\Models\Activity;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;

class ActivityService
{
    public static function log(User $user, string $type, Model $subject, array $data = []): void
    {
        Activity::log($user->id, $type, $subject, $data);
    }
}
  • Add boot() hooks to UserGame model
use App\Services\ActivityService;

protected static function boot(): void
{
    parent::boot();

    static::created(function (UserGame $ug) {
        ActivityService::log($ug->user, 'added_to_library', $ug->game, ['status' => $ug->status]);
    });

    static::updated(function (UserGame $ug) {
        if ($ug->isDirty('rating') && $ug->rating) {
            ActivityService::log($ug->user, 'rated_game', $ug->game, ['rating' => $ug->rating]);
        }
    });
}
  • Add to Review model boot() after the existing saved/deleted callbacks
use App\Services\ActivityService;

// Inside the static::created callback (add this inside the boot method after existing code):
static::created(function (Review $review) {
    ActivityService::log($review->user, 'reviewed_game', $review->game, ['rating' => $review->rating]);
});
  • Commit: git commit -m "feat: add ActivityService and log hooks on UserGame/Review"

Task 5: ActivityController + Routes + View

Files:

  • Create: app/Http/Controllers/ActivityController.php

  • Modify: routes/web.php

  • Create: resources/views/activity/index.blade.php

  • Create ActivityController

<?php
namespace App\Http\Controllers;

use App\Models\Activity;
use Illuminate\Support\Facades\DB;

class ActivityController extends Controller
{
    public function index()
    {
        $user = auth()->user();
        $followingIds = $user->following()->pluck('users.id')->push($user->id);

        $activities = Activity::with(['user', 'subject'])
            ->whereIn('user_id', $followingIds)
            ->latest('created_at')
            ->paginate(40);

        return view('activity.index', compact('activities'));
    }

    public function global()
    {
        $activities = Activity::with(['user', 'subject'])
            ->latest('created_at')
            ->paginate(40);

        return view('activity.index', compact('activities'));
    }
}
  • Add activity routes to web.php
Route::get('/activity', [\App\Http\Controllers\ActivityController::class, 'index'])->name('activity.index')->middleware('auth');
Route::get('/activity/global', [\App\Http\Controllers\ActivityController::class, 'global'])->name('activity.global');
  • Create activity/index.blade.php view (see full content below)

  • Commit: git commit -m "feat: add activity feed controller, routes, and view"


Task 6: NotificationController + Routes + View

Files:

  • Create: app/Http/Controllers/NotificationController.php

  • Modify: routes/web.php

  • Create: resources/views/notifications/index.blade.php

  • Create NotificationController

<?php
namespace App\Http\Controllers;

use App\Models\Notification;

class NotificationController extends Controller
{
    public function index()
    {
        $notifications = auth()->user()
            ->notifications()
            ->latest('created_at')
            ->limit(20)
            ->get();

        auth()->user()->notifications()->whereNull('read_at')->update(['read_at' => now()]);

        return view('notifications.index', compact('notifications'));
    }

    public function markRead(Notification $notification)
    {
        abort_if($notification->user_id !== auth()->id(), 403);
        $notification->update(['read_at' => now()]);
        return response()->json(['ok' => true]);
    }
}
  • Add notification routes to web.php (auth group)
Route::get('/notifications', [\App\Http\Controllers\NotificationController::class, 'index'])->name('notifications.index')->middleware('auth');
Route::post('/notifications/{notification}/read', [\App\Http\Controllers\NotificationController::class, 'markRead'])->name('notifications.read')->middleware('auth');
  • Add to User model: public function notifications() { return $this->hasMany(Notification::class)->latest('created_at'); }

  • Create notifications/index.blade.php view

  • Add bell icon to nav in layouts/app.blade.php

  • Commit: git commit -m "feat: add notifications controller, routes, view, and nav bell"


Task 7: Sitemap

Files:

  • Run composer require

  • Create: app/Console/Commands/GenerateSitemapCommand.php

  • Modify: routes/console.php (Laravel 11 schedules here)

  • Modify: routes/web.php

  • Modify: public/robots.txt

  • Install spatie/laravel-sitemap: composer require spatie/laravel-sitemap

  • Create GenerateSitemapCommand

  • Add weekly schedule in routes/console.php

  • Add sitemap route in web.php

  • Update robots.txt

  • Commit: git commit -m "feat: add sitemap generation command and route"


Files:

  • Run composer require + vendor:publish

  • Modify: app/Models/Game.php

  • Modify: app/Http/Controllers/GameController.php

  • Modify: .env

  • Install Scout: composer require laravel/scout && php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"

  • Add SCOUT_DRIVER=database to .env

  • Add Searchable trait and toSearchableArray to Game model

  • Update GameController@search to use Scout

  • Commit: git commit -m "feat: add Laravel Scout search to Game model"


Task 9: Performance Caching

Files:

  • Modify: app/Http/Controllers/HomeController.php

  • Modify: app/Http/Controllers/GameController.php

  • Modify: app/Models/Game.php

  • Wrap HomeController queries in Cache::remember

  • Wrap GameController topRated/newReleases in Cache::remember with page key

  • Flush caches in Game::recalculateRating()

  • Add cache header to GameController@show response

  • Commit: git commit -m "feat: add file cache to home/top-rated/new-releases queries"