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:
@@ -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`);
|
||||
Reference in New Issue
Block a user