901 lines
49 KiB
TypeScript
901 lines
49 KiB
TypeScript
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"><meta name="google-site-verification"...></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">متاتگ تزریق شده در <head> سایت:</span>
|
||
<code className="text-blue-700 select-all block break-all">
|
||
<meta name="google-site-verification" content="{seo.googleSiteVerification}" />
|
||
</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 فعال و در <head> لود میشود
|
||
</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">
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id={seo.googleAnalyticsId}"></script>
|
||
</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 در <head> و تگ noscript در <body> فعال است
|
||
</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">تزریق خودکار در <head> و ابتدای <body>:</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: <noscript><iframe src="...id={seo.googleTagManagerId}"></noscript>
|
||
</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>
|
||
);
|
||
}
|