SEO/AEO, Farb schema, breadcrumbs
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
<div class="container mx-auto p-4">
|
||||
<!-- Breadcrumbs for SEO and Navigation -->
|
||||
<app-breadcrumbs [breadcrumbs]="breadcrumbs"></app-breadcrumbs>
|
||||
|
||||
<div class="bg-white drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg rounded-lg overflow-hidden">
|
||||
@if(listing){
|
||||
<div class="p-6 relative">
|
||||
@@ -14,7 +17,7 @@
|
||||
<p class="mb-4" [innerHTML]="description"></p>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div *ngFor="let detail of propertyDetails; let i = index" class="flex flex-col sm:flex-row" [ngClass]="{ 'bg-gray-100': i % 2 === 0 }">
|
||||
<div *ngFor="let detail of propertyDetails; let i = index" class="flex flex-col sm:flex-row" [ngClass]="{ 'bg-neutral-100': i % 2 === 0 }">
|
||||
<div class="w-full sm:w-1/3 font-semibold p-2">{{ detail.label }}</div>
|
||||
|
||||
<!-- Standard Text -->
|
||||
@@ -24,9 +27,9 @@
|
||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" [innerHTML]="detail.value" *ngIf="detail.isHtml && !detail.isListingBy"></div>
|
||||
|
||||
<!-- Speziell für Listing By mit RouterLink -->
|
||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" *ngIf="detail.isListingBy">
|
||||
<a [routerLink]="['/details-user', detail.user.id]" class="text-blue-600 dark:text-blue-500 hover:underline"> {{ detail.user.firstname }} {{ detail.user.lastname }} </a>
|
||||
<img *ngIf="detail.user.hasCompanyLogo" [src]="detail.imageBaseUrl + '/pictures/logo/' + detail.imagePath + '.avif?_ts=' + detail.ts" class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" />
|
||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" *ngIf="detail.isListingBy && detail.user">
|
||||
<a [routerLink]="['/details-user', detail.user.id]" class="text-primary-600 dark:text-primary-500 hover:underline"> {{ detail.user.firstname }} {{ detail.user.lastname }} </a>
|
||||
<img *ngIf="detail.user.hasCompanyLogo" [src]="detail.imageBaseUrl + '/pictures/logo/' + detail.imagePath + '.avif?_ts=' + detail.ts" class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" width="100" height="30" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,7 +67,7 @@
|
||||
<share-button button="linkedin" showText="true" (click)="createEvent('linkedin')"></share-button>
|
||||
</div>
|
||||
<!-- Karte hinzufügen, wenn Straße vorhanden ist -->
|
||||
<div *ngIf="listing.location.street" class="mt-6">
|
||||
<div *ngIf="listing.location.latitude && listing.location.longitude" class="mt-6">
|
||||
<h2 class="text-lg font-semibold mb-2">Location Map</h2>
|
||||
<!-- <div style="height: 300px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers" [leafletCenter]="mapCenter" [leafletZoom]="mapZoom"></div> -->
|
||||
<div style="height: 400px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers" [leafletCenter]="mapCenter" [leafletZoom]="mapZoom" (leafletMapReady)="onMapReady($event)"></div>
|
||||
@@ -83,7 +86,7 @@
|
||||
}@else {
|
||||
<div class="text-2xl font-bold mb-4">Contact the Author of this Listing</div>
|
||||
}
|
||||
<p class="text-sm text-gray-600 mb-4">Please include your contact info below</p>
|
||||
<p class="text-sm text-neutral-600 mb-4">Please include your contact info below</p>
|
||||
<form class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<app-validated-input label="Your Name" name="name" [(ngModel)]="mailinfo.sender.name"></app-validated-input>
|
||||
@@ -99,7 +102,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<button (click)="mail()" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Submit</button>
|
||||
<button (click)="mail()" class="bg-primary-500 text-white px-4 py-2 rounded hover:bg-primary-600">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -108,4 +111,41 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Related Listings Section for SEO Internal Linking -->
|
||||
@if(relatedListings && relatedListings.length > 0) {
|
||||
<div class="container mx-auto p-4 mt-8">
|
||||
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg p-6">
|
||||
<h2 class="text-2xl font-bold mb-6 text-gray-900">Similar Properties You May Like</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
@for (related of relatedListings; track related.id) {
|
||||
<a [routerLink]="['/commercial-property', related.slug || related.id]" class="block group">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:scale-[1.02]">
|
||||
<div class="p-4">
|
||||
<div class="flex items-center mb-3">
|
||||
<i [class]="selectOptions.getIconAndTextColorType(related.type)" class="mr-2 text-lg"></i>
|
||||
<span [class]="selectOptions.getTextColorType(related.type)" class="font-semibold text-sm">{{ selectOptions.getCommercialProperty(related.type) }}</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold mb-2 text-gray-900 group-hover:text-primary-600 transition-colors line-clamp-2">{{ related.title }}</h3>
|
||||
<div class="space-y-1 text-sm text-gray-600">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Price:</span>
|
||||
<span class="font-bold text-primary-600">${{ related.price?.toLocaleString() || 'Contact' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">Location:</span>
|
||||
<span>{{ related.location.name || related.location.county }}, {{ selectOptions.getState(related.location.state) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<span class="inline-block bg-primary-100 text-primary-800 text-xs font-medium px-2.5 py-0.5 rounded">View Details →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -23,15 +23,17 @@ import { ImageService } from '../../../services/image.service';
|
||||
import { ListingsService } from '../../../services/listings.service';
|
||||
import { MailService } from '../../../services/mail.service';
|
||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { UserService } from '../../../services/user.service';
|
||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||
import { createMailInfo, map2User } from '../../../utils/utils';
|
||||
import { BaseDetailsComponent } from '../base-details.component';
|
||||
import { BreadcrumbItem, BreadcrumbsComponent } from '../../../components/breadcrumbs/breadcrumbs.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-details-commercial-property-listing',
|
||||
standalone: true,
|
||||
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ShareButton, ValidatedNgSelectComponent, GalleryModule, LeafletModule],
|
||||
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ShareButton, ValidatedNgSelectComponent, GalleryModule, LeafletModule, BreadcrumbsComponent],
|
||||
providers: [],
|
||||
templateUrl: './details-commercial-property-listing.component.html',
|
||||
styleUrl: '../details.scss',
|
||||
@@ -54,7 +56,7 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
||||
numScroll: 1,
|
||||
},
|
||||
];
|
||||
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
||||
private id: string | undefined = this.activatedRoute.snapshot.params['slug'] as string | undefined;
|
||||
override listing: CommercialPropertyListing;
|
||||
criteria: CommercialPropertyListingCriteria;
|
||||
mailinfo: MailInfo;
|
||||
@@ -69,6 +71,8 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
||||
faTimes = faTimes;
|
||||
propertyDetails = [];
|
||||
images: Array<ImageItem> = [];
|
||||
relatedListings: CommercialPropertyListing[] = [];
|
||||
breadcrumbs: BreadcrumbItem[] = [];
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private listingsService: ListingsService,
|
||||
@@ -85,12 +89,19 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
||||
private auditService: AuditService,
|
||||
private emailService: EMailService,
|
||||
public authService: AuthService,
|
||||
private seoService: SeoService,
|
||||
) {
|
||||
super();
|
||||
this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl };
|
||||
this.mailinfo = { sender: { name: '', email: '', phoneNumber: '', state: '', comments: '' }, email: '', url: environment.mailinfoUrl };
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// Initialize default breadcrumbs first
|
||||
this.breadcrumbs = [
|
||||
{ label: 'Home', url: '/home', icon: 'fas fa-home' },
|
||||
{ label: 'Commercial Properties', url: '/commercialPropertyListings' }
|
||||
];
|
||||
|
||||
const token = await this.authService.getToken();
|
||||
this.keycloakUser = map2User(token);
|
||||
if (this.keycloakUser) {
|
||||
@@ -125,22 +136,86 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
||||
if (this.listing.draft) {
|
||||
this.propertyDetails.push({ label: 'Draft', value: this.listing.draft ? 'Yes' : 'No' });
|
||||
}
|
||||
this.listing.imageOrder.forEach(image => {
|
||||
const imageURL = `${this.env.imageBaseUrl}/pictures/property/${this.listing.imagePath}/${this.listing.serialId}/${image}`;
|
||||
this.images.push(new ImageItem({ src: imageURL, thumb: imageURL }));
|
||||
});
|
||||
if (this.listing.location.street) {
|
||||
if (this.listing.imageOrder && Array.isArray(this.listing.imageOrder)) {
|
||||
this.listing.imageOrder.forEach(image => {
|
||||
const imageURL = `${this.env.imageBaseUrl}/pictures/property/${this.listing.imagePath}/${this.listing.serialId}/${image}`;
|
||||
this.images.push(new ImageItem({ src: imageURL, thumb: imageURL }));
|
||||
});
|
||||
}
|
||||
if (this.listing.location.latitude && this.listing.location.longitude) {
|
||||
this.configureMap();
|
||||
}
|
||||
|
||||
// Update SEO meta tags for commercial property
|
||||
const propertyData = {
|
||||
id: this.listing.id,
|
||||
propertyType: this.selectOptions.getCommercialProperty(this.listing.type),
|
||||
propertyDescription: this.listing.description?.replace(/<[^>]*>/g, '').substring(0, 200) || '',
|
||||
askingPrice: this.listing.price,
|
||||
city: this.listing.location.name || this.listing.location.county || '',
|
||||
state: this.listing.location.state,
|
||||
address: this.listing.location.street || '',
|
||||
zip: this.listing.location.zipCode || '',
|
||||
latitude: this.listing.location.latitude,
|
||||
longitude: this.listing.location.longitude,
|
||||
squareFootage: (this.listing as any).squareFeet,
|
||||
yearBuilt: (this.listing as any).yearBuilt,
|
||||
images: this.listing.imageOrder?.length > 0
|
||||
? this.listing.imageOrder.map(img =>
|
||||
`${this.env.imageBaseUrl}/pictures/property/${this.listing.imagePath}/${this.listing.serialId}/${img}`)
|
||||
: []
|
||||
};
|
||||
this.seoService.updateCommercialPropertyMeta(propertyData);
|
||||
|
||||
// Add RealEstateListing structured data
|
||||
const realEstateSchema = this.seoService.generateRealEstateListingSchema(propertyData);
|
||||
const breadcrumbSchema = this.seoService.generateBreadcrumbSchema([
|
||||
{ name: 'Home', url: '/' },
|
||||
{ name: 'Commercial Properties', url: '/commercialPropertyListings' },
|
||||
{ name: propertyData.propertyType, url: `/details-commercial-property/${this.listing.id}` }
|
||||
]);
|
||||
this.seoService.injectMultipleSchemas([realEstateSchema, breadcrumbSchema]);
|
||||
|
||||
// Generate breadcrumbs for navigation
|
||||
this.breadcrumbs = [
|
||||
{ label: 'Home', url: '/home', icon: 'fas fa-home' },
|
||||
{ label: 'Commercial Properties', url: '/commercialPropertyListings' },
|
||||
{ label: propertyData.propertyType, url: '/commercialPropertyListings' },
|
||||
{ label: this.listing.title }
|
||||
];
|
||||
|
||||
// Load related listings for internal linking (SEO improvement)
|
||||
this.loadRelatedListings();
|
||||
} catch (error) {
|
||||
this.auditService.log({ severity: 'error', text: error.error.message });
|
||||
// Set default breadcrumbs even on error
|
||||
this.breadcrumbs = [
|
||||
{ label: 'Home', url: '/home', icon: 'fas fa-home' },
|
||||
{ label: 'Commercial Properties', url: '/commercialPropertyListings' }
|
||||
];
|
||||
|
||||
const errorMessage = error?.error?.message || error?.message || 'An error occurred while loading the listing';
|
||||
this.auditService.log({ severity: 'error', text: errorMessage });
|
||||
this.router.navigate(['notfound']);
|
||||
}
|
||||
|
||||
//this.initFlowbite();
|
||||
}
|
||||
/**
|
||||
* Load related commercial property listings based on same category, location, and price range
|
||||
* Improves SEO through internal linking
|
||||
*/
|
||||
private async loadRelatedListings() {
|
||||
try {
|
||||
this.relatedListings = (await this.listingsService.getRelatedListings(this.listing, 'commercialProperty', 3)) as CommercialPropertyListing[];
|
||||
} catch (error) {
|
||||
console.error('Error loading related listings:', error);
|
||||
this.relatedListings = [];
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
|
||||
this.seoService.clearStructuredData(); // Clean up SEO structured data
|
||||
}
|
||||
private initFlowbite() {
|
||||
this.ngZone.runOutsideAngular(() => {
|
||||
@@ -177,9 +252,9 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
||||
getImageIndices(): number[] {
|
||||
return this.listing && this.listing.imageOrder ? this.listing.imageOrder.slice(1).map((e, i) => i + 1) : [];
|
||||
}
|
||||
save() {
|
||||
async save() {
|
||||
await this.listingsService.addToFavorites(this.listing.id, 'commercialProperty');
|
||||
this.listing.favoritesForUser.push(this.user.email);
|
||||
this.listingsService.save(this.listing, 'commercialProperty');
|
||||
this.auditService.createEvent(this.listing.id, 'favorite', this.user?.email);
|
||||
}
|
||||
isAlreadyFavorite() {
|
||||
@@ -187,8 +262,9 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
||||
}
|
||||
async showShareByEMail() {
|
||||
const result = await this.emailService.showShareByEMail({
|
||||
yourEmail: this.user ? this.user.email : null,
|
||||
yourName: this.user ? `${this.user.firstname} ${this.user.lastname}` : null,
|
||||
yourEmail: this.user ? this.user.email : '',
|
||||
yourName: this.user ? `${this.user.firstname} ${this.user.lastname}` : '',
|
||||
recipientEmail: '',
|
||||
url: environment.mailinfoUrl,
|
||||
listingTitle: this.listing.title,
|
||||
id: this.listing.id,
|
||||
|
||||
Reference in New Issue
Block a user