Files
skillhub/apps/web/app/sitemap.ts
airano 3f599a23fb 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>
2026-02-21 23:40:03 +03:30

131 lines
3.2 KiB
TypeScript

import type { MetadataRoute } from 'next';
import { createDb, skillQueries, categoryQueries } from '@skillhub/db';
import { getCanonicalPath, primaryDomain as BASE_URL } from '@/lib/seo';
const locales = ['en', 'fa'] as const;
const staticRoutes = [
'',
'/browse',
'/categories',
'/featured',
'/new',
'/docs',
'/docs/getting-started',
'/docs/cli',
'/docs/api',
'/about',
'/attribution',
'/claude-plugin',
'/claim',
'/contact',
'/support',
'/terms',
'/privacy',
];
function makeEntry(
locale: string,
route: string,
options?: {
lastModified?: Date;
changeFrequency?: MetadataRoute.Sitemap[number]['changeFrequency'];
priority?: number;
}
): MetadataRoute.Sitemap[number] {
return {
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}${getCanonicalPath(l, route)}`])
),
},
};
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// Mirror servers should not serve a sitemap (robots.txt already blocks crawlers)
const isPrimary = process.env.IS_PRIMARY_SERVER !== 'false';
if (!isPrimary) {
return [];
}
const db = createDb();
const entries: MetadataRoute.Sitemap = [];
// Static pages (for each locale)
for (const locale of locales) {
for (const route of staticRoutes) {
const isHome = route === '';
entries.push(
makeEntry(locale, route, {
changeFrequency: isHome ? 'daily' : 'weekly',
priority: isHome ? 1.0 : 0.5,
})
);
}
}
// Dynamic: skill pages
try {
const allSkills = await skillQueries.getAllForSitemap(db);
for (const skill of allSkills) {
for (const locale of locales) {
entries.push(
makeEntry(locale, `/skill/${skill.id}`, {
lastModified: skill.updatedAt,
changeFrequency: 'weekly',
priority: 0.7,
})
);
}
}
// Dynamic: owner pages (unique owners from skills)
const uniqueOwners = new Map<string, Date>();
for (const skill of allSkills) {
const existing = uniqueOwners.get(skill.githubOwner);
if (!existing || skill.updatedAt > existing) {
uniqueOwners.set(skill.githubOwner, skill.updatedAt);
}
}
for (const [owner, lastModified] of uniqueOwners) {
for (const locale of locales) {
entries.push(
makeEntry(locale, `/owner/${owner}`, {
lastModified,
changeFrequency: 'weekly',
priority: 0.6,
})
);
}
}
} catch {
// If DB is unavailable, return static pages only
}
// Dynamic: category pages
try {
const allCategories = await categoryQueries.getAll(db);
for (const category of allCategories) {
for (const locale of locales) {
entries.push(
makeEntry(locale, `/browse?category=${category.slug}`, {
changeFrequency: 'weekly',
priority: 0.5,
})
);
}
}
} catch {
// If DB is unavailable, skip categories
}
return entries;
}