diff --git a/backend/app/models.py b/backend/app/models.py index 6a2507e..9035623 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -119,10 +119,24 @@ class Article(Base): faqs: Mapped[list] = mapped_column(JSON, default=list) faqs_en: Mapped[Optional[list]] = mapped_column(JSON) # NULL یعنی بدون ترجمه — برگشت به فارسی author_id: Mapped[int | None] = mapped_column(Integer, nullable=True) # نویسندهٔ creator — برای مقالههای ادمین NULL + # نام نویسنده (رشتهٔ آزاد) — سربرگ مقاله و اسکیمای Article (نوع Person برای سیگنال E-E-A-T) + author: Mapped[str] = mapped_column(String(128), default="") # draft | published — پیشنویس فقط برای ادمین دیده میشود status: Mapped[str] = mapped_column(String(16), default="published") # کلمه کلیدی هدف چکلیست سئو — راهنمای نوشتن، در سایت نمایش داده نمیشود seo_keyphrase: Mapped[str] = mapped_column(String(128), default="") + # توضیحات سئوی اختصاصی مقاله — در تگ description و اسکیما، در اولویت بر چکیده + meta_description: Mapped[str] = mapped_column(Text, default="") + meta_description_en: Mapped[str] = mapped_column(Text, default="") + # نشانی انگلیسی مقاله — مسیر /en/blog/...؛ خالی یعنی همان اسلاگ فارسی + slug_en: Mapped[str] = mapped_column(String(128), default="", index=True) + # کلمه کلیدی هدف نسخه انگلیسی — چکلیست سئو + seo_keyphrase_en: Mapped[str] = mapped_column(String(128), default="") + # نام نویسنده نسخه انگلیسی + author_en: Mapped[str] = mapped_column(String(128), default="") + # هشتگها/برچسبها — هر زبان جدا + tags: Mapped[list] = mapped_column(JSON, default=list) + tags_en: Mapped[list] = mapped_column(JSON, default=list) class Testimonial(Base): diff --git a/backend/app/schemas.py b/backend/app/schemas.py index b3de70d..143c287 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -98,6 +98,15 @@ class ArticleOut(ORMModel): faqs_en: Optional[list[dict]] = None status: str seo_keyphrase: str + # نام نویسنده و توضیحات سئوی اختصاصی — سربرگ مقاله، تگ description و اسکیمای Article + author: str = "" + meta_description: str = "" + meta_description_en: str = "" + slug_en: str = "" + seo_keyphrase_en: str = "" + author_en: str = "" + tags: list[str] = [] + tags_en: list[str] = [] class TestimonialOut(ORMModel): @@ -517,6 +526,14 @@ class ArticleIn(BaseModel): faqs_en: list[dict] = [] status: Literal["draft", "published"] = "draft" seo_keyphrase: str = Field(default="", max_length=128) + seo_keyphrase_en: str = Field(default="", max_length=128) + slug_en: str = Field(default="", max_length=128) + author: str = Field(default="", max_length=128) + author_en: str = Field(default="", max_length=128) + tags: list[str] = [] + tags_en: list[str] = [] + meta_description: str = "" + meta_description_en: str = "" _validate_body = field_validator("body")(validate_doc_body) _validate_body_en = field_validator("body_en")(validate_doc_body) @@ -823,6 +840,14 @@ class ArticlePatch(BaseModel): faqs_en: Optional[list[dict]] = None status: Optional[Literal["draft", "published"]] = None seo_keyphrase: Optional[str] = None + seo_keyphrase_en: Optional[str] = None + slug_en: Optional[str] = None + author: Optional[str] = None + author_en: Optional[str] = None + tags: Optional[list[str]] = None + tags_en: Optional[list[str]] = None + meta_description: Optional[str] = None + meta_description_en: Optional[str] = None _validate_body = field_validator("body")(validate_doc_body) _validate_body_en = field_validator("body_en")(validate_doc_body) TestimonialPatch = _make_patch("TestimonialPatch", TestimonialIn) diff --git a/backend/app/seo_pages.py b/backend/app/seo_pages.py index 0dfab46..977db83 100644 --- a/backend/app/seo_pages.py +++ b/backend/app/seo_pages.py @@ -1,17 +1,28 @@ -"""سئوی سمت سرور — robots.txt، sitemap.xml و سرو HTML صفحات وبلاگ/پروژه با متای تزریقشده. +"""سئوی سمت سرور — robots.txt، sitemap.xml، llms.txt و سرو همهٔ صفحات با متای تزریقی. -فرانتاند یک SPA است؛ خزندههای اجتماعی (تلگرام، واتساپ و...) جاوااسکریپت اجرا نمیکنند. +فرانتاند یک SPA است؛ خزندههای جستوجو و شبکههای اجتماعی جاوااسکریپت اجرا نمیکنند. این ماژول همان index.html بیلد فرانتاند را میخواند و پیش از ارسال، تگهای -title / description / OG / canonical / hreflang / JSON-LD را از دیتابیس داخل آن تزریق میکند — -بازدیدکنندههای عادی همان SPA همیشگی را میبینند و خزندهها متای درست را میخوانند. +title / description / canonical / OG / hreflang / JSON-LD را از دیتابیس داخل آن تزریق میکند — +بازدیدکنندههای عادی همان SPA همیشگی را میبینند و خزندهها متای درست را در HTML اولیه میخوانند. +(اسکیمای صفحه داخل ') + # id="seo-jsonld" — سمت مرورگر همان اسکریپت بازنویسی میشود، نه اینکه تکراری ساخته شود + tags.append(f'') + if org_jsonld: + payload = json.dumps(org_jsonld, ensure_ascii=False).replace("", "<\\/") + tags.append(f'') block = "\n ".join(tags) raw = _read_index() @@ -132,14 +274,17 @@ def _shell( body = f"
{esc(description)}
" return HTMLResponse( f'' - f"