Files
aramesh-backend/frontend/src/seo.ts
T
MMD b31124e1e4 وبلاگ و سئو: پیش‌نویس/انتشار مقاله + کلمه کلیدی سئو در دیتابیس + OG/canonical/JSON-LD و متای سمت سرور + تغییر رمز و محدودسازی ورود + تست‌ها
- مقاله‌ها ستون status (draft/published) گرفتند — GET /api/site و همه انتهای عمومی فقط منتشرشده را برمی‌گردانند؛ autosave دیگر پیش‌نویس نیمه‌کاره را زنده نمی‌کند
- کلمه کلیدی هدف چک‌لیست سئو فیلد seo_keyphrase شد (قبلاً localStorage بود)
- robots.txt و sitemap.xml از بک‌اند + سرو /blog و /projects با تزریق title/description/OG/JSON-LD از دیتابیس برای خزنده‌های بدون JS (app/seo_pages.py)
- فرانت: applySeo در seo.ts (OG/توییتر/canonical/JSON-LD)، ادغام پیش‌نویس برای ادمین، برچسب پیش‌نویس در وبلاگ، حالت خالی وبلاگ، دکمه انتشار در صفحه نوشتن
- تغییر رمز ادمین POST /api/auth/change-password + دیالوگ «رمز» در نوار مدیریت
- محدودسازی ورود: ۵ تلاش ناموفق در ۱۵ دقیقه از هر IP → 429
- ساخت دکمه + برای ردیف سئوی صفحه‌های بدون ردیف (SingletonPill)
- تست‌های pytest (۱۵ مورد) + پین‌کردن requirements + حذف package.json سرگردان فرانت
2026-09-07 04:26:18 +03:30

95 lines
3.7 KiB
TypeScript

// تگ‌های سئوی سمت مرورگر — عنوان، توضیح متا، OG، توییتر، canonical و JSON-LD.
// محتوا از دیتابیس می‌آید؛ برای خزنده‌های بدون جاوااسکریپت، بک‌اند همان تگ‌ها را
// سمت سرور داخل index.html تزریق می‌کند (app/seo_pages.py).
export function setMetaDescription(content: string): void {
// بدون توضیح از دیتابیس، پیش‌فرض استاتیک index.html دست‌نخورده می‌ماند
if (!content.trim()) return
let el = document.querySelector<HTMLMetaElement>('meta[name="description"]')
if (!el) {
el = document.createElement('meta')
el.setAttribute('name', 'description')
document.head.appendChild(el)
}
el.setAttribute('content', content.trim())
}
/** کلید سئوی مسیر جاری — فقط صفحه‌های فهرست دقیق؛ صفحه‌های جزئیات خودشان عنوان می‌گذارند */
export function seoKeyForPath(pathname: string): string | null {
if (pathname === '/') return 'home'
const seg = pathname.split('/')[1] ?? ''
const known = ['about', 'services', 'projects', 'investment', 'blog', 'faq', 'contact']
// مسیرهای جزئی مثل /blog/{slug} شامل نمی‌شوند تا اثر خود صفحه جزئیات برسانند
return known.includes(seg) && pathname === `/${seg}` ? seg : null
}
function upsertMeta(attr: 'name' | 'property', key: string, content: string): void {
let el = document.querySelector<HTMLMetaElement>(`meta[${attr}="${key}"]`)
if (!el) {
el = document.createElement('meta')
el.setAttribute(attr, key)
document.head.appendChild(el)
}
el.setAttribute('content', content)
}
function upsertCanonical(path: string): void {
let el = document.querySelector<HTMLLinkElement>('link[rel="canonical"]')
if (!el) {
el = document.createElement('link')
el.setAttribute('rel', 'canonical')
document.head.appendChild(el)
}
el.setAttribute('href', new URL(path, window.location.origin).href)
}
function setJsonLd(data: Record<string, unknown> | null): void {
const id = 'seo-jsonld'
let el = document.getElementById(id)
if (!data) {
el?.remove()
return
}
if (!el) {
el = document.createElement('script')
el.setAttribute('type', 'application/ld+json')
el.id = id
document.head.appendChild(el)
}
el.textContent = JSON.stringify(data)
}
export type SeoInput = {
title: string
description?: string
/** مسیر canonical — پیش‌فرض مسیر جاری مرورگر */
path?: string
/** تصویر OG — مسیر نسبی مثل /media/... به آدرس مطلق تبدیل می‌شود */
image?: string
type?: 'website' | 'article'
jsonLd?: Record<string, unknown> | null
}
/** تنظیم همه تگ‌های سئوی صفحه در یک فراخوانی */
export function applySeo(input: SeoInput): void {
const title = input.title.trim()
const description = (input.description ?? '').trim()
const path = input.path ?? window.location.pathname
const image = (input.image ?? '').trim()
document.title = title
setMetaDescription(description)
upsertCanonical(path)
upsertMeta('property', 'og:title', title)
upsertMeta('property', 'og:description', description)
upsertMeta('property', 'og:type', input.type ?? 'website')
upsertMeta('property', 'og:url', new URL(path, window.location.origin).href)
upsertMeta('name', 'twitter:card', 'summary_large_image')
if (image) {
upsertMeta('property', 'og:image', new URL(image, window.location.origin).href)
} else {
document.querySelector<HTMLMetaElement>('meta[property="og:image"]')?.remove()
}
setJsonLd(input.jsonLd ?? null)
}