← Articles
19 min read

Auditing ibaity.com — Web Quality on a Nuxt SSR App

30+ findings across performance, accessibility, SEO, and security on Iraq's real estate platform — with code-level fixes for every issue.


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

PageURLRole
Homepageibaity.comMain landing page — property search, listings, app download
Chalets & Farmsibaity.com/chaletsListing 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

CategoryHomepageChalets & Farms
Accessibility8581
Best Practices5454
SEO9292

Core Web Vitals (Lab)

MetricHomepageChaletsThreshold
LCP1,254 ms ✅3,339 ms≤ 2,500 ms
CLS0.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 PhaseDuration% of LCP
TTFB1,045 ms83.3%
Load Delay15 ms1.2%
Load Duration45 ms3.6%
Render Delay149 ms11.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.

CurrentTextIssue
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 HeaderStatus
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" and dir="rtl" for Arabic RTL support
  • Structured data (RealEstateAgent, WebSite schemas) present
  • No zoom restrictionsuser-scalable not disabled
  • All links have discernible names and form labels are properly associated

Homepage Fix Priority

#ActionCategoryEffortImpact
1Add alt text to 3 imagesAccessibilityLowHigh
2Add aria-label to header buttonAccessibilityLowHigh
3Fix hydration mismatchBest PracticesMediumHigh
4Remove unused Hero.jpg preloadPerformanceLowMedium
5Remove unused preconnect hintsPerformanceLowMedium
6Convert home-banner-bg.png to WebPPerformanceLowHigh
7Add <main> landmarkAccessibilityLowMedium
8Fix heading hierarchyAccessibilityMediumMedium
9Add security headers (CSP, XFO, COOP)SecurityMediumHigh
10Optimize TTFB (SSR caching / ISR)PerformanceHighHigh
11Preload critical font, remove extrasPerformanceMediumMedium
12Defer third-party scriptsPerformanceMediumMedium
13Extend image cache TTLsPerformanceLowMedium

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 PhaseDuration% of LCP
TTFB1,249 ms37.4%
Resource Load Delay2,011 ms60.2%
Resource Load Duration2 ms0.1%
Element Render Delay77 ms2.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:

CheckStatus
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

MetricCurrentAfter image fixesAfter image + TTFB
LCP3,339 ms~1,300 ms~800 ms
CLS0.050.050.00
TTFB1,249 ms1,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:

CheckValue
Current canonicalhttps://ibaity.com/
Expected canonicalhttps://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:

LanguageCurrent URLExpected URL
arhttps://ibaity.com/https://www.ibaity.com/chalets
enhttps://ibaity.com/enhttps://www.ibaity.com/en/chalets
kuhttps://ibaity.com/kuhttps://www.ibaity.com/ku/chalets
x-defaulthttps://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:

TagContentIssue
H1(missing)❌ No H1
H3شاليهات و مزارع في العراقShould be H1
H5 (×10)Chalet listing titlesSkips H4
H4 (×2)Hidden call-to-action headingsNot visible
H2 (×3)Footer sectionsHigher 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):

ElementCurrent Domain
Canonicalibaity.com
og:urlibaity.com
og:imageibaity.com
twitter:imageibaity.com
Hreflang URLsibaity.com
Sitemap URLibaity.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 is 0.05 — technically “Good” but preventable. 94% of the total layout shift comes from a single unsized logo image:

ShiftScoreCause
Shift 10.003Unknown
Shift 20.050main_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

ProviderMain Thread Time
Google Tag Manager92 ms
TikTok55 ms
Facebook36 ms
Hotjar13 ms
Cloudflare4 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)

#IssueImpactEffort
1Fix canonical URL → /chalets🔴 CriticalLow
2Fix hreflang tags → chalets path🔴 CriticalLow
3Add <h1> heading🔴 CriticalVery Low
4Normalize all URLs to www.🔴 CriticalLow
5Add structured data (JSON-LD)🟠 HighMedium
6Fix sitemap (503 error)🟠 HighMedium
7Use relevant og:image🟠 HighLow
8Shorten meta description (187 → 155 chars)🟠 HighVery Low

Performance Fixes (Impact: User Experience)

#IssueImpactEffort
9Make LCP image SSR-discoverable🔴 CriticalMedium
10Remove loading="lazy" from first image🔴 CriticalVery Low
11Add fetchpriority="high" to LCP image🔴 CriticalVery Low
12Reduce TTFB via ISR/SWR caching🟠 HighMedium
13Replace .woff with .woff2, preload fonts🟠 HighLow

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.

PatternAffectedRoot Cause
Missing security headersAll pagesNo Cloudflare rule or server middleware
Slow TTFB (~1,000–1,250ms)All pagesNo SSR caching, no ISR/SWR
8-9 font files in critical chainAll pagesToo many font weights loaded globally
Unused preconnect hintsAll pagesMigrated to self-hosted fonts but kept old hints
Third-party script overhead (~200ms)All pagesAnalytics loaded eagerly, not deferred
No <main> landmarkAll pagesLayout template missing semantic element
Broken heading hierarchyAll pagesComponent-level heading choices, not page-level
www vs non-www mismatch in SEO tagsAll pagesHardcoded 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

  1. web.dev: Optimize Largest Contentful Paint
  2. web.dev: Fetch Priority API
  3. web.dev: Content Security Policy
  4. WCAG 2.2: Name, Role, Value (4.1.2)
  5. Google: Canonical URLs
  6. Google: Hreflang Tags
  7. Schema.org: LodgingBusiness
  8. web.dev: Browser-Level Image Lazy Loading
  9. web.dev: Heading Hierarchy
  10. Cloudflare: HTTP/3 and QUIC
  11. web.dev: Sitemaps

case-study · web-audit · performance · accessibility · seo · security · core-web-vitals · nuxt · lighthouse