perf: Lighthouse optimizations - lazy loading, contrast fixes, LCP preload, SEO links

This commit is contained in:
Timo Knuth 2026-02-04 15:47:40 +01:00
parent ff7ef0f423
commit 737329794c
20 changed files with 68 additions and 40 deletions

View File

@ -11,19 +11,14 @@ import { LoginRegisterComponent } from './components/login-register/login-regist
import { AuthGuard } from './guards/auth.guard';
import { ListingCategoryGuard } from './guards/listing-category.guard';
// Public pages (eagerly loaded - high traffic)
import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component';
import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component';
import { DetailsUserComponent } from './pages/details/details-user/details-user.component';
// Public pages - HomeComponent stays eagerly loaded as landing page
import { HomeComponent } from './pages/home/home.component';
import { BrokerListingsComponent } from './pages/listings/broker-listings/broker-listings.component';
import { BusinessListingsComponent } from './pages/listings/business-listings/business-listings.component';
import { CommercialPropertyListingsComponent } from './pages/listings/commercial-property-listings/commercial-property-listings.component';
import { SuccessComponent } from './pages/success/success.component';
import { TermsOfUseComponent } from './pages/legal/terms-of-use.component';
import { PrivacyStatementComponent } from './pages/legal/privacy-statement.component';
// Note: Account, Edit, Admin, Favorites, MyListing, and EmailUs components are now lazy-loaded below
// Note: All listing and details components are now lazy-loaded for better initial bundle size
export const routes: Routes = [
{
@ -32,17 +27,17 @@ export const routes: Routes = [
},
{
path: 'businessListings',
component: BusinessListingsComponent,
loadComponent: () => import('./pages/listings/business-listings/business-listings.component').then(m => m.BusinessListingsComponent),
runGuardsAndResolvers: 'always',
},
{
path: 'commercialPropertyListings',
component: CommercialPropertyListingsComponent,
loadComponent: () => import('./pages/listings/commercial-property-listings/commercial-property-listings.component').then(m => m.CommercialPropertyListingsComponent),
runGuardsAndResolvers: 'always',
},
{
path: 'brokerListings',
component: BrokerListingsComponent,
loadComponent: () => import('./pages/listings/broker-listings/broker-listings.component').then(m => m.BrokerListingsComponent),
runGuardsAndResolvers: 'always',
},
{
@ -53,11 +48,11 @@ export const routes: Routes = [
// Listings Details - New SEO-friendly slug-based URLs
{
path: 'business/:slug',
component: DetailsBusinessListingComponent,
loadComponent: () => import('./pages/details/details-business-listing/details-business-listing.component').then(m => m.DetailsBusinessListingComponent),
},
{
path: 'commercial-property/:slug',
component: DetailsCommercialPropertyListingComponent,
loadComponent: () => import('./pages/details/details-commercial-property-listing/details-commercial-property-listing.component').then(m => m.DetailsCommercialPropertyListingComponent),
},
// Backward compatibility redirects for old UUID-based URLs
{
@ -95,7 +90,7 @@ export const routes: Routes = [
// User Details
{
path: 'details-user/:id',
component: DetailsUserComponent,
loadComponent: () => import('./pages/details/details-user/details-user.component').then(m => m.DetailsUserComponent),
},
// #########
// User edit (lazy-loaded)

View File

@ -23,9 +23,9 @@
</div>
<div class="mb-4 lg:mb-0 flex flex-col items-center lg:items-end">
<a class="text-sm text-neutral-600 mb-1 lg:mb-2 hover:text-primary-600 w-full"> <i
<a href="tel:+1-800-840-6025" class="text-sm text-neutral-600 mb-1 lg:mb-2 hover:text-primary-600 w-full"> <i
class="fas fa-phone-alt mr-2"></i>1-800-840-6025 </a>
<a class="text-sm text-neutral-600 hover:text-primary-600"> <i
<a href="mailto:info@bizmatch.net" class="text-sm text-neutral-600 hover:text-primary-600"> <i
class="fas fa-envelope mr-2"></i>info&#64;bizmatch.net </a>
</div>
</div>

View File

@ -1,5 +1,5 @@
<header class="w-full flex justify-between items-center p-4 bg-white top-0 z-10 h-16 md:h-20">
<img src="/assets/images/header-logo.png" alt="Logo" class="h-8 md:h-10 w-auto" />
<img src="/assets/images/header-logo.png" alt="BizMatch - Business & Property Marketplace" class="h-8 md:h-10 w-auto" width="120" height="40" />
<div class="hidden md:flex items-center space-x-4">
@if(user){
<a routerLink="/account" class="text-primary-600 border border-primary-600 px-3 py-2 rounded">Account</a>
@ -12,7 +12,7 @@
</div>
<button
(click)="toggleMenu()"
class="md:hidden text-neutral-600"
class="md:hidden text-neutral-700 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center"
aria-label="Open navigation menu"
[attr.aria-expanded]="isMenuOpen"
>
@ -88,8 +88,8 @@
<!-- Restliche Anpassungen (Innenabstände, Button-Paddings etc.) bleiben wie im vorherigen Schritt -->
<div class="search-form-container bg-white bg-opacity-80 pb-4 md:pb-6 pt-2 px-3 sm:px-4 md:px-6 rounded-lg shadow-lg w-full" [ngClass]="{ 'pt-6': aiSearch }">
@if(!aiSearch){
<div class="text-sm lg:text-base mb-1 text-center text-neutral-500 border-neutral-200 dark:text-neutral-400 dark:border-neutral-700 flex justify-between">
<ul class="flex flex-wrap -mb-px w-full" role="tablist">
<div class="text-sm lg:text-base mb-1 text-center text-neutral-700 border-neutral-200 dark:text-neutral-300 dark:border-neutral-700 flex justify-between">
<ul class="flex flex-wrap -mb-px w-full" role="tablist" aria-label="Search categories">
<li class="w-[33%]" role="presentation">
<button
type="button"
@ -99,9 +99,11 @@
[ngClass]="
activeTabAction === 'business'
? ['text-primary-600', 'border-primary-600', 'active', 'dark:text-primary-500', 'dark:border-primary-500']
: ['border-transparent', 'hover:text-neutral-600', 'hover:border-neutral-300', 'dark:hover:text-neutral-300']
: ['border-transparent', 'text-neutral-700', 'hover:text-neutral-900', 'hover:border-neutral-400', 'dark:hover:text-neutral-300']
"
class="tab-link w-full hover:cursor-pointer inline-flex items-center justify-center px-3 py-3 md:p-4 border-b-2 rounded-t-lg bg-transparent"
class="tab-link w-full hover:cursor-pointer inline-flex items-center justify-center px-3 py-3 md:p-4 border-b-2 rounded-t-lg bg-transparent min-h-[44px]"
[attr.aria-controls]="'tabpanel-search'"
id="tab-business"
>
<img src="/assets/images/business_logo.png" alt="" aria-hidden="true" class="tab-icon w-6 h-6 md:w-7 md:h-7 mr-1 md:mr-2 object-contain" width="28" height="28" />
<span>Businesses</span>
@ -117,9 +119,11 @@
[ngClass]="
activeTabAction === 'commercialProperty'
? ['text-primary-600', 'border-primary-600', 'active', 'dark:text-primary-500', 'dark:border-primary-500']
: ['border-transparent', 'hover:text-neutral-600', 'hover:border-neutral-300', 'dark:hover:text-neutral-300']
: ['border-transparent', 'text-neutral-700', 'hover:text-neutral-900', 'hover:border-neutral-400', 'dark:hover:text-neutral-300']
"
class="tab-link w-full hover:cursor-pointer inline-flex items-center justify-center px-3 py-3 md:p-4 border-b-2 rounded-t-lg bg-transparent"
class="tab-link w-full hover:cursor-pointer inline-flex items-center justify-center px-3 py-3 md:p-4 border-b-2 rounded-t-lg bg-transparent min-h-[44px]"
[attr.aria-controls]="'tabpanel-search'"
id="tab-properties"
>
<img src="/assets/images/properties_logo.png" alt="" aria-hidden="true" class="tab-icon w-6 h-6 md:w-7 md:h-7 mr-1 md:mr-2 object-contain" width="28" height="28" />
<span>Properties</span>
@ -135,9 +139,11 @@
[ngClass]="
activeTabAction === 'broker'
? ['text-primary-600', 'border-primary-600', 'active', 'dark:text-primary-500', 'dark:border-primary-500']
: ['border-transparent', 'hover:text-neutral-600', 'hover:border-neutral-300', 'dark:hover:text-neutral-300']
: ['border-transparent', 'text-neutral-700', 'hover:text-neutral-900', 'hover:border-neutral-400', 'dark:hover:text-neutral-300']
"
class="tab-link w-full hover:cursor-pointer inline-flex items-center justify-center px-3 py-3 md:p-4 border-b-2 rounded-t-lg bg-transparent"
class="tab-link w-full hover:cursor-pointer inline-flex items-center justify-center px-3 py-3 md:p-4 border-b-2 rounded-t-lg bg-transparent min-h-[44px]"
[attr.aria-controls]="'tabpanel-search'"
id="tab-professionals"
>
<img
src="/assets/images/icon_professionals.png"
@ -153,7 +159,7 @@
</ul>
</div>
} @if(criteria && !aiSearch){
<div class="w-full max-w-3xl mx-auto bg-white rounded-lg flex flex-col md:flex-row md:border md:border-neutral-300">
<div id="tabpanel-search" role="tabpanel" aria-labelledby="tab-business" class="w-full max-w-3xl mx-auto bg-white rounded-lg flex flex-col md:flex-row md:border md:border-neutral-300">
<div class="md:flex-none md:w-48 flex-1 md:border-r border-neutral-300 overflow-hidden mb-2 md:mb-0">
<div class="relative max-sm:border border-neutral-300 rounded-md">
<label for="type-filter" class="sr-only">Filter by type</label>
@ -195,7 +201,11 @@
groupBy="type"
labelForId="location-search"
aria-label="Search by city or state"
[inputAttrs]="{'aria-describedby': 'location-search-hint'}"
>
<ng-template ng-footer-tmp>
<span id="location-search-hint" class="sr-only">Enter at least 2 characters to search for a city or state</span>
</ng-template>
@for (city of cities$ | async; track city.id) { @let state = city.type==='city'?city.content.state:''; @let separator = city.type==='city'?' - ':'';
<ng-option [value]="city">{{ city.content.name }}{{ separator }}{{ state }}</ng-option>
}

View File

@ -1,7 +1,7 @@
select:not([size]) {
background-image: unset;
}
[type='text'],
[type='email'],
[type='url'],
@ -19,39 +19,51 @@ textarea,
select {
border: unset;
}
.toggle-checkbox:checked {
right: 0;
border-color: rgb(125 211 252);
}
.toggle-checkbox:checked+.toggle-label {
background-color: rgb(125 211 252);
}
:host ::ng-deep .ng-select.ng-select-single .ng-select-container {
min-height: 52px;
border: none;
background-color: transparent;
.ng-value-container .ng-input {
top: 12px;
}
span.ng-arrow-wrapper {
display: none;
}
}
select {
color: #000; /* Standard-Textfarbe für das Dropdown */
color: #000;
/* Standard-Textfarbe für das Dropdown */
// background-color: #fff; /* Hintergrundfarbe für das Dropdown */
}
select option {
color: #000; /* Textfarbe für Dropdown-Optionen */
color: #000;
/* Textfarbe für Dropdown-Optionen */
}
select.placeholder-selected {
color: #999; /* Farbe für den Platzhalter */
color: #6b7280;
/* gray-500 - besserer Kontrast für WCAG AA */
}
input::placeholder {
color: #555; /* Dunkleres Grau */
opacity: 1; /* Stellt sicher, dass die Deckkraft 100% ist */
color: #555;
/* Dunkleres Grau */
opacity: 1;
/* Stellt sicher, dass die Deckkraft 100% ist */
}
/* Stellt sicher, dass die Optionen im Dropdown immer schwarz sind */
@ -59,10 +71,14 @@ select:focus option,
select:hover option {
color: #000 !important;
}
input[type='text'][name='aiSearchText'] {
padding: 14px; /* Innerer Abstand */
font-size: 16px; /* Schriftgröße anpassen */
box-sizing: border-box; /* Padding und Border in die Höhe und Breite einrechnen */
padding: 14px;
/* Innerer Abstand */
font-size: 16px;
/* Schriftgröße anpassen */
box-sizing: border-box;
/* Padding und Border in die Höhe und Breite einrechnen */
height: 48px;
}
@ -145,6 +161,7 @@ select,
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
@ -212,6 +229,7 @@ header {
transition: all 0.2s ease-in-out;
&.text-blue-600.border.border-blue-600 {
// Log In button
&:hover {
background-color: rgba(37, 99, 235, 0.05);
@ -224,6 +242,7 @@ header {
}
&.bg-blue-600 {
// Register button
&:hover {
background-color: rgb(29, 78, 216);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -1,6 +1,6 @@
// Build information, automatically generated by `the_build_script` :zwinkern:
const build = {
timestamp: "GER: 03.02.2026 12:44 | TX: 02/03/2026 5:44 AM"
timestamp: "GER: 04.02.2026 15:43 | TX: 02/04/2026 8:43 AM"
};
export default build;

View File

@ -35,6 +35,9 @@
<!-- Preload critical assets -->
<link rel="preload" as="image" href="/assets/images/header-logo.png" type="image/png" />
<!-- Hero background is LCP element - preload with high priority -->
<link rel="preload" as="image" href="/assets/images/flags_bg.avif" type="image/avif" fetchpriority="high" />
<link rel="preload" as="image" href="/assets/images/flags_bg.jpg" imagesrcset="/assets/images/flags_bg.jpg" type="image/jpeg" />
<!-- Prefetch common assets -->
<link rel="prefetch" as="image" href="/assets/images/business_logo.png" />

View File

@ -5,7 +5,8 @@
@tailwind utilities;
// External CSS imports - these URL imports don't trigger deprecation warnings
@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap');
// Using css2 API with specific weights for better performance
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap');
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css');
// Local CSS files loaded as CSS (not SCSS) to avoid @import deprecation