Files
skillhub/apps/web/app/api/skills/[...id]/route.ts
airano 3e22d2e238 sync: AI review scores, reviewed page, search improvements, score filter
- Surface AI review scores across skill pages, browse, and search
- Add dedicated /reviewed page with score filtering
- Add recommended sort using Meilisearch relevance + AI scores
- Fix search sort defaulting to stars instead of recommended
- Fix CLI TypeScript null check for aiScore in search command
- Adjust cache TTL for reviewed content

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 05:05:37 +03:30

117 lines
3.5 KiB
TypeScript

import { NextResponse, type NextRequest } from 'next/server';
import { createDb, skillQueries, skillReviewQueries } from '@skillhub/db';
import { shouldCountView, getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache';
// Create database connection
const db = createDb();
/**
* Get client IP from request headers
* Handles various proxy headers (Cloudflare, nginx, etc.)
*/
function getClientIp(request: NextRequest): string {
// Try various headers in order of preference
const cfConnectingIp = request.headers.get('cf-connecting-ip');
if (cfConnectingIp) return cfConnectingIp;
const xRealIp = request.headers.get('x-real-ip');
if (xRealIp) return xRealIp;
const xForwardedFor = request.headers.get('x-forwarded-for');
if (xForwardedFor) {
// x-forwarded-for can contain multiple IPs, take the first one
return xForwardedFor.split(',')[0].trim();
}
// Fallback to a default (shouldn't happen in production)
return 'unknown';
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string[] }> }
) {
try {
const { id } = await params;
const skillId = id.join('/');
// Get skill and latest review from database (cached 1h)
const [skill, review] = await Promise.all([
getOrSetCache(
cacheKeys.skill(skillId),
cacheTTL.skill,
() => skillQueries.getById(db, skillId)
),
getOrSetCache(
cacheKeys.skillReview(skillId),
cacheTTL.skill,
() => skillReviewQueries.getLatestBySkillId(db, skillId)
),
]);
if (!skill) {
return NextResponse.json(
{ error: 'Skill not found' },
{ status: 404 }
);
}
// Increment view count only on primary server (mirror DB is read-only)
const isPrimary = process.env.IS_PRIMARY_SERVER !== 'false';
if (isPrimary) {
const clientIp = getClientIp(request);
const shouldCount = await shouldCountView(skillId, clientIp);
if (shouldCount) {
await skillQueries.incrementViews(db, skillId);
}
}
return NextResponse.json({
id: skill.id,
name: skill.name,
description: skill.description,
githubOwner: skill.githubOwner,
githubRepo: skill.githubRepo,
skillPath: skill.skillPath,
branch: skill.branch,
version: skill.version,
license: skill.license,
author: skill.author,
homepage: skill.homepage,
githubStars: skill.githubStars,
githubForks: skill.githubForks,
downloadCount: skill.downloadCount,
viewCount: skill.viewCount,
securityScore: skill.securityScore,
isVerified: skill.isVerified,
isFeatured: skill.isFeatured,
compatibility: skill.compatibility,
triggers: skill.triggers,
rawContent: skill.rawContent,
sourceFormat: skill.sourceFormat || 'skill.md',
reviewStatus: skill.reviewStatus,
aiScore: skill.latestAiScore,
createdAt: skill.createdAt,
updatedAt: skill.updatedAt,
indexedAt: skill.indexedAt,
review: review ? {
aiScore: review.aiScore,
instructionQuality: review.instructionQuality,
descriptionPrecision: review.descriptionPrecision,
usefulness: review.usefulness,
technicalSoundness: review.technicalSoundness,
reviewNotes: review.reviewNotes,
reviewer: review.reviewer,
reviewedAt: review.reviewedAt,
} : null,
});
} catch (error) {
console.error('Error fetching skill:', error);
return NextResponse.json(
{ error: 'Failed to fetch skill' },
{ status: 500 }
);
}
}