Import source: app/scripts/check-persian-seo.mjs

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:23:12 +03:30
parent ca973ce7df
commit 4e39c76842
+88
View File
@@ -0,0 +1,88 @@
import { existsSync } from 'node:fs';
import { resolve } from 'node:path';
import { TeknavData } from '../teknav-data.js';
const today = new Date().toISOString().slice(0, 10);
function clean(value) {
return String(value ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
}
function isPastOrTodayDate(date) {
const value = String(date ?? '').slice(0, 10);
return /^\d{4}-\d{2}-\d{2}$/.test(value) && value <= today;
}
function isPublishedArticle(article) {
if (article.status === 'منتشرشده') return true;
if (article.status === 'پیش‌نویس' || article.status === 'در انتظار بررسی') return false;
return !!article.dateEn;
}
function ogImageExists(article) {
const image = article.ogImage || `/images/og/${article.slug}.jpg`;
return existsSync(resolve(`public${image}`));
}
const problems = [];
const warnings = [];
const rows = TeknavData.articles
.filter((article) => isPublishedArticle(article) && isPastOrTodayDate(article.dateEn))
.map((article) => {
const h2 = (article.content?.match(/<h2/g) || []).length;
const words = clean(article.content).split(/\s+/).filter(Boolean).length;
const keywordCount = new Set([
...(article.keywords || []),
...(article.tags || []),
article.title,
article.subtitle,
article.categoryName,
article.type,
'تحلیل فارسی فناوری',
'تکناو',
].filter(Boolean)).size;
const checks = {
meta: clean(article.metaDescription).length >= 120,
keywords: keywordCount >= 7,
headings: h2 >= 4,
body: words >= 600,
image: ogImageExists(article),
reviewed: !!article.factCheckedAt,
canonical: !!article.canonicalPath,
};
for (const [name, ok] of Object.entries({
meta: checks.meta,
headings: checks.headings,
image: checks.image,
reviewed: checks.reviewed,
canonical: checks.canonical,
})) {
if (!ok) problems.push(`${article.slug}: ${name}`);
}
if (!checks.keywords) warnings.push(`${article.slug}: broaden keywords/tags`);
if (!checks.body) warnings.push(`${article.slug}: consider expanding article body`);
return {
slug: article.slug,
words,
h2,
keywordCount,
metaLength: clean(article.metaDescription).length,
image: checks.image,
reviewed: checks.reviewed,
};
});
console.table(rows);
if (problems.length > 0) {
console.error(`[seo-quality] ${problems.length} issue(s):`);
for (const problem of problems) console.error(`- ${problem}`);
process.exit(1);
}
if (warnings.length > 0) {
console.warn(`[seo-quality] ${warnings.length} warning(s):`);
for (const warning of warnings) console.warn(`- ${warning}`);
}
console.log(`[seo-quality] passed ${rows.length} Persian indexable articles`);