SEO/AEO, Farb schema, breadcrumbs

This commit is contained in:
2025-11-29 23:41:54 +01:00
parent 4fa24c8f3d
commit d2953fd0d9
87 changed files with 5672 additions and 579 deletions

View File

@@ -7,22 +7,28 @@ import { Subject, takeUntil } from 'rxjs';
import dayjs from 'dayjs';
import { BusinessListing, SortByOptions } from '../../../../../../bizmatch-server/src/models/db.model';
import { BusinessListingCriteria, LISTINGS_PER_PAGE, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { BusinessListingCriteria, KeycloakUser, LISTINGS_PER_PAGE, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { BreadcrumbItem, BreadcrumbsComponent } from '../../../components/breadcrumbs/breadcrumbs.component';
import { PaginatorComponent } from '../../../components/paginator/paginator.component';
import { ModalService } from '../../../components/search-modal/modal.service';
import { SearchModalComponent } from '../../../components/search-modal/search-modal.component';
import { LazyLoadImageDirective } from '../../../directives/lazy-load-image.directive';
import { AltTextService } from '../../../services/alt-text.service';
import { AuthService } from '../../../services/auth.service';
import { FilterStateService } from '../../../services/filter-state.service';
import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service';
import { SearchService } from '../../../services/search.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { SeoService } from '../../../services/seo.service';
import { map2User } from '../../../utils/utils';
@UntilDestroy()
@Component({
selector: 'app-business-listings',
standalone: true,
imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent, SearchModalComponent],
imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent, SearchModalComponent, LazyLoadImageDirective, BreadcrumbsComponent],
templateUrl: './business-listings.component.html',
styleUrls: ['./business-listings.component.scss', '../../pages.scss'],
})
@@ -47,8 +53,19 @@ export class BusinessListingsComponent implements OnInit, OnDestroy {
// UI state
ts = new Date().getTime();
emailToDirName = emailToDirName;
isLoading = false;
// Breadcrumbs
breadcrumbs: BreadcrumbItem[] = [
{ label: 'Home', url: '/', icon: 'fas fa-home' },
{ label: 'Business Listings' }
];
// User for favorites
user: KeycloakUser | null = null;
constructor(
public altText: AltTextService,
public selectOptions: SelectOptionsService,
private listingsService: ListingsService,
private router: Router,
@@ -58,9 +75,23 @@ export class BusinessListingsComponent implements OnInit, OnDestroy {
private modalService: ModalService,
private filterStateService: FilterStateService,
private route: ActivatedRoute,
private seoService: SeoService,
private authService: AuthService,
) {}
ngOnInit(): void {
async ngOnInit(): Promise<void> {
// Load user for favorites functionality
const token = await this.authService.getToken();
this.user = map2User(token);
// Set SEO meta tags for business listings page
this.seoService.updateMetaTags({
title: 'Businesses for Sale - Find Profitable Business Opportunities | BizMatch',
description: 'Browse thousands of businesses for sale across the United States. Find restaurants, franchises, retail stores, and more. Verified listings from business owners and brokers.',
keywords: 'businesses for sale, buy a business, business opportunities, franchise for sale, restaurant for sale, retail business for sale, business broker listings',
type: 'website'
});
// Subscribe to state changes
this.filterStateService
.getState$('businessListings')
@@ -82,6 +113,9 @@ export class BusinessListingsComponent implements OnInit, OnDestroy {
async search(): Promise<void> {
try {
// Show loading state
this.isLoading = true;
// Get current criteria from service
this.criteria = this.filterStateService.getCriteria('businessListings') as BusinessListingCriteria;
@@ -98,6 +132,12 @@ export class BusinessListingsComponent implements OnInit, OnDestroy {
this.pageCount = Math.ceil(this.totalRecords / LISTINGS_PER_PAGE);
this.page = this.criteria.page || 1;
// Hide loading state
this.isLoading = false;
// Update pagination SEO links
this.updatePaginationSEO();
// Update view
this.cdRef.markForCheck();
this.cdRef.detectChanges();
@@ -106,6 +146,7 @@ export class BusinessListingsComponent implements OnInit, OnDestroy {
// Handle error appropriately
this.listings = [];
this.totalRecords = 0;
this.isLoading = false;
this.cdRef.markForCheck();
}
}
@@ -164,8 +205,85 @@ export class BusinessListingsComponent implements OnInit, OnDestroy {
getDaysListed(listing: BusinessListing) {
return dayjs().diff(listing.created, 'day');
}
/**
* Filter by popular category
*/
filterByCategory(category: string): void {
this.filterStateService.updateCriteria('businessListings', {
types: [category],
page: 1,
start: 0,
length: LISTINGS_PER_PAGE,
});
// Search will be triggered automatically through state subscription
}
/**
* Check if listing is already in user's favorites
*/
isFavorite(listing: BusinessListing): boolean {
if (!this.user?.email || !listing.favoritesForUser) return false;
return listing.favoritesForUser.includes(this.user.email);
}
/**
* Toggle favorite status for a listing
*/
async toggleFavorite(event: Event, listing: BusinessListing): Promise<void> {
event.stopPropagation();
event.preventDefault();
if (!this.user?.email) {
// User not logged in - redirect to login or show message
this.router.navigate(['/login']);
return;
}
try {
if (this.isFavorite(listing)) {
// Remove from favorites
await this.listingsService.removeFavorite(listing.id, 'business');
listing.favoritesForUser = listing.favoritesForUser.filter(email => email !== this.user!.email);
} else {
// Add to favorites
await this.listingsService.addToFavorites(listing.id, 'business');
if (!listing.favoritesForUser) {
listing.favoritesForUser = [];
}
listing.favoritesForUser.push(this.user.email);
}
this.cdRef.detectChanges();
} catch (error) {
console.error('Error toggling favorite:', error);
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
// Clean up pagination links when leaving the page
this.seoService.clearPaginationLinks();
}
/**
* Update pagination SEO links (rel="next/prev") and CollectionPage schema
*/
private updatePaginationSEO(): void {
const baseUrl = `${this.seoService.getBaseUrl()}/businessListings`;
// Inject rel="next" and rel="prev" links
this.seoService.injectPaginationLinks(baseUrl, this.page, this.pageCount);
// Inject CollectionPage schema for paginated results
const collectionSchema = this.seoService.generateCollectionPageSchema({
name: 'Businesses for Sale',
description: 'Browse thousands of businesses for sale across the United States. Find restaurants, franchises, retail stores, and more.',
totalItems: this.totalRecords,
itemsPerPage: LISTINGS_PER_PAGE,
currentPage: this.page,
baseUrl: baseUrl
});
this.seoService.injectStructuredData(collectionSchema);
}
}