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:
+14
-4
@@ -16,14 +16,24 @@
|
||||
file_server
|
||||
}
|
||||
|
||||
# مسیریابی SPA — مسیرهای ناشناختهٔ غیرasset به index.html برمیگردند
|
||||
# هر مسیر دارای پسوند فایل (فونت، تصویر، نقشه، آیکون، …): فقط فایل واقعی سرو میشود؛
|
||||
# فایل ناموجود ۴۰۴ میگیرد، نه HTML اسپا — خزندهها (گوگل) پاسخ HTML برای فونت/تصویر
|
||||
# را «خطای منابع» ثبت میکنند و CDN هم همان HTML را کش میکند
|
||||
@filelike path_regexp fileext \.[a-zA-Z0-9]{2,5}$
|
||||
handle @filelike {
|
||||
file_server
|
||||
}
|
||||
|
||||
# مسیریابی SPA — فقط مسیرهای بدون پسوند (مسیرهای اپ) به index.html برمیگردند
|
||||
handle {
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
|
||||
# بقیه (از جمله index.html و fallback اسپا): بدون کش مرورگر تا هر استقرار فوری دیده شود
|
||||
@notassets not path /assets/*
|
||||
header @notassets Cache-Control "no-cache"
|
||||
# index.html و fallback اسپا: بدون کش مرورگر تا هر استقرار فوری دیده شود
|
||||
header Cache-Control "no-cache"
|
||||
# فایلهای ثابت (فونت/تصویر): یک ساعت کش مرورگر
|
||||
header @filelike Cache-Control "public, max-age=3600"
|
||||
# داراییهای هشدار: کش یکساله
|
||||
header @assets Cache-Control "public, immutable, max-age=31536000"
|
||||
}
|
||||
|
||||
+5
-1
@@ -2,7 +2,11 @@
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<!-- آیکونهای سایت از لوگوی رسمی برند (قبلاً به vite.svg پیشفرض اشاره میکرد که وجود نداشت) -->
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- پیشبارگذاری فونتهای اصلی — بدون این، swap فونت با تأخیر میآید (LCP) -->
|
||||
<link rel="preload" href="/fonts/pinar/Pinar-Light.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
63730e04d66942f8b422d2d017f67fe4
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1 @@
|
||||
f48526ce084d4b76a6e522283d3ad68f
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user