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:
50
apps/web/lib/seo.ts
Normal file
50
apps/web/lib/seo.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { locales } from '@/i18n';
|
||||
|
||||
export const primaryDomain =
|
||||
process.env.NEXT_PUBLIC_APP_URL || 'https://skills.palebluedot.live';
|
||||
|
||||
const DEFAULT_LOCALE = 'en';
|
||||
|
||||
/**
|
||||
* Get canonical URL path for a locale
|
||||
* (no prefix for default locale, matching localePrefix: 'as-needed')
|
||||
*/
|
||||
export function getCanonicalPath(locale: string, route: string): string {
|
||||
// Normalize route to always start with a slash and not end with a slash
|
||||
let formattedRoute = route;
|
||||
if (!formattedRoute.startsWith('/')) {
|
||||
formattedRoute = `/${formattedRoute}`;
|
||||
}
|
||||
if (formattedRoute.length > 1 && formattedRoute.endsWith('/')) {
|
||||
formattedRoute = formattedRoute.slice(0, -1);
|
||||
}
|
||||
|
||||
if (locale === DEFAULT_LOCALE) {
|
||||
if (formattedRoute === '/') return '';
|
||||
return formattedRoute;
|
||||
}
|
||||
|
||||
if (formattedRoute === '/') return `/${locale}`;
|
||||
return `/${locale}${formattedRoute}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Next.js alternates metadata for a specific route
|
||||
* @param locale Current active locale
|
||||
* @param route The route path (e.g. '/about', '/categories')
|
||||
*/
|
||||
export function getPageAlternates(locale: string, route: string) {
|
||||
// Ensure we fall back to / if it's empty
|
||||
const canonicalPath = getCanonicalPath(locale, route) || '/';
|
||||
|
||||
const languages: Record<string, string> = {};
|
||||
|
||||
for (const l of locales) {
|
||||
languages[l] = `${primaryDomain}${getCanonicalPath(l, route) || '/'}`;
|
||||
}
|
||||
|
||||
return {
|
||||
canonical: `${primaryDomain}${canonicalPath}`,
|
||||
languages,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user