feat(seo): add sitemaps, RSS feeds, robots.txt, and Torob crawler endpoints directory to SeoSettingsPage
Some checks failed
Deploy Canina / deploy (push) Successful in 32s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 6s

This commit is contained in:
parsa aghaei 2026-09-02 13:32:33 +03:30
parent 83c19903ab
commit 0b37825094

View File

@ -1,5 +1,9 @@
import { useState, useEffect } from 'react';
import { Globe, Save, Sliders, Monitor, Sparkles, Search, BarChart3, Code2, CheckCircle2, HelpCircle, Activity, ExternalLink, ShieldCheck } from 'lucide-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';
@ -29,6 +33,16 @@ export default function SeoSettingsPage() {
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;
@ -635,6 +649,227 @@ export default function SeoSettingsPage() {
</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"