// تگ‌های سئوی سمت مرورگر — عنوان، توضیح متا، OG، توییتر، canonical، hreflang و JSON-LD. // محتوا از دیتابیس می‌آید؛ برای خزنده‌های بدون جاوااسکریپت، بک‌اند همان تگ‌ها را // سمت سرور داخل index.html تزریق می‌کند (app/seo_pages.py). import { localeHref, langFromPathname } from './i18n/lang' import type { Lang } from './i18n/lang' export function setMetaDescription(content: string): void { // بدون توضیح از دیتابیس، پیش‌فرض استاتیک index.html دست‌نخورده می‌ماند if (!content.trim()) return let el = document.querySelector('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 { // زبان از پیشوند /en جدا می‌شود — کلیدهای سئو برای هر دو زبان یکی است const clean = pathname === '/en' ? '/' : pathname.startsWith('/en/') ? pathname.slice(3) : pathname if (clean === '/') return 'home' const seg = clean.split('/')[1] ?? '' const known = ['about', 'services', 'projects', 'investment', 'blog', 'faq', 'contact'] // مسیرهای جزئی مثل /blog/{slug} شامل نمی‌شوند تا اثر خود صفحه جزئیات برسانند return known.includes(seg) && clean === `/${seg}` ? seg : null } function upsertMeta(attr: 'name' | 'property', key: string, content: string): void { let el = document.querySelector(`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('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) } const HREFLANGS: [Lang, string][] = [ ['fa', 'fa'], ['en', 'en'], ['fa', 'x-default'], ] /** لینک‌های alternate دو زبانه — مسیر جاری و معادل آن در زبان دیگر */ function upsertAlternates(path: string): void { for (const [targetLang, hreflang] of HREFLANGS) { // x-default همیشه نسخه فارسی است const target = localeHref(path, targetLang) const attr = `data-hreflang="${hreflang}"` let el = document.querySelector(`link[${attr}]`) if (!el) { el = document.createElement('link') el.setAttribute('rel', 'alternate') el.setAttribute('hreflang', hreflang) el.setAttribute('data-hreflang', hreflang) document.head.appendChild(el) } el.setAttribute('href', new URL(target, window.location.origin).href) } } function setJsonLd(data: Record | Record[] | null): void { const id = 'seo-jsonld' let el = document.getElementById(id) if (!data || (Array.isArray(data) && data.length === 0)) { 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' /** زبان جاری — برای og:locale و لینک‌های hreflang */ lang?: Lang /** اسکیمای JSON-LD — یک شیء یا آرایه‌ای از چند اسکیما (مثل Article + FAQPage) */ jsonLd?: Record | Record[] | 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() const lang: Lang = input.lang ?? langFromPathname(path) 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('property', 'og:locale', lang === 'en' ? 'en_US' : 'fa_IR') upsertMeta('name', 'twitter:card', 'summary_large_image') if (image) { upsertMeta('property', 'og:image', new URL(image, window.location.origin).href) } else { document.querySelector('meta[property="og:image"]')?.remove() } upsertAlternates(path) setJsonLd(input.jsonLd ?? null) }