canina/frontend/admin-panel/src/pages/SeoSettingsPage.tsx
parsa aghaei 0b37825094
Some checks failed
Deploy Canina / deploy (push) Successful in 32s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 6s
feat(seo): add sitemaps, RSS feeds, robots.txt, and Torob crawler endpoints directory to SeoSettingsPage
2026-09-02 13:32:33 +03:30

901 lines
49 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import {
Globe, Save, Sliders, Sparkles, Search, BarChart3, Code2,
CheckCircle2, Activity, ExternalLink, Copy, Check, Rss,
FileCode, Bot, ShoppingBag, Layers, Info
} from 'lucide-react';
import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import MediaSelector from '../components/ui/MediaSelector';
import Button from '../components/ui/Button';
import type { SeoSettings } from '../types/admin';
export default function SeoSettingsPage() {
const [seo, setSeo] = useState<SeoSettings>({
brandNameFa: 'کنینا ایران',
brandNameEn: 'Canina Iran',
titleSeparator: '|',
titlePosition: 'suffix',
defaultMetaTitle: 'کنینا ایران | نماینده رسمی مکمل‌های دارویی Canina آلمان',
defaultMetaDescription: 'فروشگاه تخصصی و نماینده انحصاری مکمل‌های درمانی و تقویتی سگ و گربه کنینا آلمان با فرمولاسیون دارویی و تاییدیه بالینی.',
keywords: 'مکمل سگ, مکمل گربه, داروی حیوانات, Canina Germany',
canonicalBaseUrl: 'https://canina.ir',
ogImageUrl: '/assets/images/regenerated_image_1779109861747.png',
googleSiteVerification: '',
googleAnalyticsId: '',
googleTagManagerId: '',
enableGoogleAnalytics: true,
enableGoogleTagManager: true,
clarityProjectId: '',
});
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
const [copiedKey, setCopiedKey] = useState<string | null>(null);
const handleCopy = (text: string, key: string, label: string) => {
navigator.clipboard.writeText(text);
setCopiedKey(key);
toast.success(`آدرس ${label} کپی شد`);
setTimeout(() => {
setCopiedKey((curr) => (curr === key ? null : curr));
}, 2500);
};
useEffect(() => {
let isSubscribed = true;
api.get('/settings/seo')
.then(res => {
if (!isSubscribed) return;
const data = res.data?.data || res.data;
if (data) {
setSeo(prev => ({
brandNameFa: data.brandNameFa || data.brand_name_fa || prev.brandNameFa,
brandNameEn: data.brandNameEn || data.brand_name_en || prev.brandNameEn,
titleSeparator: data.titleSeparator || data.title_separator || prev.titleSeparator,
titlePosition: data.titlePosition || data.title_position || prev.titlePosition,
defaultMetaTitle: data.defaultMetaTitle || prev.defaultMetaTitle,
defaultMetaDescription: data.defaultMetaDescription || prev.defaultMetaDescription,
keywords: data.keywords || prev.keywords,
canonicalBaseUrl: data.canonicalBaseUrl || prev.canonicalBaseUrl,
ogImageUrl: data.ogImageUrl || prev.ogImageUrl,
googleSiteVerification: data.googleSiteVerification || data.google_site_verification || '',
googleAnalyticsId: data.googleAnalyticsId || data.ga_measurement_id || '',
googleTagManagerId: data.googleTagManagerId || data.gtm_id || '',
enableGoogleAnalytics: data.enableGoogleAnalytics !== undefined ? Boolean(data.enableGoogleAnalytics) : true,
enableGoogleTagManager: data.enableGoogleTagManager !== undefined ? Boolean(data.enableGoogleTagManager) : true,
clarityProjectId: data.clarityProjectId || data.clarity_project_id || '',
}));
}
})
.catch(err => {
console.warn('Failed to fetch SEO settings, using default state', err);
})
.finally(() => {
if (isSubscribed) setIsLoading(false);
});
return () => {
isSubscribed = false;
};
}, []);
const handleSave = async (e: React.FormEvent) => {
e.preventDefault();
try {
setIsSaving(true);
await api.patch('/settings/seo', seo);
toast.success('تنظیمات سئو و ساختار عناوین با موفقیت ذخیره شد');
} catch (err) {
console.error('Failed to update SEO settings:', err);
toast.error('خطا در بروزرسانی تنظیمات سئو');
} finally {
setIsSaving(false);
}
};
// Sample formatted preview
const previewSamplePageTitle = 'فروشگاه مکمل‌های تخصصی سگ و گربه';
const brandName = seo.brandNameFa || 'کنینا ایران';
const sep = seo.titleSeparator || '|';
const pos = seo.titlePosition || 'suffix';
const previewFormattedTitle =
pos === 'prefix'
? `${brandName} ${sep} ${previewSamplePageTitle}`
: pos === 'none'
? previewSamplePageTitle
: `${previewSamplePageTitle} ${sep} ${brandName}`;
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-black text-gray-900 flex items-center gap-2">
<Globe className="w-6 h-6 text-purple-600" />
تنظیمات عمومی سئو و فرمت عناوین صفحات (Global SEO & Titles)
</h2>
<p className="text-gray-500 font-medium mt-1">
پیکربندی هوشمند و یکپارچه نام برند، ساختار عناوین تب مرورگر، متادیتاهای گوگل و کلمات کلیدی
</p>
</div>
</div>
{/* Live Preview Box */}
<div className="bg-gradient-to-r from-purple-900 via-indigo-900 to-slate-900 text-white p-6 rounded-3xl shadow-lg border border-purple-800/40 relative overflow-hidden">
<div className="relative z-10 space-y-3">
<div className="flex items-center gap-2 text-purple-300 text-xs font-bold uppercase tracking-wider">
<Sparkles className="w-4 h-4 text-amber-400" />
پیشنمایش زنده ساختار عنوان در تب مرورگر و نتایج گوگل
</div>
<div className="bg-white/10 backdrop-blur-md border border-white/20 rounded-2xl p-4 space-y-2" dir="rtl">
<div className="flex items-center gap-2 text-xs text-purple-200">
<span className="w-3 h-3 rounded-full bg-emerald-400 inline-block animate-pulse" />
<span>عنوان نمونه برای صفحه داخلی:</span>
</div>
<p className="text-lg font-black text-white select-all">
{previewFormattedTitle}
</p>
<p className="text-xs text-purple-300/80">
الگوی اعمال شده: {pos === 'suffix' ? 'عنوان صفحه + جداکننده + نام برند (استاندارد توصیه‌شده گوگل)' : pos === 'prefix' ? 'نام برند + جداکننده + عنوان صفحه' : 'فقط عنوان صفحه بدون نام برند'}
</p>
</div>
</div>
</div>
{isLoading ? (
<div className="flex justify-center p-12"><Spinner size="lg" className="text-purple-600" /></div>
) : (
<form onSubmit={handleSave} className="bg-white p-6 sm:p-8 rounded-3xl shadow-sm border border-gray-200 space-y-8">
{/* Section 1: Brand & Title Format Settings */}
<div className="space-y-4">
<div className="flex items-center gap-2 pb-2 border-b border-gray-100">
<Sliders className="w-5 h-5 text-purple-600" />
<h3 className="text-base font-black text-gray-900">
تنظیمات فرمت عنوان صفحات و نام برند (Title & Brand Scheme)
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">
نام رسمی برند در عنوان صفحات (فارسی) *
</label>
<input
required
type="text"
value={seo.brandNameFa || ''}
onChange={(e) => setSeo({ ...seo, brandNameFa: e.target.value })}
placeholder="مثال: کنینا ایران"
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-bold text-sm"
/>
<span className="text-xs text-gray-400 mt-1 block">نام برند که در انتهای یا ابتدای عنوان تمام صفحات درج میشود</span>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">
نام رسمی برند (انگلیسی)
</label>
<input
type="text"
value={seo.brandNameEn || ''}
onChange={(e) => setSeo({ ...seo, brandNameEn: e.target.value })}
placeholder="مثال: Canina Iran"
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-bold text-sm"
dir="ltr"
/>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">
کاراکتر جداکننده عنوان (Separator) *
</label>
<div className="flex gap-2">
{['|', '-', '•', '—', '/'].map((char) => (
<button
key={char}
type="button"
onClick={() => setSeo({ ...seo, titleSeparator: char })}
className={`px-3 py-2 rounded-xl text-sm font-bold border cursor-pointer transition-all ${
seo.titleSeparator === char
? 'bg-purple-600 text-white border-purple-600 shadow-xs'
: 'bg-gray-50 text-gray-700 border-gray-200 hover:bg-gray-100'
}`}
>
{char}
</button>
))}
<input
type="text"
value={seo.titleSeparator || '|'}
onChange={(e) => setSeo({ ...seo, titleSeparator: e.target.value })}
className="w-16 text-center px-2 py-2 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-bold text-sm"
maxLength={3}
/>
</div>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">
نحوه قرارگیری نام برند در عنوان (Title Format) *
</label>
<select
value={seo.titlePosition || 'suffix'}
onChange={(e) => setSeo({ ...seo, titlePosition: e.target.value as any })}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-bold text-sm bg-white cursor-pointer"
>
<option value="suffix">پسوند: عنوان صفحه | نام برند (استاندارد توصیهشده گوگل)</option>
<option value="prefix">پیشوند: نام برند | عنوان صفحه</option>
<option value="none">فقط عنوان صفحه (بدون نام برند)</option>
</select>
</div>
</div>
</div>
{/* Section 2: Default Meta Tags */}
<div className="space-y-4 pt-4 border-t border-gray-100">
<div className="flex items-center gap-2 pb-2">
<Globe className="w-5 h-5 text-purple-600" />
<h3 className="text-base font-black text-gray-900">
متادیتای پیشفرض سئو (Default SEO Meta Tags)
</h3>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">عنوان متای پیشفرض صفحه اصلی (defaultMetaTitle) *</label>
<input
required
type="text"
value={seo.defaultMetaTitle}
onChange={(e) => setSeo({ ...seo, defaultMetaTitle: e.target.value })}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-bold text-sm"
/>
<span className="text-xs text-gray-400 mt-1 block">در صورت عدم تعیین عنوان اختصاصی برای یک صفحه، از این عنوان استفاده خواهد شد.</span>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">توضیحات متای پیشفرض (defaultMetaDescription) *</label>
<textarea
required
rows={3}
value={seo.defaultMetaDescription}
onChange={(e) => setSeo({ ...seo, defaultMetaDescription: e.target.value })}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm leading-relaxed"
/>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">کلمات کلیدی اصلی (keywords)</label>
<input
type="text"
value={seo.keywords}
onChange={(e) => setSeo({ ...seo, keywords: e.target.value })}
placeholder="با کاما جدا کنید (مثال: مکمل سگ, مکمل گربه, کنینا)"
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">آدرس پایه Canonical (canonicalBaseUrl)</label>
<input
type="text"
value={seo.canonicalBaseUrl}
onChange={(e) => setSeo({ ...seo, canonicalBaseUrl: e.target.value })}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
dir="ltr"
/>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">تصویر پیشفرض شبکه اجتماعی (ogImageUrl)</label>
<div className="flex gap-2">
<input
type="text"
value={seo.ogImageUrl}
onChange={(e) => setSeo({ ...seo, ogImageUrl: e.target.value })}
className="flex-1 px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
dir="ltr"
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setIsMediaSelectorOpen(true)}
>
انتخاب از رسانه
</Button>
</div>
</div>
</div>
</div>
{/* Section 3: Google Services & Webmaster Analytics */}
<div className="space-y-6 pt-6 border-t border-gray-100">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 pb-2">
<div className="flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-purple-600" />
<h3 className="text-base font-black text-gray-900">
سرویسهای تحلیل و وبمستر گوگل (Google Services & Analytics)
</h3>
</div>
<span className="text-xs text-purple-700 bg-purple-50 px-3 py-1 rounded-full font-bold self-start sm:self-auto">
اتصال آنی به فرانتاستور (Zero Build Delay)
</span>
</div>
{/* Grid of Webmaster Cards */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Google Search Console Card */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-xs space-y-4 relative overflow-hidden">
<div className="flex items-center justify-between border-b pb-3 border-gray-100">
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center font-black text-xs">
GSC
</div>
<div>
<h4 className="text-sm font-bold text-gray-900 flex items-center gap-1.5">
<span>گوگل سرچ کنسول (Google Search Console)</span>
</h4>
<p className="text-[11px] text-gray-500">تأیید مالکیت دامنه در نتایج جستجوی گوگل</p>
</div>
</div>
{seo.googleSiteVerification ? (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[11px] font-bold bg-emerald-50 text-emerald-700 border border-emerald-200">
<CheckCircle2 className="w-3.5 h-3.5" />
متصل
</span>
) : (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[11px] font-bold bg-gray-100 text-gray-600">
تنظیم نشده
</span>
)}
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700 flex items-center justify-between">
<span>کد متاتگ تأیید مالکیت (HTML Verification Tag)</span>
<a
href="https://search.google.com/search-console"
target="_blank"
rel="noreferrer"
className="text-[11px] text-blue-600 hover:underline flex items-center gap-1 font-bold"
>
ورود به سرچ کنسول
<ExternalLink className="w-3 h-3" />
</a>
</label>
<input
type="text"
value={seo.googleSiteVerification || ''}
onChange={(e) => {
let val = e.target.value.trim();
const match = val.match(/content=["']([^"']+)["']/i);
if (match && match[1]) {
val = match[1];
toast.success('کد تأییدیه گوگل از متاتگ با موفقیت استخراج شد');
}
setSeo({ ...seo, googleSiteVerification: val });
}}
placeholder="مثال: abcdef1234567890 یا کل تگ <meta...>"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-500 outline-none text-xs font-mono"
dir="ltr"
/>
<p className="text-[11px] text-gray-400 leading-relaxed">
میتوانید کل تگ <code className="bg-gray-100 px-1 rounded text-purple-700">&lt;meta name="google-site-verification"...&gt;</code> یا فقط مقدار داخل <code className="bg-gray-100 px-1 rounded text-purple-700">content</code> را پیست کنید.
</p>
</div>
{seo.googleSiteVerification && (
<div className="p-3 bg-gray-50 rounded-xl border border-gray-200 text-[11px] font-mono text-gray-600 space-y-1" dir="ltr">
<span className="text-[10px] text-gray-400 font-sans block text-right font-bold">متاتگ تزریق شده در &lt;head&gt; سایت:</span>
<code className="text-blue-700 select-all block break-all">
&lt;meta name="google-site-verification" content="{seo.googleSiteVerification}" /&gt;
</code>
</div>
)}
</div>
{/* Google Analytics 4 (GA4) Card */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-xs space-y-4 relative overflow-hidden">
<div className="flex items-center justify-between border-b pb-3 border-gray-100">
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-xl bg-amber-50 text-amber-600 flex items-center justify-center font-black text-xs">
GA4
</div>
<div>
<h4 className="text-sm font-bold text-gray-900 flex items-center gap-1.5">
<span>گوگل آنالیتیکس ۴ (Google Analytics 4)</span>
</h4>
<p className="text-[11px] text-gray-500">رهگیری بازدیدها، نرخ تبدیل و رفتار مشتریان</p>
</div>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={seo.enableGoogleAnalytics !== false}
onChange={(e) => setSeo({ ...seo, enableGoogleAnalytics: e.target.checked })}
/>
<div className="w-10 h-5 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-amber-500"></div>
</label>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700 flex items-center justify-between">
<span>شناسه اندازهگیری گوگل آنالیتیکس (Measurement ID) یا کل کد اسکریپت</span>
<a
href="https://analytics.google.com"
target="_blank"
rel="noreferrer"
className="text-[11px] text-amber-600 hover:underline flex items-center gap-1 font-bold"
>
ورود به Google Analytics
<ExternalLink className="w-3 h-3" />
</a>
</label>
<input
type="text"
value={seo.googleAnalyticsId || ''}
onChange={(e) => {
let val = e.target.value.trim();
// Auto-extract G-XXXXX if user pasted full <script> tag or gtag code
const match = val.match(/G-[A-Z0-9]+/i);
if (match) {
val = match[0].toUpperCase();
if (val !== e.target.value.trim()) {
toast.success(`شناسه ${val} به صورت خودکار از اسکریپت استخراج شد`);
}
}
setSeo({ ...seo, googleAnalyticsId: val });
}}
placeholder="مثال: G-FH4LH3WVMX یا کل کد <!-- Google tag (gtag.js) -->..."
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-amber-500 outline-none text-xs font-mono font-bold"
dir="ltr"
/>
<p className="text-[11px] text-gray-400 leading-relaxed">
میتوانید شناسه با پیشوند <strong className="text-gray-700">G-</strong> یا کل کد اسکریپت Google tag را مستقیماً پیست کنید (خودکار استخراج میشود).
</p>
</div>
<div className="flex items-center gap-2 pt-1">
{seo.googleAnalyticsId && seo.enableGoogleAnalytics !== false ? (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-xs font-bold bg-amber-50 text-amber-800 border border-amber-200">
<Activity className="w-3.5 h-3.5 text-amber-600 animate-pulse" />
اسکریپت gtag.js فعال و در &lt;head&gt; لود میشود
</span>
) : (
<span className="text-[11px] text-gray-400 font-medium">
{!seo.googleAnalyticsId ? 'شناسه GA4 هنوز ثبت نشده است.' : 'آمارگیر غیرفعال شده است.'}
</span>
)}
</div>
{seo.googleAnalyticsId && (
<div className="p-3 bg-gray-50 rounded-xl border border-gray-200 text-[10px] font-mono text-gray-600 space-y-1" dir="ltr">
<span className="text-[10px] text-gray-400 font-sans block text-right font-bold">کد لود شده در سایت:</span>
<code className="text-amber-700 select-all block break-all leading-relaxed">
&lt;script async src="https://www.googletagmanager.com/gtag/js?id={seo.googleAnalyticsId}"&gt;&lt;/script&gt;
</code>
</div>
)}
</div>
{/* Google Tag Manager (GTM) Card */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-xs space-y-4 relative overflow-hidden">
<div className="flex items-center justify-between border-b pb-3 border-gray-100">
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-xl bg-blue-50 text-blue-700 flex items-center justify-center font-black text-xs">
GTM
</div>
<div>
<h4 className="text-sm font-bold text-gray-900 flex items-center gap-1.5">
<span>گوگل تگ منیجر (Google Tag Manager)</span>
</h4>
<p className="text-[11px] text-gray-500">مدیریت متمرکز تگها، ایونتها و تبلیغات گوگل ادز</p>
</div>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={seo.enableGoogleTagManager !== false}
onChange={(e) => setSeo({ ...seo, enableGoogleTagManager: e.target.checked })}
/>
<div className="w-10 h-5 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"></div>
</label>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700 flex items-center justify-between">
<span>شناسه کانتینر تگ منیجر (Container ID) یا کل اسکریپت</span>
<a
href="https://tagmanager.google.com"
target="_blank"
rel="noreferrer"
className="text-[11px] text-blue-600 hover:underline flex items-center gap-1 font-bold"
>
ورود به Tag Manager
<ExternalLink className="w-3 h-3" />
</a>
</label>
<input
type="text"
value={seo.googleTagManagerId || ''}
onChange={(e) => {
let val = e.target.value.trim();
// Auto-extract GTM-XXXXX if user pasted full <script> or <noscript> snippet
const match = val.match(/GTM-[A-Z0-9]+/i);
if (match) {
val = match[0].toUpperCase();
if (val !== e.target.value.trim()) {
toast.success(`شناسه ${val} به صورت خودکار از اسکریپت استخراج شد`);
}
}
setSeo({ ...seo, googleTagManagerId: val });
}}
placeholder="مثال: GTM-XXXXXXX یا کل کد <script>...(dataLayer)...</script>"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-500 outline-none text-xs font-mono font-bold"
dir="ltr"
/>
<p className="text-[11px] text-gray-400 leading-relaxed">
میتوانید شناسه با پیشوند <strong className="text-gray-700">GTM-</strong> یا کل کد اسکریپت Head/Body را پیست کنید (خودکار استخراج میشود).
</p>
</div>
<div className="flex items-center gap-2 pt-1">
{seo.googleTagManagerId && seo.enableGoogleTagManager !== false ? (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-xs font-bold bg-blue-50 text-blue-800 border border-blue-200">
<Code2 className="w-3.5 h-3.5 text-blue-600" />
اسکریپت GTM در &lt;head&gt; و تگ noscript در &lt;body&gt; فعال است
</span>
) : (
<span className="text-[11px] text-gray-400 font-medium">
{!seo.googleTagManagerId ? 'شناسه GTM ثبت نشده است.' : 'تگ منیجر غیرفعال شده است.'}
</span>
)}
</div>
{seo.googleTagManagerId && (
<div className="p-3 bg-gray-50 rounded-xl border border-gray-200 text-[10px] font-mono text-gray-600 space-y-1.5" dir="ltr">
<span className="text-[10px] text-gray-400 font-sans block text-right font-bold">تزریق خودکار در &lt;head&gt; و ابتدای &lt;body&gt;:</span>
<code className="text-blue-700 select-all block break-all leading-relaxed">
Head: https://www.googletagmanager.com/gtm.js?id={seo.googleTagManagerId}
</code>
<code className="text-gray-500 select-all block break-all leading-relaxed">
Body: &lt;noscript&gt;&lt;iframe src="...id={seo.googleTagManagerId}"&gt;&lt;/noscript&gt;
</code>
</div>
)}
</div>
{/* Microsoft Clarity Card */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-xs space-y-4 relative overflow-hidden">
<div className="flex items-center justify-between border-b pb-3 border-gray-100">
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-xl bg-orange-50 text-orange-600 flex items-center justify-center font-black text-xs">
MS
</div>
<div>
<h4 className="text-sm font-bold text-gray-900 flex items-center gap-1.5">
<span>مایکروسافت کلاریتی (Microsoft Clarity)</span>
</h4>
<p className="text-[11px] text-gray-500">ضبط جلسات، نقشه حرارتی (Heatmap) و تحلیل کلیکها</p>
</div>
</div>
{seo.clarityProjectId ? (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[11px] font-bold bg-emerald-50 text-emerald-700 border border-emerald-200">
<CheckCircle2 className="w-3.5 h-3.5" />
فعال
</span>
) : (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[11px] font-bold bg-gray-100 text-gray-600">
اختیاری
</span>
)}
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700 flex items-center justify-between">
<span>شناسه پروژه کلاریتی (Project ID) یا کل اسکریپت</span>
<a
href="https://clarity.microsoft.com"
target="_blank"
rel="noreferrer"
className="text-[11px] text-orange-600 hover:underline flex items-center gap-1 font-bold"
>
ورود به Microsoft Clarity
<ExternalLink className="w-3 h-3" />
</a>
</label>
<input
type="text"
value={seo.clarityProjectId || ''}
onChange={(e) => {
let val = e.target.value.trim();
// Auto-extract Clarity ID if user pasted full script snippet or URL
const scriptMatch = val.match(/["']clarity["']\s*,\s*["']script["']\s*,\s*["']([a-zA-Z0-9]+)["']/i);
const urlMatch = val.match(/clarity\.ms\/tag\/([a-zA-Z0-9]+)/i);
if (scriptMatch && scriptMatch[1]) {
val = scriptMatch[1];
toast.success(`شناسه کلاریتی ${val} با موفقیت استخراج شد`);
} else if (urlMatch && urlMatch[1]) {
val = urlMatch[1];
toast.success(`شناسه کلاریتی ${val} با موفقیت استخراج شد`);
}
setSeo({ ...seo, clarityProjectId: val });
}}
placeholder="مثال: ybybyi7z4p یا کل کد <script>...(clarity)...</script>"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-500 outline-none text-xs font-mono font-bold"
dir="ltr"
/>
<p className="text-[11px] text-gray-400 leading-relaxed">
میتوانید شناسه پروژه یا کل کد اسکریپت Clarity را پیست کنید (خودکار استخراج میشود).
</p>
</div>
{seo.clarityProjectId && (
<div className="p-3 bg-gray-50 rounded-xl border border-gray-200 text-[10px] font-mono text-gray-600 space-y-1" dir="ltr">
<span className="text-[10px] text-gray-400 font-sans block text-right font-bold">اسکریپت فعال در سایت:</span>
<code className="text-orange-700 select-all block break-all leading-relaxed">
https://www.clarity.ms/tag/{seo.clarityProjectId}
</code>
</div>
)}
</div>
</div>
</div>
{/* Section 4: Sitemaps, Feeds & Price Crawlers (GSC, Torob, RSS) */}
<div className="space-y-6 pt-6 border-t border-gray-100">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 pb-2">
<div className="flex items-center gap-2">
<FileCode className="w-5 h-5 text-purple-600" />
<h3 className="text-base font-black text-gray-900">
آدرسهای فنی سئو، نقشههای سایت و فیدهای خزش (Sitemaps, RSS & Crawlers)
</h3>
</div>
<span className="text-xs text-gray-500 bg-gray-100 px-3 py-1 rounded-full font-bold self-start sm:self-auto">
تولید خودکار و بلادرنگ (Dynamic SSR)
</span>
</div>
{/* Quick GSC Help Banner */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200/80 rounded-2xl p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex items-start gap-3">
<div className="w-9 h-9 rounded-xl bg-blue-600 text-white flex items-center justify-center shrink-0 mt-0.5">
<Search className="w-4 h-4" />
</div>
<div>
<h4 className="text-sm font-black text-blue-900">
راهنمای ثبت در گوگل سرچ کنسول (Google Search Console):
</h4>
<p className="text-xs text-blue-700 leading-relaxed mt-0.5">
در پنل سرچ کنسول به منوی <strong>Sitemaps</strong> بروید و در کادر افزودن نقشه سایت، فقط عبارت <code className="bg-white px-1.5 py-0.5 rounded border border-blue-200 font-bold font-mono text-blue-900">sitemap.xml</code> را وارد کرده و دکمه <strong>Submit</strong> را بزنید.
</p>
</div>
</div>
<a
href="https://search.google.com/search-console/sitemaps"
target="_blank"
rel="noreferrer"
className="inline-flex items-center justify-center gap-1.5 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-xl text-xs font-bold transition-all shrink-0 shadow-xs"
>
ورود به بخش Sitemaps سرچ کنسول
<ExternalLink className="w-3.5 h-3.5" />
</a>
</div>
{/* Endpoints Groups */}
<div className="space-y-6">
{[
{
groupTitle: 'نقشه‌های سایت گوگل و موتورهای جستجو (XML Sitemaps)',
groupBadge: 'Google & Bing',
groupBadgeColor: 'bg-blue-50 text-blue-700 border-blue-200',
items: [
{
key: 'sitemap-main',
title: 'نقشه سایت اصلی و جامع (Main Index Sitemap)',
path: '/sitemap.xml',
url: `${(seo.canonicalBaseUrl || 'https://canina.ir').replace(/\/+$/, '')}/sitemap.xml`,
desc: 'نقشه سایت اصلی که باید در گوگل سرچ کنسول ثبت شود (تمام محصولات، دسته‌ها و صفحات را پوشش می‌دهد).',
tag: 'اصلی (GSC)',
tagColor: 'bg-emerald-50 text-emerald-700 border-emerald-200',
},
{
key: 'sitemap-products',
title: 'نقشه اختصاصی تمام محصولات (Products XML)',
path: '/sitemap-products.xml',
url: `${(seo.canonicalBaseUrl || 'https://canina.ir').replace(/\/+$/, '')}/sitemap-products.xml`,
desc: 'فهرست مستقیم تمام مکمل‌ها و محصولات دارویی با تاریخ آخرین ویرایش.',
tag: 'محصولات',
tagColor: 'bg-blue-50 text-blue-700 border-blue-200',
},
{
key: 'sitemap-categories',
title: 'نقشه اختصاصی دسته‌بندی‌ها (Categories XML)',
path: '/sitemap-categories.xml',
url: `${(seo.canonicalBaseUrl || 'https://canina.ir').replace(/\/+$/, '')}/sitemap-categories.xml`,
desc: 'فهرست لینک‌های دسته‌بندی موضوعی و تخصصی کالاها.',
tag: 'دسته‌ها',
tagColor: 'bg-purple-50 text-purple-700 border-purple-200',
},
{
key: 'robots-txt',
title: 'فایل راهنمای خزنده‌ها و ربات‌ها (Robots.txt)',
path: '/robots.txt',
url: `${(seo.canonicalBaseUrl || 'https://canina.ir').replace(/\/+$/, '')}/robots.txt`,
desc: 'دستورالعمل دسترسی ربات‌های Googlebot و اعلام مسیر رسمی نقشه سایت.',
tag: 'ربات‌ها',
tagColor: 'bg-gray-100 text-gray-700 border-gray-200',
},
],
},
{
groupTitle: 'فیدهای وب‌سرویس ترب و مقایسه‌گرها (Torob & Price Aggregators)',
groupBadge: 'Torob API',
groupBadgeColor: 'bg-red-50 text-red-700 border-red-200',
items: [
{
key: 'torob-products',
title: 'فید تجمیعی تمام محصولات ترب (Torob Products Feed API)',
path: '/api/torob/products',
url: `${(seo.canonicalBaseUrl || 'https://canina.ir').replace(/\/+$/, '')}/api/torob/products`,
desc: 'آدرس رسمی جهت تحویل به پشتیبانی ترب برای خزش خودکار موجودی، قیمت‌های تومانی و تخفیف‌ها.',
tag: 'پنل فروشندگان ترب',
tagColor: 'bg-red-50 text-red-700 border-red-200 font-bold',
},
{
key: 'torob-single',
title: 'استعلام لحظه‌ای تکی کالا در ترب (Torob Single Product API)',
path: '/api/torob/products/[id]',
url: `${(seo.canonicalBaseUrl || 'https://canina.ir').replace(/\/+$/, '')}/api/torob/products/{productId}`,
desc: 'اندپوینت استعلام فوری وضعیت موجودی و قیمت محصول هنگام کلیک کاربر در ترب.',
tag: 'تکی ترب',
tagColor: 'bg-orange-50 text-orange-700 border-orange-200',
},
],
},
{
groupTitle: 'فیدهای RSS و اشتراک محتوا (RSS & XML Feeds)',
groupBadge: 'RSS Feeds',
groupBadgeColor: 'bg-amber-50 text-amber-700 border-amber-200',
items: [
{
key: 'rss-blog',
title: 'فید آر‌اس‌اس وبلاگ و مقالات (Blog RSS Feed)',
path: '/blog/rss.xml',
url: `${(seo.canonicalBaseUrl || 'https://canina.ir').replace(/\/+$/, '')}/blog/rss.xml`,
desc: 'فید استاندارد XML آخرین مقالات تخصصی، اخبار و راهنماهای سلامتی حیوانات.',
tag: 'RSS وبلاگ',
tagColor: 'bg-amber-50 text-amber-700 border-amber-200',
},
{
key: 'feed-shop',
title: 'فید کالاهای فروشگاه (Products Feed XML)',
path: '/shop/feed.xml',
url: `${(seo.canonicalBaseUrl || 'https://canina.ir').replace(/\/+$/, '')}/shop/feed.xml`,
desc: 'فید XML کالاهای فروشگاه جهت اشتراک در کانال‌ها و موتورهای توزیع محتوا.',
tag: 'فید فروشگاه',
tagColor: 'bg-teal-50 text-teal-700 border-teal-200',
},
],
},
].map((group, gIdx) => (
<div key={gIdx} className="bg-gray-50/70 p-5 rounded-2xl border border-gray-200/80 space-y-4">
<div className="flex items-center justify-between pb-2 border-b border-gray-200">
<h4 className="text-sm font-black text-gray-800 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-purple-600"></span>
{group.groupTitle}
</h4>
<span className={`text-[11px] font-bold px-2.5 py-0.5 rounded-full border ${group.groupBadgeColor}`}>
{group.groupBadge}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3.5">
{group.items.map((item) => {
const isCopied = copiedKey === item.key;
return (
<div
key={item.key}
className="bg-white p-4 rounded-xl border border-gray-200/90 shadow-xs hover:border-purple-300 transition-all flex flex-col justify-between space-y-3"
>
<div className="space-y-1">
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-black text-gray-900 leading-tight">
{item.title}
</span>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-md border shrink-0 ${item.tagColor}`}>
{item.tag}
</span>
</div>
<p className="text-[11px] text-gray-500 leading-relaxed">
{item.desc}
</p>
</div>
<div className="space-y-2 pt-1">
<div
dir="ltr"
className="px-2.5 py-1.5 bg-gray-50 rounded-lg border border-gray-200 text-[11px] font-mono text-gray-700 select-all truncate"
title={item.url}
>
{item.url}
</div>
<div className="flex items-center gap-2 justify-end">
<a
href={item.url}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-bold text-gray-600 hover:text-purple-700 bg-gray-100 hover:bg-purple-50 rounded-lg transition-all"
>
مشاهده
<ExternalLink className="w-3 h-3" />
</a>
<button
type="button"
onClick={() => handleCopy(item.url, item.key, item.title)}
className={`inline-flex items-center gap-1 px-3 py-1 text-[11px] font-bold rounded-lg transition-all ${
isCopied
? 'bg-emerald-600 text-white shadow-xs'
: 'bg-purple-600 hover:bg-purple-700 text-white shadow-xs'
}`}
>
{isCopied ? (
<>
<Check className="w-3.5 h-3.5" />
کپی شد!
</>
) : (
<>
<Copy className="w-3.5 h-3.5" />
کپی لینک
</>
)}
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
))}
</div>
</div>
<div className="flex justify-end pt-4 border-t border-gray-100">
<Button
type="submit"
variant="primary"
size="md"
startIcon={Save}
isLoading={isSaving}
>
ذخیره جامع تنظیمات سئو و سرویسهای گوگل
</Button>
</div>
</form>
)}
{isMediaSelectorOpen && (
<MediaSelector
isOpen={isMediaSelectorOpen}
onClose={() => setIsMediaSelectorOpen(false)}
onSelect={(url) => {
setSeo({ ...seo, ogImageUrl: url });
setIsMediaSelectorOpen(false);
}}
/>
)}
</div>
);
}