import { Injectable, inject } from '@angular/core'; import { Meta, Title } from '@angular/platform-browser'; import { environment } from '../../environments/environment'; import { Item } from '../models'; import { getDiscountedPrice, getMainImage } from '../utils/item.utils'; @Injectable({ providedIn: 'root' }) export class SeoService { private meta = inject(Meta); private title = inject(Title); private readonly siteUrl = `https://${environment.domain}`; private readonly siteName = environment.brandFullName; /** * Set Open Graph & Twitter Card meta tags for a product/item page. */ setItemMeta(item: Item): void { const price = item.discount > 0 ? getDiscountedPrice(item) : item.price; const imageUrl = this.resolveUrl(getMainImage(item)); const itemUrl = `${this.siteUrl}/item/${item.itemID}`; const description = this.truncate(this.stripHtml(item.description), 160); const titleText = `${item.name} — ${this.siteName}`; this.title.setTitle(titleText); this.setOrUpdate([ // Open Graph { property: 'og:type', content: 'product' }, { property: 'og:title', content: item.name }, { property: 'og:description', content: description }, { property: 'og:image', content: imageUrl }, { property: 'og:url', content: itemUrl }, { property: 'og:site_name', content: this.siteName }, { property: 'og:locale', content: 'ru_RU' }, // Product-specific OG tags { property: 'product:price:amount', content: price.toFixed(2) }, { property: 'product:price:currency', content: item.currency || 'RUB' }, // Twitter Card { name: 'twitter:card', content: 'summary_large_image' }, { name: 'twitter:title', content: item.name }, { name: 'twitter:description', content: description }, { name: 'twitter:image', content: imageUrl }, // Standard meta { name: 'description', content: description }, ]); } /** * Reset meta tags back to defaults (call on navigation away from item page). */ resetToDefaults(): void { const defaultTitle = `${this.siteName} — Маркетплейс товаров и услуг`; const defaultDescription = 'Современный маркетплейс для покупки цифровых товаров. Широкий выбор товаров, удобный поиск, быстрая доставка.'; const defaultImage = `${this.siteUrl}/og-image.jpg`; this.title.setTitle(defaultTitle); this.setOrUpdate([ { property: 'og:type', content: 'website' }, { property: 'og:title', content: defaultTitle }, { property: 'og:description', content: defaultDescription }, { property: 'og:image', content: defaultImage }, { property: 'og:url', content: this.siteUrl }, { property: 'og:site_name', content: this.siteName }, { property: 'og:locale', content: 'ru_RU' }, { name: 'twitter:card', content: 'summary_large_image' }, { name: 'twitter:title', content: defaultTitle }, { name: 'twitter:description', content: defaultDescription }, { name: 'twitter:image', content: defaultImage }, { name: 'description', content: defaultDescription }, ]); // Remove product-specific tags this.meta.removeTag("property='product:price:amount'"); this.meta.removeTag("property='product:price:currency'"); } private setOrUpdate(tags: Array<{ property?: string; name?: string; content: string }>): void { for (const tag of tags) { const selector = tag.property ? `property='${tag.property}'` : `name='${tag.name}'`; const existing = this.meta.getTag(selector); if (existing) { this.meta.updateTag(tag as any, selector); } else { this.meta.addTag(tag as any); } } } /** Convert relative URLs to absolute */ private resolveUrl(url: string): string { if (!url || url.startsWith('http')) return url; return `${this.siteUrl}${url.startsWith('/') ? '' : '/'}${url}`; } /** Strip HTML tags from a string */ private stripHtml(html: string): string { return html?.replace(/<[^>]*>/g, '') || ''; } /** Truncate text to maxLength, adding ellipsis */ private truncate(text: string, maxLength: number): string { if (!text || text.length <= maxLength) return text || ''; return text.substring(0, maxLength - 1) + '…'; } }