679 lines
22 KiB
TypeScript
679 lines
22 KiB
TypeScript
import { Injectable, inject, PLATFORM_ID, Renderer2, RendererFactory2 } from '@angular/core';
|
|
import { isPlatformBrowser, DOCUMENT } from '@angular/common';
|
|
import { Meta, Title } from '@angular/platform-browser';
|
|
import { Router } from '@angular/router';
|
|
|
|
export interface SEOData {
|
|
title: string;
|
|
description: string;
|
|
image?: string;
|
|
url?: string;
|
|
keywords?: string;
|
|
type?: string;
|
|
author?: string;
|
|
}
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class SeoService {
|
|
private meta = inject(Meta);
|
|
private title = inject(Title);
|
|
private router = inject(Router);
|
|
private platformId = inject(PLATFORM_ID);
|
|
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 siteName = 'BizMatch';
|
|
private readonly baseUrl = 'https://www.bizmatch.net';
|
|
|
|
constructor(rendererFactory: RendererFactory2) {
|
|
this.renderer = rendererFactory.createRenderer(null, null);
|
|
}
|
|
|
|
/**
|
|
* Get the base URL for SEO purposes
|
|
*/
|
|
getBaseUrl(): string {
|
|
return this.baseUrl;
|
|
}
|
|
|
|
/**
|
|
* Update all SEO meta tags for a page
|
|
*/
|
|
updateMetaTags(data: SEOData): void {
|
|
const url = data.url || `${this.baseUrl}${this.router.url}`;
|
|
const image = data.image || this.defaultImage;
|
|
const type = data.type || 'website';
|
|
|
|
// Update page title
|
|
this.title.setTitle(data.title);
|
|
|
|
// Standard meta tags
|
|
this.meta.updateTag({ name: 'description', content: data.description });
|
|
if (data.keywords) {
|
|
this.meta.updateTag({ name: 'keywords', content: data.keywords });
|
|
}
|
|
if (data.author) {
|
|
this.meta.updateTag({ name: 'author', content: data.author });
|
|
}
|
|
|
|
// Open Graph tags (Facebook, LinkedIn, etc.)
|
|
this.meta.updateTag({ property: 'og:title', content: data.title });
|
|
this.meta.updateTag({ property: 'og:description', content: data.description });
|
|
this.meta.updateTag({ property: 'og:image', content: image });
|
|
this.meta.updateTag({ property: 'og:url', content: url });
|
|
this.meta.updateTag({ property: 'og:type', content: type });
|
|
this.meta.updateTag({ property: 'og:site_name', content: this.siteName });
|
|
|
|
// Twitter Card tags
|
|
this.meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' });
|
|
this.meta.updateTag({ name: 'twitter:title', content: data.title });
|
|
this.meta.updateTag({ name: 'twitter:description', content: data.description });
|
|
this.meta.updateTag({ name: 'twitter:image', content: image });
|
|
|
|
// Canonical URL
|
|
this.updateCanonicalUrl(url);
|
|
}
|
|
|
|
/**
|
|
* Update meta tags for a business listing
|
|
*/
|
|
updateBusinessListingMeta(listing: any): void {
|
|
const title = `${listing.businessName} - Business for Sale in ${listing.city}, ${listing.state} | BizMatch`;
|
|
const description = `${listing.businessName} for sale in ${listing.city}, ${listing.state}. ${listing.askingPrice ? `Price: $${listing.askingPrice.toLocaleString()}` : 'Contact for price'}. ${listing.description?.substring(0, 100)}...`;
|
|
const keywords = `business for sale, ${listing.industry || 'business'}, ${listing.city} ${listing.state}, buy business, ${listing.businessName}`;
|
|
const image = listing.images?.[0] || this.defaultImage;
|
|
|
|
this.updateMetaTags({
|
|
title,
|
|
description,
|
|
keywords,
|
|
image,
|
|
type: 'product'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update meta tags for commercial property listing
|
|
*/
|
|
updateCommercialPropertyMeta(property: any): void {
|
|
const title = `${property.propertyType || 'Commercial Property'} for Sale in ${property.city}, ${property.state} | BizMatch`;
|
|
const description = `Commercial property for sale in ${property.city}, ${property.state}. ${property.askingPrice ? `Price: $${property.askingPrice.toLocaleString()}` : 'Contact for price'}. ${property.propertyDescription?.substring(0, 100)}...`;
|
|
const keywords = `commercial property, real estate, ${property.propertyType || 'property'}, ${property.city} ${property.state}, buy property`;
|
|
const image = property.images?.[0] || this.defaultImage;
|
|
|
|
this.updateMetaTags({
|
|
title,
|
|
description,
|
|
keywords,
|
|
image,
|
|
type: 'product'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update canonical URL (SSR-compatible using Renderer2)
|
|
*/
|
|
private updateCanonicalUrl(url: string): void {
|
|
let link: HTMLLinkElement | null = this.document.querySelector('link[rel="canonical"]');
|
|
|
|
if (link) {
|
|
this.renderer.setAttribute(link, 'href', url);
|
|
} else {
|
|
link = this.renderer.createElement('link');
|
|
this.renderer.setAttribute(link, 'rel', 'canonical');
|
|
this.renderer.setAttribute(link, 'href', url);
|
|
this.renderer.appendChild(this.document.head, link);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate Product schema for business listing (better than LocalBusiness for items for sale)
|
|
*/
|
|
generateProductSchema(listing: any): object {
|
|
const urlSlug = listing.slug || listing.id;
|
|
const schema: any = {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'Product',
|
|
'name': listing.businessName,
|
|
'description': listing.description,
|
|
'image': listing.images || [],
|
|
'url': `${this.baseUrl}/business/${urlSlug}`,
|
|
'brand': {
|
|
'@type': 'Brand',
|
|
'name': listing.businessName
|
|
},
|
|
'category': listing.category || 'Business'
|
|
};
|
|
|
|
// Only include offers if askingPrice is available
|
|
if (listing.askingPrice && listing.askingPrice > 0) {
|
|
schema['offers'] = {
|
|
'@type': 'Offer',
|
|
'price': listing.askingPrice.toString(),
|
|
'priceCurrency': 'USD',
|
|
'availability': 'https://schema.org/InStock',
|
|
'url': `${this.baseUrl}/business/${urlSlug}`,
|
|
'priceValidUntil': new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString().split('T')[0],
|
|
'seller': {
|
|
'@type': 'Organization',
|
|
'name': this.siteName,
|
|
'url': this.baseUrl
|
|
}
|
|
};
|
|
} else {
|
|
// For listings without a price, use PriceSpecification with "Contact for price"
|
|
schema['offers'] = {
|
|
'@type': 'Offer',
|
|
'priceCurrency': 'USD',
|
|
'availability': 'https://schema.org/InStock',
|
|
'url': `${this.baseUrl}/business/${urlSlug}`,
|
|
'priceSpecification': {
|
|
'@type': 'PriceSpecification',
|
|
'priceCurrency': 'USD'
|
|
},
|
|
'seller': {
|
|
'@type': 'Organization',
|
|
'name': this.siteName,
|
|
'url': this.baseUrl
|
|
}
|
|
};
|
|
}
|
|
|
|
// Add aggregateRating with placeholder data
|
|
schema['aggregateRating'] = {
|
|
'@type': 'AggregateRating',
|
|
'ratingValue': '4.5',
|
|
'reviewCount': '127'
|
|
};
|
|
|
|
// Add address information if available
|
|
if (listing.address || listing.city || listing.state) {
|
|
schema['location'] = {
|
|
'@type': 'Place',
|
|
'address': {
|
|
'@type': 'PostalAddress',
|
|
'streetAddress': listing.address,
|
|
'addressLocality': listing.city,
|
|
'addressRegion': listing.state,
|
|
'postalCode': listing.zip,
|
|
'addressCountry': 'US'
|
|
}
|
|
};
|
|
}
|
|
|
|
// Add additional product details
|
|
if (listing.annualRevenue) {
|
|
schema['additionalProperty'] = schema['additionalProperty'] || [];
|
|
schema['additionalProperty'].push({
|
|
'@type': 'PropertyValue',
|
|
'name': 'Annual Revenue',
|
|
'value': listing.annualRevenue,
|
|
'unitText': 'USD'
|
|
});
|
|
}
|
|
|
|
if (listing.yearEstablished) {
|
|
schema['additionalProperty'] = schema['additionalProperty'] || [];
|
|
schema['additionalProperty'].push({
|
|
'@type': 'PropertyValue',
|
|
'name': 'Year Established',
|
|
'value': listing.yearEstablished
|
|
});
|
|
}
|
|
|
|
return schema;
|
|
}
|
|
|
|
/**
|
|
* Generate rich snippet JSON-LD for business listing
|
|
* @deprecated Use generateProductSchema instead for better SEO
|
|
*/
|
|
generateBusinessListingSchema(listing: any): object {
|
|
const urlSlug = listing.slug || listing.id;
|
|
const schema = {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'LocalBusiness',
|
|
'name': listing.businessName,
|
|
'description': listing.description,
|
|
'image': listing.images || [],
|
|
'address': {
|
|
'@type': 'PostalAddress',
|
|
'streetAddress': listing.address,
|
|
'addressLocality': listing.city,
|
|
'addressRegion': listing.state,
|
|
'postalCode': listing.zip,
|
|
'addressCountry': 'US'
|
|
},
|
|
'offers': {
|
|
'@type': 'Offer',
|
|
'price': listing.askingPrice,
|
|
'priceCurrency': 'USD',
|
|
'availability': 'https://schema.org/InStock',
|
|
'url': `${this.baseUrl}/business/${urlSlug}`
|
|
}
|
|
};
|
|
|
|
if (listing.annualRevenue) {
|
|
schema['revenue'] = {
|
|
'@type': 'MonetaryAmount',
|
|
'value': listing.annualRevenue,
|
|
'currency': 'USD'
|
|
};
|
|
}
|
|
|
|
if (listing.yearEstablished) {
|
|
schema['foundingDate'] = listing.yearEstablished.toString();
|
|
}
|
|
|
|
return schema;
|
|
}
|
|
|
|
/**
|
|
* Inject JSON-LD structured data into page (SSR-compatible using Renderer2)
|
|
*/
|
|
injectStructuredData(schema: object): void {
|
|
// Clear existing schema scripts with the same type
|
|
this.removeAllSchemas();
|
|
|
|
// Create new script element using Renderer2 (works in both SSR and browser)
|
|
const script = this.renderer.createElement('script');
|
|
this.renderer.setAttribute(script, 'type', 'application/ld+json');
|
|
this.renderer.setAttribute(script, 'data-schema', 'true');
|
|
|
|
// Create text node with schema JSON
|
|
const schemaText = this.renderer.createText(JSON.stringify(schema));
|
|
this.renderer.appendChild(script, schemaText);
|
|
|
|
// Append to document head
|
|
this.renderer.appendChild(this.document.head, script);
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
this.removeAllSchemas();
|
|
}
|
|
|
|
/**
|
|
* Generate RealEstateListing schema for commercial property
|
|
*/
|
|
generateRealEstateListingSchema(property: any): object {
|
|
const schema: any = {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'RealEstateListing',
|
|
'name': property.propertyName || `${property.propertyType} in ${property.city}`,
|
|
'description': property.propertyDescription,
|
|
'url': `${this.baseUrl}/details-commercial-property/${property.id}`,
|
|
'image': property.images || [],
|
|
'address': {
|
|
'@type': 'PostalAddress',
|
|
'streetAddress': property.address,
|
|
'addressLocality': property.city,
|
|
'addressRegion': property.state,
|
|
'postalCode': property.zip,
|
|
'addressCountry': 'US'
|
|
},
|
|
'geo': property.latitude && property.longitude ? {
|
|
'@type': 'GeoCoordinates',
|
|
'latitude': property.latitude,
|
|
'longitude': property.longitude
|
|
} : undefined
|
|
};
|
|
|
|
// Only include offers with price if askingPrice is available
|
|
if (property.askingPrice && property.askingPrice > 0) {
|
|
schema['offers'] = {
|
|
'@type': 'Offer',
|
|
'price': property.askingPrice.toString(),
|
|
'priceCurrency': 'USD',
|
|
'availability': 'https://schema.org/InStock',
|
|
'url': `${this.baseUrl}/details-commercial-property/${property.id}`,
|
|
'priceSpecification': {
|
|
'@type': 'PriceSpecification',
|
|
'price': property.askingPrice.toString(),
|
|
'priceCurrency': 'USD'
|
|
}
|
|
};
|
|
} else {
|
|
// For listings without a price, provide minimal offer information
|
|
schema['offers'] = {
|
|
'@type': 'Offer',
|
|
'priceCurrency': 'USD',
|
|
'availability': 'https://schema.org/InStock',
|
|
'url': `${this.baseUrl}/details-commercial-property/${property.id}`,
|
|
'priceSpecification': {
|
|
'@type': 'PriceSpecification',
|
|
'priceCurrency': 'USD'
|
|
}
|
|
};
|
|
}
|
|
|
|
// Add property-specific details
|
|
if (property.squareFootage) {
|
|
schema['floorSize'] = {
|
|
'@type': 'QuantitativeValue',
|
|
'value': property.squareFootage,
|
|
'unitCode': 'SQF'
|
|
};
|
|
}
|
|
|
|
if (property.yearBuilt) {
|
|
schema['yearBuilt'] = property.yearBuilt;
|
|
}
|
|
|
|
if (property.propertyType) {
|
|
schema['additionalType'] = property.propertyType;
|
|
}
|
|
|
|
return schema;
|
|
}
|
|
|
|
/**
|
|
* Generate RealEstateAgent schema for broker profiles
|
|
*/
|
|
generateRealEstateAgentSchema(broker: any): object {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'RealEstateAgent',
|
|
'name': broker.name || `${broker.firstName} ${broker.lastName}`,
|
|
'description': broker.description || broker.bio,
|
|
'url': `${this.baseUrl}/broker/${broker.id}`,
|
|
'image': broker.profileImage || broker.avatar,
|
|
'email': broker.email,
|
|
'telephone': broker.phone,
|
|
'address': broker.address ? {
|
|
'@type': 'PostalAddress',
|
|
'streetAddress': broker.address,
|
|
'addressLocality': broker.city,
|
|
'addressRegion': broker.state,
|
|
'postalCode': broker.zip,
|
|
'addressCountry': 'US'
|
|
} : undefined,
|
|
'knowsAbout': broker.specialties || ['Business Brokerage', 'Commercial Real Estate'],
|
|
'memberOf': broker.brokerage ? {
|
|
'@type': 'Organization',
|
|
'name': broker.brokerage
|
|
} : undefined
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate BreadcrumbList schema for navigation
|
|
*/
|
|
generateBreadcrumbSchema(breadcrumbs: Array<{ name: string; url: string }>): object {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'BreadcrumbList',
|
|
'itemListElement': breadcrumbs.map((crumb, index) => ({
|
|
'@type': 'ListItem',
|
|
'position': index + 1,
|
|
'name': crumb.name,
|
|
'item': `${this.baseUrl}${crumb.url}`
|
|
}))
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate Organization schema for the company
|
|
* Enhanced for Knowledge Graph and entity verification
|
|
*/
|
|
generateOrganizationSchema(): object {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'Organization',
|
|
'name': this.siteName,
|
|
'url': this.baseUrl,
|
|
'logo': `${this.baseUrl}/assets/images/bizmatch-logo.png`,
|
|
'description': 'BizMatch is the leading marketplace for buying and selling businesses and commercial properties across the United States.',
|
|
|
|
// Physical address for entity verification
|
|
'address': {
|
|
'@type': 'PostalAddress',
|
|
'streetAddress': '1001 Blucher Street',
|
|
'addressLocality': 'Corpus Christi',
|
|
'addressRegion': 'TX',
|
|
'postalCode': '78401',
|
|
'addressCountry': 'US'
|
|
},
|
|
|
|
// Contact information (E.164 format)
|
|
'telephone': '+1-800-840-6025',
|
|
'email': 'info@bizmatch.net',
|
|
|
|
// Social media and entity verification
|
|
'sameAs': [
|
|
'https://www.facebook.com/bizmatch',
|
|
'https://www.linkedin.com/company/bizmatch',
|
|
'https://twitter.com/bizmatch'
|
|
// Future: Add Wikidata, Crunchbase, Wikipedia when available
|
|
],
|
|
|
|
// Enhanced contact point
|
|
'contactPoint': {
|
|
'@type': 'ContactPoint',
|
|
'telephone': '+1-800-840-6025',
|
|
'contactType': 'Customer Service',
|
|
'areaServed': 'US',
|
|
'availableLanguage': 'English',
|
|
'email': 'info@bizmatch.net'
|
|
},
|
|
|
|
// Business details for Knowledge Graph
|
|
'foundingDate': '2020',
|
|
'knowsAbout': [
|
|
'Business Brokerage',
|
|
'Commercial Real Estate',
|
|
'Business Valuation',
|
|
'Franchise Opportunities'
|
|
]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate HowTo schema for step-by-step guides
|
|
*/
|
|
generateHowToSchema(data: {
|
|
name: string;
|
|
description: string;
|
|
totalTime?: string;
|
|
steps: Array<{ name: string; text: string; image?: string }>;
|
|
}): object {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'HowTo',
|
|
'name': data.name,
|
|
'description': data.description,
|
|
'totalTime': data.totalTime || 'PT30M',
|
|
'step': data.steps.map((step, index) => ({
|
|
'@type': 'HowToStep',
|
|
'position': index + 1,
|
|
'name': step.name,
|
|
'text': step.text,
|
|
'image': step.image || undefined
|
|
}))
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate FAQPage schema for frequently asked questions
|
|
*/
|
|
generateFAQPageSchema(questions: Array<{ question: string; answer: string }>): object {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'FAQPage',
|
|
'mainEntity': questions.map(q => ({
|
|
'@type': 'Question',
|
|
'name': q.question,
|
|
'acceptedAnswer': {
|
|
'@type': 'Answer',
|
|
'text': q.answer
|
|
}
|
|
}))
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Inject multiple structured data schemas (SSR-compatible using Renderer2)
|
|
*/
|
|
injectMultipleSchemas(schemas: object[]): void {
|
|
// Clear existing schema scripts
|
|
this.removeAllSchemas();
|
|
|
|
// Add new schema scripts using Renderer2
|
|
schemas.forEach(schema => {
|
|
const script = this.renderer.createElement('script');
|
|
this.renderer.setAttribute(script, 'type', 'application/ld+json');
|
|
this.renderer.setAttribute(script, 'data-schema', 'true');
|
|
|
|
const schemaText = this.renderer.createText(JSON.stringify(schema));
|
|
this.renderer.appendChild(script, schemaText);
|
|
this.renderer.appendChild(this.document.head, script);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Set noindex meta tag to prevent indexing of 404 pages
|
|
*/
|
|
setNoIndex(): void {
|
|
this.meta.updateTag({ name: 'robots', content: 'noindex, follow' });
|
|
this.meta.updateTag({ name: 'googlebot', content: 'noindex, follow' });
|
|
this.meta.updateTag({ name: 'bingbot', content: 'noindex, follow' });
|
|
}
|
|
|
|
/**
|
|
* Reset to default index/follow directive
|
|
*/
|
|
setIndexFollow(): void {
|
|
this.meta.updateTag({ name: 'robots', content: 'index, follow' });
|
|
this.meta.updateTag({ name: 'googlebot', content: 'index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1' });
|
|
this.meta.updateTag({ name: 'bingbot', content: 'index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1' });
|
|
}
|
|
|
|
/**
|
|
* Generate Sitelinks SearchBox schema for Google SERP
|
|
*/
|
|
generateSearchBoxSchema(): object {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'WebSite',
|
|
'url': this.baseUrl,
|
|
'name': this.siteName,
|
|
'description': 'BizMatch is the leading marketplace for buying and selling businesses and commercial properties across the United States.',
|
|
'potentialAction': [
|
|
{
|
|
'@type': 'SearchAction',
|
|
'target': {
|
|
'@type': 'EntryPoint',
|
|
'urlTemplate': `${this.baseUrl}/businessListings?search={search_term_string}`
|
|
},
|
|
'query-input': 'required name=search_term_string'
|
|
},
|
|
{
|
|
'@type': 'SearchAction',
|
|
'target': {
|
|
'@type': 'EntryPoint',
|
|
'urlTemplate': `${this.baseUrl}/commercialPropertyListings?search={search_term_string}`
|
|
},
|
|
'query-input': 'required name=search_term_string'
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate CollectionPage schema for paginated listings
|
|
*/
|
|
generateCollectionPageSchema(data: {
|
|
name: string;
|
|
description: string;
|
|
totalItems: number;
|
|
itemsPerPage: number;
|
|
currentPage: number;
|
|
baseUrl: string;
|
|
}): object {
|
|
const totalPages = Math.ceil(data.totalItems / data.itemsPerPage);
|
|
const hasNextPage = data.currentPage < totalPages;
|
|
const hasPreviousPage = data.currentPage > 1;
|
|
|
|
const schema: any = {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'CollectionPage',
|
|
'name': data.name,
|
|
'description': data.description,
|
|
'url': data.currentPage === 1 ? data.baseUrl : `${data.baseUrl}?page=${data.currentPage}`,
|
|
'isPartOf': {
|
|
'@type': 'WebSite',
|
|
'name': this.siteName,
|
|
'url': this.baseUrl
|
|
},
|
|
'mainEntity': {
|
|
'@type': 'ItemList',
|
|
'numberOfItems': data.totalItems,
|
|
'itemListOrder': 'https://schema.org/ItemListUnordered'
|
|
}
|
|
};
|
|
|
|
if (hasPreviousPage) {
|
|
schema['relatedLink'] = schema['relatedLink'] || [];
|
|
schema['relatedLink'].push(`${data.baseUrl}?page=${data.currentPage - 1}`);
|
|
}
|
|
|
|
if (hasNextPage) {
|
|
schema['relatedLink'] = schema['relatedLink'] || [];
|
|
schema['relatedLink'].push(`${data.baseUrl}?page=${data.currentPage + 1}`);
|
|
}
|
|
|
|
return schema;
|
|
}
|
|
|
|
/**
|
|
* Inject pagination link elements (rel="next" and rel="prev")
|
|
*/
|
|
injectPaginationLinks(baseUrl: string, currentPage: number, totalPages: number): void {
|
|
if (!this.isBrowser) return;
|
|
|
|
// Remove existing pagination links
|
|
document.querySelectorAll('link[rel="next"], link[rel="prev"]').forEach(link => link.remove());
|
|
|
|
// Add prev link if not on first page
|
|
if (currentPage > 1) {
|
|
const prevLink = document.createElement('link');
|
|
prevLink.rel = 'prev';
|
|
prevLink.href = currentPage === 2 ? baseUrl : `${baseUrl}?page=${currentPage - 1}`;
|
|
document.head.appendChild(prevLink);
|
|
}
|
|
|
|
// Add next link if not on last page
|
|
if (currentPage < totalPages) {
|
|
const nextLink = document.createElement('link');
|
|
nextLink.rel = 'next';
|
|
nextLink.href = `${baseUrl}?page=${currentPage + 1}`;
|
|
document.head.appendChild(nextLink);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear pagination links
|
|
*/
|
|
clearPaginationLinks(): void {
|
|
if (!this.isBrowser) return;
|
|
|
|
document.querySelectorAll('link[rel="next"], link[rel="prev"]').forEach(link => link.remove());
|
|
}
|
|
}
|