import { useMemo } from 'react' import { AlertCircleIcon, CheckCircle2Icon, XCircleIcon } from 'lucide-react' import type { ArticleBodyDoc } from '../../data/site' import { docHeadings, docImages, docLinks, docToText, firstParagraph } from '../../utils/tiptapDoc' export type SeoInput = { title: string slug: string excerpt: string body: ArticleBodyDoc | string[] keyphrase: string /** نام نویسنده — سیگنال E-E-A-T و اسکیمای Article */ author?: string } type Status = 'good' | 'ok' | 'bad' type Check = { label: string; status: Status; hint: string } // ---------- محاسبه بررسی‌ها ---------- export function useSeoChecks(input: SeoInput): Check[] { return useMemo(() => { const text = docToText(input.body) const words = text.split(/\s+/).filter(Boolean) const kp = input.keyphrase.trim() // تطبیق کلمه کلیدی بی‌توجه به بزرگ/کوچکی حروف — و نرمال‌سازی عربی/فارسی const norm = (s: string) => s.toLowerCase().replace(/ي/g, 'ی').replace(/ك/g, 'ک') const has = (s: string, needle: string) => norm(s).includes(norm(needle)) const checks: Check[] = [] // عنوان const tLen = input.title.trim().length if (tLen === 0) checks.push({ label: 'عنوان مقاله', status: 'bad', hint: 'برای مقاله عنوان بنویسید.' }) else if (tLen >= 20 && tLen <= 60) checks.push({ label: `عنوان (${tLen} نویسه)`, status: 'good', hint: 'طول عنوان مناسب است.' }) else if (tLen >= 10 && tLen <= 70) checks.push({ label: `عنوان (${tLen} نویسه)`, status: 'ok', hint: 'طول پیشنهادی عنوان بین ۲۰ تا ۶۰ نویسه است.' }) else checks.push({ label: `عنوان (${tLen} نویسه)`, status: 'bad', hint: 'طول عنوان خارج از بازه ۱۰ تا ۷۰ نویسه است.' }) // کلمه کلیدی هدف if (!kp) { checks.push({ label: 'کلمه کلیدی هدف', status: 'ok', hint: 'کلمه کلیدی هدف را بنویسید تا حضور آن در متن بررسی شود.' }) } else { checks.push( has(input.title, kp) ? { label: 'کلمه کلیدی در عنوان', status: 'good', hint: 'کلمه کلیدی در عنوان هست.' } : { label: 'کلمه کلیدی در عنوان', status: 'bad', hint: `کلمه «${kp}» در عنوان نیست.` }, ) const firstP = firstParagraph(input.body) checks.push( has(firstP, kp) ? { label: 'کلمه کلیدی در پاراگراف اول', status: 'good', hint: 'کلمه کلیدی در ابتدای متن هست.' } : { label: 'کلمه کلیدی در پاراگراف اول', status: 'ok', hint: 'بهتر است کلمه کلیدی در پاراگراف اول بیاید.' }, ) const h2s = docHeadings(input.body, 2) if (h2s.length === 0) checks.push({ label: 'کلمه کلیدی در تیترها', status: 'ok', hint: 'هنوز تیتر H2 در متن نیست.' }) else checks.push( h2s.some((h) => has(h, kp)) ? { label: 'کلمه کلیدی در تیترها', status: 'good', hint: 'کلمه کلیدی در یکی از تیترها هست.' } : { label: 'کلمه کلیدی در تیترها', status: 'ok', hint: 'کلمه کلیدی در هیچ تیتر H2 نیست.' }, ) } // چکیده (توضیح متا) const eLen = input.excerpt.trim().length if (eLen === 0) checks.push({ label: 'چکیده (توضیح متا)', status: 'bad', hint: 'چکیده بنویسید — روی کارت مقاله و توضیح متای گوگل نمایش داده می‌شود.' }) else if (eLen >= 70 && eLen <= 160) checks.push({ label: `چکیده (${eLen} نویسه)`, status: 'good', hint: 'طول چکیده برای توضیح متا ایده‌آل است.' }) else if (eLen >= 40 && eLen <= 200) checks.push({ label: `چکیده (${eLen} نویسه)`, status: 'ok', hint: 'طول پیشنهادی چکیده بین ۷۰ تا ۱۶۰ نویسه است.' }) else checks.push({ label: `چکیده (${eLen} نویسه)`, status: 'bad', hint: 'چکیده خیلی کوتاه یا بلندتر از ۲۰۰ نویسه است.' }) // طول متن if (words.length >= 150) checks.push({ label: `طول متن (${words.length} واژه)`, status: 'good', hint: 'طول متن برای محتوای ارزشمند کافی است.' }) else if (words.length >= 80) checks.push({ label: `طول متن (${words.length} واژه)`, status: 'ok', hint: 'متن کمی کوتاه است — حداقل ۱۵۰ واژه پیشنهاد می‌شود.' }) else checks.push({ label: `طول متن (${words.length} واژه)`, status: 'bad', hint: 'متن خیلی کوتاه است — حداقل ۸۰ و ترجیحاً ۱۵۰ واژه بنویسید.' }) // تیتر H2 const h2s = docHeadings(input.body, 2) if (h2s.length > 0) checks.push({ label: `تیتربندی (${h2s.length} تیتر H2)`, status: 'good', hint: 'متن تیتربندی شده است.' }) else checks.push({ label: 'تیتربندی', status: 'bad', hint: 'برای خوانایی، متن را با تیتر H2 بخش‌بندی کنید.' }) // تصاویر const imgs = docImages(input.body) if (imgs.length === 0) checks.push({ label: 'تصاویر', status: 'ok', hint: 'تصویری داخل متن نیست — افزودن تصویر به جذابیت مقاله کمک می‌کند.' }) else if (imgs.every((i) => i.alt.trim())) checks.push({ label: `تصاویر (${imgs.length} تصویر با متن جایگزین)`, status: 'good', hint: 'همه تصاویر متن جایگزین دارند.' }) else checks.push({ label: 'تصاویر', status: 'bad', hint: 'برای همه تصاویر، متن جایگزین (alt) بنویسید.' }) // لینک‌ها const links = docLinks(input.body) if (links.length > 0) checks.push({ label: `پیوندها (${links.length} لینک)`, status: 'good', hint: 'متن پیوند دارد.' }) else checks.push({ label: 'پیوندها', status: 'ok', hint: 'یک لینک مرتبط (داخلی به پروژه/خدمات یا خارجی) اضافه کنید.' }) // نشانی if (/^article-\d+$/.test(input.slug)) checks.push({ label: 'نشانی (slug)', status: 'bad', hint: 'نشانی پیش‌فرض را به یک نشانی خوانا (انگلیسی) تغییر دهید.' }) else if (input.slug.length > 96) checks.push({ label: 'نشانی (slug)', status: 'ok', hint: 'نشانی خیلی بلند است — کوتاهش کنید.' }) else checks.push({ label: 'نشانی (slug)', status: 'good', hint: 'نشانی مناسب است.' }) // ——— سئوی مولد (GEO) — آماده‌سازی برای ارجاع در پاسخ‌های هوش مصنوعی ——— // پاسخ مستقیم در ابتدای مقاله — دستیارهای هوشمند بخش‌های خودبسندهٔ آغازین را نقل می‌کنند const firstPWords = firstParagraph(input.body).split(/\s+/).filter(Boolean).length if (firstPWords >= 20) checks.push({ label: 'پاسخ مستقیم در ابتدا (GEO)', status: 'good', hint: 'پاراگراف اول پاسخ نسبتاً کاملی می‌دهد — دقیقاً چیزی که هوش مصنوعی نقل می‌کند.' }) else checks.push({ label: 'پاسخ مستقیم در ابتدا (GEO)', status: 'ok', hint: 'پاراگراف اول را پاسخ‌وار بنویسید (حدود ۴۰ تا ۶۰ واژه، پاسخ مستقیم پرسش اصلی مقاله).' }) // آمار و عدد — قوی‌ترین سیگنال ارجاع هوش مصنوعی (به‌همراه ذکر منبع) const numberTokens = words.filter((w) => /\d|[۰-۹]/.test(w)).length if (numberTokens >= 5) checks.push({ label: `آمار و اعداد (${numberTokens} مورد)`, status: 'good', hint: 'متن داده‌محور است — سیگنال قوی برای استناد.' }) else checks.push({ label: 'آمار و اعداد', status: 'ok', hint: 'حداقل ۵ عدد یا درصد (با ذکر منبع) به متن اضافه کنید — محتوای داده‌محور بیش از همه ارجاع می‌گیرد.' }) // تیتر پرسشی — هم‌راستا با الگوی پرسش کاربران و «People Also Ask» if (h2s.some((h) => h.includes('؟') || h.includes('?'))) checks.push({ label: 'تیتر پرسشی', status: 'good', hint: 'حداقل یک تیتر به شکل پرسش است.' }) else checks.push({ label: 'تیتر پرسشی', status: 'ok', hint: 'یک تیتر H2 پرسشی (با «؟») اضافه کنید — همان‌طور که کاربران می‌پرسند.' }) // نویسنده — سیگنال اعتماد و هویت روشن محتوا if ((input.author ?? '').trim()) checks.push({ label: 'نویسنده', status: 'good', hint: 'نام نویسنده ثبت شده — در اسکیمای مقاله هم می‌آید.' }) else checks.push({ label: 'نویسنده', status: 'ok', hint: 'نام نویسنده را بنویسید — سیگنال اعتماد (E-E-A-T) و در اسکیمای مقاله استفاده می‌شود.' }) return checks }, [input]) } const STATUS_STYLE = { good: { icon: CheckCircle2Icon, color: 'text-emerald-400' }, ok: { icon: AlertCircleIcon, color: 'text-amber-400' }, bad: { icon: XCircleIcon, color: 'text-red-400' }, } as const /** پنل بررسی سئو — فهرست سبز/نارنجی/قرمز به سبک یوست، سازگار با فارسی */ export function SeoChecklist({ input }: { input: SeoInput }) { const checks = useSeoChecks(input) const good = checks.filter((c) => c.status === 'good').length return (

بررسی سئو

{good} از {checks.length} مورد سبز
) }