sync: Redis caching for browse/skill API, progress bar on search, default sort by stars

- Add Redis caching to browse search results and skill detail API
- Show progress bar on homepage search
- Default sort to stars

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-22 16:32:07 +03:30
parent d19113795e
commit 328520bb70
6 changed files with 73 additions and 41 deletions

View File

@@ -6,7 +6,7 @@ import { BrowseFilters, SearchBar, Pagination, ActiveFilters, EmptyState } from
import { SkillCard } from '@/components/SkillCard';
import { toPersianNumber } from '@/lib/format-number';
import { getPageAlternates } from '@/lib/seo';
import { getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache';
import { getOrSetCache, cacheKeys, cacheTTL, hashSearchParams } from '@/lib/cache';
// Force dynamic rendering to fetch fresh data from database
@@ -25,6 +25,7 @@ interface BrowsePageProps {
}
// Get skills directly from database with filters - all filtering at database level
// Results are cached in Redis for 30 minutes, keyed by a hash of all filter parameters
async function getSkills(params: {
q?: string;
platform?: string;
@@ -34,48 +35,62 @@ async function getSkills(params: {
category?: string;
}) {
try {
const db = createDb();
const limit = 20;
const page = parseInt(params.page || '1');
const offset = (page - 1) * limit;
const sortMap: Record<string, 'stars' | 'downloads' | 'rating' | 'updated' | 'lastDownloaded'> = {
'stars': 'stars',
'downloads': 'downloads',
'recent': 'updated',
'rating': 'rating',
'lastDownloaded': 'lastDownloaded',
};
// Build filter options - push ALL filters to database level
const filterOptions = {
query: params.q,
const hash = hashSearchParams({
q: params.q,
category: params.category,
platform: params.platform && params.platform !== 'all' ? params.platform : undefined,
sourceFormat: params.format || 'skill.md',
sortBy: sortMap[params.sort || 'lastDownloaded'] || 'lastDownloaded',
sortOrder: 'desc' as const,
limit,
offset,
};
// Fetch paginated results directly from database
const skills = await skillQueries.search(db, filterOptions);
// Get accurate total count for pagination
const total = await skillQueries.count(db, {
query: params.q,
category: params.category,
platform: params.platform && params.platform !== 'all' ? params.platform : undefined,
sourceFormat: params.format || 'skill.md',
format: params.format || 'skill.md',
sort: params.sort || 'lastDownloaded',
page,
});
const totalPages = Math.ceil(total / limit);
return await getOrSetCache(
cacheKeys.searchSkills(hash),
cacheTTL.search,
async () => {
const db = createDb();
const offset = (page - 1) * limit;
return {
skills,
pagination: { total, page, totalPages },
};
const sortMap: Record<string, 'stars' | 'downloads' | 'rating' | 'updated' | 'lastDownloaded'> = {
'stars': 'stars',
'downloads': 'downloads',
'recent': 'updated',
'rating': 'rating',
'lastDownloaded': 'lastDownloaded',
};
const filterOptions = {
query: params.q,
category: params.category,
platform: params.platform && params.platform !== 'all' ? params.platform : undefined,
sourceFormat: params.format || 'skill.md',
sortBy: sortMap[params.sort || 'lastDownloaded'] || 'lastDownloaded',
sortOrder: 'desc' as const,
limit,
offset,
};
const [skills, total] = await Promise.all([
skillQueries.search(db, filterOptions),
skillQueries.count(db, {
query: params.q,
category: params.category,
platform: params.platform && params.platform !== 'all' ? params.platform : undefined,
sourceFormat: params.format || 'skill.md',
}),
]);
const totalPages = Math.ceil(total / limit);
return {
skills,
pagination: { total, page, totalPages },
};
}
);
} catch (error) {
console.error('Error fetching skills:', error);
return { skills: [], pagination: { total: 0, page: 1, totalPages: 1 } };

View File

@@ -1,6 +1,6 @@
import { NextResponse, type NextRequest } from 'next/server';
import { createDb, skillQueries } from '@skillhub/db';
import { shouldCountView } from '@/lib/cache';
import { shouldCountView, getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache';
// Create database connection
const db = createDb();
@@ -35,8 +35,12 @@ export async function GET(
const { id } = await params;
const skillId = id.join('/');
// Get skill from database
const skill = await skillQueries.getById(db, skillId);
// Get skill from database (cached 1h)
const skill = await getOrSetCache(
cacheKeys.skill(skillId),
cacheTTL.skill,
() => skillQueries.getById(db, skillId)
);
if (!skill) {
return NextResponse.json(

View File

@@ -1,6 +1,6 @@
import { NextResponse, type NextRequest } from 'next/server';
import { createDb, skillQueries } from '@skillhub/db';
import { invalidateCache, cacheKeys, shouldCountDownload } from '@/lib/cache';
import { invalidateCache, invalidateCachePattern, cacheKeys, shouldCountDownload } from '@/lib/cache';
// Create database connection
const db = createDb();
@@ -69,12 +69,13 @@ export async function POST(request: NextRequest) {
await skillQueries.incrementDownloads(db, skillId);
}
// Invalidate relevant caches so featured/recent lists reflect the new download
// Invalidate relevant caches so featured/recent/browse lists reflect the new download
await Promise.all([
invalidateCache(cacheKeys.featuredSkills()),
invalidateCache(cacheKeys.recentSkills()),
invalidateCache(cacheKeys.stats()),
invalidateCache(cacheKeys.skill(skillId)),
invalidateCachePattern('skills:search:*'),
]);
return NextResponse.json({