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:
@@ -2,6 +2,20 @@ import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { Code2, Globe, Shield } from 'lucide-react';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/about'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function AboutPage({
|
||||
params,
|
||||
|
||||
@@ -3,6 +3,8 @@ import Link from 'next/link';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { Github, Heart, Users, Code, GitFork, ExternalLink, Database, Clock } from 'lucide-react';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -65,6 +67,18 @@ function formatLicenseName(license: string, locale: string): string {
|
||||
return names ? names[locale as 'en' | 'fa'] || names.en : license;
|
||||
}
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/attribution'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function AttributionPage({
|
||||
params,
|
||||
}: {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { createDb, skillQueries, categoryQueries } from '@skillhub/db';
|
||||
import { BrowseFilters, SearchBar, Pagination, ActiveFilters, EmptyState } from '@/components/BrowseFilters';
|
||||
import { SkillCard } from '@/components/SkillCard';
|
||||
import { toPersianNumber } from '@/lib/format-number';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
// Force dynamic rendering to fetch fresh data from database
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -79,6 +81,18 @@ async function getSkills(params: {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/browse'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BrowsePage({ params, searchParams }: BrowsePageProps) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { createDb, categoryQueries } from '@skillhub/db';
|
||||
import { formatNumber } from '@/lib/format-number';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
// Force dynamic rendering to fetch fresh data from database
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -96,6 +98,18 @@ async function getHierarchicalCategories() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/categories'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function CategoriesPage({
|
||||
params,
|
||||
}: {
|
||||
|
||||
@@ -2,9 +2,23 @@ import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { ClaimForm } from '@/components/ClaimForm';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/claim'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ClaimPage({
|
||||
params,
|
||||
}: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
import Link from 'next/link';
|
||||
import type { Metadata } from 'next';
|
||||
import {
|
||||
@@ -31,6 +32,7 @@ export async function generateMetadata({
|
||||
return {
|
||||
title: t('metadata.title'),
|
||||
description: t('metadata.description'),
|
||||
alternates: getPageAlternates(locale, '/claude-plugin'),
|
||||
openGraph: {
|
||||
title: t('metadata.title'),
|
||||
description: t('metadata.description'),
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/contact'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ContactPage({
|
||||
params,
|
||||
|
||||
@@ -5,6 +5,20 @@ import { ApiEndpointSection } from '@/components/ApiEndpointSection';
|
||||
import type { EndpointDef } from '@/components/ApiEndpointSection';
|
||||
import Link from 'next/link';
|
||||
import { ArrowLeft, ArrowRight, Search, FileCode, Users, Compass, Mail } from 'lucide-react';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/docs/api'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ApiDocsPage({
|
||||
params,
|
||||
|
||||
@@ -3,6 +3,20 @@ import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import Link from 'next/link';
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/docs/cli'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function CliDocsPage({
|
||||
params,
|
||||
|
||||
@@ -6,6 +6,8 @@ import Link from 'next/link';
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
import { createDb, skills, sql } from '@skillhub/db';
|
||||
import { formatPromptSkillCount } from '@/lib/format-number';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -15,7 +17,7 @@ async function getSkillCount(): Promise<string> {
|
||||
const result = await db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(skills)
|
||||
.where(sql`${skills.isDuplicate} = false AND (${skills.skillType} IS NULL OR ${skills.skillType} != 'aggregator')`);
|
||||
.where(sql`${skills.isDuplicate} = false`);
|
||||
return formatPromptSkillCount(result[0]?.count ?? 16000);
|
||||
} catch {
|
||||
return '16,000+';
|
||||
@@ -56,6 +58,18 @@ Search for unfamiliar tech or complex tasks. Only install "Pass" security status
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/docs/getting-started'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function GettingStartedPage({
|
||||
params,
|
||||
}: {
|
||||
|
||||
@@ -3,6 +3,20 @@ import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { BookOpen, Terminal, Code } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/docs'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function DocsPage({
|
||||
params,
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Footer } from '@/components/Footer';
|
||||
import { FavoritesList } from '@/components/FavoritesList';
|
||||
import { FavoritesSignIn } from '@/components/FavoritesSignIn';
|
||||
import { createDb, userQueries } from '@skillhub/db';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
// Force dynamic rendering
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -14,6 +16,18 @@ interface FavoritesPageProps {
|
||||
params: Promise<{ locale: string }>;
|
||||
}
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/favorites'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function FavoritesPage({ params }: FavoritesPageProps) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
@@ -5,6 +5,8 @@ import { createDb, skillQueries } from '@skillhub/db';
|
||||
import { toPersianNumber } from '@/lib/format-number';
|
||||
import { Pagination } from '@/components/BrowseFilters';
|
||||
import { SkillCard } from '@/components/SkillCard';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
// Force dynamic rendering to fetch fresh data from database
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -37,6 +39,18 @@ async function getFeaturedSkills(page: number, limit: number) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/featured'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function FeaturedPage({
|
||||
params,
|
||||
searchParams,
|
||||
|
||||
@@ -7,6 +7,8 @@ import { Clock, RefreshCw } from 'lucide-react';
|
||||
import { createDb, skillQueries } from '@skillhub/db';
|
||||
import { toPersianNumber } from '@/lib/format-number';
|
||||
import { Pagination } from '@/components/BrowseFilters';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
// Force dynamic rendering to fetch fresh data from database
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -75,6 +77,18 @@ async function getTabCounts() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/new'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function NewSkillsPage({
|
||||
params,
|
||||
searchParams,
|
||||
|
||||
@@ -7,9 +7,23 @@ import { createDb, skillQueries } from '@skillhub/db';
|
||||
import { formatCompactNumber, toPersianNumber } from '@/lib/format-number';
|
||||
import { ExternalLink, Download, Package, Eye, GitFork, ArrowUpDown, FolderGit2 } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import type { Metadata } from 'next';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string; username: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { locale, username } = await params;
|
||||
return {
|
||||
title: `${decodeURIComponent(username)} | SkillHub`,
|
||||
alternates: getPageAlternates(locale, `/owner/${username}`),
|
||||
};
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE = 24;
|
||||
|
||||
type SortOption = 'popularity' | 'downloads' | 'stars';
|
||||
@@ -183,11 +197,10 @@ export default async function OwnerPage({ params, searchParams }: OwnerPageProps
|
||||
<span className="text-sm text-text-secondary">{t('repo.filter')}:</span>
|
||||
<Link
|
||||
href={buildUrl({ repo: '', page: 1 })}
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||||
!activeRepo
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${!activeRepo
|
||||
? 'bg-primary text-primary-foreground font-medium'
|
||||
: 'bg-surface-elevated text-text-secondary hover:bg-surface-subtle border border-border'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
{t('repo.all')} ({locale === 'fa' ? toPersianNumber(stats.totalSkills) : stats.totalSkills})
|
||||
</Link>
|
||||
@@ -195,11 +208,10 @@ export default async function OwnerPage({ params, searchParams }: OwnerPageProps
|
||||
<Link
|
||||
key={r.repo}
|
||||
href={buildUrl({ repo: r.repo, page: 1 })}
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||||
activeRepo === r.repo
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${activeRepo === r.repo
|
||||
? 'bg-primary text-primary-foreground font-medium'
|
||||
: 'bg-surface-elevated text-text-secondary hover:bg-surface-subtle border border-border'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
{r.repo} ({locale === 'fa' ? toPersianNumber(r.skillCount) : r.skillCount})
|
||||
</Link>
|
||||
@@ -227,11 +239,10 @@ export default async function OwnerPage({ params, searchParams }: OwnerPageProps
|
||||
<Link
|
||||
key={opt.value}
|
||||
href={buildUrl({ sort: opt.value, page: 1 })}
|
||||
className={`px-3 py-1.5 text-sm rounded-lg transition-colors ${
|
||||
sort === opt.value
|
||||
className={`px-3 py-1.5 text-sm rounded-lg transition-colors ${sort === opt.value
|
||||
? 'bg-primary text-primary-foreground font-medium'
|
||||
: 'bg-surface-elevated text-text-secondary hover:bg-surface-subtle border border-border'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</Link>
|
||||
|
||||
@@ -7,6 +7,8 @@ import { HeroSearch } from '@/components/HeroSearch';
|
||||
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';
|
||||
|
||||
|
||||
// Force dynamic rendering to fetch fresh data from database
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -16,8 +18,8 @@ async function getStats() {
|
||||
try {
|
||||
const db = createDb();
|
||||
|
||||
// Browse-ready filter: exclude duplicates and aggregators
|
||||
const browseReady = sql`${skills.isDuplicate} = false AND (${skills.skillType} IS NULL OR ${skills.skillType} != 'aggregator')`;
|
||||
// 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
|
||||
@@ -82,6 +84,18 @@ async function getFeaturedSkills() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function HomePage({
|
||||
params,
|
||||
}: {
|
||||
|
||||
@@ -3,6 +3,8 @@ import Link from 'next/link';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { Shield, Database, Cookie, Users, Lock, Mail, ArrowRight, ArrowLeft } from 'lucide-react';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -15,6 +17,18 @@ const sectionIcons = {
|
||||
contact: Mail,
|
||||
};
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/privacy'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PrivacyPage({
|
||||
params,
|
||||
}: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,8 @@ import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { Mail, Bitcoin, ExternalLink } from 'lucide-react';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -9,6 +11,18 @@ interface SupportPageProps {
|
||||
params: Promise<{ locale: string }>;
|
||||
}
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/support'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function SupportPage({ params }: SupportPageProps) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
|
||||
@@ -3,6 +3,8 @@ import Link from 'next/link';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { FileText, UserCheck, AlertTriangle, Scale, Trash2, RefreshCw, ArrowRight, ArrowLeft } from 'lucide-react';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -15,6 +17,18 @@ const sectionIcons = {
|
||||
changes: RefreshCw,
|
||||
};
|
||||
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/terms'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function TermsPage({
|
||||
params,
|
||||
}: {
|
||||
|
||||
@@ -34,8 +34,8 @@ export async function GET(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
// Browse-ready filter: exclude duplicates and aggregators
|
||||
const browseReady = sql`${skills.isDuplicate} = false AND (${skills.skillType} IS NULL OR ${skills.skillType} != 'aggregator')`;
|
||||
// Browse-ready filter: exclude duplicates (matches browseReadyFilter in queries.ts)
|
||||
const browseReady = sql`${skills.isDuplicate} = false`;
|
||||
|
||||
// Browse-ready skill stats (skills count + contributors)
|
||||
const statsResult = await db
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
import { createDb, skillQueries, categoryQueries } from '@skillhub/db';
|
||||
|
||||
const BASE_URL =
|
||||
process.env.NEXT_PUBLIC_APP_URL || 'https://skills.palebluedot.live';
|
||||
import { getCanonicalPath, primaryDomain as BASE_URL } from '@/lib/seo';
|
||||
|
||||
const locales = ['en', 'fa'] as const;
|
||||
const DEFAULT_LOCALE = 'en';
|
||||
|
||||
/** Get canonical URL path for a locale (no prefix for default locale, matching localePrefix: 'as-needed') */
|
||||
function canonicalPath(locale: string, route: string): string {
|
||||
if (locale === DEFAULT_LOCALE) return route || '/';
|
||||
return `/${locale}${route}`;
|
||||
}
|
||||
|
||||
const staticRoutes = [
|
||||
'',
|
||||
@@ -43,13 +34,13 @@ function makeEntry(
|
||||
}
|
||||
): MetadataRoute.Sitemap[number] {
|
||||
return {
|
||||
url: `${BASE_URL}${canonicalPath(locale, route)}`,
|
||||
url: `${BASE_URL}${getCanonicalPath(locale, route)}`,
|
||||
lastModified: options?.lastModified,
|
||||
changeFrequency: options?.changeFrequency,
|
||||
priority: options?.priority,
|
||||
alternates: {
|
||||
languages: Object.fromEntries(
|
||||
locales.map((l) => [l, `${BASE_URL}${canonicalPath(l, route)}`])
|
||||
locales.map((l) => [l, `${BASE_URL}${getCanonicalPath(l, route)}`])
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user