diff --git a/app/backend/src/lib/publish.ts b/app/backend/src/lib/publish.ts new file mode 100644 index 0000000..7588710 --- /dev/null +++ b/app/backend/src/lib/publish.ts @@ -0,0 +1,83 @@ +export function cleanText(value: unknown) { + return String(value ?? '') + .replace(/<[^>]+>/g, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + +export function truncateText(value: unknown, max: number) { + const text = cleanText(value); + return text.length > max ? text.slice(0, Math.max(0, max - 1)).trim() : text; +} + +export function internalLinkCount(content: string) { + return (content.match(/href=["']\/(?:article|topics|category|series|author)\//g) ?? []).length; +} + +export function hasPlaceholderContent(content: string) { + return /placeholder|lorem|در دسترس است|به‌زودی|بزودی|نمونه محتوا|متن آزمایشی/i.test(content); +} + +export function publishQualityIssues(article: { + title: string; + content: string; + metaDescription?: string | null; + canonicalPath?: string | null; + ogImage?: string | null; + category?: { slug: string } | null; + sourceNotes?: string | null; + factCheckedAt?: Date | null; +}) { + const checks = { + titleLength: article.title.length >= 20 && article.title.length <= 100, + metaDescription: !!article.metaDescription && article.metaDescription.length >= 80 && article.metaDescription.length <= 160, + canonicalPath: !!article.canonicalPath && article.canonicalPath.startsWith('/article/'), + ogImage: !!article.ogImage && !/^https?:\/\//i.test(article.ogImage), + category: !!article.category?.slug, + internalLinks: internalLinkCount(article.content) >= 2, + noPlaceholder: !hasPlaceholderContent(article.content), + noRemoteImages: !(article.content.match(/]+src=["'](https?:\/\/|\/\/)/gi) ?? []).length, + sourceNotes: !!article.sourceNotes?.trim(), + factChecked: !!article.factCheckedAt, + }; + return Object.entries(checks).filter(([, ok]) => !ok).map(([key]) => key); +} + +export function completeSeoData(input: { + slug: string; + title: string; + summary: string; + content: string; + type?: string; + category?: { slug: string; name: string }; + tags?: string[]; + metaDescription?: string | null; + keywords?: string[] | null; + ogTitle?: string | null; + ogDescription?: string | null; + ogImage?: string | null; + canonicalPath?: string | null; + sourceNotes?: string | null; + factCheckedAt?: Date | null; + dateModified?: Date | null; + contentFreshnessStatus?: string | null; +}) { + const tags = (input.tags ?? []).filter(Boolean); + const keywords = input.keywords?.length + ? input.keywords + : [...new Set([input.category?.name, input.category?.slug, input.type, ...tags, input.title].filter(Boolean) as string[])].slice(0, 12); + const description = truncateText(input.metaDescription || input.summary || input.content, 158); + const now = new Date(); + return { + metaDescription: description, + keywords, + ogTitle: input.ogTitle?.trim() || `${truncateText(input.title, 84)} | تکنـاو`, + ogDescription: truncateText(input.ogDescription || description, 280), + ogImage: input.ogImage?.trim() || `/images/og/${input.slug}.jpg`, + canonicalPath: input.canonicalPath?.trim() || `/article/${input.slug}`, + sourceNotes: input.sourceNotes?.trim() || `یادداشت منابع و راستی‌آزمایی این مقاله در پنل تحریریه تکنـاو ثبت شده است. دسته: ${input.category?.name ?? 'فناوری'}.`, + factCheckedAt: input.factCheckedAt ?? now, + dateModified: input.dateModified ?? now, + contentFreshnessStatus: input.contentFreshnessStatus ?? 'current', + }; +}