Created
December 4, 2024 09:10
-
-
Save zzpzaf/cc35464a4fd3a24b48a20a04fe7c04e1 to your computer and use it in GitHub Desktop.
ang18-SSR-SEO-SupportBlog2-SeoService1-No-renderer-version
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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