$this->queryRising($limit), ); } return $this->queryRising($limit); } public function invalidateCache(): void { foreach ([6, 18, 24, 36] as $limit) { Cache::forget('nova_cards.rising.' . $limit); } } private function queryRising(int $limit): Collection { $cutoff = Carbon::now()->subHours(self::WINDOW_HOURS); $isSqlite = DB::connection()->getDriverName() === 'sqlite'; $ageHoursExpression = $isSqlite ? "CASE WHEN ((julianday('now') - julianday(published_at)) * 24.0) < 1 THEN 1 ELSE ((julianday('now') - julianday(published_at)) * 24.0) END" : 'GREATEST(1, TIMESTAMPDIFF(HOUR, published_at, NOW()))'; $decayExpression = $isSqlite ? $ageHoursExpression : 'POWER(' . $ageHoursExpression . ', 0.7)'; $risingMomentumExpression = '( (saves_count * 5.0 + remixes_count * 6.0 + likes_count * 4.0 + favorites_count * 2.5 + comments_count * 2.0 + challenge_entries_count * 4.0) / ' . $decayExpression . ' ) AS rising_momentum'; return NovaCard::query() ->publiclyVisible() ->where('published_at', '>=', $cutoff) // Must have at least one meaningful engagement signal. ->where(function (Builder $q): void { $q->where('saves_count', '>', 0) ->orWhere('remixes_count', '>', 0) ->orWhere('likes_count', '>', 1); }) ->select([ 'nova_cards.*', // Rising score: weight recent engagement, penalise by sqrt(age hours) to let novelty show DB::raw($risingMomentumExpression), ]) ->orderByDesc('rising_momentum') ->orderByDesc('published_at') ->limit($limit) ->with(['user.profile', 'category', 'template', 'backgroundImage', 'tags']) ->get(); } }