Context
ibaity.com is Iraq’s real estate marketplace — a platform for buying, renting (monthly and daily), and listing properties, chalets, farms, and residential complexes across all Iraqi governorates.
The site is built with Nuxt (Vue SSR), served through Cloudflare CDN over HTTP/3, and uses the IPX image proxy for on-the-fly image optimization. It integrates five third-party analytics services: Google Tag Manager, TikTok Pixel, Facebook Pixel, Hotjar, and Cloudflare Web Analytics.
I ran a comprehensive web quality audit across two key pages using Lighthouse 13.3.0, Chrome DevTools (Performance Trace, DOM analysis, Network panel), and axe-core 4.11.4. The audit covered four categories: Performance, Accessibility, SEO, and Best Practices/Security.
Pages Audited
| Page | URL | Role |
|---|---|---|
| Homepage | ibaity.com | Main landing page — property search, listings, app download |
| Chalets & Farms | ibaity.com/chalets | Listing page for chalets and farm rentals |
These two pages tell very different stories. The homepage has passing Core Web Vitals but poor security. The chalets page has a failing LCP and critical SEO misconfigurations that likely prevent Google from indexing it at all.
Scores at a Glance
| Category | Homepage | Chalets & Farms |
|---|---|---|
| Accessibility | 85 | 81 |
| Best Practices | 54 | 54 |
| SEO | 92 | 92 |
Core Web Vitals (Lab)
| Metric | Homepage | Chalets | Threshold |
|---|---|---|---|
| LCP | 1,254 ms ✅ | 3,339 ms ❌ | ≤ 2,500 ms |
| CLS | 0.001 ✅ | 0.05 ✅ | ≤ 0.1 |
| TTFB | ~1,045 ms ⚠️ | 1,249 ms ❌ | ≤ 800 ms |
Both pages share the same Best Practices score of 54 (no security headers) and the same SEO score of 92 (Lighthouse can’t detect the chalets page’s canonical URL misconfiguration). The key difference is LCP — the homepage passes comfortably, while the chalets page fails by 839ms.
Homepage Audit
LCP Breakdown: Fast Paint, Slow Server
The homepage’s LCP element is a hero image (Hero.webp). LCP passes at 1,254ms, but the breakdown reveals a fragile foundation — 83% of that time is just waiting for the server to respond.
| LCP Phase | Duration | % of LCP |
|---|---|---|
| TTFB | 1,045 ms | 83.3% |
| Load Delay | 15 ms | 1.2% |
| Load Duration | 45 ms | 3.6% |
| Render Delay | 149 ms | 11.9% |
Despite using Cloudflare CDN and HTTP/3, the Nuxt SSR render is slow. The homepage content changes infrequently — it’s a perfect candidate for ISR or edge caching:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { swr: 300 }
}
})
Even a 60-second stale-while-revalidate cache at the Cloudflare edge would eliminate most SSR overhead for repeat visitors.
Accessibility: Basic Gaps
Button Without an Accessible Name
A button in the header has no text content, no aria-label, and no aria-labelledby. Screen readers announce it as “button” with zero context.
<!-- What the browser sees -->
<button type="button" data-slot="base"
class="font-medium items-center disabled:cursor-not-allowed ...">
<!-- Nothing here -->
</button>
The fix is one attribute:
<button type="button" aria-label="القائمة" data-slot="base" class="...">
This is a WCAG 4.1.2 (Level A) violation — the most basic level of accessibility compliance. Every interactive element needs a name.
Three Images Missing Alt Text
Three images lack alt attributes entirely — not empty alt="" (which is valid for decorative images), but completely absent. The images include the app download banner, the footer logo, and the header logo.
<!-- Informative images need descriptive text -->
<img src="/_ipx/q_80/Banner_Phone.png" alt="تطبيق بيتي على الهاتف" />
<img src="/_ipx/q_80/main_logo_inv.png" alt="شعار بيتي" />
<!-- Purely decorative images use empty alt -->
<img src="/_ipx/q_80/main_logo_inv.svg" alt="" role="presentation" />
Broken Heading Hierarchy
The page uses headings inconsistently — skipping from H1 directly to H3, using H5 for property listings, and reserving H2 exclusively for the footer.
| Current | Text | Issue |
|---|---|---|
| H1 | سوق العقارات الشامل في العراق | ✅ Correct |
| H3 | احصائيات اسعار العقارات | ❌ Skips H2 |
| H5 (×30) | Property listing titles | ❌ Skips H3, H4 |
| H2 (×3) | Footer sections | ❌ Only used in footer |
Correct structure:
H1 — Page title
H2 — Statistics section
H2 — Properties for Sale
H3 — Property card titles
H2 — Properties for Rent
H3 — Property card titles
H2 — Download App
No <main> Landmark
The document wraps page content in a generic <div> instead of a <main> element. Screen reader users can’t skip directly to primary content.
Security: No Protection Headers
The page ships with no CSP, no X-Frame-Options, and no Cross-Origin-Opener-Policy. This leaves the site open to XSS injection, clickjacking, and cross-origin attacks.
| Security Header | Status |
|---|---|
| Content-Security-Policy | ❌ Missing |
| X-Frame-Options | ❌ Missing |
| Cross-Origin-Opener-Policy | ❌ Missing |
| Strict-Transport-Security | ⚠️ Missing includeSubDomains |
The CSP needs to allowlist the five analytics origins, inline styles (Nuxt requires 'unsafe-inline' for styles), and the CloudFront image CDN:
Content-Security-Policy: default-src 'self'; script-src 'self'
https://www.googletagmanager.com https://analytics.tiktok.com
https://connect.facebook.net https://static.hotjar.com
https://static.cloudflareinsights.com 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' https://d2mvhtn02x76us.cloudfront.net data:;
font-src 'self'; connect-src 'self'
https://www.google-analytics.com https://analytics.tiktok.com
https://www.facebook.com;
X-Frame-Options: DENY
Cross-Origin-Opener-Policy: same-origin
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Performance: Resource Waste
2MB Unoptimized Background Image
home-banner-bg.png is served as a raw PNG. The site already uses Nuxt Image’s IPX proxy for other images — this one just got missed.
<picture>
<source srcset="/home-banner-bg.avif" type="image/avif" />
<source srcset="/home-banner-bg.webp" type="image/webp" />
<img src="/home-banner-bg.png" alt="" loading="lazy" />
</picture>
8 Font Files Chained Off a Single CSS Request
The critical request chain loads 8 .woff2 files sequentially through a CSS dependency. The browser can’t discover these fonts until it parses the CSS, which can’t start until the HTML arrives.
HTML (1,357ms)
└── entry.irYBy1ob.css (17ms)
├── font-1.woff2 (1,204ms)
├── font-2.woff2 (1,204ms)
├── font-3.woff2 (1,204ms)
└── ... (5 more)
Two unused <link rel="preconnect"> hints for fonts.googleapis.com and fonts.gstatic.com also exist — the fonts are self-hosted, so these connections are wasted.
The fix is to preload the most critical font variant and cut down the total number of font files:
<link rel="preload" href="/_fonts/C_7asvEGN...woff2"
as="font" type="font/woff2" crossorigin />
8 font files for a single page is excessive. Subsetting to Arabic + Latin ranges and reducing to 3-4 key weights would cut the load significantly.
Preloaded Hero.jpg Never Used
The page preloads Hero.jpg, but actually renders Hero.webp via Nuxt Image. This wastes a preload slot and triggers a browser warning:
The resource https://www.ibaity.com/Hero.jpg was preloaded using link preload
but not used within a few seconds from the window's load event.
Remove the stale preload or update it to match the actual resource:
<link rel="preload" href="/_ipx/q_80/Hero.webp" as="image" type="image/webp" />
Hydration Mismatch
Nuxt logs a hydration mismatch error, meaning the server-rendered HTML doesn’t match what the client produces. Common culprits in Nuxt apps: accessing window, localStorage, or Date.now() during SSR.
<!-- Wrap client-only content -->
<ClientOnly>
<UserLocationBadge />
<template #fallback>
<div class="h-6 w-20 animate-pulse bg-gray-200 rounded" />
</template>
</ClientOnly>
194 Network Requests
The page fires 194 requests on initial load: 80+ JavaScript files, 54 images, 11 stylesheets, 8 fonts, and 40+ analytics/tracking requests. Lazy loading below-fold property cards and deferring non-critical CSS would bring this down substantially.
What the Homepage Gets Right
- HTTPS everywhere with no mixed content
- HTTP/3 protocol for modern transport
- Zstd compression on HTML responses
- Immutable caching on
/_nuxt/static assets (1-year TTL) - Excellent CLS (0.001) — almost no layout shifts
- LCP under 2.5s despite the slow TTFB
- Good color contrast passing WCAG AA
- Correct
lang="ar"anddir="rtl"for Arabic RTL support - Structured data (RealEstateAgent, WebSite schemas) present
- No zoom restrictions —
user-scalablenot disabled - All links have discernible names and form labels are properly associated
Homepage Fix Priority
| # | Action | Category | Effort | Impact |
|---|---|---|---|---|
| 1 | Add alt text to 3 images | Accessibility | Low | High |
| 2 | Add aria-label to header button | Accessibility | Low | High |
| 3 | Fix hydration mismatch | Best Practices | Medium | High |
| 4 | Remove unused Hero.jpg preload | Performance | Low | Medium |
| 5 | Remove unused preconnect hints | Performance | Low | Medium |
| 6 | Convert home-banner-bg.png to WebP | Performance | Low | High |
| 7 | Add <main> landmark | Accessibility | Low | Medium |
| 8 | Fix heading hierarchy | Accessibility | Medium | Medium |
| 9 | Add security headers (CSP, XFO, COOP) | Security | Medium | High |
| 10 | Optimize TTFB (SSR caching / ISR) | Performance | High | High |
| 11 | Preload critical font, remove extras | Performance | Medium | Medium |
| 12 | Defer third-party scripts | Performance | Medium | Medium |
| 13 | Extend image cache TTLs | Performance | Low | Medium |
Items 1–6 are all low-effort changes that a developer could ship in a single afternoon. The combination would move Best Practices from 54 to ~75 and Accessibility from 85 to ~95.
Chalets & Farms Audit
The chalets page shares many of the homepage’s issues (missing security headers, font chain, third-party overhead) but introduces two categories of problems the homepage doesn’t have: a failing LCP and critical SEO misconfigurations.
Performance: Why LCP Fails at 3,339ms
The LCP element is a chalet listing card image. Breaking down where the 3,339ms goes reveals two compounding problems:
| LCP Phase | Duration | % of LCP |
|---|---|---|
| TTFB | 1,249 ms | 37.4% |
| Resource Load Delay | 2,011 ms | 60.2% |
| Resource Load Duration | 2 ms | 0.1% |
| Element Render Delay | 77 ms | 2.3% |
60% of LCP time is pure load delay — the gap between the browser receiving the HTML and starting to fetch the LCP image. The image downloads in 2ms once requested, but the browser doesn’t even know it exists for over 2 seconds.
Why the Browser Can’t Find the Image
The LCP image fails all three discovery checks:
| Check | Status |
|---|---|
fetchpriority="high" applied | ❌ Failed |
Not using loading="lazy" | ❌ Failed (lazy-loaded) |
| Discoverable in initial HTML | ❌ Failed |
The image is not in the server-rendered HTML. It’s injected by JavaScript after Nuxt hydrates, making it invisible to the browser’s preload scanner. On top of that, it has loading="lazy" — which defers loading until the element is near the viewport — and no fetchpriority hint, so it starts at Low priority.
This is the textbook worst case for LCP: a client-rendered, lazy-loaded, low-priority image.
The Fix: Make It Server-Rendered and Eager
<!-- Before: client-rendered, lazy-loaded, low priority -->
<img src="/_ipx/q_80&s_560x416/..." loading="lazy"
class="w-full h-full object-cover">
<!-- After: SSR-rendered, eager-loaded, high priority -->
<img src="/_ipx/q_80&s_560x416/..."
loading="eager"
fetchpriority="high"
width="560" height="416"
class="w-full h-full object-cover">
For the SSR side, the chalet data fetch needs to happen server-side so the first card image is in the initial HTML:
const { data: chalets } = await useFetch('/api/chalets', {
query: { searchCountry: 'IQ', currency: 'IQD' }
})
And preload the first image in the document <head>:
const firstImage = chalets.value?.[0]?.mainImage
if (firstImage) {
useHead({
link: [{
rel: 'preload',
as: 'image',
href: `/_ipx/q_80&s_560x416/${firstImage}`,
fetchpriority: 'high'
}]
})
}
The .woff Bottleneck
The critical path maxes out at 3,257ms. Unlike the homepage (which only loads .woff2 files), the chalets page also loads a .woff font — an Arabic fallback that’s ~30% larger:
Document (1,275 ms)
└── entry.irYBy1ob.css (1,289 ms)
├── font-1.woff2 (1,364 ms)
├── font-2.woff2 (1,364 ms)
├── ... (6 more .woff2)
└── font-9.woff (3,257 ms) ← the bottleneck
Replacing it with a .woff2 equivalent and preloading the 2-3 most critical Arabic font variants would eliminate this bottleneck.
Expected Performance After Fixes
| Metric | Current | After image fixes | After image + TTFB |
|---|---|---|---|
| LCP | 3,339 ms | ~1,300 ms | ~800 ms |
| CLS | 0.05 | 0.05 | 0.00 |
| TTFB | 1,249 ms | 1,249 ms | ~400 ms |
Making the LCP image discoverable alone should bring LCP under the 2,500ms Good threshold. Combined with TTFB improvements, it could drop below 1 second.
SEO: The Invisible Page
The performance issues slow the page down. The SEO issues make it invisible. The Lighthouse SEO score of 92 is misleading — Lighthouse checks structural SEO (meta tags exist, viewport is set, text is readable), but it doesn’t verify that a canonical URL points to the right page. The critical issues on this page are invisible to automated scoring.
The Canonical URL Points to the Homepage
This is the single most damaging issue on the entire site:
| Check | Value |
|---|---|
| Current canonical | https://ibaity.com/ |
| Expected canonical | https://www.ibaity.com/chalets |
The canonical URL tells Google: “this page is a duplicate of the homepage.” Google will likely not index the chalets page as a distinct page, attribute all ranking signals to the homepage instead, and effectively treat this page as if it doesn’t exist in search.
useHead({
link: [
{ rel: 'canonical', href: 'https://www.ibaity.com/chalets' }
]
})
All Hreflang Tags Point to Root
The multilingual hreflang tags have the same problem — every language variant points to the homepage path instead of the chalets path:
| Language | Current URL | Expected URL |
|---|---|---|
| ar | https://ibaity.com/ | https://www.ibaity.com/chalets |
| en | https://ibaity.com/en | https://www.ibaity.com/en/chalets |
| ku | https://ibaity.com/ku | https://www.ibaity.com/ku/chalets |
| x-default | https://ibaity.com/ | https://www.ibaity.com/chalets |
This tells Google’s multilingual crawler that the Arabic, English, and Kurdish versions of this page are all the homepage.
useHead({
link: [
{ rel: 'alternate', hreflang: 'ar', href: 'https://www.ibaity.com/chalets' },
{ rel: 'alternate', hreflang: 'en', href: 'https://www.ibaity.com/en/chalets' },
{ rel: 'alternate', hreflang: 'ku', href: 'https://www.ibaity.com/ku/chalets' },
{ rel: 'alternate', hreflang: 'x-default', href: 'https://www.ibaity.com/chalets' }
]
})
No H1 Heading
The page has no <h1>. The main heading “شاليهات و مزارع في العراق” is wrapped in an <h3>, while the <h2> tag is reserved for footer sections. The heading hierarchy is inverted:
| Tag | Content | Issue |
|---|---|---|
| H1 | (missing) | ❌ No H1 |
| H3 | شاليهات و مزارع في العراق | Should be H1 |
| H5 (×10) | Chalet listing titles | Skips H4 |
| H4 (×2) | Hidden call-to-action headings | Not visible |
| H2 (×3) | Footer sections | Higher rank than content |
<h1>شاليهات و مزارع في العراق</h1>
<h2>مزرعة الفهد</h2>
<h2>مزرعة ارماس</h2>
<h2>مزرعة بغداد</h2>
www vs. Non-www Mismatch
The site is served on www.ibaity.com, but almost every SEO tag references ibaity.com (without www):
| Element | Current Domain |
|---|---|
| Canonical | ibaity.com |
| og:url | ibaity.com |
| og:image | ibaity.com |
| twitter:image | ibaity.com |
| Hreflang URLs | ibaity.com |
| Sitemap URL | ibaity.com |
This domain inconsistency splits link equity and confuses crawlers. Every SEO-relevant URL must use the same canonical domain.
Sitemap Returns 503
The sitemap at https://ibaity.com/sitemap.xml returns a 503 Service Unavailable. Google Search Console will flag this as a critical crawl error, and new or updated pages won’t be discovered through the sitemap.
No Structured Data
The homepage has RealEstateAgent and WebSite schemas. This page has nothing — no BreadcrumbList, no ItemList, no LodgingBusiness. This means no rich snippet eligibility in search results.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ItemList",
"name": "شاليهات ومزارع في العراق",
"numberOfItems": 10,
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"item": {
"@type": "LodgingBusiness",
"name": "مزرعة الفهد",
"image": "https://d2mvhtn02x76us.cloudfront.net/uploads/...",
"url": "https://www.ibaity.com/chalets/..."
}
}
]
}
</script>
Generic og:image
The page uses the site-wide Hero.jpg as its Open Graph image instead of a relevant chalet photo. When someone shares this page on social media, they see the generic homepage hero — not chalets.
const featuredImage = chalets.value?.[0]?.mainImage
useHead({
meta: [
{
property: 'og:image',
content: featuredImage
? `https://www.ibaity.com/_ipx/q_80&s_1200x630/${featuredImage}`
: 'https://www.ibaity.com/Hero.jpg'
}
]
})
Additional Chalets Findings
CLS from Unsized Logo
CLS is 0.05 — technically “Good” but preventable. 94% of the total layout shift comes from a single unsized logo image:
| Shift | Score | Cause |
|---|---|---|
| Shift 1 | 0.003 | Unknown |
| Shift 2 | 0.050 | main_logo.png (128×104) loaded without dimensions |
<img src="/_ipx/q_80&s_128x104/main_logo.png"
width="128" height="104" alt="بيتي">
5-Minute Image Cache TTLs
Chalet listing images are cached for only 300 seconds (5 minutes). These images rarely change — a 1-day cache would be appropriate:
export default defineNuxtConfig({
routeRules: {
'/_ipx/**': {
headers: { 'cache-control': 'public, max-age=86400, s-maxage=86400' }
}
}
})
Third-Party Script Overhead
| Provider | Main Thread Time |
|---|---|
| Google Tag Manager | 92 ms |
| TikTok | 55 ms |
| 36 ms | |
| Hotjar | 13 ms |
| Cloudflare | 4 ms |
| Total | ~200 ms |
Deferring these until after first interaction or using Nuxt’s Partytown module would reclaim 200ms of main thread time.
Chalets Fix Priority
SEO Fixes (Impact: Indexing)
| # | Issue | Impact | Effort |
|---|---|---|---|
| 1 | Fix canonical URL → /chalets | 🔴 Critical | Low |
| 2 | Fix hreflang tags → chalets path | 🔴 Critical | Low |
| 3 | Add <h1> heading | 🔴 Critical | Very Low |
| 4 | Normalize all URLs to www. | 🔴 Critical | Low |
| 5 | Add structured data (JSON-LD) | 🟠 High | Medium |
| 6 | Fix sitemap (503 error) | 🟠 High | Medium |
| 7 | Use relevant og:image | 🟠 High | Low |
| 8 | Shorten meta description (187 → 155 chars) | 🟠 High | Very Low |
Performance Fixes (Impact: User Experience)
| # | Issue | Impact | Effort |
|---|---|---|---|
| 9 | Make LCP image SSR-discoverable | 🔴 Critical | Medium |
| 10 | Remove loading="lazy" from first image | 🔴 Critical | Very Low |
| 11 | Add fetchpriority="high" to LCP image | 🔴 Critical | Very Low |
| 12 | Reduce TTFB via ISR/SWR caching | 🟠 High | Medium |
| 13 | Replace .woff with .woff2, preload fonts | 🟠 High | Low |
Cross-Page Patterns
Looking at both pages together, several systemic issues emerge that aren’t page-specific — they’re architectural decisions (or oversights) that affect every route.
| Pattern | Affected | Root Cause |
|---|---|---|
| Missing security headers | All pages | No Cloudflare rule or server middleware |
| Slow TTFB (~1,000–1,250ms) | All pages | No SSR caching, no ISR/SWR |
| 8-9 font files in critical chain | All pages | Too many font weights loaded globally |
| Unused preconnect hints | All pages | Migrated to self-hosted fonts but kept old hints |
| Third-party script overhead (~200ms) | All pages | Analytics loaded eagerly, not deferred |
No <main> landmark | All pages | Layout template missing semantic element |
| Broken heading hierarchy | All pages | Component-level heading choices, not page-level |
| www vs non-www mismatch in SEO tags | All pages | Hardcoded non-www domain in Nuxt config |
These are the fixes that would have the widest impact, because they apply to every page at once.
Reflection
The two pages reveal a split personality. The homepage has solid Core Web Vitals but poor security and accessibility fundamentals. The chalets page has failing performance and critical SEO misconfigurations that likely prevent Google indexing.
The most concerning finding across both pages is the canonical URL on the chalets page pointing to the homepage. This single misconfiguration likely means the chalets page doesn’t exist as a distinct page in Google’s index. A user searching for “شاليهات للإيجار في العراق” (chalets for rent in Iraq) would never find this page, even though it’s exactly what they’re looking for. Fixing this one tag could have a larger impact on organic traffic than every performance optimization combined.
On the performance side, the chalets page’s LCP failure is entirely a discovery problem — the image downloads in 2ms once the browser knows about it. Making it server-rendered and removing loading="lazy" would drop LCP by over 2 seconds with zero infrastructure changes.
The homepage’s LCP passes today, but the 83% TTFB ratio is a ticking bomb — as the page grows heavier or server load increases, LCP will cross the 2.5s threshold. Adding SWR caching now would create a safety margin.
The systemic issues — missing security headers, font chain bloat, eager analytics loading — are the best return on investment. A single Cloudflare rule for CSP, a Nuxt config change for ISR, and removing two unused preconnect tags would improve every page on the site in one deploy.
Most of the fixes are low-effort configuration changes — the kind of work that takes an afternoon but pays off for months.
References
- web.dev: Optimize Largest Contentful Paint
- web.dev: Fetch Priority API
- web.dev: Content Security Policy
- WCAG 2.2: Name, Role, Value (4.1.2)
- Google: Canonical URLs
- Google: Hreflang Tags
- Schema.org: LodgingBusiness
- web.dev: Browser-Level Image Lazy Loading
- web.dev: Heading Hierarchy
- Cloudflare: HTTP/3 and QUIC
- web.dev: Sitemaps