Archived
Next.js 14 + TypeScript + Tailwind + Prisma/SQLite. - LLM synthesis abstraction (OpenAI/Anthropic/Ollama) + admin settings UI - RSS ingestion pipeline (parser + cheerio content) + node-cron worker - AdSense AdUnit placements (header/in-feed/in-article/sidebar) - Sources analysis attribution, nofollow links, canonical/OG, sitemap/robots - Admin auth (ADMIN_API_KEY, timingSafeEqual, fail-closed 401) - Multi-cell lady-ga-ga marker, sitemap, robots, legal pages - Comprehensive README (setup, LLM config, AdSense, migrations, deploy)
43 lines
1.2 KiB
TypeScript
43 lines
1.2 KiB
TypeScript
import { createHash } from 'crypto';
|
|
|
|
/**
|
|
* URL-friendly slug from any source string.
|
|
* Deterministic so the same title always maps to the same URL segment.
|
|
*/
|
|
export function slugify(input: string): string {
|
|
const slug = input
|
|
.toLowerCase()
|
|
.normalize('NFKD')
|
|
.replace(/[\u0300-\u036f]/g, '') // strip diacritics
|
|
.replace(/^https?:\/\//, '')
|
|
.replace(/[^\w\s-]/g, '')
|
|
.trim()
|
|
.replace(/\s+/g, '-')
|
|
.replace(/-+/g, '-')
|
|
.replace(/^-|-$/g, '')
|
|
.slice(0, 90)
|
|
.replace(/-$/g, '');
|
|
|
|
return slug || 'article';
|
|
}
|
|
|
|
/**
|
|
* Stable dedup key for a story title: case-fold, strip punctuation, collapse
|
|
* whitespace. Two feeds covering the same story usually share enough title
|
|
* text to collide here; we add URL-host as a tiebreaker in the pipeline.
|
|
*/
|
|
export function dedupeKeyOf(title: string, host: string): string {
|
|
const normalized = title
|
|
.toLowerCase()
|
|
.normalize('NFKD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.replace(/[^a-z0-9\s]/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
return createHash('sha1').update(`${normalized}::${host}`).digest('hex').slice(0, 32);
|
|
}
|
|
|
|
export function sha1(input: string): string {
|
|
return createHash('sha1').update(input).digest('hex');
|
|
}
|