"""سئوی سمت سرور — robots.txt، sitemap.xml و سرو HTML صفحات وبلاگ/پروژه با متای تزریقشده. فرانتاند یک SPA است؛ خزندههای اجتماعی (تلگرام، واتساپ و...) جاوااسکریپت اجرا نمیکنند. این ماژول همان index.html بیلد فرانتاند را میخواند و پیش از ارسال، تگهای title / description / OG / canonical / hreflang / JSON-LD را از دیتابیس داخل آن تزریق میکند — بازدیدکنندههای عادی همان SPA همیشگی را میبینند و خزندهها متای درست را میخوانند. دو زبانه بودن: نسخه فارسی روی مسیرهای اصلی و نسخه انگلیسی روی /en/... سرو میشود؛ هر دو در sitemap با hreflang متقابل و در head هر صفحه با link alternate معرفی میشوند. """ import html import json import re from pathlib import Path from fastapi import APIRouter, Depends, Response from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session from . import models from .config import settings from .database import get_db router = APIRouter() STATIC_PATHS = ["/", "/about", "/services", "/projects", "/investment", "/blog", "/faq", "/contact"] # صفحاتی که متای آنها از seo_meta خوانده میشود — کلید همان page_key است META_KEYS = ["home", "about", "services", "projects", "investment", "blog", "faq", "contact"] FALLBACK_FA_BRAND = "گروه ساختمانی آرامش" FALLBACK_EN_BRAND = "Aramesh Construction Group" def _absolute(path: str) -> str: if not path: return "" if path.startswith(("http://", "https://")): return path site = settings.site_url.rstrip("/") return f"{site}{path}" if site else path def _en_path(path: str) -> str: """مسیر انگلیسی معادل یک مسیر فارسی — / و /en یکدیگرند.""" return "/en" if path == "/" else f"/en{path}" def _fa_path(path: str) -> str: """مسیر فارسی معادل یک مسیر انگلیسی.""" if path == "/en": return "/" return path[3:] if path.startswith("/en/") else path def _alternates(path: str, lang: str) -> str: """تگهای hreflang متقابل فارسی/انگلیسی برای head.""" fa = path if lang == "fa" else _fa_path(path) en = _en_path(path) if lang == "fa" else path return ( f'\n' f' \n' f' ' ) def _read_index() -> str | None: try: return (Path(settings.frontend_dist) / "index.html").read_text(encoding="utf-8") except OSError: return None def _doc_text(doc: object, limit: int = 300) -> str: """متن ساده سند TipTap — برای noscript و پیشنمایش.""" words: list[str] = [] def walk(node: object) -> None: if not isinstance(node, dict): return if node.get("type") == "text" and isinstance(node.get("text"), str): words.append(node["text"]) for child in node.get("content") or []: walk(child) if isinstance(doc, dict): for block in doc.get("content") or []: walk(block) text = " ".join(words).strip() return text[:limit] + ("…" if len(text) > limit else "") def _shell( *, title: str, description: str, path: str, lang: str = "fa", og_image: str = "", og_type: str = "website", jsonld: dict | None = None, noscript: str = "", ) -> HTMLResponse: """index.html فرانتاند با متای تزریقشده؛ اگر بیلد در دسترس نبود، صفحه حداقلی.""" title = title.strip() or (FALLBACK_EN_BRAND if lang == "en" else FALLBACK_FA_BRAND) description = description.strip() canonical = _absolute(path) image = _absolute(og_image) esc = html.escape alternates = _alternates(path, lang) # تگ title جای تگ موجود index.html را میگیرد و در بلوک تکرار نمیشود tags = [ f'', f'', f'', f'', f'', f'', f'', ] if image: tags.append(f'') if jsonld: payload = json.dumps(jsonld, ensure_ascii=False).replace("", "<\\/") tags.append(f'') block = "\n ".join(tags) raw = _read_index() if raw is None: # بیلد فرانتاند سوار نشده — صفحه حداقلی با خودِ متا تا خزندهها دست خالی نمانند lang_attr = 'lang="en" dir="ltr"' if lang == "en" else 'lang="fa" dir="rtl"' body = f"
{esc(description)}
" return HTMLResponse( f'' f"