Schema.org

This commit is contained in:
Timo Knuth 2026-02-05 12:49:09 +01:00
parent 70a50e0ff6
commit 6f1109d593
4 changed files with 122 additions and 49 deletions

View File

@ -1,5 +1,5 @@
import { Injectable, inject, PLATFORM_ID } from '@angular/core'; import { Injectable, inject, PLATFORM_ID, Renderer2, RendererFactory2 } from '@angular/core';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser, DOCUMENT } from '@angular/common';
import { Meta, Title } from '@angular/platform-browser'; import { Meta, Title } from '@angular/platform-browser';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@ -22,11 +22,17 @@ export class SeoService {
private router = inject(Router); private router = inject(Router);
private platformId = inject(PLATFORM_ID); private platformId = inject(PLATFORM_ID);
private isBrowser = isPlatformBrowser(this.platformId); private isBrowser = isPlatformBrowser(this.platformId);
private document = inject(DOCUMENT);
private renderer: Renderer2;
private readonly defaultImage = 'https://www.bizmatch.net/assets/images/bizmatch-og-image.jpg'; private readonly defaultImage = 'https://www.bizmatch.net/assets/images/bizmatch-og-image.jpg';
private readonly siteName = 'BizMatch'; private readonly siteName = 'BizMatch';
private readonly baseUrl = 'https://www.bizmatch.net'; private readonly baseUrl = 'https://www.bizmatch.net';
constructor(rendererFactory: RendererFactory2) {
this.renderer = rendererFactory.createRenderer(null, null);
}
/** /**
* Get the base URL for SEO purposes * Get the base URL for SEO purposes
*/ */
@ -109,20 +115,18 @@ export class SeoService {
} }
/** /**
* Update canonical URL * Update canonical URL (SSR-compatible using Renderer2)
*/ */
private updateCanonicalUrl(url: string): void { private updateCanonicalUrl(url: string): void {
if (!this.isBrowser) return; let link: HTMLLinkElement | null = this.document.querySelector('link[rel="canonical"]');
let link: HTMLLinkElement | null = document.querySelector('link[rel="canonical"]');
if (link) { if (link) {
link.setAttribute('href', url); this.renderer.setAttribute(link, 'href', url);
} else { } else {
link = document.createElement('link'); link = this.renderer.createElement('link');
link.setAttribute('rel', 'canonical'); this.renderer.setAttribute(link, 'rel', 'canonical');
link.setAttribute('href', url); this.renderer.setAttribute(link, 'href', url);
document.head.appendChild(link); this.renderer.appendChild(this.document.head, link);
} }
} }
@ -269,32 +273,40 @@ export class SeoService {
} }
/** /**
* Inject JSON-LD structured data into page * Inject JSON-LD structured data into page (SSR-compatible using Renderer2)
*/ */
injectStructuredData(schema: object): void { injectStructuredData(schema: object): void {
if (!this.isBrowser) return; // Clear existing schema scripts with the same type
this.removeAllSchemas();
// Remove existing schema script // Create new script element using Renderer2 (works in both SSR and browser)
const existingScript = document.querySelector('script[type="application/ld+json"]'); const script = this.renderer.createElement('script');
if (existingScript) { this.renderer.setAttribute(script, 'type', 'application/ld+json');
existingScript.remove(); this.renderer.setAttribute(script, 'data-schema', 'true');
}
// Add new schema script // Create text node with schema JSON
const script = document.createElement('script'); const schemaText = this.renderer.createText(JSON.stringify(schema));
script.type = 'application/ld+json'; this.renderer.appendChild(script, schemaText);
script.text = JSON.stringify(schema);
document.head.appendChild(script); // Append to document head
this.renderer.appendChild(this.document.head, script);
} }
/** /**
* Clear all structured data * Remove all schema scripts (internal helper, SSR-compatible)
*/
private removeAllSchemas(): void {
const existingScripts = this.document.querySelectorAll('script[data-schema="true"]');
existingScripts.forEach(script => {
this.renderer.removeChild(this.document.head, script);
});
}
/**
* Clear all structured data (SSR-compatible)
*/ */
clearStructuredData(): void { clearStructuredData(): void {
if (!this.isBrowser) return; this.removeAllSchemas();
const scripts = document.querySelectorAll('script[type="application/ld+json"]');
scripts.forEach(script => script.remove());
} }
/** /**
@ -516,20 +528,21 @@ export class SeoService {
} }
/** /**
* Inject multiple structured data schemas * Inject multiple structured data schemas (SSR-compatible using Renderer2)
*/ */
injectMultipleSchemas(schemas: object[]): void { injectMultipleSchemas(schemas: object[]): void {
if (!this.isBrowser) return; // Clear existing schema scripts
this.removeAllSchemas();
// Remove existing schema scripts // Add new schema scripts using Renderer2
this.clearStructuredData();
// Add new schema scripts
schemas.forEach(schema => { schemas.forEach(schema => {
const script = document.createElement('script'); const script = this.renderer.createElement('script');
script.type = 'application/ld+json'; this.renderer.setAttribute(script, 'type', 'application/ld+json');
script.text = JSON.stringify(schema); this.renderer.setAttribute(script, 'data-schema', 'true');
document.head.appendChild(script);
const schemaText = this.renderer.createText(JSON.stringify(schema));
this.renderer.appendChild(script, schemaText);
this.renderer.appendChild(this.document.head, script);
}); });
} }

View File

@ -1,12 +1,25 @@
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
// Development: use browser-bunyan for rich logging // Lightweight logger implementation for both dev and production
// Production: use lightweight console wrapper to avoid loading 50KB+ library // Avoids dynamic require() which causes build issues
export const createLogger = environment.production const createLoggerImpl = (name: string) => ({
? (name: string) => ({ info: (...args: any[]) => {
info: (...args: any[]) => console.log(`[${name}]`, ...args), if (!environment.production) {
console.log(`[${name}]`, ...args);
}
},
warn: (...args: any[]) => console.warn(`[${name}]`, ...args), warn: (...args: any[]) => console.warn(`[${name}]`, ...args),
error: (...args: any[]) => console.error(`[${name}]`, ...args), error: (...args: any[]) => console.error(`[${name}]`, ...args),
debug: () => {}, // no-op in production debug: (...args: any[]) => {
}) if (!environment.production) {
: require('browser-bunyan').createLogger; console.debug(`[${name}]`, ...args);
}
},
trace: (...args: any[]) => {
if (!environment.production) {
console.trace(`[${name}]`, ...args);
}
}
});
export const createLogger = createLoggerImpl;

View File

@ -1,6 +1,6 @@
// Build information, automatically generated by `the_build_script` :zwinkern: // Build information, automatically generated by `the_build_script` :zwinkern:
const build = { const build = {
timestamp: "GER: 04.02.2026 21:20 | TX: 02/04/2026 2:20 PM" timestamp: "GER: 05.02.2026 12:45 | TX: 02/05/2026 5:45 AM"
}; };
export default build; export default build;

View File

@ -61,6 +61,53 @@
<link rel="icon" href="/assets/cropped-Favicon-32x32.png" sizes="32x32" /> <link rel="icon" href="/assets/cropped-Favicon-32x32.png" sizes="32x32" />
<link rel="icon" href="/assets/cropped-Favicon-192x192.png" sizes="192x192" /> <link rel="icon" href="/assets/cropped-Favicon-192x192.png" sizes="192x192" />
<link rel="apple-touch-icon" href="/assets/cropped-Favicon-180x180.png" /> <link rel="apple-touch-icon" href="/assets/cropped-Favicon-180x180.png" />
<!-- Schema.org Structured Data (Static) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "BizMatch",
"url": "https://www.bizmatch.net",
"logo": "https://www.bizmatch.net/assets/images/bizmatch-logo.png",
"description": "Buy and sell businesses, commercial properties, and franchises. Browse thousands of verified listings across the United States.",
"address": {
"@type": "PostalAddress",
"streetAddress": "1001 Blucher Street",
"addressLocality": "Corpus Christi",
"addressRegion": "TX",
"postalCode": "78401",
"addressCountry": "US"
},
"contactPoint": {
"@type": "ContactPoint",
"contactType": "Customer Service",
"email": "info@bizmatch.net"
},
"sameAs": [
"https://www.facebook.com/bizmatch",
"https://www.linkedin.com/company/bizmatch",
"https://twitter.com/bizmatch"
]
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "BizMatch",
"url": "https://www.bizmatch.net",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://www.bizmatch.net/businessListings?search={search_term_string}"
},
"query-input": "required name=search_term_string"
}
}
</script>
</head> </head>
<body class="flex flex-col min-h-screen"> <body class="flex flex-col min-h-screen">