پیشنمایش خصوصی: سایت آنلاین اما غیرعمومی تا زمان انتشار
- گیت ورود Traefik (Basic Auth) روی همه مسیرها: سایت، API، تصاویر و چت — بدون رمز فقط ۴۰۱ - لایه دوم noindex: هدر X-Robots-Tag روی همه پاسخها + robots.txt مسدودکننده + نقشه سایت خالی - اسکریپت scripts/preview-mode.sh on|off|status — ساخت خودکار نام کاربری/رمز و هماهنگکردن .env - دو دامنه (DOMAIN + DOMAIN_ALIAS) با گواهی TLS و ریدایرکت ۳۰۱ دامنه دوم به اصلی در زمان انتشار - اسکیمای FAQPage در پوسته سئوی سمت سرور (فارسی و انگلیسی) برای خزندههای بدون جاوااسکریپت - مستندسازی جریان پیشنمایش و چکلیست انتشار در README و .env.example
This commit is contained in:
@@ -188,6 +188,12 @@ def _meta_for(db: Session, page_key: str, lang: str) -> tuple[str, str]:
|
||||
|
||||
@router.get("/robots.txt", response_class=Response)
|
||||
def robots(db: Session = Depends(get_db)):
|
||||
# پیشنمایش خصوصی — هیچ خزندهای نباید چیزی ذخیره کند
|
||||
if settings.preview_mode:
|
||||
return Response(
|
||||
content="User-agent: *\nDisallow: /\n",
|
||||
media_type="text/plain; charset=utf-8",
|
||||
)
|
||||
lines = ["User-agent: *", "Disallow: /admin", "Disallow: /api", ""]
|
||||
if settings.site_url:
|
||||
lines.append(f"Sitemap: {_absolute('/sitemap.xml')}")
|
||||
@@ -206,6 +212,13 @@ def _sitemap_entry(loc: str, fa: str, en: str) -> str:
|
||||
|
||||
@router.get("/sitemap.xml", response_class=Response)
|
||||
def sitemap(db: Session = Depends(get_db)):
|
||||
# پیشنمایش خصوصی — نقشه سایت خالی تا هیچ نشانیای اعلام نشود
|
||||
if settings.preview_mode:
|
||||
xml = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>\n'
|
||||
)
|
||||
return Response(content=xml, media_type="application/xml; charset=utf-8")
|
||||
fa_urls = list(STATIC_PATHS)
|
||||
fa_urls += [
|
||||
f"/blog/{a.slug}"
|
||||
@@ -262,7 +275,7 @@ def blog_detail(slug: str, db: Session = Depends(get_db)):
|
||||
brand = _brand_name(db)
|
||||
title, excerpt = _article_pair(article, "fa")
|
||||
first_text = _doc_text(article.body) if not isinstance(article.body, list) else " ".join(article.body[:2])
|
||||
jsonld = {
|
||||
jsonld: object = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": title,
|
||||
@@ -273,6 +286,10 @@ def blog_detail(slug: str, db: Session = Depends(get_db)):
|
||||
"publisher": {"@type": "Organization", "name": brand},
|
||||
"mainEntityOfPage": _absolute(f"/blog/{article.slug}"),
|
||||
}
|
||||
# پرسشوپاسخ مقاله → اسکیمای FAQPage در همان بلوک (آرایهای از دو اسکیما)
|
||||
faq_schema = _faq_jsonld(article, "fa")
|
||||
if faq_schema:
|
||||
jsonld = [jsonld, faq_schema]
|
||||
noscript = f"<h1>{html.escape(title)}</h1><p>{html.escape(first_text)}</p>"
|
||||
return _shell(
|
||||
title=f"{title} | {brand}",
|
||||
@@ -325,12 +342,35 @@ def project_detail(slug: str, db: Session = Depends(get_db)):
|
||||
# ---------- صفحات انگلیسی (/en/...) ----------
|
||||
|
||||
|
||||
def _faq_jsonld(article: models.Article, lang: str) -> dict | None:
|
||||
"""اسکیمای FAQPage برای نتایج غنی گوگل — از پرسشوپاسخهای پایان مقاله.
|
||||
نسخه انگلیسی فقط وقتی ترجمه ثبت شده باشد جایگزین میشود."""
|
||||
faqs = article.faqs if isinstance(article.faqs, list) else []
|
||||
if lang == "en":
|
||||
faqs_en = article.faqs_en if isinstance(article.faqs_en, list) else None
|
||||
if faqs_en:
|
||||
faqs = faqs_en
|
||||
entities = [
|
||||
{"@type": "Question", "name": (f.get("question") or "").strip(),
|
||||
"acceptedAnswer": {"@type": "Answer", "text": (f.get("answer") or "").strip()}}
|
||||
for f in faqs
|
||||
if isinstance(f, dict) and (f.get("question") or "").strip()
|
||||
]
|
||||
if not entities:
|
||||
return None
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": entities,
|
||||
}
|
||||
|
||||
|
||||
def _en_blog_detail(article: models.Article, db: Session) -> HTMLResponse:
|
||||
brand = _brand_name(db, "en")
|
||||
title, excerpt = _article_pair(article, "en")
|
||||
body = article.body_en if isinstance(article.body_en, dict) else article.body
|
||||
first_text = _doc_text(body) if not isinstance(body, list) else " ".join(body[:2])
|
||||
jsonld = {
|
||||
jsonld: object = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": title,
|
||||
@@ -341,6 +381,9 @@ def _en_blog_detail(article: models.Article, db: Session) -> HTMLResponse:
|
||||
"publisher": {"@type": "Organization", "name": brand},
|
||||
"mainEntityOfPage": _absolute(f"/en/blog/{article.slug}"),
|
||||
}
|
||||
faq_schema = _faq_jsonld(article, "en")
|
||||
if faq_schema:
|
||||
jsonld = [jsonld, faq_schema]
|
||||
noscript = f"<h1>{html.escape(title)}</h1><p>{html.escape(first_text)}</p>"
|
||||
return _shell(
|
||||
title=f"{title} | {brand}",
|
||||
|
||||
Reference in New Issue
Block a user