Skip to content

Instantly share code, notes, and snippets.

@zzpzaf
Created December 4, 2024 09:10
Show Gist options
  • Save zzpzaf/cc35464a4fd3a24b48a20a04fe7c04e1 to your computer and use it in GitHub Desktop.
Save zzpzaf/cc35464a4fd3a24b48a20a04fe7c04e1 to your computer and use it in GitHub Desktop.
ang18-SSR-SEO-SupportBlog2-SeoService1-No-renderer-version
import { inject, Injectable } from '@angular/core';
import { PostStructuredData, PostTags } from '../objects/seoObjects';
import { ArticleDTO } from '../objects/dataObjects';
import { environment } from '../../environments/environment';
import { Pages } from '../objects/blogObjects';
import { Meta, Title } from '@angular/platform-browser';
import { DOCUMENT } from '@angular/common';
@Injectable({
providedIn: 'root',
})
export class SeoService {
private document = inject(DOCUMENT);
private metaService = inject(Meta);
private titleService = inject(Title);
private postsPrefix: string = environment.postsPrefix;
constructor() {}
// Main method to update meta tags and structured data
updateTags(pgNr: number, article: ArticleDTO, content: string): void {
this.clearMetaTags();
const postTags = pgNr === 0
? this.createArticleMetaTags(article, content)
: this.createPageMetaTags(pgNr);
this.applyMetaTags(postTags, content);
this.addStructuredData(postTags, content);
}
// Creates meta tags for an article
private createArticleMetaTags(article: ArticleDTO, content: string): PostTags {
const postTags = new PostTags();
postTags.postTitle = this.trimText(article.articleTitle, 60);
postTags.postDescription = article.articleSubTitle.trim() || 'No Article Subtitle';
postTags.postCanonicalUrl = `${window.location.origin}/${this.postsPrefix}${article.articleSlug}`;
postTags.ogTitle = this.trimText(article.articleTitle, 50);
postTags.ogDescription = this.trimText(article.articleDescription || article.articleSubTitle, 200);
postTags.postUser = article.userSlugName;
postTags.postCreationTimestamp = article.articleCreationTimestamp;
return postTags;
}
// Creates meta tags for a (static) page
private createPageMetaTags(pgNr: number): PostTags {
const postTags = new PostTags();
const page = Pages.find((p) => p.PageId === pgNr);
if (page) {
postTags.postTitle = page.PageTitle;
postTags.postDescription = page.PageDescription;
postTags.postCanonicalUrl = `${window.location.origin}/${page.PageSlug}`;
postTags.ogTitle = this.trimText(page.PageTitle, 50);
postTags.ogDescription = this.trimText(page.PageDescription, 200);
}
return postTags;
}
// Applies meta tags to the document
private applyMetaTags(postTags: PostTags, content: string): void {
this.titleService.setTitle(postTags.postTitle);
this.updateCanonicalLink(postTags.postCanonicalUrl);
this.metaService.updateTag({ name: 'description', content: postTags.postDescription });
this.metaService.updateTag({ property: 'og:title', content: postTags.ogTitle });
this.metaService.updateTag({ property: 'og:description', content: postTags.ogDescription });
this.metaService.updateTag({ property: 'og:url', content: postTags.postCanonicalUrl });
const firstImgUrl = this.getPostFirstImageAsMain(content);
if (firstImgUrl) {
this.metaService.updateTag({ property: 'og:image', content: firstImgUrl });
this.metaService.updateTag({ name: 'twitter:image', content: firstImgUrl });
}
this.metaService.updateTag({ name: 'twitter:card', content: postTags.postTwiiterCardValue });
this.metaService.updateTag({ name: 'twitter:title', content: postTags.ogTitle });
this.metaService.updateTag({ name: 'twitter:description', content: postTags.ogDescription });
}
// Adds structured data to the document
private addStructuredData(postTags: PostTags, content: string): void {
const firstImgUrl = this.getPostFirstImageAsMain(content);
const logoUrl = `${window.location.origin}/assets/images/logo.webp`;
const structuredData = new PostStructuredData().getStructuredDataWithDefaults(
postTags.postTitle,
postTags.postDescription,
firstImgUrl || '',
postTags.postUser,
this.formatDate(postTags.postCreationTimestamp),
logoUrl
);
const script = this.document.createElement('script');
script.type = 'application/ld+json';
script.text = JSON.stringify(structuredData);
this.document.head.appendChild(script);
}
// Clears existing meta tags and structured data
private clearMetaTags(): void {
const tagsToRemove = [
"name='description'",
"rel='canonical'",
"name='robots'",
"property='og:title'",
"property='og:description'",
"property='og:url'",
"name='twitter:card'",
"name='twitter:title'",
"name='twitter:description'",
"name='twitter:image'",
];
tagsToRemove.forEach((selector) => this.metaService.removeTag(selector));
const canonical = this.document.querySelector("link[rel='canonical']");
if (canonical) canonical.remove();
const scripts = this.document.querySelectorAll('script[type="application/ld+json"]');
scripts.forEach((script) => script.remove());
}
// Updates or creates a canonical link
private updateCanonicalLink(url: string): void {
let canonicalLink = this.document.querySelector("link[rel='canonical']");
if (!canonicalLink) {
canonicalLink = this.document.createElement('link');
canonicalLink.setAttribute('rel', 'canonical');
this.document.head.appendChild(canonicalLink);
}
canonicalLink.setAttribute('href', url);
}
// Formats a date for structured data
private formatDate(date: any): string {
const defDate = '2024-01-01T00:00:01.001+00:00';
try {
return new Date(date || defDate).toISOString();
} catch {
return new Date(defDate).toISOString();
}
}
// Extracts the first image URL from the content
private getPostFirstImageAsMain(content: string): string {
const imgRegex = /(?:<img[\s\S]*?src=["']([^"']+)["'])|(?:!\[[^\]]*\]\(([^)\s]+)[^\)]*\))/i;
const match = imgRegex.exec(content.replace(/\s+/g, ' ').trim());
const firstImageUrl = match?.[1] || match?.[2] || '';
return firstImageUrl ? this.getFullURL(firstImageUrl) : '';
}
// Constructs a full URL if the given URL is relative
private getFullURL(url: string): string {
return /^https?:\/\//i.test(url) ? url : `${window.location.origin}${url.startsWith('/') ? '' : '/'}${url}`;
}
// Trims text to a specific length with ellipsis
private trimText(text: string, maxLength: number): string {
return text.length > maxLength ? `${text.substring(0, maxLength - 3)}...` : text;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment