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

@@ -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>

View File

@@ -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,