SEO/AEO, Farb schema, breadcrumbs
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user