feat(seo): ایندکس‌پذیری کامل، محتوای سمت سرور برای خزنده‌های AI و رفع باگ ایندکس

سئوی سمت سرور (backend/app/seo_pages.py)
- متا/canonical/hreflang/JSON-LD برای همهٔ صفحات عمومی (شش صفحهٔ استاتیک فارسی که قبلاً پوستهٔ خالی بودند)
- canonical سمت سرور (قبلاً فقط با JS ست می‌شد)، ۴۰۴ واقعی برای مسیرهای ناموجود
- اسکیما: Organization با لوگوی ImageObject، FAQPage، LocalBusiness، BreadcrumbList،
  CreativeWork پروژه با همهٔ تصاویر به‌صورت ImageObject (نام/توضیح + موقعیت پروژه)
- hreflang فقط وقتی ترجمهٔ انگلیسی واقعاً وجود دارد؛ sitemap با URLهای درصد‌گذاری‌شده
- llms.txt و robots.txt تازه‌ساز از دیتابیس؛ سیاست صریح خزنده‌های AI + Allow: /api/site
- _site_content_html: محتوای واقعی سایت به‌صورت HTML خوانا در <noscript> —
  خزنده‌های بدون جاوااسکریپت (GPTBot/OAI-SearchBot/ClaudeBot/PerplexityBot) از ۲۴۰ نویسه
  به ۸٬۵۰۰+ نویسه محتوای معنایی می‌رسند
- Cache-Control: no-store روی HTML و llms.txt (جلوگیری از کش کهنهٔ CDN)، max-age روی sitemap

فرانت‌اند
- api.tsx: خواندن دادهٔ تزریق‌شدهٔ سرور (window.__SITE__) به‌عنوان حالت اولیه — رندر بدون XHR
- App.tsx: اسکلت بارگذاری به‌جای DOM خالی (رفع Soft 404 گوگل)
- index.html: فاوآیکون واقعی برند (قبلاً به vite.svg ناموجود اشاره می‌کرد) + preload فونت

زیرساخت
- Dockerfile: آینهٔ PyPI (Tsinghua/Aliyun) — PyPI برای IP ایران بلاک است
- Caddyfile: هر مسیر پسوند‌دار بدون فایل واقعی ۴۰۴ می‌گیرد (قبلاً HTML اسپا برمی‌گرداند)
- docker-compose.prod.yml: مسیرهای seo برای صفحات استاتیک + llms.txt + کلیدهای IndexNow

داده
- seed_data*.py: حذف دفاتر و معرفی‌های دمو (که در هر استقرار برمی‌گشتند)؛
  تنها دفتر واقعی: اصفهان، ملاصدرا شمالی

دارایی‌ها
- فاوآیکون/آیکون‌های برند از لوگوی رسمی + کلیدهای IndexNow
This commit is contained in:
aramesh-site deploy
2026-10-09 23:29:26 +00:00
parent 353d4639b4
commit 1d0c1ae53d
17 changed files with 397 additions and 66 deletions
+8 -1
View File
@@ -149,7 +149,14 @@ function AppShell() {
<ScrollToTop />
<AppChrome />
</>
) : null}
) : (
// اسکلت اولیه تا رسیدن داده‌های API — صفحه هرگز خالی رندر نمی‌شود.
// (خزنده‌های رندرکننده — مثل گوگل — صفحهٔ خالی را Soft 404 می‌بینند)
<div className="flex min-h-screen flex-col items-center justify-center px-6 text-center">
<h1 className="text-2xl font-bold text-ivory">{t('brandFallback')}</h1>
<p className="mt-4 text-sm font-light text-ivory/70">{t('loading')}</p>
</div>
)}
</div>
);
}
+15 -3
View File
@@ -228,6 +228,14 @@ interface SiteContextValue {
updateSite: (mutate: (data: SiteData) => void) => void
}
declare global {
interface Window {
/** دادهٔ سایت که بک‌اند داخل HTML تزریق می‌کند (app/seo_pages.py) —
* رندر بدون انتظار برای درخواست /api/site؛ خزنده‌های رندرکننده صفحهٔ کامل می‌بینند */
__SITE__?: SiteData
}
}
const SiteContext = createContext<SiteContextValue>({
data: null,
error: false,
@@ -242,7 +250,10 @@ const SiteContext = createContext<SiteContextValue>({
const SITE_TIMEOUT_MS = 15_000
export function SiteProvider({ children }: { children: ReactNode }) {
const [data, setData] = useState<SiteData | null>(null)
// دادهٔ تزریق‌شدهٔ سرور (window.__SITE__) به‌عنوان حالت اولیه — اولین رندر کامل است
// و به درخواست /api/site وابسته نیست؛ درخواست همچنان در پس‌زمینه تازه‌سازی می‌کند
const embedded = typeof window !== 'undefined' ? window.__SITE__ : undefined
const [data, setData] = useState<SiteData | null>(embedded ?? null)
const [error, setError] = useState(false)
const load = useCallback(() => {
@@ -284,11 +295,12 @@ export function SiteProvider({ children }: { children: ReactNode }) {
}
if (!cancelled) {
if (json.brand && json.hero && json.footer && json.nav) setData(json)
else setError(true)
// با دادهٔ تزریق‌شده، پاسخ نامعتبر فقط یعنی «تازه‌سازی نشد» — خطا نشان نده
else if (!embedded) setError(true)
}
})
.catch(() => {
if (!cancelled) setError(true)
if (!cancelled && !embedded) setError(true)
})
return () => {
cancelled = true