sync: SEO canonical URLs, curation improvements, batch scripts, stats fix

- Dynamic canonical URLs and alternates for all pages (SEO fix)
- Shared seo.ts utility for canonical path generation
- Improved duplicate detection: standalone > aggregator priority, forks/created_at tie-breakers
- browseReadyFilter now includes unique aggregator skills
- Stats aligned with browseReadyFilter (was showing 16k instead of 62k+)
- Re-review mechanism: content_hash changes trigger needs-re-review
- review_status field added to skills schema
- Batch security scan and quality score scripts
- Indexer Dockerfile updated for curation deps
- Removed roadmap from README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-21 23:40:03 +03:30
parent 77917bf91f
commit 3f599a23fb
30 changed files with 1092 additions and 50 deletions

View File

@@ -16,10 +16,32 @@ import { createDb, skillQueries } from '@skillhub/db';
import { FORMAT_LABELS } from 'skillhub-core';
import { formatCompactNumber } from '@/lib/format-number';
import { shouldCountView } from '@/lib/cache';
import type { Metadata } from 'next';
import { getPageAlternates } from '@/lib/seo';
// Force dynamic rendering to fetch fresh data from database
export const dynamic = 'force-dynamic';
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string; id: string[] }>;
}): Promise<Metadata> {
const { locale, id } = await params;
const skillId = id.join('/');
// Optionally fetch skill name for title
const dbSkill = await getSkill(skillId);
const title = dbSkill ? `${dbSkill.name} | SkillHub` : 'SkillHub';
const description = dbSkill ? dbSkill.description : undefined;
return {
title,
description,
alternates: getPageAlternates(locale, `/skill/${skillId}`),
};
}
interface SkillPageProps {
params: Promise<{ locale: string; id: string[] }>;
}
@@ -71,9 +93,9 @@ export default async function SkillPage({ params }: SkillPageProps) {
const db = createDb();
shouldCountView(dbSkill.id, clientIp).then((shouldCount) => {
if (shouldCount) {
skillQueries.incrementViews(db, dbSkill.id).catch(() => {});
skillQueries.incrementViews(db, dbSkill.id).catch(() => { });
}
}).catch(() => {});
}).catch(() => { });
}
// Map database response to expected format