sync: Redis caching, progress bar rewrite, sentry filtering, UI improvements
- Add Redis caching to all pages and uncached API routes - Rewrite progress bar with direct DOM manipulation for search/filters/navigation - Filter generic network errors from Sentry client reporting - Fix non-null assertion for repo.owner in popular-repos strategy - Remove next-nprogress-bar dependency Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +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';
|
||||
|
||||
|
||||
// Force dynamic rendering to fetch fresh data from database
|
||||
@@ -108,7 +109,7 @@ export default async function BrowsePage({ params, searchParams }: BrowsePagePro
|
||||
{ id: 'rating', name: t('filters.sortOptions.rating') },
|
||||
];
|
||||
|
||||
// Fetch categories hierarchically for filter dropdown with translations
|
||||
// Fetch categories hierarchically for filter dropdown with translations (cached 12h)
|
||||
const tCategories = await getTranslations('categories');
|
||||
type HierarchicalCategory = {
|
||||
id: string;
|
||||
@@ -119,8 +120,14 @@ export default async function BrowsePage({ params, searchParams }: BrowsePagePro
|
||||
};
|
||||
let categories: HierarchicalCategory[] = [];
|
||||
try {
|
||||
const db = createDb();
|
||||
const rawCategories = await categoryQueries.getHierarchical(db);
|
||||
const rawCategories = await getOrSetCache(
|
||||
cacheKeys.categoriesHierarchical(),
|
||||
cacheTTL.categories,
|
||||
async () => {
|
||||
const db = createDb();
|
||||
return await categoryQueries.getHierarchical(db);
|
||||
}
|
||||
);
|
||||
categories = rawCategories.map(parent => ({
|
||||
id: parent.id,
|
||||
name: tCategories(`parents.${parent.slug}`) || parent.name,
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
import { createDb, categoryQueries } from '@skillhub/db';
|
||||
import { formatNumber } from '@/lib/format-number';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
import { getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache';
|
||||
|
||||
|
||||
// Force dynamic rendering to fetch fresh data from database
|
||||
@@ -87,11 +88,13 @@ const parentColorMap: Record<string, string> = {
|
||||
'specialized': 'bg-gray-50 text-gray-600 dark:bg-gray-800 dark:text-gray-400',
|
||||
};
|
||||
|
||||
// Get categories hierarchically from database
|
||||
// Get categories hierarchically with Redis caching (12 hour TTL)
|
||||
async function getHierarchicalCategories() {
|
||||
try {
|
||||
const db = createDb();
|
||||
return await categoryQueries.getHierarchical(db);
|
||||
return await getOrSetCache(cacheKeys.categoriesHierarchical(), cacheTTL.categories, async () => {
|
||||
const db = createDb();
|
||||
return await categoryQueries.getHierarchical(db);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories:', error);
|
||||
return [];
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Footer } from '@/components/Footer';
|
||||
import { EarlyAccessForm } from '@/components/EarlyAccessForm';
|
||||
import { createDb, skills, sql } from '@skillhub/db';
|
||||
import { formatCompactNumber } from '@/lib/format-number';
|
||||
import { getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -42,11 +43,13 @@ export async function generateMetadata({
|
||||
|
||||
async function getStats() {
|
||||
try {
|
||||
const db = createDb();
|
||||
const skillsResult = await db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(skills);
|
||||
return skillsResult[0]?.count ?? 0;
|
||||
return await getOrSetCache(cacheKeys.pageCount('claude-plugin'), cacheTTL.pageCount, async () => {
|
||||
const db = createDb();
|
||||
const skillsResult = await db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(skills);
|
||||
return skillsResult[0]?.count ?? 0;
|
||||
});
|
||||
} catch {
|
||||
return 119000;
|
||||
}
|
||||
|
||||
@@ -7,18 +7,21 @@ import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
import { createDb, skills, sql } from '@skillhub/db';
|
||||
import { formatPromptSkillCount } from '@/lib/format-number';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
import { getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache';
|
||||
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
async function getSkillCount(): Promise<string> {
|
||||
try {
|
||||
const db = createDb();
|
||||
const result = await db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(skills)
|
||||
.where(sql`${skills.isDuplicate} = false`);
|
||||
return formatPromptSkillCount(result[0]?.count ?? 16000);
|
||||
return await getOrSetCache(cacheKeys.pageCount('getting-started'), cacheTTL.pageCount, async () => {
|
||||
const db = createDb();
|
||||
const result = await db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(skills)
|
||||
.where(sql`${skills.isDuplicate} = false`);
|
||||
return formatPromptSkillCount(result[0]?.count ?? 16000);
|
||||
});
|
||||
} catch {
|
||||
return '16,000+';
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { toPersianNumber } from '@/lib/format-number';
|
||||
import { Pagination } from '@/components/BrowseFilters';
|
||||
import { SkillCard } from '@/components/SkillCard';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
import { getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache';
|
||||
|
||||
|
||||
// Force dynamic rendering to fetch fresh data from database
|
||||
@@ -16,23 +17,25 @@ interface FeaturedPageProps {
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
}
|
||||
|
||||
// Get featured skills with pagination
|
||||
// Get featured skills with pagination and Redis caching (2 hour TTL)
|
||||
async function getFeaturedSkills(page: number, limit: number) {
|
||||
try {
|
||||
const db = createDb();
|
||||
const offset = (page - 1) * limit;
|
||||
return await getOrSetCache(cacheKeys.featuredPage(page), cacheTTL.featured, async () => {
|
||||
const db = createDb();
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// Try featured first, fall back to combined popularity score
|
||||
let featuredSkills = await skillQueries.getFeatured(db, limit, offset);
|
||||
let total = await skillQueries.countFeatured(db);
|
||||
// Try featured first, fall back to combined popularity score
|
||||
let featuredSkills = await skillQueries.getFeatured(db, limit, offset);
|
||||
let total = await skillQueries.countFeatured(db);
|
||||
|
||||
// If no featured skills, use adaptive popularity with owner/repo diversity
|
||||
if (total === 0) {
|
||||
featuredSkills = await skillQueries.getFeaturedWithDiversity(db, limit, 2, 3);
|
||||
total = await skillQueries.countAll(db);
|
||||
}
|
||||
// If no featured skills, use adaptive popularity with owner/repo diversity
|
||||
if (total === 0) {
|
||||
featuredSkills = await skillQueries.getFeaturedWithDiversity(db, limit, 2, 3);
|
||||
total = await skillQueries.countAll(db);
|
||||
}
|
||||
|
||||
return { skills: featuredSkills, total };
|
||||
return { skills: featuredSkills, total };
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching featured skills:', error);
|
||||
return { skills: [], total: 0 };
|
||||
|
||||
@@ -8,6 +8,7 @@ import { createDb, skillQueries } from '@skillhub/db';
|
||||
import { toPersianNumber } from '@/lib/format-number';
|
||||
import { Pagination } from '@/components/BrowseFilters';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
import { getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache';
|
||||
|
||||
|
||||
// Force dynamic rendering to fetch fresh data from database
|
||||
@@ -41,36 +42,40 @@ function formatTimeAgo(date: Date | null, locale: string): string {
|
||||
return 'Just now';
|
||||
}
|
||||
|
||||
// Get skills based on tab with pagination
|
||||
// Get skills based on tab with pagination and Redis caching (30 min TTL)
|
||||
async function getSkillsForTab(tab: 'new' | 'updated', page: number, limit: number) {
|
||||
try {
|
||||
const db = createDb();
|
||||
const offset = (page - 1) * limit;
|
||||
return await getOrSetCache(cacheKeys.newSkills(tab, page), cacheTTL.newSkills, async () => {
|
||||
const db = createDb();
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
if (tab === 'new') {
|
||||
const skills = await skillQueries.getNewSkills(db, limit, offset);
|
||||
const total = await skillQueries.countNewSkills(db);
|
||||
return { skills, total };
|
||||
} else {
|
||||
const skills = await skillQueries.getUpdatedSkills(db, limit, offset);
|
||||
const total = await skillQueries.countUpdatedSkills(db);
|
||||
return { skills, total };
|
||||
}
|
||||
if (tab === 'new') {
|
||||
const skills = await skillQueries.getNewSkills(db, limit, offset);
|
||||
const total = await skillQueries.countNewSkills(db);
|
||||
return { skills, total };
|
||||
} else {
|
||||
const skills = await skillQueries.getUpdatedSkills(db, limit, offset);
|
||||
const total = await skillQueries.countUpdatedSkills(db);
|
||||
return { skills, total };
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching skills:', error);
|
||||
return { skills: [], total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// Get counts for both tabs
|
||||
// Get counts for both tabs with Redis caching (30 min TTL)
|
||||
async function getTabCounts() {
|
||||
try {
|
||||
const db = createDb();
|
||||
const [newCount, updatedCount] = await Promise.all([
|
||||
skillQueries.countNewSkills(db),
|
||||
skillQueries.countUpdatedSkills(db),
|
||||
]);
|
||||
return { newCount, updatedCount };
|
||||
return await getOrSetCache(cacheKeys.newSkillsCounts(), cacheTTL.newSkills, async () => {
|
||||
const db = createDb();
|
||||
const [newCount, updatedCount] = await Promise.all([
|
||||
skillQueries.countNewSkills(db),
|
||||
skillQueries.countUpdatedSkills(db),
|
||||
]);
|
||||
return { newCount, updatedCount };
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching counts:', error);
|
||||
return { newCount: 0, updatedCount: 0 };
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ExternalLink, Download, Package, Eye, GitFork, ArrowUpDown, FolderGit2
|
||||
import Link from 'next/link';
|
||||
import type { Metadata } from 'next';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
import { getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -49,11 +50,15 @@ export default async function OwnerPage({ params, searchParams }: OwnerPageProps
|
||||
|
||||
const db = createDb();
|
||||
|
||||
// Fetch stats, count, and repo list in parallel
|
||||
// Fetch stats and repo list with caching (30 min TTL), count is dynamic per filter
|
||||
const [stats, totalSkills, ownerRepos] = await Promise.all([
|
||||
skillQueries.getOwnerStats(db, username),
|
||||
getOrSetCache(cacheKeys.ownerStats(username), cacheTTL.owner, () =>
|
||||
skillQueries.getOwnerStats(db, username)
|
||||
),
|
||||
skillQueries.countByOwner(db, username, activeRepo || undefined),
|
||||
skillQueries.getOwnerRepos(db, username),
|
||||
getOrSetCache(cacheKeys.ownerRepos(username), cacheTTL.owner, () =>
|
||||
skillQueries.getOwnerRepos(db, username)
|
||||
),
|
||||
]);
|
||||
|
||||
if (stats.totalSkills === 0) {
|
||||
|
||||
@@ -8,75 +8,70 @@ import { SkillCard } from '@/components/SkillCard';
|
||||
import { createDb, categoryQueries, skillQueries, skills, sql } from '@skillhub/db';
|
||||
import { formatCompactNumber } from '@/lib/format-number';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
import { getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache';
|
||||
|
||||
|
||||
// Force dynamic rendering to fetch fresh data from database
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// Get stats directly from database
|
||||
// Get stats with Redis caching (1 hour TTL)
|
||||
async function getStats() {
|
||||
try {
|
||||
const db = createDb();
|
||||
return await getOrSetCache(cacheKeys.homeStats(), cacheTTL.stats, async () => {
|
||||
const db = createDb();
|
||||
|
||||
// Browse-ready filter: exclude duplicates (matches browseReadyFilter in queries.ts)
|
||||
const browseReady = sql`${skills.isDuplicate} = false`;
|
||||
// Browse-ready filter: exclude duplicates (matches browseReadyFilter in queries.ts)
|
||||
const browseReady = sql`${skills.isDuplicate} = false`;
|
||||
|
||||
// Get total skills count (browse-ready, SKILL.md only)
|
||||
const skillsResult = await db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(skills)
|
||||
.where(sql`${skills.sourceFormat} = 'skill.md' AND ${skills.isBlocked} = false AND ${browseReady}`);
|
||||
const totalSkills = skillsResult[0]?.count ?? 0;
|
||||
// Run all independent count queries in parallel
|
||||
const [skillsResult, downloadsResult, categories, contributorsResult, totalIndexedResult] = await Promise.all([
|
||||
// Get total skills count (browse-ready, SKILL.md only)
|
||||
db.select({ count: sql<number>`count(*)::int` })
|
||||
.from(skills)
|
||||
.where(sql`${skills.sourceFormat} = 'skill.md' AND ${skills.isBlocked} = false AND ${browseReady}`),
|
||||
// Get total downloads (ALL skills — downloads are real user actions)
|
||||
db.select({ sum: sql<number>`coalesce(sum(${skills.downloadCount}), 0)::int` })
|
||||
.from(skills),
|
||||
// Get total categories
|
||||
categoryQueries.getAll(db),
|
||||
// Get unique contributors (browse-ready skills only)
|
||||
db.select({ count: sql<number>`count(distinct ${skills.githubOwner})::int` })
|
||||
.from(skills)
|
||||
.where(browseReady),
|
||||
// Get total indexed skills (all, before curation) for curation note
|
||||
db.select({ count: sql<number>`count(*)::int` })
|
||||
.from(skills)
|
||||
.where(sql`${skills.isBlocked} = false`),
|
||||
]);
|
||||
|
||||
// Get total downloads (ALL skills — downloads are real user actions)
|
||||
const downloadsResult = await db
|
||||
.select({ sum: sql<number>`coalesce(sum(${skills.downloadCount}), 0)::int` })
|
||||
.from(skills);
|
||||
const totalDownloads = downloadsResult[0]?.sum ?? 0;
|
||||
|
||||
// Get total categories
|
||||
const categories = await categoryQueries.getAll(db);
|
||||
const totalCategories = categories.length;
|
||||
|
||||
// Get unique contributors (browse-ready skills only)
|
||||
const contributorsResult = await db
|
||||
.select({ count: sql<number>`count(distinct ${skills.githubOwner})::int` })
|
||||
.from(skills)
|
||||
.where(browseReady);
|
||||
const totalContributors = contributorsResult[0]?.count ?? 0;
|
||||
|
||||
// Get total indexed skills (all, before curation) for curation note
|
||||
const totalIndexedResult = await db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(skills)
|
||||
.where(sql`${skills.isBlocked} = false`);
|
||||
const totalIndexed = totalIndexedResult[0]?.count ?? 0;
|
||||
|
||||
return {
|
||||
totalSkills,
|
||||
totalDownloads,
|
||||
totalCategories,
|
||||
totalContributors,
|
||||
totalIndexed,
|
||||
platforms: 5,
|
||||
};
|
||||
return {
|
||||
totalSkills: skillsResult[0]?.count ?? 0,
|
||||
totalDownloads: downloadsResult[0]?.sum ?? 0,
|
||||
totalCategories: categories.length,
|
||||
totalContributors: contributorsResult[0]?.count ?? 0,
|
||||
totalIndexed: totalIndexedResult[0]?.count ?? 0,
|
||||
platforms: 5,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching stats:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get featured skills directly from database
|
||||
// Get featured skills with Redis caching (2 hour TTL)
|
||||
async function getFeaturedSkills() {
|
||||
try {
|
||||
const db = createDb();
|
||||
// Get featured skills, or top skills by popularity if none are featured
|
||||
let featuredSkills = await skillQueries.getFeatured(db, 6);
|
||||
if (featuredSkills.length === 0) {
|
||||
// Fallback to adaptive popularity with owner/repo diversity
|
||||
featuredSkills = await skillQueries.getFeaturedWithDiversity(db, 6, 2, 3);
|
||||
}
|
||||
return featuredSkills;
|
||||
return await getOrSetCache(cacheKeys.homeFeatured(), cacheTTL.featured, async () => {
|
||||
const db = createDb();
|
||||
// Get featured skills, or top skills by popularity if none are featured
|
||||
let featuredSkills = await skillQueries.getFeatured(db, 6);
|
||||
if (featuredSkills.length === 0) {
|
||||
// Fallback to adaptive popularity with owner/repo diversity
|
||||
featuredSkills = await skillQueries.getFeaturedWithDiversity(db, 6, 2, 3);
|
||||
}
|
||||
return featuredSkills;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching featured skills:', error);
|
||||
return [];
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ShareButton } from '@/components/ShareButton';
|
||||
import { createDb, skillQueries } from '@skillhub/db';
|
||||
import { FORMAT_LABELS } from 'skillhub-core';
|
||||
import { formatCompactNumber } from '@/lib/format-number';
|
||||
import { shouldCountView } from '@/lib/cache';
|
||||
import { shouldCountView, getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache';
|
||||
import type { Metadata } from 'next';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
@@ -46,11 +46,13 @@ interface SkillPageProps {
|
||||
params: Promise<{ locale: string; id: string[] }>;
|
||||
}
|
||||
|
||||
// Get skill directly from database
|
||||
// Get skill with Redis caching (1 hour TTL)
|
||||
async function getSkill(skillId: string) {
|
||||
try {
|
||||
const db = createDb();
|
||||
return await skillQueries.getById(db, skillId);
|
||||
return await getOrSetCache(cacheKeys.skillDetail(skillId), cacheTTL.skill, async () => {
|
||||
const db = createDb();
|
||||
return await skillQueries.getById(db, skillId);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching skill:', error);
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user