optimising and making it better
This commit is contained in:
117
src/app/services/seo.service.ts
Normal file
117
src/app/services/seo.service.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
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) + '…';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user