Import source: app/backend/src/lib/publish

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:19:30 +03:30
parent 1a53a3f5f6
commit 5593dd4496
+83
View File
@@ -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(/<img[^>]+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',
};
}