All files / app/common/services seo.service.ts

100% Statements 47/47
100% Branches 17/17
100% Functions 4/4
100% Lines 44/44

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 822x 2x 2x                       2x   14x 14x 14x       4x   4x 4x 4x 4x     4x 4x 4x 4x 2x       4x 4x 4x 4x 2x   4x 2x     4x 4x       7x 7x   5x 5x 4x 4x 4x   5x       4x 4x 4x   3x 3x 2x 2x 2x 2x     3x      
import { Inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { Meta, Title } from '@angular/platform-browser';
 
export interface SeoMetadata {
  title: string;
  description: string;
  keywords: string[];
  canonicalUrl?: string;
  imageUrl?: string;
  twitterHandle?: string;
}
 
@Injectable({ providedIn: 'root' })
export class SeoService {
  constructor(
    private readonly meta: Meta,
    private readonly title: Title,
    @Inject(DOCUMENT) private readonly document: Document,
  ) {}
 
  setDefaultTags(metaData: SeoMetadata): void {
    this.title.setTitle(metaData.title);
 
    this.meta.updateTag({ name: 'description', content: metaData.description });
    this.meta.updateTag({ name: 'keywords', content: metaData.keywords.join(', ') });
    this.meta.updateTag({ name: 'robots', content: 'index, follow' });
    this.meta.updateTag({ name: 'theme-color', content: '#a33360' });
 
    // Open Graph
    this.meta.updateTag({ property: 'og:title', content: metaData.title });
    this.meta.updateTag({ property: 'og:description', content: metaData.description });
    this.meta.updateTag({ property: 'og:type', content: 'website' });
    if (metaData.imageUrl) {
      this.meta.updateTag({ property: 'og:image', content: metaData.imageUrl });
    }
 
    // Twitter
    this.meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' });
    this.meta.updateTag({ name: 'twitter:title', content: metaData.title });
    this.meta.updateTag({ name: 'twitter:description', content: metaData.description });
    if (metaData.imageUrl) {
      this.meta.updateTag({ name: 'twitter:image', content: metaData.imageUrl });
    }
    if (metaData.twitterHandle) {
      this.meta.updateTag({ name: 'twitter:creator', content: metaData.twitterHandle });
    }
 
    const canonicalUrl = metaData.canonicalUrl ?? this.document?.location?.href ?? '';
    this.setCanonical(canonicalUrl);
  }
 
  setCanonical(url: string): void {
    const head = this.document.head;
    if (!head) return;
 
    let linkEl = head.querySelector("link[rel='canonical']") as HTMLLinkElement | null;
    if (!linkEl) {
      linkEl = this.document.createElement('link');
      linkEl.setAttribute('rel', 'canonical');
      head.appendChild(linkEl);
    }
    linkEl.setAttribute('href', url);
  }
 
  attachStructuredData(schema: Record<string, unknown>): void {
    const scriptId = 'pnb-structured-data';
    const head = this.document.head;
    if (!head) return;
 
    let scriptEl = this.document.getElementById(scriptId) as HTMLScriptElement | null;
    if (!scriptEl) {
      scriptEl = this.document.createElement('script');
      scriptEl.id = scriptId;
      scriptEl.type = 'application/ld+json';
      head.appendChild(scriptEl);
    }
 
    scriptEl.textContent = JSON.stringify(schema, null, 2);
  }
}